diff --git a/companion/README.md b/companion/README.md index 51af9a2f..4801ecbc 100644 --- a/companion/README.md +++ b/companion/README.md @@ -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. diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/CompanionApplication.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/CompanionApplication.java index cccc8c8a..74a85781 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/CompanionApplication.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/CompanionApplication.java @@ -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; @@ -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; @@ -109,7 +109,7 @@ public void connecting() { "Waiting for Minecraft to finish the authenticated connection." )); } - + @Override public void connected() { updateGameStatus(new ServiceStatus( @@ -118,7 +118,7 @@ public void connected() { "Minecraft is connected and authenticated." )); } - + @Override public void disconnected() { scriptCompiler.runtimeDisconnected(); @@ -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); @@ -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); @@ -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)); @@ -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); } } @@ -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(); } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/ASTCache.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/ASTCache.java index 23d2b0f7..67b95cd8 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/ASTCache.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/ASTCache.java @@ -11,31 +11,31 @@ public class ASTCache { - private static final Map CACHE = new HashMap<>(); - private static final Map>> LISTENERS = + private final Map cache = new HashMap<>(); + private final Map>> listeners = new ConcurrentHashMap<>(); - public static CompletableFuture update(String key, String className, String contents) { + public CompletableFuture update(String key, String className, String contents) { return update(key, className, contents, JavaEditorSource.identity(contents)); } - public static CompletableFuture update(String key, String className, String editorContents, JavaEditorSource source) { + public CompletableFuture 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> 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; @@ -45,51 +45,51 @@ public static CompletableFuture 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 listener) { + public Runnable addChangeListener(String key, BiConsumer 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; @@ -97,9 +97,9 @@ public static CompilationUnit getFromCache(String key) { } } - 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); } } @@ -107,30 +107,30 @@ public static Snapshot getSnapshot(String key) { 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; } } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/CustomJavaLinkGenerator.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/CustomJavaLinkGenerator.java index 4a62e2dc..a708bb3d 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/CustomJavaLinkGenerator.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/CustomJavaLinkGenerator.java @@ -28,16 +28,17 @@ interface ElementResolver { IJavaElement resolve(int offset) throws JavaModelException; } + private final ASTCache cache; private final ElementResolver elementResolver; private final IntFunction ownerClassResolver; private final BiConsumer packageNavigator; private final Consumer navigator; private final Path sourcePath; - public CustomJavaLinkGenerator(String identifier, BiConsumer packageNavigator, Consumer navigator) { + public CustomJavaLinkGenerator(ASTCache cache, String identifier, BiConsumer packageNavigator, Consumer 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 @@ -45,22 +46,23 @@ public CustomJavaLinkGenerator(String identifier, BiConsumer pac } CustomJavaLinkGenerator( - ElementResolver elementResolver, + ASTCache cache, ElementResolver elementResolver, IntFunction ownerClassResolver, BiConsumer packageNavigator, Path sourcePath ) { - this(elementResolver, ownerClassResolver, packageNavigator, sourcePath, ignored -> { + this(cache, elementResolver, ownerClassResolver, packageNavigator, sourcePath, ignored -> { }); } CustomJavaLinkGenerator( - ElementResolver elementResolver, + ASTCache cache, ElementResolver elementResolver, IntFunction ownerClassResolver, BiConsumer packageNavigator, Path sourcePath, Consumer navigator ) { + this.cache = cache; this.elementResolver = Objects.requireNonNull(elementResolver, "elementResolver"); this.ownerClassResolver = Objects.requireNonNull(ownerClassResolver, "ownerClassResolver"); this.packageNavigator = Objects.requireNonNull(packageNavigator, "packageNavigator"); @@ -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; } @@ -163,6 +165,7 @@ public int getSourceOffset() { private void navigateResolvedSymbol() throws JavaModelException { JavaSymbolResolver.Resolution resolution = JavaSymbolResolver.resolve( + cache, sourcePath.toString(), this.navigationOffset ); diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/CustomJavaParser.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/CustomJavaParser.java index f3bb31b7..0e37512b 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/CustomJavaParser.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/CustomJavaParser.java @@ -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; } @@ -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; } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/semanticHighlighting/CustomJavaTokenMaker.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/semanticHighlighting/CustomJavaTokenMaker.java index 4b31161c..5352fdef 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/semanticHighlighting/CustomJavaTokenMaker.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/semanticHighlighting/CustomJavaTokenMaker.java @@ -26,10 +26,10 @@ public class CustomJavaTokenMaker extends JavaTokenMaker { private RSyntaxDocument document; private Map 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; } @@ -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); } }); diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/symbol/JavaSymbolResolver.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/symbol/JavaSymbolResolver.java index cc439de7..56ad0e9b 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/symbol/JavaSymbolResolver.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/symbol/JavaSymbolResolver.java @@ -37,13 +37,13 @@ public final class JavaSymbolResolver { private JavaSymbolResolver() { } - public static IJavaElement selectElement(String editorIdentifier, int offset) throws JavaModelException { - CompilationUnit unit = ASTCache.getFromCache(editorIdentifier); + public static IJavaElement selectElement(ASTCache cache, String editorIdentifier, int offset) throws JavaModelException { + CompilationUnit unit = cache.getFromCache(editorIdentifier); if (unit == null) { return null; } - int generatedOffset = ASTCache.toGeneratedOffset(editorIdentifier, offset); + int generatedOffset = cache.toGeneratedOffset(editorIdentifier, offset); if (generatedOffset < 0) { return null; } @@ -58,25 +58,25 @@ public static IJavaElement selectElement(String editorIdentifier, int offset) th return elements[0]; } - public static Resolution resolve(String editorIdentifier, int offset) throws JavaModelException { - CompilationUnit unit = ASTCache.getFromCache(editorIdentifier); + public static Resolution resolve(ASTCache cache, String editorIdentifier, int offset) throws JavaModelException { + CompilationUnit unit = cache.getFromCache(editorIdentifier); if (unit == null) { return Resolution.unavailable("Java model is still loading"); } - int generatedOffset = ASTCache.toGeneratedOffset(editorIdentifier, offset); + int generatedOffset = cache.toGeneratedOffset(editorIdentifier, offset); return generatedOffset < 0 ? Resolution.unavailable("The selected text is not part of the generated Java source") : resolve(unit, generatedOffset); } /** Resolves the concrete class that owns a qualified package segment at an editor offset. */ - public static String navigationOwnerClass(String editorIdentifier, int offset) { - CompilationUnit unit = ASTCache.getFromCache(editorIdentifier); + public static String navigationOwnerClass(ASTCache cache, String editorIdentifier, int offset) { + CompilationUnit unit = cache.getFromCache(editorIdentifier); if (unit == null) { return null; } - int generatedOffset = ASTCache.toGeneratedOffset(editorIdentifier, offset); + int generatedOffset = cache.toGeneratedOffset(editorIdentifier, offset); if (generatedOffset < 0) { return null; } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/JavaEditorContext.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/JavaEditorContext.java index f6a0575d..900062f6 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/JavaEditorContext.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/JavaEditorContext.java @@ -1,9 +1,12 @@ package com.github.minecraft_ta.totalDebugCompanion.model; +import com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics.ASTCache; import java.util.function.IntConsumer; /** Read-only access to the Java model state owned by an editor. */ public interface JavaEditorContext { + ASTCache astCache(); + String astKey(); int caretOffset(); diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/navigation/NavigationService.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/navigation/NavigationService.java index 37576ed6..61ccb7e8 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/navigation/NavigationService.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/navigation/NavigationService.java @@ -200,18 +200,18 @@ private CompletableFuture performNavigation(NavigationTarget target, Activ -1, activation ); - case NavigationTarget.SymbolUsages usages -> onEdt(() -> openRuntimeEditor(requestedRuntime, + case NavigationTarget.SymbolUsages usages -> dispatchNavigation(() -> openRuntimeEditor(requestedRuntime, UsagesView.class, view -> view.symbol().equals(usages.symbol()), () -> new UsagesView(editors.get(), usages.symbol(), requestedRuntime) ).thenAccept(UsagesView::restartSearch), activation); - case NavigationTarget.LiteralUsages usages -> onEdt(() -> openRuntimeEditor(requestedRuntime, + case NavigationTarget.LiteralUsages usages -> dispatchNavigation(() -> openRuntimeEditor(requestedRuntime, LiteralUsagesView.class, view -> view.literal().equals(usages.literal()), () -> new LiteralUsagesView(editors.get(), usages.literal(), requestedRuntime) ).thenAccept(LiteralUsagesView::restartSearch), activation); case NavigationTarget.RuntimePackage runtimePackage -> revealPackage(runtimePackage); - case NavigationTarget.ModuleSearch search -> onEdt(() -> { + case NavigationTarget.ModuleSearch search -> dispatchNavigation(() -> { this.window.openSearchEverywhere(search); return CompletableFuture.completedFuture(null); }, Activation.KEEP_CURRENT_WINDOW); @@ -306,7 +306,7 @@ private CompletableFuture captureDestination(NavigationTarget t } private CompletableFuture restoreSelectedEntry(NavigationEntry entry, Context context) { - return onEdt(() -> { + return dispatchNavigation(() -> { if (!isCurrent(context)) return CompletableFuture.failedFuture(new CancellationException("Project changed")); IEditorPanel editor = this.tabs.getSelectedEditor(); if (!isEditorDestination(entry.target())) { @@ -406,7 +406,7 @@ private CompletableFuture openRuntimeSource( var service = installed.decompiler(); return service.load(binaryName).thenCompose(source -> { int offset = offsetResolver.applyAsInt(source); - return onEdt(() -> { + return dispatchNavigation(() -> { if (!isCurrent(context) || service != requireProject().requireRuntime().decompiler()) { return CompletableFuture.failedFuture(new CancellationException("Runtime changed during source navigation")); } @@ -434,14 +434,14 @@ private CompletableFuture openLocalFile(NavigationTarget.LocalFile target, if (path.getParent().equals(scripts) && fileName.endsWith(ScriptView.FILE_EXTENSION)) { String scriptName = fileName.substring(0, fileName.length() - ScriptView.FILE_EXTENSION.length()); - return onEdt(() -> this.tabs.focusOrCreateIfAbsent( + return dispatchNavigation(() -> this.tabs.focusOrCreateIfAbsent( ScriptView.class, view -> view.getTitle().equals(fileName), () -> new ScriptView(editors.get(), scriptName) ).thenAccept(view -> view.navigateToOffset(target.offset())), activation); } if (fileName.endsWith(".java")) { - return onEdt(() -> this.tabs.focusOrCreateIfAbsent( + return dispatchNavigation(() -> this.tabs.focusOrCreateIfAbsent( CodeView.class, view -> view.getPath().equals(path), () -> new CodeView(editors.get(), path, target.offset()) @@ -452,7 +452,7 @@ private CompletableFuture openLocalFile(NavigationTarget.LocalFile target, private CompletableFuture openResource(ContentSource source, Activation activation) { RuntimeBinding installed = source instanceof ArchiveEntrySource ? captureContext().runtime() : null; - return onEdt(() -> openRuntimeEditor(installed, + return dispatchNavigation(() -> openRuntimeEditor(installed, ResourceView.class, view -> view.source().identity().equals(source.identity()), () -> new ResourceView(editors.get(), source, installed) @@ -528,7 +528,7 @@ private CompletableFuture revealArchiveDirectory(NavigationTarget.ArchiveD ); } - private CompletableFuture onEdt( + private CompletableFuture dispatchNavigation( Supplier> operation, Activation activation ) { diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/search/reference/ReferenceSearchService.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/search/reference/ReferenceSearchService.java index 58d08451..b4954867 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/search/reference/ReferenceSearchService.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/search/reference/ReferenceSearchService.java @@ -1,12 +1,12 @@ package com.github.minecraft_ta.totalDebugCompanion.search.reference; +import com.github.minecraft_ta.totalDebugCompanion.util.UIUtils; import com.github.minecraft_ta.totalDebugCompanion.bytecode.reference.IndexedReferenceSearch; import com.github.minecraft_ta.totalDebugCompanion.bytecode.reference.ReferenceUsagePage; import com.github.minecraft_ta.totalDebugCompanion.bytecode.reference.ReferenceQuery; import com.github.minecraft_ta.totalDebugCompanion.runtime.RuntimeSourceCatalog; import com.github.tth05.jindex.ClassIndex; -import javax.swing.SwingUtilities; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -108,7 +108,7 @@ private void run() { try { ReferenceUsagePage result = searcher.search(this.query, this.limit); if (!this.cancelled.get()) { - dispatch(() -> { + UIUtils.onEdt(() -> { if (!this.cancelled.get()) { this.listener.onCompleted(result); } @@ -116,7 +116,7 @@ private void run() { } } catch (RuntimeException failure) { if (!this.cancelled.get() && !Thread.currentThread().isInterrupted()) { - dispatch(() -> { + UIUtils.onEdt(() -> { if (!this.cancelled.get()) { this.listener.onFailed(failure); } @@ -139,14 +139,6 @@ public boolean isDone() { } } - private static void dispatch(Runnable callback) { - if (SwingUtilities.isEventDispatchThread()) { - callback.run(); - } else { - SwingUtilities.invokeLater(callback); - } - } - private static ExecutorService newExecutor() { return Executors.newSingleThreadExecutor(runnable -> Thread.ofPlatform() .daemon(true) diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/EditorContext.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/EditorContext.java index 29cde2b5..929fe85f 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/EditorContext.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/EditorContext.java @@ -1,5 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.ui; +import com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics.ASTCache; import com.github.minecraft_ta.totalDebugCompanion.debugger.DebugEngine; import com.github.minecraft_ta.totalDebugCompanion.debugger.DebuggerSessionController; import com.github.minecraft_ta.totalDebugCompanion.navigation.NavigationService; @@ -11,7 +12,7 @@ import java.util.function.BiConsumer; /** The collaborators shared by Java editors in one project. */ -public record EditorContext(Window owner, ProjectScope project, CodeInsightService insights, +public record EditorContext(ASTCache astCache, Window owner, ProjectScope project, CodeInsightService insights, DebuggerSessionController debugger, NavigationService navigation, ScriptExecutionService scripts, CompanionSession session, BiConsumer inspectVariable) { } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/AbstractCodeViewPanel.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/AbstractCodeViewPanel.java index 0bbcccc1..a61993c6 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/AbstractCodeViewPanel.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/AbstractCodeViewPanel.java @@ -25,6 +25,7 @@ public class AbstractCodeViewPanel extends AbstractTextViewPanel implements Java protected final EditorContext context; protected final String identifier; private boolean astDisposed; + private final Runnable unsubscribeSemantic; public AbstractCodeViewPanel(EditorContext context, String identifier, String className) { this(context, identifier, className, JavaEditorSource::identity); @@ -40,13 +41,13 @@ protected AbstractCodeViewPanel( installNavigationHistoryMenu(context.navigation()); this.identifier = identifier; - this.editorPane.setLinkGenerator(new CustomJavaLinkGenerator(identifier, context.navigation()::revealPackage, target -> context.navigation().navigate(target))); + this.editorPane.setLinkGenerator(new CustomJavaLinkGenerator(context.astCache(), identifier, context.navigation()::revealPackage, target -> context.navigation().navigate(target))); this.editorPane.getDocument().addDocumentListener((DocumentChangeListener) event -> { - if (event.getType() == DocumentEvent.EventType.CHANGE) { + if (astDisposed || event.getType() == DocumentEvent.EventType.CHANGE) { return; } String editorText = UIUtils.getText(this.editorPane); - ASTCache.update(identifier, className, editorText, sourceFactory.apply(editorText)); + context.astCache().update(identifier, className, editorText, sourceFactory.apply(editorText)); }); setSyntaxStyle(RSyntaxTextArea.SYNTAX_STYLE_JAVA); @@ -54,7 +55,7 @@ protected AbstractCodeViewPanel( var document = this.editorPane.getDocument(); var field = document.getClass().getDeclaredField("tokenMaker"); field.setAccessible(true); - ((CustomJavaTokenMaker) field.get(document)).setASTKey(identifier, this.editorPane); + this.unsubscribeSemantic = ((CustomJavaTokenMaker) field.get(document)).setASTKey(context.astCache(), identifier, this.editorPane); } catch (Throwable throwable) { throw new RuntimeException(throwable); } @@ -65,6 +66,8 @@ protected void applyAdditionalSyntaxColors(SyntaxScheme scheme, EditorPalette pa CodeUtils.initJavaSemanticColors(scheme, palette); } + @Override public ASTCache astCache() { return context.astCache(); } + @Override public String astKey() { return this.identifier; @@ -87,7 +90,8 @@ public Runnable addCaretOffsetListener(IntConsumer listener) { public void dispose() { if (!this.astDisposed) { this.astDisposed = true; - ASTCache.removeFromCache(this.identifier); + this.unsubscribeSemantic.run(); + context.astCache().removeFromCache(this.identifier); } super.dispose(); } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/CodeViewPanel.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/CodeViewPanel.java index 5ac152a0..6dca5342 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/CodeViewPanel.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/CodeViewPanel.java @@ -9,7 +9,6 @@ import com.github.minecraft_ta.totalDebugCompanion.debugger.DebugEngine; import com.github.minecraft_ta.totalDebugCompanion.debugger.DebuggerSessionController; import com.github.minecraft_ta.totalDebugCompanion.debugger.DebuggerBreakpointResolver; -import com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics.ASTCache; import com.github.minecraft_ta.totalDebugCompanion.jdt.insight.SourceDeclaration; import com.github.minecraft_ta.totalDebugCompanion.jdt.insight.ExpressionScopeAnalyzer; import com.github.minecraft_ta.totalDebugCompanion.jdt.symbol.CodeSymbol; @@ -172,6 +171,7 @@ public void hidePreview() { } ); this.codeVisionController = new CodeVisionController( + context.astCache(), this.identifier, this.insightService, this.codeVisionLayerUI, @@ -245,7 +245,7 @@ public void breakpointsMutedChanged(boolean muted) { this.removeDebuggerPresentationListener = () -> { }; } else { - this.removeInlineAstListener = ASTCache.addChangeListener(this.identifier, (unit, version) -> + this.removeInlineAstListener = context.astCache().addChangeListener(this.identifier, (unit, version) -> SwingUtilities.invokeLater(this::updateInlineValueHints) ); this.removeDebuggerPresentationListener = DebuggerEditorPresentation.addListener(snapshot -> @@ -298,8 +298,8 @@ private void updateInlineValueHints() { return; } this.codeVisionLayerUI.setInlineValues(DebuggerInlineValueHints.create( - ASTCache.getFromCache(this.identifier), - ASTCache.getContents(this.identifier), + context.astCache().getFromCache(this.identifier), + context.astCache().getContents(this.identifier), snapshot ), this.codeVisionLayer); } @@ -511,8 +511,8 @@ public void remove(int line) { } private ExpressionCompletionSupport.CompletionProvider completionProviderAtLine(int displayedLine) { - String source = ASTCache.getContents(this.identifier); - var unit = ASTCache.getFromCache(this.identifier); + String source = context.astCache().getContents(this.identifier); + var unit = context.astCache().getFromCache(this.identifier); if (source == null || unit == null) { return (text, caret, explicit) -> java.util.concurrent.CompletableFuture.completedFuture(List.of()); } @@ -552,7 +552,7 @@ private void configureBreakpoint( } private Optional breakpointRequestAtLine(int displayedLine) { - var unit = ASTCache.getFromCache(this.identifier); + var unit = context.astCache().getFromCache(this.identifier); if (unit == null) { throw new IllegalStateException("Source analysis is still loading"); } @@ -659,7 +659,7 @@ private void navigateHierarchy( private void resolveSelectedSymbol(String action, java.util.function.Consumer consumer) { try { - var resolution = JavaSymbolResolver.resolve(this.identifier, this.editorPane.getCaretPosition()); + var resolution = JavaSymbolResolver.resolve(context.astCache(), this.identifier, this.editorPane.getCaretPosition()); if (!resolution.isResolved()) { this.bottomInformationBar.setDefaultInfoText(resolution.unavailableReason()); return; diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/CodeVisionController.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/CodeVisionController.java index 5a95f184..09e053f4 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/CodeVisionController.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/CodeVisionController.java @@ -28,7 +28,7 @@ final class CodeVisionController implements AutoCloseable { private boolean closed; CodeVisionController( - String editorIdentifier, + ASTCache cache, String editorIdentifier, CodeInsightService service, CodeVisionLayerUI layerUI, JLayer layer, @@ -39,8 +39,8 @@ final class CodeVisionController implements AutoCloseable { this.layerUI = Objects.requireNonNull(layerUI, "layerUI"); this.layer = Objects.requireNonNull(layer, "layer"); this.gutterMarkers = Objects.requireNonNull(gutterMarkers, "gutterMarkers"); - this.unsubscribeAst = ASTCache.addChangeListener(this.editorIdentifier, (unit, version) -> { - String source = ASTCache.getContents(this.editorIdentifier); + this.unsubscribeAst = cache.addChangeListener(this.editorIdentifier, (unit, version) -> { + String source = cache.getContents(this.editorIdentifier); if (source != null) { analyze(SourceDeclarationAnalyzer.analyze(unit, source)); } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/ScriptPanel.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/ScriptPanel.java index 7a357a0a..fb15a7da 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/ScriptPanel.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/ScriptPanel.java @@ -139,7 +139,7 @@ public ScriptPanel(EditorContext context, ScriptView scriptView) { setHeaderComponent(headerBar); this.editorPane.setParserDelay(400); - this.editorPane.addParser(new CustomJavaParser(scriptView.getPath().toString())); + this.editorPane.addParser(new CustomJavaParser(context.astCache(), scriptView.getPath().toString())); this.editorPane.setText(scriptView.getSourceText()); this.editorPane.getActionMap().put(DefaultEditorKit.deletePrevCharAction, new CustomDeletePrevCharAction()); diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/global/ApplicationStatusBar.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/global/ApplicationStatusBar.java index f73cf138..c6ae70f1 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/global/ApplicationStatusBar.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/global/ApplicationStatusBar.java @@ -1,7 +1,8 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.components.global; +import java.awt.CardLayout; +import com.github.minecraft_ta.totalDebugCompanion.util.UIUtils; import com.github.minecraft_ta.totalDebugCompanion.Icons; -import com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics.ASTCache; import com.github.minecraft_ta.totalDebugCompanion.model.EditorLocation; import com.github.minecraft_ta.totalDebugCompanion.model.IEditorPanel; import com.github.minecraft_ta.totalDebugCompanion.model.JavaEditorContext; @@ -25,7 +26,6 @@ import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JProgressBar; -import javax.swing.SwingUtilities; import javax.swing.Timer; import java.awt.Component; import java.awt.Dimension; @@ -38,7 +38,7 @@ public final class ApplicationStatusBar extends JPanel { private final JLabel editorStatusLabel = new JLabel(); private final JLabel taskLabel = new JLabel(); private final JButton taskState = new JButton(); - private final JPanel taskCards = new JPanel(new java.awt.CardLayout()); + private final JPanel taskCards = new JPanel(new CardLayout()); private final ServiceStatusWidget gameStatus = new ServiceStatusWidget( "Game", new ServiceStatus( @@ -147,7 +147,7 @@ public void setEditor(IEditorPanel editor) { if (this.selectedJavaContext != null && this.selectedTarget != null) { JavaEditorContext context = this.selectedJavaContext; this.removeCaretListener = context.addCaretOffsetListener(ignored -> requestMemberRefresh()); - this.removeAstListener = ASTCache.addChangeListener( + this.removeAstListener = context.astCache().addChangeListener( context.astKey(), (unit, version) -> requestMemberRefresh() ); @@ -161,11 +161,7 @@ public void setEditor(IEditorPanel editor) { } private void requestMemberRefresh() { - if (!SwingUtilities.isEventDispatchThread()) { - SwingUtilities.invokeLater(this::requestMemberRefresh); - return; - } - this.memberDebounce.restart(); + UIUtils.onEdt(this.memberDebounce::restart); } private void refreshMember() { @@ -173,7 +169,7 @@ private void refreshMember() { NavigationTarget target = this.selectedTarget; JavaBreadcrumbResolver.Member member = null; if (context != null && target != null) { - var unit = ASTCache.getFromCache(context.astKey()); + var unit = context.astCache().getFromCache(context.astKey()); if (unit != null) { member = JavaBreadcrumbResolver.resolve(unit, context.caretOffset(), target); } @@ -193,26 +189,25 @@ private void renderBreadcrumbs() { } public void setRuntimeStatus(RuntimeIndexService.Status status) { - if (!SwingUtilities.isEventDispatchThread()) { - SwingUtilities.invokeLater(() -> setRuntimeStatus(status)); - return; - } - this.runtimeStatus = status; - java.awt.CardLayout cards = (java.awt.CardLayout) this.taskCards.getLayout(); - if (status.active()) { - this.taskLabel.setText(status.detail()); - this.taskLabel.setToolTipText(status.detail()); - cards.show(this.taskCards, "progress"); - return; - } - this.taskState.setIcon(switch (status.phase()) { - case READY -> Icons.SUCCESS; - case FAILED -> Icons.ERROR; - default -> Icons.INFORMATION; + UIUtils.onEdt(() -> { + this.runtimeStatus = status; + CardLayout cards = (CardLayout) this.taskCards.getLayout(); + if (status.active()) { + this.taskLabel.setText(status.detail()); + this.taskLabel.setToolTipText(status.detail()); + cards.show(this.taskCards, "progress"); + return; + } + this.taskState.setIcon(switch (status.phase()) { + case READY -> Icons.SUCCESS; + case FAILED -> Icons.ERROR; + default -> Icons.INFORMATION; + }); + this.taskState.setText(status.detail()); + this.taskState.setToolTipText("Show background activity"); + cards.show(this.taskCards, "state"); + }); - this.taskState.setText(status.detail()); - this.taskState.setToolTipText("Show background activity"); - cards.show(this.taskCards, "state"); } public void setGameStatus(ServiceStatus status) { @@ -224,23 +219,22 @@ public void setMcpStatus(ServiceStatus status) { } private void setEditorStatus(BottomInformationBar.State state) { - if (!SwingUtilities.isEventDispatchThread()) { - SwingUtilities.invokeLater(() -> setEditorStatus(state)); - return; - } - this.editorStatusLabel.setText(state.text()); - this.editorStatusLabel.setForeground( - state.style() == BottomInformationBar.Style.PLAIN ? ThemeColors.mutedText() : getForeground() - ); - if (state.style() != BottomInformationBar.Style.PROCESS) { - this.processIcon.stop(); - } - this.editorStatusLabel.setIcon(switch (state.style()) { - case INFORMATION -> Icons.INFORMATION; - case PROCESS -> this.processIcon; - case SUCCESS -> Icons.SUCCESS; - case FAILURE -> Icons.ERROR; - case PLAIN -> null; + UIUtils.onEdt(() -> { + this.editorStatusLabel.setText(state.text()); + this.editorStatusLabel.setForeground( + state.style() == BottomInformationBar.Style.PLAIN ? ThemeColors.mutedText() : getForeground() + ); + if (state.style() != BottomInformationBar.Style.PROCESS) { + this.processIcon.stop(); + } + this.editorStatusLabel.setIcon(switch (state.style()) { + case INFORMATION -> Icons.INFORMATION; + case PROCESS -> this.processIcon; + case SUCCESS -> Icons.SUCCESS; + case FAILURE -> Icons.ERROR; + case PLAIN -> null; + }); + }); } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/global/EditorTabs.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/global/EditorTabs.java index b16e7be7..6649e97b 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/global/EditorTabs.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/global/EditorTabs.java @@ -1,5 +1,7 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.components.global; +import com.github.minecraft_ta.totalDebugCompanion.util.UIUtils; +import com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics.ASTCache; import com.github.minecraft_ta.totalDebugCompanion.model.IEditorPanel; import com.github.minecraft_ta.totalDebugCompanion.ui.speedsearch.SpeedSearch; @@ -16,6 +18,10 @@ public class EditorTabs extends JTabbedPane { + private final ASTCache astCache = new ASTCache(); + + public ASTCache astCache() { return astCache; } + private final List editors = new ArrayList<>(); private final List> selectedEditorListeners = new ArrayList<>(); @@ -108,11 +114,7 @@ public CompletableFuture openEditorTab(IEditorPanel editorPanel) { future.complete(null); }; - if (SwingUtilities.isEventDispatchThread()) { - open.run(); - } else { - SwingUtilities.invokeLater(open); - } + UIUtils.onEdt(open); return future; } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/global/ServiceStatusWidget.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/global/ServiceStatusWidget.java index 0c77fc86..1360c1a1 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/global/ServiceStatusWidget.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/global/ServiceStatusWidget.java @@ -1,5 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.components.global; +import com.github.minecraft_ta.totalDebugCompanion.util.UIUtils; import com.github.minecraft_ta.totalDebugCompanion.model.ServiceStatus; import com.github.minecraft_ta.totalDebugCompanion.ui.theme.ThemeColors; @@ -8,7 +9,6 @@ import javax.swing.JButton; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; -import javax.swing.SwingUtilities; import java.awt.Color; import java.awt.Component; import java.awt.Graphics; @@ -38,11 +38,7 @@ final class ServiceStatusWidget extends JButton { } void setStatus(ServiceStatus status) { - if (!SwingUtilities.isEventDispatchThread()) { - SwingUtilities.invokeLater(() -> setStatus(status)); - return; - } - applyStatus(status); + UIUtils.onEdt(() -> applyStatus(status)); } private void applyStatus(ServiceStatus status) { diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/treeView/lazyFileTree/LazyFileJTree.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/treeView/lazyFileTree/LazyFileJTree.java index 25bfad7d..316e68bc 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/treeView/lazyFileTree/LazyFileJTree.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/treeView/lazyFileTree/LazyFileJTree.java @@ -1,5 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.components.treeView.lazyFileTree; +import com.github.minecraft_ta.totalDebugCompanion.util.UIUtils; import com.github.minecraft_ta.totalDebugCompanion.ui.presentation.PrimarySecondaryLabel; import com.github.minecraft_ta.totalDebugCompanion.ui.speedsearch.SpeedSearch; @@ -165,15 +166,12 @@ public DefaultTreeModel getModel() { } public void loadItemsForTopLevelItem(TreeItem item) { - if (!SwingUtilities.isEventDispatchThread()) { - SwingUtilities.invokeLater(() -> loadItemsForTopLevelItem(item)); - return; - } - var node = findTopLevelNodeForItem(item); - if (node == null) - return; - node.markChildrenStale(); - loadItemsForNode(node); + UIUtils.onEdt(() -> { + var node = findTopLevelNodeForItem(item); + if (node == null) return; + node.markChildrenStale(); + loadItemsForNode(node); + }); } private CompletableFuture loadItemsForNode(LazyTreeNode node) { diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/speedsearch/SpeedSearch.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/speedsearch/SpeedSearch.java index dec76193..02919536 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/speedsearch/SpeedSearch.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/speedsearch/SpeedSearch.java @@ -1,11 +1,11 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.speedsearch; +import com.github.minecraft_ta.totalDebugCompanion.util.UIUtils; import javax.swing.JComponent; import javax.swing.JList; import javax.swing.JTable; import javax.swing.JTabbedPane; import javax.swing.JTree; -import javax.swing.SwingUtilities; import javax.swing.tree.TreePath; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; @@ -302,11 +302,7 @@ private void contentChanged() { if (!this.active) { return; } - if (SwingUtilities.isEventDispatchThread()) { - updateSelection(); - } else { - SwingUtilities.invokeLater(this::updateSelection); - } + UIUtils.onEdt(this::updateSelection); } private static boolean isFindShortcut(KeyEvent event) { diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/MainWindow.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/MainWindow.java index fef0b219..7d23df1e 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/MainWindow.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/MainWindow.java @@ -183,6 +183,7 @@ public void windowClosing(WindowEvent event) { closeProjectWindows(); editorTabs.closeMatching(editor -> true); statusBar.dispose(); + editorTabs.astCache().clear(); } super.dispose(); } @@ -195,7 +196,7 @@ public void windowClosing(WindowEvent event) { @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); + return new EditorContext(editorTabs.astCache(), this, project.get(), insights, debugger, navigation(), scripts, session, this::showDebuggerValue); } private void updateWindowIcon(CompanionTheme theme) { @@ -294,7 +295,7 @@ private DebuggerWindow debuggerWindow(DebuggerSessionController debugger) { private BreakpointsWindow breakpointsWindow(DebuggerSessionController debugger) { if (this.breakpointsWindow == null) { this.breakpointsWindow = new BreakpointsWindow( - this, + editorTabs.astCache(), this, debugger, target -> this.navigationService.navigate(target) ); @@ -313,8 +314,13 @@ private EvaluateExpressionWindow evaluateExpressionWindow() { } public void showDebuggerValue(DebugEngine.StackFrame frame, DebugEngine.Variable variable) { - SwingUtilities.invokeLater(() -> - debuggerWindow(debugger).showVariable(frame, variable)); + ProjectScope scope = project.get(); + if (scope == null || !scope.isActive()) return; + SwingUtilities.invokeLater(() -> { + // A queued inline-value event may arrive after the project closed or changed. + if (disposed || project.get() != scope || !scope.isActive()) return; + debuggerWindow(debugger).showVariable(frame, variable); + }); } @Override @@ -433,6 +439,7 @@ public NavigationService navigation() { if (this.editorTabs.getTabCount() != 0) return false; closeProjectWindows(); this.statusBar.setEditor(null); + editorTabs.astCache().clear(); setEnabled(false); return true; } @@ -452,11 +459,7 @@ private void closeProjectWindows() { } public void refreshRuntimeSources() { - if (SwingUtilities.isEventDispatchThread()) { - this.fileTreeView.reloadProfile(); - } else { - SwingUtilities.invokeLater(this.fileTreeView::reloadProfile); - } + UIUtils.onEdt(this.fileTreeView::reloadProfile); } private void refreshActions() { diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/BreakpointsWindow.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/BreakpointsWindow.java index fa2b1358..07161cfe 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/BreakpointsWindow.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/BreakpointsWindow.java @@ -55,6 +55,7 @@ public final class BreakpointsWindow extends JDialog { private static final Dimension DEFAULT_SIZE = new Dimension(940, 680); + private final ASTCache cache; private final DebuggerSessionController controller; private final Consumer navigation; private final DefaultListModel model = new DefaultListModel<>(); @@ -89,11 +90,12 @@ public void breakpointsMutedChanged(boolean muted) { private BreakpointKey editingBreakpoint; public BreakpointsWindow( - Window owner, + ASTCache cache, Window owner, DebuggerSessionController controller, Consumer navigation ) { super(owner, "Breakpoints", ModalityType.MODELESS); + this.cache = cache; this.controller = Objects.requireNonNull(controller, "controller"); this.navigation = Objects.requireNonNull(navigation, "navigation"); @@ -418,7 +420,7 @@ private ExpressionCompletionSupport.CompletionProvider completionProvider( String key = entry.sourceUri().getScheme().equalsIgnoreCase("file") ? Path.of(entry.sourceUri()).toString() : entry.sourceUri().toString(); - var unit = ASTCache.getFromCache(key); + var unit = cache.getFromCache(key); if (unit == null) { unit = JavaAst.parse(entry.binaryName(), source.contents()); } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/DebuggerActions.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/DebuggerActions.java index 74346c3c..f8ecb651 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/DebuggerActions.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/DebuggerActions.java @@ -1,12 +1,12 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.views.debugger; +import com.github.minecraft_ta.totalDebugCompanion.util.UIUtils; import com.github.minecraft_ta.totalDebugCompanion.Icons; import com.github.minecraft_ta.totalDebugCompanion.debugger.DebuggerSessionController; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.Icon; -import javax.swing.SwingUtilities; import java.awt.event.ActionEvent; import java.util.Objects; @@ -65,25 +65,24 @@ public Action detach() { void applyStatus(DebuggerSessionController.Status status) { Objects.requireNonNull(status, "status"); - if (!SwingUtilities.isEventDispatchThread()) { - SwingUtilities.invokeLater(() -> applyStatus(status)); - return; - } - if (this.closed) { - return; - } + UIUtils.onEdt(() -> { + if (this.closed) { + return; + } + + boolean paused = status.phase() == DebuggerSessionController.Phase.PAUSED + && this.controller.evaluationStatus() == null; + this.attach.setEnabled(status.phase() == DebuggerSessionController.Phase.DETACHED + || status.phase() == DebuggerSessionController.Phase.FAILED); + this.resume.setEnabled(paused); + this.stepOver.setEnabled(paused); + this.stepInto.setEnabled(paused); + this.stepOut.setEnabled(paused); + this.detach.setEnabled(switch (status.phase()) { + case ATTACHING, RUNNING, PAUSED, DETACHING -> true; + default -> false; + }); - boolean paused = status.phase() == DebuggerSessionController.Phase.PAUSED - && this.controller.evaluationStatus() == null; - this.attach.setEnabled(status.phase() == DebuggerSessionController.Phase.DETACHED - || status.phase() == DebuggerSessionController.Phase.FAILED); - this.resume.setEnabled(paused); - this.stepOver.setEnabled(paused); - this.stepInto.setEnabled(paused); - this.stepOut.setEnabled(paused); - this.detach.setEnabled(switch (status.phase()) { - case ATTACHING, RUNNING, PAUSED, DETACHING -> true; - default -> false; }); } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/DebuggerInspector.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/DebuggerInspector.java index b9fc40a0..0184e4cb 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/DebuggerInspector.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/DebuggerInspector.java @@ -1,5 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.views.debugger; +import com.github.minecraft_ta.totalDebugCompanion.util.UIUtils; import com.github.minecraft_ta.totalDebugCompanion.storage.InstanceState; import com.github.minecraft_ta.totalDebugCompanion.GlobalConfig; import com.github.minecraft_ta.totalDebugCompanion.Icons; @@ -110,7 +111,7 @@ CompletableFuture inspect( private final JButton addWatch = createAddWatchButton(); private final DebuggerExpressionModel expressions; private final Map expressionNodes = new LinkedHashMap<>(); - private final PropertyChangeListener previewSettingsListener = event -> onEventThread(this::refreshPreviewMode); + private final PropertyChangeListener previewSettingsListener = event -> UIUtils.onEdt(this::refreshPreviewMode); private long revision; private long previewRevision; @@ -471,7 +472,7 @@ private void submitExpression( this.pendingExpression = request; String pauseId = this.controller.snapshot().pauseId(); future.thenCompose(result -> this.runtime.retainValue(pauseId, result.variablesReference()) - .thenApply(lease -> new RetainedResult(result, lease))).whenComplete((retained, failure) -> onEventThread(() -> { + .thenApply(lease -> new RetainedResult(result, lease))).whenComplete((retained, failure) -> UIUtils.onEdt(() -> { if (this.pendingExpression == request) this.expressionPending = false; Outcome outcome = failure == null ? Outcome.success(DebugValue.from(key.expression(), retained.result())) @@ -545,7 +546,7 @@ private void previewRoot(List nodes, int index, DebugEng DebugValue value = Objects.requireNonNull(debugValue(node.getUserObject())); long started = System.nanoTime(); this.runtime.preview(requestFrame, value.variablesReference()).whenComplete((preview, failure) -> - onEventThread(() -> { + UIUtils.onEdt(() -> { if (isStale(requestFrame, requestRevision) || requestPreviewRevision != this.previewRevision) return; if (failure == null && preview != null) { applyPreview(node, value, preview); @@ -567,7 +568,7 @@ private void requestTreePreview(DefaultMutableTreeNode node, long requestRevisio return; } this.runtime.preview(requestFrame, value.variablesReference()).whenComplete((preview, failure) -> - onEventThread(() -> { + UIUtils.onEdt(() -> { if (isStale(requestFrame, requestRevision) || node.getParent() == null || failure != null && isCancellation(failure)) { return; @@ -672,7 +673,7 @@ private void requestPage( } int count = owner.indexedVariables() > 0 ? CHILD_PAGE_SIZE : 0; this.runtime.variables(requestFrame, owner.variablesReference(), start, count) - .whenComplete((children, failure) -> onEventThread(() -> { + .whenComplete((children, failure) -> UIUtils.onEdt(() -> { if (isStale(requestFrame, requestRevision)) { return; } @@ -773,7 +774,7 @@ private void navigateToDeclaration( DebugEngine.Source source = frame.sourceUri() == null ? null : this.controller.source(frame.sourceUri()); CompletableFuture.supplyAsync(() -> DebuggerVariableNavigation.declarationTarget(source, frame, variable, parent) - ).whenComplete((target, failure) -> onEventThread(() -> { + ).whenComplete((target, failure) -> UIUtils.onEdt(() -> { if (failure != null) { showOperationFailure("Unable to Jump to Source", failure); } else if (target.isEmpty()) { @@ -807,7 +808,7 @@ private void setValue(DebugEngine.Variable variable) { return; } this.controller.setVariable(variable, replacement.toString(), currentFrame) - .whenComplete((variables, failure) -> onEventThread(() -> { + .whenComplete((variables, failure) -> UIUtils.onEdt(() -> { if (failure != null) { showOperationFailure("Unable to Set Value", failure); } else { @@ -909,14 +910,6 @@ private static void copy(String text) { Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(text), null); } - private static void onEventThread(Runnable action) { - if (SwingUtilities.isEventDispatchThread()) { - action.run(); - } else { - SwingUtilities.invokeLater(action); - } - } - private static DebugEngine.Variable parentVariable(DefaultMutableTreeNode node) { if (!(node.getParent() instanceof DefaultMutableTreeNode parent)) { return null; diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/DebuggerPanel.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/DebuggerPanel.java index e6fb4072..57e6d279 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/DebuggerPanel.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/DebuggerPanel.java @@ -1,5 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.views.debugger; +import com.github.minecraft_ta.totalDebugCompanion.util.UIUtils; import com.github.minecraft_ta.totalDebugCompanion.storage.InstanceState; import com.github.minecraft_ta.totalDebugCompanion.Icons; import com.github.minecraft_ta.totalDebugCompanion.debugger.DebugEngine; @@ -18,7 +19,6 @@ import javax.swing.JPanel; import javax.swing.JSplitPane; import javax.swing.JToggleButton; -import javax.swing.SwingUtilities; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; @@ -59,17 +59,17 @@ public interface FrameNavigation { private final DebuggerSessionController.Listener listener = new DebuggerSessionController.Listener() { @Override public void statusChanged(DebuggerSessionController.Status status) { - onEventThread(() -> applyStatus(status)); + UIUtils.onEdt(() -> applyStatus(status)); } @Override public void paused(DebuggerSessionController.PausedState state) { - onEventThread(() -> showPausedState(state)); + UIUtils.onEdt(() -> showPausedState(state)); } @Override public void breakpointsMutedChanged(boolean muted) { - onEventThread(() -> DebuggerPanel.this.muteBreakpoints.setSelected(muted)); + UIUtils.onEdt(() -> DebuggerPanel.this.muteBreakpoints.setSelected(muted)); } }; @@ -249,7 +249,7 @@ private void selectFrame(int row) { return; } - this.controller.variablesForFrame(frame).whenComplete((loaded, failure) -> onEventThread(() -> { + this.controller.variablesForFrame(frame).whenComplete((loaded, failure) -> UIUtils.onEdt(() -> { if (!isCurrent(frame, revision)) { return; } @@ -351,14 +351,6 @@ private static JToggleButton createMuteBreakpointsButton() { return button; } - private static void onEventThread(Runnable action) { - if (SwingUtilities.isEventDispatchThread()) { - action.run(); - } else { - SwingUtilities.invokeLater(action); - } - } - private static final class MutedLabel extends JLabel { @Override protected void paintComponent(Graphics graphics) { diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/util/UIUtils.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/util/UIUtils.java index 071e94ef..e5e44502 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/util/UIUtils.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/util/UIUtils.java @@ -6,11 +6,22 @@ import javax.swing.*; import javax.swing.text.*; import java.awt.*; +import java.lang.reflect.InvocationTargetException; public class UIUtils { private static final System.Logger LOGGER = System.getLogger(UIUtils.class.getName()); private static final double NAVIGATION_TARGET_VERTICAL_POSITION = 1.0 / 3.0; + public static void onEdt(Runnable action) { + if (SwingUtilities.isEventDispatchThread()) action.run(); + else SwingUtilities.invokeLater(action); + } + + public static void onEdtAndWait(Runnable action) throws InvocationTargetException, InterruptedException { + if (SwingUtilities.isEventDispatchThread()) action.run(); + else SwingUtilities.invokeAndWait(action); + } + public static int getFontWidth(JComponent component, String s) { return component.getFontMetrics(component.getFont()).stringWidth(s); } diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/UiScenarioDriver.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/UiScenarioDriver.java index 5ab6a30e..90d4ecc0 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/UiScenarioDriver.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/UiScenarioDriver.java @@ -5,7 +5,6 @@ import javax.swing.Timer; import com.github.minecraft_ta.totalDebugCompanion.jdt.symbol.CodeSymbol; import com.github.minecraft_ta.totalDebugCompanion.debugger.DebugEngine; -import com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics.ASTCache; import com.github.minecraft_ta.totalDebugCompanion.model.CodeView; import com.github.minecraft_ta.totalDebugCompanion.model.ServiceStatus; import com.github.minecraft_ta.totalDebugCompanion.model.UsagesView; @@ -338,7 +337,7 @@ private void advanceMethodBreakpoint(ScenarioContext context) throws Exception { selectCodeEditor(context); var selected = mainWindow.getEditorTabs().getSelectedEditor(); if (!(selected instanceof CodeView codeView) - || ASTCache.getFromCache(codeView.getPath().toString()) == null) { + || mainWindow.getEditorTabs().astCache().getFromCache(codeView.getPath().toString()) == null) { return; } RSyntaxTextArea editor = findComponent(mainWindow, RSyntaxTextArea.class); @@ -394,7 +393,7 @@ private void advanceImplementationChooser(ScenarioContext context) { return; } if (!(mainWindow.getEditorTabs().getSelectedEditor() instanceof CodeView codeView) - || ASTCache.getFromCache(codeView.getPath().toString()) == null) { + || mainWindow.getEditorTabs().astCache().getFromCache(codeView.getPath().toString()) == null) { return; } context.once("open-chooser", () -> { diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/JavaSymbolResolverTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/JavaSymbolResolverTest.java index 078f53f3..00ec19df 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/JavaSymbolResolverTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/JavaSymbolResolverTest.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; final class JavaSymbolResolverTest { + private static final ASTCache cache = new ASTCache(); private static final String SOURCE = """ package example; @@ -60,10 +61,10 @@ static void initializeClassIndex() throws IOException { @AfterAll static void closeClassIndex() { - ASTCache.removeFromCache("symbols"); - ASTCache.removeFromCache("constructor"); - ASTCache.removeFromCache("local"); - ASTCache.removeFromCache("navigation"); + cache.removeFromCache("symbols"); + cache.removeFromCache("constructor"); + cache.removeFromCache("local"); + cache.removeFromCache("navigation"); CompanionClassIndex.get().close(); CompanionClassIndex.clear(); } @@ -72,9 +73,9 @@ static void closeClassIndex() { void resolvesExactJvmClassFieldAndMethodSymbols() throws Exception { String key = prepareAst("symbols"); - var type = JavaSymbolResolver.resolve(key, SOURCE.indexOf("String field")); - var field = JavaSymbolResolver.resolve(key, SOURCE.indexOf("this.field") + "this.".length()); - var method = JavaSymbolResolver.resolve(key, SOURCE.lastIndexOf("run(")); + var type = JavaSymbolResolver.resolve(cache, key, SOURCE.indexOf("String field")); + var field = JavaSymbolResolver.resolve(cache, key, SOURCE.indexOf("this.field") + "this.".length()); + var method = JavaSymbolResolver.resolve(cache, key, SOURCE.lastIndexOf("run(")); assertEquals(new CodeSymbol.ClassSymbol("java.lang.String"), type.symbol()); assertEquals( @@ -96,7 +97,7 @@ void resolvesConstructorToJvmInit() throws Exception { String key = prepareAst("constructor"); int constructorUse = SOURCE.indexOf("new Target") + "new ".length(); - var resolution = JavaSymbolResolver.resolve(key, constructorUse); + var resolution = JavaSymbolResolver.resolve(cache, key, constructorUse); assertEquals( new CodeSymbol.MethodSymbol("example.Target", "", "(Ljava/lang/String;)V"), @@ -108,7 +109,7 @@ void resolvesConstructorToJvmInit() throws Exception { void resolvesAnExternalIndexedMethodToItsRuntimeOwner() throws Exception { String key = prepareAst("external-method"); - var method = JavaSymbolResolver.resolve(key, SOURCE.indexOf("isBlank")); + var method = JavaSymbolResolver.resolve(cache, key, SOURCE.indexOf("isBlank")); assertEquals( new CodeSymbol.MethodSymbol("java.lang.String", "isBlank", "()Z"), @@ -121,7 +122,7 @@ void rejectsLocalVariablesWithAnExactReason() throws Exception { String key = prepareAst("local"); int localUse = SOURCE.indexOf("return local") + "return ".length(); - var resolution = JavaSymbolResolver.resolve(key, localUse); + var resolution = JavaSymbolResolver.resolve(cache, key, localUse); assertFalse(resolution.isResolved()); assertTrue(resolution.unavailableReason().contains("Local-variable")); @@ -131,15 +132,15 @@ void rejectsLocalVariablesWithAnExactReason() throws Exception { void resolvesTheConcreteImportedTypeFromAPackageSegment() throws Exception { String key = prepareAst("navigation"); - String owner = JavaSymbolResolver.navigationOwnerClass(key, SOURCE.indexOf("java.util")); + String owner = JavaSymbolResolver.navigationOwnerClass(cache, key, SOURCE.indexOf("java.util")); assertEquals("java.util.List", owner); } private static String prepareAst(String key) throws InterruptedException { CountDownLatch parsed = new CountDownLatch(1); - ASTCache.addChangeListener(key, (unit, version) -> parsed.countDown()); - ASTCache.update(key, "Target", SOURCE); + cache.addChangeListener(key, (unit, version) -> parsed.countDown()); + cache.update(key, "Target", SOURCE); assertTrue(parsed.await(5, TimeUnit.SECONDS), "Timed out waiting for the Java model"); return key; } diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/ASTCacheTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/ASTCacheTest.java new file mode 100644 index 00000000..4809683e --- /dev/null +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/ASTCacheTest.java @@ -0,0 +1,49 @@ +package com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics; + +import com.github.minecraft_ta.totalDebugCompanion.jdt.CompanionClassIndex; +import com.github.tth05.jindex.ClassIndex; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import static org.junit.jupiter.api.Assertions.*; + +class ASTCacheTest { + private static ClassIndex index; + @BeforeAll static void bindIndex() throws Exception { + try (var bytes = Object.class.getResourceAsStream("Object.class")) { + index = ClassIndex.fromBytes(List.of(bytes.readAllBytes())); + } + CompanionClassIndex.set(index); + } + @AfterAll static void closeIndex() { CompanionClassIndex.clear(); index.close(); } + + @Test void identicalEditorKeysHaveIndependentModelsAndListeners() throws Exception { + var first = new ASTCache(); + var second = new ASTCache(); + var secondUpdates = new AtomicInteger(); + second.addChangeListener("same.java", (unit, version) -> secondUpdates.incrementAndGet()); + first.update("same.java", "Sample", "class Sample { int first; }").get(3, TimeUnit.SECONDS); + second.update("same.java", "Sample", "class Sample { int second; }").get(3, TimeUnit.SECONDS); + assertNotSame(first.getFromCache("same.java"), second.getFromCache("same.java")); + assertTrue(first.getContents("same.java").contains("first")); + assertTrue(second.getContents("same.java").contains("second")); + first.clear(); + assertNull(first.getFromCache("same.java")); + assertNotNull(second.getFromCache("same.java")); + second.update("same.java", "Sample", "class Sample { int retained; }").get(3, TimeUnit.SECONDS); + assertEquals(2, secondUpdates.get()); + second.clear(); + } + + @Test void pendingPublicationCannotRestoreAClearedEntry() throws Exception { + var cache = new ASTCache(); + var pending = cache.update("closed.java", "Sample", "class Sample { int field; }"); + cache.clear(); + pending.get(3, TimeUnit.SECONDS); + assertNull(cache.getFromCache("closed.java")); + assertNull(cache.getContents("closed.java")); + } +} diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/CustomJavaLinkGeneratorTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/CustomJavaLinkGeneratorTest.java index cf89febb..e0dcf770 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/CustomJavaLinkGeneratorTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/CustomJavaLinkGeneratorTest.java @@ -22,7 +22,7 @@ void packageImportSegmentRevealsTheSelectedPackage() { String source = "import java.util.List; final class Sample { List values; }"; IJavaElement packageFragment = packageFragment("java.util"); AtomicReference revealedPackage = new AtomicReference<>(); - var generator = new CustomJavaLinkGenerator( + var generator = new CustomJavaLinkGenerator(new ASTCache(), offset -> packageFragment, offset -> "java.util.List", (packageName, ownerClass) -> revealedPackage.set(packageName + " in " + ownerClass), @@ -40,7 +40,7 @@ void packageImportSegmentRevealsTheSelectedPackage() { void unresolvedPackageOwnerIsPassedToTheDiagnosticRoute() { String source = "import missing.Type; class Sample {}"; AtomicReference diagnostic = new AtomicReference<>(); - var generator = new CustomJavaLinkGenerator(offset -> packageFragment("missing"), offset -> null, + var generator = new CustomJavaLinkGenerator(new ASTCache(), offset -> packageFragment("missing"), offset -> null, (name, owner) -> { assertNull(owner); diagnostic.set("Unresolved owner for " + name); @@ -57,7 +57,7 @@ void linkResultStartsAtTheTokenSoRSyntaxTextAreaCanUnderlineIt() { int hoverOffset = source.indexOf("List") + 2; RSyntaxTextArea textArea = new RSyntaxTextArea(source); textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA); - var generator = new CustomJavaLinkGenerator( + var generator = new CustomJavaLinkGenerator(new ASTCache(), offset -> packageFragment("java.util"), offset -> "java.util.List", (packageName, ownerClass) -> { @@ -81,7 +81,7 @@ void punctuationNextToAResolvableSymbolIsNotALink() { String source = "final class Sample { Object value = target.call(); }"; RSyntaxTextArea textArea = new RSyntaxTextArea(source); textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA); - var generator = new CustomJavaLinkGenerator( + var generator = new CustomJavaLinkGenerator(new ASTCache(), offset -> packageFragment("example"), offset -> "example.Target", (packageName, ownerClass) -> { diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/insight/ExpressionScopeAnalyzerTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/insight/ExpressionScopeAnalyzerTest.java index cde387ee..4d90cb45 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/insight/ExpressionScopeAnalyzerTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/insight/ExpressionScopeAnalyzerTest.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; class ExpressionScopeAnalyzerTest { + private static final ASTCache cache = new ASTCache(); private static final String SOURCE = """ class Sample { int field; @@ -154,9 +155,9 @@ void run(ExternalCompletionType parameter) { void ignoresUnrelatedOpenTypesWithTheSameSimpleName() throws Exception { String key = "unrelated-external-completion-type"; var parsed = new java.util.concurrent.CompletableFuture(); - Runnable removeListener = ASTCache.addChangeListener(key, (unit, version) -> parsed.complete(null)); + Runnable removeListener = cache.addChangeListener(key, (unit, version) -> parsed.complete(null)); try { - ASTCache.update(key, "ExternalCompletionType", """ + cache.update(key, "ExternalCompletionType", """ package unrelated; class ExternalCompletionType { int unrelatedField; @@ -180,7 +181,7 @@ void run(ExternalCompletionType parameter) { assertFalse(completions.contains("unrelatedField"), completions.toString()); } finally { removeListener.run(); - ASTCache.removeFromCache(key); + cache.removeFromCache(key); } } diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/semanticHighlighting/SemanticHighlightingPublicationTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/semanticHighlighting/SemanticHighlightingPublicationTest.java index b2b5c9ea..c02463d3 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/semanticHighlighting/SemanticHighlightingPublicationTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/semanticHighlighting/SemanticHighlightingPublicationTest.java @@ -28,6 +28,7 @@ import static org.junit.jupiter.api.Assertions.fail; final class SemanticHighlightingPublicationTest { + private static final ASTCache cache = new ASTCache(); private static final String SOURCE = """ package example; @@ -73,7 +74,7 @@ void publishesParsedColorsOnlyWhenTheEdtProcessesTheResult() throws Exception { return null; }); } finally { - ASTCache.removeFromCache(key); + cache.removeFromCache(key); } } @@ -100,7 +101,7 @@ void rejectsAQueuedResultAfterTheDocumentChanges() throws Exception { return null; }); } finally { - ASTCache.removeFromCache(key); + cache.removeFromCache(key); } } @@ -115,7 +116,7 @@ void rejectsAnOlderCallbackDeliveredAfterANewerParse() throws Exception { AtomicReference oldUnit = new AtomicReference<>(); // Register before the token maker to pause delivery of the first parsed AST. - ASTCache.addChangeListener(key, (unit, version) -> { + cache.addChangeListener(key, (unit, version) -> { if (holdFirstCallback.compareAndSet(true, false)) { oldUnit.set(unit); oldCallbackEntered.countDown(); @@ -125,7 +126,7 @@ void rejectsAnOlderCallbackDeliveredAfterANewerParse() throws Exception { try { RSyntaxTextArea area = onEdt(() -> editor(key, SOURCE)); - ASTCache.addChangeListener(key, (unit, version) -> { + cache.addChangeListener(key, (unit, version) -> { if (unit == oldUnit.get()) { oldCallbackDelivered.countDown(); } else { @@ -133,13 +134,13 @@ void rejectsAnOlderCallbackDeliveredAfterANewerParse() throws Exception { } }); - ASTCache.update(key, "Target", SOURCE); + cache.update(key, "Target", SOURCE); await(oldCallbackEntered, "The first AST callback did not reach its gate"); String currentSource = "// newer source with different token positions\n" + SOURCE; onEdt(() -> { area.setText(currentSource); - ASTCache.update(key, "Target", currentSource); + cache.update(key, "Target", currentSource); return null; }); await(newCallbackDelivered, "The newer AST was not delivered"); @@ -162,7 +163,7 @@ void rejectsAnOlderCallbackDeliveredAfterANewerParse() throws Exception { await(oldCallbackDelivered, "The gated AST callback did not finish during cleanup"); } } finally { - ASTCache.removeFromCache(key); + cache.removeFromCache(key); } } } @@ -172,7 +173,7 @@ private static RSyntaxTextArea editor(String key, String source) { CustomJavaTokenMaker tokenMaker = new CustomJavaTokenMaker(); ((RSyntaxDocument) area.getDocument()).setSyntaxStyle(tokenMaker); area.setText(source); - tokenMaker.setASTKey(key, area); + tokenMaker.setASTKey(cache, key, area); return area; } @@ -181,40 +182,40 @@ void clearingAProjectPreventsOldCallbacksFromReachingAReusedKey() throws Excepti String key = newKey(); CountDownLatch entered = new CountDownLatch(1); CountDownLatch release = new CountDownLatch(1); - ASTCache.addChangeListener(key, (unit, version) -> { + cache.addChangeListener(key, (unit, version) -> { entered.countDown(); awaitCallbackRelease(release); }); - var old = ASTCache.update(key, "Target", SOURCE); + var old = cache.update(key, "Target", SOURCE); try { await(entered, "Old parse did not enter its callback"); - ASTCache.clear(); + cache.clear(); var received = new java.util.concurrent.CopyOnWriteArrayList(); - ASTCache.addChangeListener(key, (unit, version) -> received.add(unit)); + cache.addChangeListener(key, (unit, version) -> received.add(unit)); String next = "// next project\n" + SOURCE; - ASTCache.update(key, "Target", next).get(10, TimeUnit.SECONDS); + cache.update(key, "Target", next).get(10, TimeUnit.SECONDS); release.countDown(); old.get(10, TimeUnit.SECONDS); assertEquals(1, received.size()); - assertEquals(next, ASTCache.getContents(key)); - assertEquals(ASTCache.getFromCache(key), received.getFirst()); + assertEquals(next, cache.getContents(key)); + assertEquals(cache.getFromCache(key), received.getFirst()); } finally { release.countDown(); old.get(10, TimeUnit.SECONDS); - ASTCache.removeFromCache(key); + cache.removeFromCache(key); } } private static void parseAndAwaitDelivery(String key, String source) throws InterruptedException { - CompilationUnit previous = ASTCache.getFromCache(key); + CompilationUnit previous = cache.getFromCache(key); CountDownLatch delivered = new CountDownLatch(1); - Runnable removeListener = ASTCache.addChangeListener(key, (unit, version) -> { + Runnable removeListener = cache.addChangeListener(key, (unit, version) -> { if (unit != previous) { delivered.countDown(); } }); try { - ASTCache.update(key, "Target", source); + cache.update(key, "Target", source); await(delivered, "The Java AST was not delivered to the semantic token listener"); } finally { removeListener.run(); diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/CodeVisionDisposalTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/CodeVisionDisposalTest.java index a0a74d3a..ed04656f 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/CodeVisionDisposalTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/CodeVisionDisposalTest.java @@ -19,13 +19,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals; class CodeVisionDisposalTest { + private static final ASTCache cache = new ASTCache(); @Test void closingControllerUnsubscribesWithoutRemovingOtherEditorListeners() throws Exception { - var field = ASTCache.class.getDeclaredField("LISTENERS"); + var field = ASTCache.class.getDeclaredField("listeners"); field.setAccessible(true); - var listeners = (Map) field.get(null); + var listeners = (Map) field.get(cache); String key = "code-vision-disposal-test"; - Runnable unsubscribeOther = ASTCache.addChangeListener(key, (unit, version) -> {}); + Runnable unsubscribeOther = cache.addChangeListener(key, (unit, version) -> {}); try (var service = new CodeInsightService(() -> { throw new AssertionError("No analysis expected"); }, RuntimeSourceCatalog.empty())) { SwingUtilities.invokeAndWait(() -> { var editor = new RSyntaxTextArea(); @@ -41,12 +42,12 @@ public void navigate(SourceDeclaration declaration, HierarchyRelation relation, public void preview(SourceDeclaration declaration, HierarchyRelation relation, int count, boolean mixed) {} public void hidePreview() {} }); - var controller = new CodeVisionController(key, service, layerUI, layer, gutter); + var controller = new CodeVisionController(cache, key, service, layerUI, layer, gutter); assertEquals(2, ((Collection) listeners.get(key)).size()); controller.close(); controller.close(); assertEquals(1, ((Collection) listeners.get(key)).size()); }); - } finally { unsubscribeOther.run(); ASTCache.removeFromCache(key); } + } finally { unsubscribeOther.run(); cache.removeFromCache(key); } } } diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/BreakpointsWindowPreview.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/BreakpointsWindowPreview.java index 4a62a8ba..94950361 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/BreakpointsWindowPreview.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/BreakpointsWindowPreview.java @@ -1,5 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.views.debugger; +import com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics.ASTCache; import com.github.minecraft_ta.totalDebugCompanion.debugger.DebugEngine; import com.github.minecraft_ta.totalDebugCompanion.debugger.DebuggerSessionController; @@ -34,7 +35,7 @@ public static BreakpointsWindow open(Window owner) { null )).join(); - BreakpointsWindow window = new BreakpointsWindow(owner, controller, target -> { + BreakpointsWindow window = new BreakpointsWindow(new ASTCache(), owner, controller, target -> { }); window.setBounds(owner.getX() + 50, owner.getY() + 20, 940, 680); window.showWindow();