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 @@ -55,6 +55,6 @@ The scope publishes one `RuntimeBinding` for the installed inventory. The bindin

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. 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.
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`. Each `EditorTabs` owns an `ASTCache`; parsers, highlighting, symbol lookup, code insight and breakpoint completion receive that cache explicitly. Closing project editors clears their cache and detaches semantic listeners. Identical paths in other editor contexts retain their own models.

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.
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. Equivalent nonblocking Swing dispatch uses `UIUtils.onEdt`; guarded navigation and synchronous lifecycle handoffs retain their distinct boundaries.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import com.github.tth05.scnet.message.AbstractMessage;
import org.eclipse.jdt.core.dom.ASTParser;
import javax.swing.SwingUtilities;
import com.github.minecraft_ta.totalDebugCompanion.util.UIUtils;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
Expand All @@ -54,7 +55,6 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.LinkedHashMap;
import com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics.ASTCache;
import com.github.minecraft_ta.totaldebug.protocol.scnet.ClientHelloMessage;
import java.util.concurrent.CountDownLatch;
import java.util.function.Consumer;
Expand Down Expand Up @@ -109,7 +109,7 @@ public void connecting() {
"Waiting for Minecraft to finish the authenticated connection."
));
}

@Override
public void connected() {
updateGameStatus(new ServiceStatus(
Expand All @@ -118,7 +118,7 @@ public void connected() {
"Minecraft is connected and authenticated."
));
}

@Override
public void disconnected() {
scriptCompiler.runtimeDisconnected();
Expand All @@ -133,17 +133,17 @@ public void disconnected() {
current.runtimeDisconnected();
}
}

@Override
public void runtimeInventory(RuntimeInventoryMessage message) {
handleRuntimeInventory(message);
}

@Override
public void serverManifest(ServerManifestMessage message) {
scriptCompiler.acceptServerManifest(message);
}

@Override
public void debugTarget(DebugTargetMessage message) {
handleDebugTarget(message);
Expand Down Expand Up @@ -186,7 +186,7 @@ public void start() throws IOException {
CompanionUi view = ui;
ui = null;
if (view != null) {
try { onEdtAndWait(view::dispose); }
try { UIUtils.onEdtAndWait(view::dispose); }
catch (InvocationTargetException | InterruptedException failure) {
if (failure instanceof InterruptedException) Thread.currentThread().interrupt();
reportCleanupFailure("Close UI", failure);
Expand Down Expand Up @@ -317,7 +317,6 @@ private void switchProject(CompanionProfile requested) throws IOException {
try { old.close(); }
catch (IOException | RuntimeException failure) { reportCleanupFailure("Close retired project", failure); }
}
ASTCache.clear();
synchronized (lifecycleLock) { current = replacement; }
installed = true;
runCleanup("Restore debugger preferences", () -> restoreProjectState(replacement));
Expand Down Expand Up @@ -587,7 +586,7 @@ private void checkWindowCreation() {
private void refreshUiProfile() {
CompanionUi view = ui;
if (view == null) return;
try { onEdtAndWait(view::refreshProfile); }
try { UIUtils.onEdtAndWait(view::refreshProfile); }
catch (InvocationTargetException failure) { throw new IllegalStateException("Unable to refresh the Companion UI", failure.getCause()); }
catch (InterruptedException failure) { Thread.currentThread().interrupt(); throw new IllegalStateException("Interrupted refreshing the Companion UI", failure); }
}
Expand Down Expand Up @@ -624,11 +623,6 @@ public void exit() {
exitRequested.countDown();
}

private static void onEdtAndWait(Runnable action) throws InvocationTargetException, InterruptedException {
if (SwingUtilities.isEventDispatchThread()) action.run();
else SwingUtilities.invokeAndWait(action);
}

public boolean isConnected() {
return !closed && !switching && session != null && session.isConnected();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,31 @@

public class ASTCache {

private static final Map<String, Entry> CACHE = new HashMap<>();
private static final Map<String, CopyOnWriteArrayList<BiConsumer<CompilationUnit, Integer>>> LISTENERS =
private final Map<String, Entry> cache = new HashMap<>();
private final Map<String, CopyOnWriteArrayList<BiConsumer<CompilationUnit, Integer>>> listeners =
new ConcurrentHashMap<>();

public static CompletableFuture<Void> update(String key, String className, String contents) {
public CompletableFuture<Void> update(String key, String className, String contents) {
return update(key, className, contents, JavaEditorSource.identity(contents));
}

public static CompletableFuture<Void> update(String key, String className, String editorContents, JavaEditorSource source) {
public CompletableFuture<Void> update(String key, String className, String editorContents, JavaEditorSource source) {
Entry selected;
int version;
synchronized (CACHE) {
selected = CACHE.computeIfAbsent(key, ignored -> new Entry());
synchronized (cache) {
selected = cache.computeIfAbsent(key, ignored -> new Entry());
version = ++selected.version;
}

int finalVersion = version;
return CompletableFuture.runAsync(() -> {
synchronized (CACHE) {
if (CACHE.get(key) != selected || selected.version != finalVersion) return;
synchronized (cache) {
if (cache.get(key) != selected || selected.version != finalVersion) return;
}
var ast = JavaAst.parse(className, source.text());
List<BiConsumer<CompilationUnit, Integer>> listeners;
synchronized (CACHE) {
var entry = CACHE.get(key);
synchronized (cache) {
var entry = cache.get(key);
//There's already something newer available
if (entry != selected || entry.version != finalVersion)
return;
Expand All @@ -45,92 +45,92 @@ public static CompletableFuture<Void> update(String key, String className, Strin
entry.contents = editorContents;
entry.sourceMap = source.sourceMap();
entry.privilegedAccess = source.privilegedAccess();
listeners = List.copyOf(LISTENERS.getOrDefault(key, new CopyOnWriteArrayList<>()));
listeners = List.copyOf(this.listeners.getOrDefault(key, new CopyOnWriteArrayList<>()));
}
for (var listener : listeners) {
synchronized (CACHE) { if (CACHE.get(key) != selected) return; }
synchronized (cache) { if (cache.get(key) != selected) return; }
listener.accept(ast, finalVersion);
}
});
}

public static Runnable addChangeListener(String key, BiConsumer<CompilationUnit, Integer> listener) {
public Runnable addChangeListener(String key, BiConsumer<CompilationUnit, Integer> listener) {
Objects.requireNonNull(key, "key");
Objects.requireNonNull(listener, "listener");
var listeners = LISTENERS.computeIfAbsent(key, ignored -> new CopyOnWriteArrayList<>());
var listeners = this.listeners.computeIfAbsent(key, ignored -> new CopyOnWriteArrayList<>());
listeners.add(listener);
Entry existing;
synchronized (CACHE) {
existing = CACHE.get(key);
synchronized (cache) {
existing = cache.get(key);
}
if (existing != null && existing.unit != null)
listener.accept(existing.unit, existing.version);
return () -> {
listeners.remove(listener);
if (listeners.isEmpty()) {
LISTENERS.remove(key, listeners);
this.listeners.remove(key, listeners);
}
};
}

public static void removeFromCache(String key) {
synchronized (CACHE) {
CACHE.remove(key);
public void removeFromCache(String key) {
synchronized (cache) {
cache.remove(key);
}
LISTENERS.remove(key);
this.listeners.remove(key);
}

public static void clear() {
synchronized (CACHE) {
CACHE.clear();
LISTENERS.clear();
public void clear() {
synchronized (cache) {
cache.clear();
this.listeners.clear();
}
}

public static CompilationUnit getFromCache(String key) {
synchronized (CACHE) {
var entry = CACHE.get(key);
public CompilationUnit getFromCache(String key) {
synchronized (cache) {
var entry = cache.get(key);
if (entry == null)
return null;

return entry.unit;
}
}

public static Snapshot getSnapshot(String key) {
synchronized (CACHE) {
Entry entry = CACHE.get(key);
public Snapshot getSnapshot(String key) {
synchronized (cache) {
Entry entry = cache.get(key);
return entry == null || entry.unit == null ? null : new Snapshot(entry.unit, entry.contents, entry.sourceMap);
}
}

public record Snapshot(CompilationUnit unit, String contents, JavaSourceMap sourceMap) {
}

public static String getContents(String key) {
synchronized (CACHE) {
var entry = CACHE.get(key);
public String getContents(String key) {
synchronized (cache) {
var entry = cache.get(key);
return entry == null ? null : entry.contents;
}
}

public static int toGeneratedOffset(String key, int editorOffset) {
synchronized (CACHE) {
Entry entry = CACHE.get(key);
public int toGeneratedOffset(String key, int editorOffset) {
synchronized (cache) {
Entry entry = cache.get(key);
return entry == null ? editorOffset : entry.sourceMap.toGeneratedOffset(editorOffset);
}
}

public static int toEditorOffset(String key, int generatedOffset) {
synchronized (CACHE) {
Entry entry = CACHE.get(key);
public int toEditorOffset(String key, int generatedOffset) {
synchronized (cache) {
Entry entry = cache.get(key);
return entry == null ? generatedOffset : entry.sourceMap.toEditorOffset(generatedOffset);
}
}

public static boolean allowsPrivilegedAccess(String key) {
synchronized (CACHE) {
Entry entry = CACHE.get(key);
public boolean allowsPrivilegedAccess(String key) {
synchronized (cache) {
Entry entry = cache.get(key);
return entry != null && entry.privilegedAccess;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,39 +28,41 @@ interface ElementResolver {
IJavaElement resolve(int offset) throws JavaModelException;
}

private final ASTCache cache;
private final ElementResolver elementResolver;
private final IntFunction<String> ownerClassResolver;
private final BiConsumer<String, String> packageNavigator;
private final Consumer<NavigationTarget> navigator;
private final Path sourcePath;

public CustomJavaLinkGenerator(String identifier, BiConsumer<String, String> packageNavigator, Consumer<NavigationTarget> navigator) {
public CustomJavaLinkGenerator(ASTCache cache, String identifier, BiConsumer<String, String> packageNavigator, Consumer<NavigationTarget> navigator) {
this(
offset -> JavaSymbolResolver.selectElement(identifier, offset),
offset -> JavaSymbolResolver.navigationOwnerClass(identifier, offset),
cache, offset -> JavaSymbolResolver.selectElement(cache, identifier, offset),
offset -> JavaSymbolResolver.navigationOwnerClass(cache, identifier, offset),
packageNavigator,
Path.of(identifier),
navigator
);
}

CustomJavaLinkGenerator(
ElementResolver elementResolver,
ASTCache cache, ElementResolver elementResolver,
IntFunction<String> ownerClassResolver,
BiConsumer<String, String> packageNavigator,
Path sourcePath
) {
this(elementResolver, ownerClassResolver, packageNavigator, sourcePath, ignored -> {
this(cache, elementResolver, ownerClassResolver, packageNavigator, sourcePath, ignored -> {
});
}

CustomJavaLinkGenerator(
ElementResolver elementResolver,
ASTCache cache, ElementResolver elementResolver,
IntFunction<String> ownerClassResolver,
BiConsumer<String, String> packageNavigator,
Path sourcePath,
Consumer<NavigationTarget> navigator
) {
this.cache = cache;
this.elementResolver = Objects.requireNonNull(elementResolver, "elementResolver");
this.ownerClassResolver = Objects.requireNonNull(ownerClassResolver, "ownerClassResolver");
this.packageNavigator = Objects.requireNonNull(packageNavigator, "packageNavigator");
Expand Down Expand Up @@ -120,7 +122,7 @@ public HyperlinkEvent execute() {
return null;
}

int editorOffset = ASTCache.toEditorOffset(sourcePath.toString(), sourceRange.getOffset());
int editorOffset = cache.toEditorOffset(sourcePath.toString(), sourceRange.getOffset());
if (editorOffset < 0) {
return null;
}
Expand Down Expand Up @@ -163,6 +165,7 @@ public int getSourceOffset() {

private void navigateResolvedSymbol() throws JavaModelException {
JavaSymbolResolver.Resolution resolution = JavaSymbolResolver.resolve(
cache,
sourcePath.toString(),
this.navigationOffset
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@

public class CustomJavaParser extends AbstractParser {

private final ASTCache cache;
private final String astKey;

public CustomJavaParser(String astKey) {
public CustomJavaParser(ASTCache cache, String astKey) {
this.cache = cache;
this.astKey = astKey;
}

Expand All @@ -21,16 +23,16 @@ public ParseResult parse(RSyntaxDocument doc, String style) {
if (!SyntaxConstants.SYNTAX_STYLE_JAVA.equals(style))
return result;

var ast = ASTCache.getFromCache(this.astKey);
var ast = cache.getFromCache(this.astKey);
if (ast == null)
return result;

for (IProblem problem : ast.getProblems()) {
if (ASTCache.allowsPrivilegedAccess(this.astKey) && isAccessProblem(problem.getID())) {
if (cache.allowsPrivilegedAccess(this.astKey) && isAccessProblem(problem.getID())) {
continue;
}
int start = ASTCache.toEditorOffset(this.astKey, problem.getSourceStart());
int end = ASTCache.toEditorOffset(this.astKey, problem.getSourceEnd());
int start = cache.toEditorOffset(this.astKey, problem.getSourceStart());
int end = cache.toEditorOffset(this.astKey, problem.getSourceEnd());
if (start < 0 || end < start) {
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ public class CustomJavaTokenMaker extends JavaTokenMaker {
private RSyntaxDocument document;
private Map<Integer, SemanticToken> overwrittenTokenTypes;

public void setASTKey(String identifier, RSyntaxTextArea textArea) {
public Runnable setASTKey(ASTCache cache, String identifier, RSyntaxTextArea textArea) {
trackDocument(textArea);
ASTCache.addChangeListener(identifier, (ast, version) -> {
var snapshot = ASTCache.getSnapshot(identifier);
return cache.addChangeListener(identifier, (ast, version) -> {
var snapshot = cache.getSnapshot(identifier);
if (snapshot == null || snapshot.unit() != ast) {
return;
}
Expand All @@ -45,7 +45,7 @@ public void setASTKey(String identifier, RSyntaxTextArea textArea) {
}
});
SwingUtilities.invokeLater(() -> {
if (ASTCache.getFromCache(identifier) == ast && snapshot.contents().equals(textArea.getText())) {
if (cache.getFromCache(identifier) == ast && snapshot.contents().equals(textArea.getText())) {
setSemanticTokenTypes(editorTokenTypes, textArea);
}
});
Expand Down
Loading