Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions companion/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ The [MCP API](MCP.md) exposes source queries, Java execution and debugger operat

## Ownership

`CompanionApp` owns the session, debugger, compiler, index loader and project worker. One `ProjectScope` owns the selected profile, instance state, navigation history, pending navigation and nullable `RuntimeBinding`. A scope admits work while ACTIVE; SWITCHING rejects new work but can be cancelled after an editor veto or failed state flush; RETIRED is terminal. Check-and-submit uses the same lifecycle lock as runtime installation. Swing hops and debugger waits run outside that lock.
`CompanionApp` is the process bootstrap: launch arguments, process lock, logging, look and feel, and application construction. `CompanionApplication` owns the session, debugger, compiler, index loader and project worker. It can run without a UI; `CompanionUi` is the boundary for window lifecycle and navigation. One `ProjectScope` owns the selected profile, instance state, navigation history, pending navigation and nullable `RuntimeBinding`. A scope admits work while ACTIVE; SWITCHING rejects new work but can be cancelled after an editor veto or failed state flush; RETIRED is terminal. Check-and-submit uses the same lifecycle lock as runtime installation. Swing hops and debugger waits run outside that lock.

The scope publishes one `RuntimeBinding` for the installed inventory. The binding groups its identity, source catalog, classpath, decompiler and reference search, and owns the native index after installation succeeds. `CompanionClassIndex` is only JDT's process-wide lookup hook; setting or clearing it never closes an index.

The index loader retains ownership while a candidate is prepared. The application detaches the previous runtime, attaches the new compiler/insight bindings, and completes publication under the existing lifecycle lock. Debugger and UI follow-up runs afterward and cannot return an installed index to the loader's failure cleanup. Closing a runtime detaches its consumers before releasing the index, and is idempotent. A rejected candidate closes its own prepared consumers while leaving index disposal to the loader.

The script compiler and code-insight worker remain application-lived. Open local editors retain the code-insight service, so a runtime changes its binding rather than replacing that service instance. MCP tool declarations identify project-bound requests. Mutations use the captured scope's atomic admission gate; late results check that scope, and navigation also checks runtime identity. Instance state is flushed before retirement and detach. Stateless JDT parsing is in `JavaAst`; editor analysis/listeners remain in `ASTCache`.
The script compiler and code-insight worker remain application-lived. Open local editors retain the code-insight service, so a runtime changes its binding rather than replacing that service instance. MCP tool declarations identify project-bound requests. Mutations use the captured scope's atomic admission gate; late results check that scope, and navigation also checks runtime identity. Instance state is flushed before retirement and detach. JDT workspace initialization receives its metadata path explicitly through `JDTHacks.init`. Its process globals initialize once; tests share an explicit cache per JVM. Stateless JDT parsing is in `JavaAst`; editor analysis/listeners remain in `ASTCache` until the cache ownership slice.

