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
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,11 @@ public static void addRuntimeIndexStatusListener(Consumer<RuntimeIndexService.St
}
}

public static void removeRuntimeIndexStatusListener(Consumer<RuntimeIndexService.Status> listener) {
RuntimeIndexService service = runtimeIndexService;
if (service != null) service.removeStatusListener(listener);
}

public static void retryRuntimeIndex() {
RuntimeIndexService service = runtimeIndexService;
if (service != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ public void restartSearch() {
this.panel.restartSearch();
}

@Override
public void dispose() {
this.panel.dispose();
}

@Override
public String getTitle() {
return "Text: " + quotedPreview(this.literal);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.github.minecraft_ta.totalDebugCompanion.CompanionApp;
import com.github.minecraft_ta.totalDebugCompanion.Icons;
import com.github.minecraft_ta.totalDebugCompanion.jdt.JavaSnippetSource;
import com.github.minecraft_ta.totaldebug.protocol.scnet.ExecutionResultMessage;
import com.github.minecraft_ta.totalDebugCompanion.navigation.NavigationTarget;
import com.github.minecraft_ta.totalDebugCompanion.navigation.NavigationViewState;
import com.github.minecraft_ta.totalDebugCompanion.ui.components.editors.ScriptPanel;
Expand Down Expand Up @@ -121,9 +120,6 @@ public void restoreNavigationViewState(NavigationViewState state) {
@Override
public void dispose() {
if (this.scriptPanel != null) {
if (CompanionApp.SERVER != null) {
CompanionApp.SERVER.getMessageBus().unregister(ExecutionResultMessage.class, this.scriptPanel);
}
this.scriptPanel.dispose();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ public void restartSearch() {
this.panel.restartSearch();
}

@Override
public void dispose() {
this.panel.dispose();
}

@Override
public String getTitle() {
return "Usages: " + shortName(this.symbol);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ public void waiting(String detail) {
}
}

public void removeStatusListener(Consumer<Status> listener) {
this.listeners.remove(listener);
}

public void restore(Path dataDirectory) {
synchronized (this.lifecycleLock) {
ensureOpen();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ final class CodeVisionController implements AutoCloseable {
private final JLayer<JComponent> layer;
private final HierarchyGutterMarkers gutterMarkers;

private final Runnable unsubscribeAst;
private CodeInsightService.SearchHandle activeAnalysis;
private long generation;
private boolean closed;
Expand All @@ -38,7 +39,7 @@ final class CodeVisionController implements AutoCloseable {
this.layerUI = Objects.requireNonNull(layerUI, "layerUI");
this.layer = Objects.requireNonNull(layer, "layer");
this.gutterMarkers = Objects.requireNonNull(gutterMarkers, "gutterMarkers");
ASTCache.addChangeListener(this.editorIdentifier, (unit, version) -> {
this.unsubscribeAst = ASTCache.addChangeListener(this.editorIdentifier, (unit, version) -> {
String source = ASTCache.getContents(this.editorIdentifier);
if (source != null) {
analyze(SourceDeclarationAnalyzer.analyze(unit, source));
Expand Down Expand Up @@ -106,6 +107,7 @@ public synchronized void close() {
return;
}
this.closed = true;
this.unsubscribeAst.run();
if (this.activeAnalysis != null) {
this.activeAnalysis.cancel();
this.activeAnalysis = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ public class ScriptPanel extends AbstractCodeViewPanel {
private final int scriptId = SCRIPT_ID++;
private final ScriptView scriptView;

private static final CodeCompletionPopup codeCompletionPopup = new CodeCompletionPopup(MainWindow.INSTANCE);
private static final SignatureHelpPopup signatureHelpPopup = new SignatureHelpPopup(MainWindow.INSTANCE);
private final CodeCompletionPopup codeCompletionPopup = new CodeCompletionPopup(MainWindow.INSTANCE);
private final SignatureHelpPopup signatureHelpPopup = new SignatureHelpPopup(MainWindow.INSTANCE);

private final FlatIconButton runButton = new FlatIconButton(Icons.RUN, false);
private final FlatIconButton runServerButton = new FlatIconButton(Icons.RUN_SERVER, false);
Expand Down Expand Up @@ -108,6 +108,8 @@ public Dimension getPreferredSize() {

private final SnippetCompletionAdapter snippetCompletionAdapter = new SnippetCompletionAdapter(this.editorPane);
private CustomCompletionRequestor completionRequestor;
private boolean disposed;
private final Runnable unsubscribeResults;
private boolean didTypeBeforeCaretMove;
private int lastCaretPos;
private JavaSnippetSource.GeneratedSource lastGeneratedSource;
Expand Down Expand Up @@ -144,12 +146,14 @@ public ScriptPanel(ScriptView scriptView) {
setupAutocompletion();
setupFormatting();

CompanionApp.SERVER.getMessageBus().listenAlways(ExecutionResultMessage.class, this, this::acceptResult);
var messageBus = CompanionApp.SERVER.getMessageBus();
messageBus.listenAlways(ExecutionResultMessage.class, this, this::acceptResult);
this.unsubscribeResults = () -> messageBus.unregister(ExecutionResultMessage.class, this);
}

private void acceptResult(ExecutionResultMessage m) {
SwingUtilities.invokeLater(() -> {
if (m.scriptId() != this.scriptId)
if (this.disposed || m.scriptId() != this.scriptId)
return;

ExecutionStatus status = m.result().status();
Expand Down Expand Up @@ -578,10 +582,14 @@ private void hideCompletionPopup() {

@Override
public void dispose() {
if (this.disposed) return;
this.disposed = true;
this.unsubscribeResults.run();
this.saveTimer.stop();
if (this.completionRequestor != null)
this.completionRequestor.setCanceled(true);
hideCompletionPopup();
this.codeCompletionPopup.dispose();
this.signatureHelpPopup.dispose();
super.dispose();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public final class UsagesViewPanel extends JPanel {
private ReferenceSearchService.SearchHandle activeSearch;
private long searchGeneration;
private boolean detached;
private boolean disposed;
private int resultLimit = INITIAL_RESULT_LIMIT;
private boolean resultTruncated;
private List<ReferenceUsage> currentUsages = List.of();
Expand Down Expand Up @@ -110,8 +111,16 @@ public UsagesViewPanel(
});
}

public void dispose() {
requireEdt();
this.disposed = true;
this.detached = true;
cancelActiveSearch();
}

public void restartSearch() {
requireEdt();
if (this.disposed) return;
this.detached = false;
this.resultLimit = INITIAL_RESULT_LIMIT;
this.resultTruncated = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ public void actionPerformed(ActionEvent event) {
private final ModuleFilterPopup moduleFilterPopup;
private final Consumer<CompanionTheme> themeListener = theme -> applyTheme();

private final Consumer<RuntimeIndexService.Status> indexStatusListener = status -> SwingUtilities.invokeLater(() -> {
if (!isDisplayable()) return;
if (CompanionClassIndex.isOpen()) {
syncRuntimeModules();
refreshResults();
} else {
showIndexStatus(status);
}
});

private RuntimeSourceCatalog sourceCatalog = RuntimeSourceCatalog.empty();
private List<RuntimeInventory.RuntimeModule> modules = List.of();
private Category category = Category.ALL;
Expand Down Expand Up @@ -130,14 +140,7 @@ public void actionPerformed(ActionEvent event) {
JComponent.WHEN_IN_FOCUSED_WINDOW
);

CompanionApp.addRuntimeIndexStatusListener(status -> SwingUtilities.invokeLater(() -> {
if (CompanionClassIndex.isOpen()) {
syncRuntimeModules();
refreshResults();
} else {
showIndexStatus(status);
}
}));
CompanionApp.addRuntimeIndexStatusListener(this.indexStatusListener);

((JPanel) getContentPane()).setBorder(PopupChrome.border());
setUndecorated(true);
Expand Down Expand Up @@ -182,6 +185,7 @@ void open(Set<String> moduleIds, String query) {

@Override
public void dispose() {
CompanionApp.removeRuntimeIndexStatusListener(this.indexStatusListener);
ThemeManager.removeThemeChangeListener(this.themeListener);
this.searchGeneration.incrementAndGet();
if (this.pendingSearch != null) {
Expand Down Expand Up @@ -423,6 +427,7 @@ private void moveSelection(int delta) {
}

private void refreshResults() {
if (this.searchExecutor.isShutdown()) return;
long generation = this.searchGeneration.incrementAndGet();
if (this.pendingSearch != null) {
this.pendingSearch.cancel(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.github.minecraft_ta.totalDebugCompanion;

import com.github.minecraft_ta.totalDebugCompanion.model.ScriptView;
import com.github.minecraft_ta.totalDebugCompanion.session.CompanionLaunchConfiguration;
import com.github.minecraft_ta.totalDebugCompanion.session.CompanionProfile;
import com.github.minecraft_ta.totalDebugCompanion.ui.components.editors.ScriptPanel;
import com.github.tth05.scnet.Server;
import com.github.tth05.scnet.message.AbstractMessage;
import com.github.tth05.scnet.message.impl.DefaultMessageBus;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import javax.swing.SwingUtilities;
import java.awt.Window;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

import static org.junit.jupiter.api.Assertions.*;

class ScriptPanelDisposalTest {
@TempDir Path directory;

@Test
void panelsOwnTheirPopupsAndUnsubscribeOnDirectDisposal() throws Exception {
String classpath = System.getProperty("totaldebug.testClasspath", System.getProperty("java.class.path"));
Path log = directory.resolve("probe.log");
Process process = new ProcessBuilder(Path.of(System.getProperty("java.home"), "bin", "java").toString(),
"-Djava.awt.headless=false", "-cp", classpath, getClass().getName(), directory.toString())
.redirectErrorStream(true).redirectOutput(log.toFile()).start();
try {
assertTrue(process.waitFor(25, TimeUnit.SECONDS), () -> read(log));
assertEquals(0, process.exitValue(), () -> read(log));
} finally { if (process.isAlive()) process.destroyForcibly(); }
}

public static void main(String[] args) {
try {
Path root = Path.of(args[0]);
Path appHome = Files.createDirectories(root.resolve("app"));
var launch = CompanionApp.class.getDeclaredField("launchConfiguration");
launch.setAccessible(true);
launch.set(null, new CompanionLaunchConfiguration(appHome));
GlobalConfig.getInstance().loadFrom(appHome);
CompanionApp.configureWithoutSession(CompanionProfile.forGame(Files.createDirectories(root.resolve("game"))));
CompanionApp.configureLookAndFeel();
CompanionApp.configureTokenMakers();
try (var server = new Server()) {
var bus = new TrackingBus();
server.setMessageBus(bus);
CompanionApp.SERVER = server;
SwingUtilities.invokeAndWait(() -> {
var first = (ScriptPanel) new ScriptView("First").getComponent();
var second = (ScriptPanel) new ScriptView("Second").getComponent();
Window firstCompletion = popup(first, "codeCompletionPopup");
Window firstSignature = popup(first, "signatureHelpPopup");
Window secondCompletion = popup(second, "codeCompletionPopup");
assertNotSame(firstCompletion, secondCompletion);
firstCompletion.pack();
firstSignature.pack();
secondCompletion.pack();
assertEquals(Set.of(first, second), bus.owners);
try (var replacement = new Server()) {
CompanionApp.SERVER = replacement;
first.dispose();
first.dispose();
assertFalse(firstCompletion.isDisplayable());
assertFalse(firstSignature.isDisplayable());
assertTrue(secondCompletion.isDisplayable());
assertEquals(Set.of(second), bus.owners);
second.dispose();
assertTrue(bus.owners.isEmpty());
}
});
}
System.exit(0);
} catch (Throwable failure) { failure.printStackTrace(); System.exit(1); }
}

private static Window popup(ScriptPanel panel, String name) {
try {
var field = ScriptPanel.class.getDeclaredField(name);
field.setAccessible(true);
return (Window) field.get(panel);
} catch (ReflectiveOperationException failure) { throw new AssertionError(failure); }
}

private static String read(Path file) {
try { return Files.readString(file); }
catch (Exception failure) { return failure.toString(); }
}

private static final class TrackingBus extends DefaultMessageBus {
private final Set<Object> owners = new HashSet<>();
@Override public <T extends AbstractMessage> void listenAlways(Class<T> type, Object owner, Consumer<T> listener) {
super.listenAlways(type, owner, listener);
owners.add(owner);
}
@Override public <T extends AbstractMessage> void unregister(Class<T> type, Object owner) {
super.unregister(type, owner);
owners.remove(owner);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.github.minecraft_ta.totalDebugCompanion.bytecode.reference.ReferenceLocation;
import com.github.minecraft_ta.totalDebugCompanion.bytecode.reference.ReferenceUsagePage;
import com.github.minecraft_ta.totalDebugCompanion.bytecode.reference.ReferenceQuery;
import com.github.minecraft_ta.totalDebugCompanion.ui.components.editors.UsagesViewPanel;
import com.github.minecraft_ta.totalDebugCompanion.jdt.symbol.CodeSymbol;
import com.github.minecraft_ta.totalDebugCompanion.bytecode.reference.ReferenceUsage;
import com.github.tth05.jindex.ReferenceKind;
import org.junit.jupiter.api.Test;
Expand All @@ -22,6 +24,46 @@
import static org.junit.jupiter.api.Assertions.assertTrue;

class ReferenceSearchServiceTest {
@Test
void disposingAnUnattachedUsagePanelCancelsItsSearch() throws Exception {
var started = new CountDownLatch(1);
var release = new CountDownLatch(1);
var calls = new AtomicInteger();
try (var service = new ReferenceSearchService((query, limit) -> {
calls.incrementAndGet();
started.countDown();
try { release.await(5, TimeUnit.SECONDS); }
catch (InterruptedException failure) { Thread.currentThread().interrupt(); }
return new ReferenceUsagePage(List.of(), false);
})) {
var panel = new AtomicReference<UsagesViewPanel>();
var handle = new AtomicReference<ReferenceSearchService.SearchHandle>();
SwingUtilities.invokeAndWait(() -> {
panel.set(new UsagesViewPanel(
new CodeSymbol.ClassSymbol("example.Target"), service));
panel.get().restartSearch();
try {
var field = panel.get().getClass().getDeclaredField("activeSearch");
field.setAccessible(true);
handle.set((ReferenceSearchService.SearchHandle) field.get(panel.get()));
} catch (ReflectiveOperationException failure) { throw new AssertionError(failure); }
});
assertTrue(started.await(5, TimeUnit.SECONDS));
SwingUtilities.invokeAndWait(() -> {
panel.get().dispose();
// Cancellation stops delivery; it does not pretend the running query has finished.
assertFalse(handle.get().isDone());
try {
var cancelled = handle.get().getClass().getDeclaredField("cancelled");
cancelled.setAccessible(true);
assertTrue(((AtomicBoolean) cancelled.get(handle.get())).get());
} catch (ReflectiveOperationException failure) { throw new AssertionError(failure); }
panel.get().restartSearch();
});
assertEquals(1, calls.get());
} finally { release.countDown(); }
}

@Test
void deliversBoundedResultsOnTheSwingEventThread() throws Exception {
CountDownLatch completed = new CountDownLatch(1);
Expand Down
Loading