Java editors receive an `EditorContext` containing their project, runtime-facing services, navigation and window callbacks. Search and settings windows receive their own specific collaborators. `ScriptExecutionService` shares authenticated execution and cancellation between UI and MCP; execution-result subscriptions belong to `CompanionSession` and are removed by their UI/job owners. Window disposal and project switching share the same auxiliary-window cleanup.
1 change: 1 addition & 0 deletions companion/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ tasks.named('assemble') {

tasks.named('test') {
useJUnitPlatform()
systemProperty 'junit.jupiter.extensions.autodetection.enabled', 'true'
systemProperty 'totaldebug.testClasspath', sourceSets.test.runtimeClasspath.asPath
}

Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.github.minecraft_ta.totalDebugCompanion.jdt;

import com.github.minecraft_ta.totalDebugCompanion.CompanionApp;
import com.github.minecraft_ta.totalDebugCompanion.jdt.impls.BundleContextImpl;
import com.github.minecraft_ta.totalDebugCompanion.jdt.impls.ContentTypeManagerImpl;
import com.github.minecraft_ta.totalDebugCompanion.jdt.impls.DummyJarPackageFragmentRoot;
Expand All @@ -11,7 +10,7 @@
import org.eclipse.core.internal.runtime.MetaDataKeeper;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import java.nio.file.Path;
import org.eclipse.core.runtime.content.IContentTypeManager;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IType;
Expand All @@ -26,22 +25,28 @@

public class JDTHacks {

public static final JavaProject DUMMY_JAVA_PROJECT;
private static final PackageFragmentRoot PACKAGE_FRAGMENT_ROOT;
public static JavaProject DUMMY_JAVA_PROJECT;
private static PackageFragmentRoot PACKAGE_FRAGMENT_ROOT;
private static final Unsafe UNSAFE;
static {
try {
var theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
UNSAFE = (Unsafe) theUnsafe.get(null);

init();
} catch (Throwable e) {
throw new RuntimeException(e);
}

DUMMY_JAVA_PROJECT = new JavaProjectImpl();
PACKAGE_FRAGMENT_ROOT = new DummyJarPackageFragmentRoot();
}

public static synchronized void init(Path metadataPath) {
if (DUMMY_JAVA_PROJECT != null) return;
try {
initializeWorkspace(metadataPath);
DUMMY_JAVA_PROJECT = new JavaProjectImpl();
PACKAGE_FRAGMENT_ROOT = new DummyJarPackageFragmentRoot();
} catch (Throwable failure) { throw new IllegalStateException("Unable to initialize JDT", failure); }
}

public static PackageFragment createPackageFragment(String name) {
Expand Down Expand Up @@ -99,7 +104,7 @@ public static void setField(Class<?> clazz, Object o, String fieldName, Object v
}
}

private static void init() throws Throwable {
private static void initializeWorkspace(Path metadataPath) throws Throwable {
//Set global instance
new JavaCore();

Expand Down Expand Up @@ -130,7 +135,7 @@ private static void init() throws Throwable {
value.open(true);
field.set(InternalPlatform.getDefault(), value);

IPath metadataLocation = Path.fromOSString(CompanionApp.appPaths().jdtCache().toString());
IPath metadataLocation = org.eclipse.core.runtime.Path.fromOSString(metadataPath.toString());

//cachedInstanceLocation
field = InternalPlatform.class.getDeclaredField("cachedInstanceLocation");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package com.github.minecraft_ta.totalDebugCompanion.mcp;

import com.github.minecraft_ta.totalDebugCompanion.CompanionApp;
import com.github.minecraft_ta.totalDebugCompanion.CompanionApplication;
import com.github.minecraft_ta.totalDebugCompanion.project.ProjectScope;
import com.github.minecraft_ta.totalDebugCompanion.jdt.CompanionClassIndex;
import io.modelcontextprotocol.json.McpJsonDefaults;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpSyncServer;
Expand All @@ -28,7 +27,7 @@
/** Loopback MCP host for Companion code mode. */
public final class CompanionMcpServer implements AutoCloseable {
private static final String MCP_ENDPOINT = "/mcp";
static final int MCP_PORT = 32_123;
public static final int MCP_PORT = 32_123;
private static final int MAX_REQUEST_BYTES = 1_048_576;

private final Path dataDirectory;
Expand All @@ -45,13 +44,11 @@ public final class CompanionMcpServer implements AutoCloseable {
private String endpointUrl;
private boolean closed;

public CompanionMcpServer(Path dataDirectory, CodeModeJobService jobs) {
this(dataDirectory, jobs, MCP_PORT);
}

CompanionMcpServer(Path dataDirectory, CodeModeJobService jobs, int port) {
this(dataDirectory, jobs, port, new DebuggerMcpService(CompanionApp::getDebuggerController,
name -> CompanionApp.getDecompilationService().loadDebugSource(name)), CompanionApp::requireProject);
public CompanionMcpServer(CompanionApplication application, CodeModeJobService jobs, int port) {
this(application.appPaths().home(), jobs, port,
new DebuggerMcpService(application::getDebuggerController,
name -> application.requireProject().requireRuntime().decompiler().loadDebugSource(name)),
application::requireProject);
}

CompanionMcpServer(Path dataDirectory, CodeModeJobService jobs, int port, DebuggerMcpService debugger, Supplier<ProjectScope> project) {
Expand All @@ -60,10 +57,10 @@ public CompanionMcpServer(Path dataDirectory, CodeModeJobService jobs) {
this.endpointDescriptor = new com.github.minecraft_ta.totaldebug.storage.AppPaths(this.dataDirectory).mcpEndpoint();
this.jobs = Objects.requireNonNull(jobs, "jobs");
this.debugger = Objects.requireNonNull(debugger, "debugger");
this.runtimeSource = new CompanionMcpRuntimeSource(CompanionApp::getDecompilationService);
this.runtimeSource = new CompanionMcpRuntimeSource(() -> this.project.get().requireRuntime().decompiler());
this.search = new CompanionMcpSearchService(
CompanionClassIndex::get,
sourceId -> CompanionApp.getRuntimeSourceCatalog().moduleFor(sourceId)
() -> this.project.get().requireRuntime().snapshot().index(),
sourceId -> this.project.get().requireRuntime().sources().moduleFor(sourceId)
);
if (port < 0 || port > 65_535) {
throw new IllegalArgumentException("port is out of range");
Expand Down Expand Up @@ -205,7 +202,7 @@ private Map<String, Object> status() {
return Map.of(
"companion_available", true,
"minecraft_connected", this.jobs.isAvailable(),
"debugger_connected", CompanionApp.isDebuggerConnected()
"debugger_connected", this.debugger.isConnected()
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.github.minecraft_ta.totalDebugCompanion.mcp;

import com.github.minecraft_ta.totalDebugCompanion.CompanionApp;
import com.github.minecraft_ta.totalDebugCompanion.project.ProjectScope;

import com.github.minecraft_ta.totalDebugCompanion.debugger.DebugEngine;
Expand Down Expand Up @@ -28,6 +27,13 @@ final class DebuggerMcpService {
this.sources = Objects.requireNonNull(sources, "sources");
}

boolean isConnected() {
DebuggerSessionController session = controller.get();
if (session == null) return false;
var phase = session.status().phase();
return phase == DebuggerSessionController.Phase.RUNNING || phase == DebuggerSessionController.Phase.PAUSED;
}

Map<String, Object> call(String tool, Map<String, Object> args, ProjectScope project) throws IOException {
DebuggerSessionController session = this.controller.get();
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.github.minecraft_ta.totalDebugCompanion.ui;

import com.github.minecraft_ta.totalDebugCompanion.model.ServiceStatus;
import com.github.minecraft_ta.totalDebugCompanion.navigation.NavigationService;
import com.github.minecraft_ta.totalDebugCompanion.navigation.NavigationTarget;
import com.github.minecraft_ta.totalDebugCompanion.runtime.RuntimeIndexService;

/** The application may run without a window; only these lifecycle operations cross into Swing. */
public interface CompanionUi {
boolean prepareProjectSwitch();
boolean closeProjectViews();
boolean canExit();
void setSwitching(boolean switching);
void refreshProfile();
void runtimeChanged();
void setGameStatus(ServiceStatus status);
void setMcpStatus(ServiceStatus status);
void setRuntimeIndexStatus(RuntimeIndexService.Status status);
void navigate(NavigationTarget target, NavigationService.Activation activation);
void focus();
void showError(String title, String message);
void dispose();
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.github.minecraft_ta.totalDebugCompanion.ui.components.AnimatedFlatSVGIcon;
import com.github.minecraft_ta.totalDebugCompanion.ui.theme.ThemeColors;
import com.github.minecraft_ta.totalDebugCompanion.ui.theme.ThemeManager;
import com.github.minecraft_ta.totalDebugCompanion.ui.theme.CompanionTheme;

import javax.swing.BorderFactory;
import javax.swing.Box;
Expand All @@ -32,6 +33,7 @@
import java.util.function.Consumer;

public final class ApplicationStatusBar extends JPanel {
private final Consumer<CompanionTheme> themeListener = theme -> applyTheme();
private final BreadcrumbBar breadcrumbs;
private final JLabel editorStatusLabel = new JLabel();
private final JLabel taskLabel = new JLabel();
Expand Down Expand Up @@ -118,10 +120,15 @@ public ApplicationStatusBar(Consumer<NavigationTarget> navigator, Runnable retry
add(this.mcpStatus);
add(this.taskCards);
applyTheme();
ThemeManager.addThemeChangeListener(theme -> applyTheme());
ThemeManager.addThemeChangeListener(this.themeListener);
setRuntimeStatus(this.runtimeStatus);
}

public void dispose() {
ThemeManager.removeThemeChangeListener(this.themeListener);
setEditor(null);
}

public void setEditor(IEditorPanel editor) {
this.memberDebounce.stop();
this.removeCaretListener.run();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public static void addThemeChangeListener(Consumer<CompanionTheme> listener) {
LISTENERS.add(listener);
}

static int listenerCount() { return LISTENERS.size(); }

public static void removeThemeChangeListener(Consumer<CompanionTheme> listener) {
LISTENERS.remove(listener);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,15 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Consumer;
import com.github.minecraft_ta.totalDebugCompanion.ui.theme.CompanionTheme;

/** Global, history-backed Java expression evaluator for a running Minecraft session. */
public final class EvaluateExpressionWindow extends JDialog {
private final Consumer<CompanionTheme> themeListener = theme -> {
setIconImages(Icons.createWindowIconImages(theme));
applyTheme();
};
private static final String CLASS_NAME = "CompanionExpression";

private final JavaExpressionField expression = new JavaExpressionField(54);
Expand Down Expand Up @@ -203,10 +209,7 @@ private void configureWindow() {
setMinimumSize(new Dimension(720, 360));
setSize(860, 500);
setIconImages(Icons.createWindowIconImages(ThemeManager.current()));
ThemeManager.addThemeChangeListener(theme -> {
setIconImages(Icons.createWindowIconImages(theme));
applyTheme();
});
ThemeManager.addThemeChangeListener(this.themeListener);
}

private void evaluate() {
Expand Down Expand Up @@ -503,6 +506,7 @@ else if (!this.evaluationRunning && this.status.getText().startsWith("Frame no l
}

@Override public void dispose() {
ThemeManager.removeThemeChangeListener(this.themeListener);
clearDebuggerResults();
editorContext.debugger().removeListener(this.debuggerListener);
super.dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
import com.github.minecraft_ta.totalDebugCompanion.session.CompanionSession;
import java.util.function.Supplier;
import java.util.function.Consumer;
import com.github.minecraft_ta.totalDebugCompanion.CompanionApp;
import com.github.minecraft_ta.totalDebugCompanion.ui.CompanionUi;
import com.github.minecraft_ta.totalDebugCompanion.util.UIUtils;
import com.github.minecraft_ta.totalDebugCompanion.Icons;
import com.github.minecraft_ta.totalDebugCompanion.debugger.DebugEngine;
import com.github.minecraft_ta.totalDebugCompanion.debugger.DebuggerSessionController;
Expand Down Expand Up @@ -42,9 +43,8 @@
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

public class MainWindow extends JFrame implements AWTEventListener {
public class MainWindow extends JFrame implements AWTEventListener, CompanionUi {

public static final MainWindow INSTANCE = CompanionApp.createMainWindow();

private final EditorTabs editorTabs = new EditorTabs();
private final FileTreeView fileTreeView;
Expand Down Expand Up @@ -182,11 +182,18 @@ public void windowClosing(WindowEvent event) {
Toolkit.getDefaultToolkit().removeAWTEventListener(this);
closeProjectWindows();
editorTabs.closeMatching(editor -> true);
statusBar.setEditor(null);
statusBar.dispose();
}
super.dispose();
}

@Override public boolean canExit() { return editorTabs.canCloseAll(); }
@Override public void setSwitching(boolean switching) { setEnabled(!switching); }
@Override public void runtimeChanged() { navigationService.runtimeChanged(); refreshRuntimeSources(); }
@Override public void navigate(NavigationTarget target, NavigationService.Activation activation) { navigation().navigate(target, activation); }
@Override public void focus() { UIUtils.focusWindow(this); }
@Override public void showError(String title, String message) { JOptionPane.showMessageDialog(this, message, title, JOptionPane.ERROR_MESSAGE); }

public EditorContext editorContext() {
return new EditorContext(this, project.get(), insights, debugger, navigation(), scripts, session, this::showDebuggerValue);
}
Expand Down Expand Up @@ -406,21 +413,21 @@ public NavigationService navigation() {
return this.navigationService;
}

public void refreshProfile() {
@Override public void refreshProfile() {
setDebuggerState(debugger.status());
this.navigationService.projectChanged(project.get());
this.fileTreeView.reloadProfile();
refreshActions();
}

public boolean prepareProjectSwitch() {
@Override public boolean prepareProjectSwitch() {
if (!SwingUtilities.isEventDispatchThread()) throw new IllegalStateException("Project views must close on the EDT");
if (!this.editorTabs.canCloseAll()) return false;
setEnabled(false);
return true;
}

public boolean closeProjectViews() {
@Override public boolean closeProjectViews() {
if (!SwingUtilities.isEventDispatchThread()) throw new IllegalStateException("Project views must close on the EDT");
this.editorTabs.closeMatching(editor -> true);
if (this.editorTabs.getTabCount() != 0) return false;
Expand Down Expand Up @@ -460,19 +467,19 @@ private void refreshActions() {
this.debuggerState.setVisible(hasProfile);
}

public void setGameStatus(ServiceStatus status) {
@Override public void setGameStatus(ServiceStatus status) {
this.statusBar.setGameStatus(status);
if (status.state() != ServiceStatus.State.AVAILABLE && this.snippetExecutions != null) {
this.snippetExecutions.runtimeDisconnected();
}
refreshActions();
}

public void setMcpStatus(ServiceStatus status) {
@Override public void setMcpStatus(ServiceStatus status) {
this.statusBar.setMcpStatus(status);
}

public void setRuntimeIndexStatus(RuntimeIndexService.Status status) {
@Override public void setRuntimeIndexStatus(RuntimeIndexService.Status status) {
this.statusBar.setRuntimeStatus(status);
}
}
Loading