Skip to content

Commit b10d63c

Browse files
authored
fix(markdown): avoid workspace-wide diagnostics refresh thread pileup (#2008)
1 parent 903712e commit b10d63c

File tree

3 files changed

+357
-56
lines changed

3 files changed

+357
-56
lines changed

org.eclipse.wildwebdeveloper.tests/src/org/eclipse/wildwebdeveloper/tests/TestMarkdown.java

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,20 @@
1616
import static org.eclipse.wildwebdeveloper.markdown.MarkdownDiagnosticsManager.MARKDOWN_MARKER_TYPE;
1717
import static org.junit.jupiter.api.Assertions.assertTrue;
1818

19+
import java.lang.reflect.InvocationHandler;
20+
import java.lang.reflect.Proxy;
1921
import java.nio.charset.StandardCharsets;
2022
import java.util.ArrayList;
2123
import java.util.Arrays;
2224
import java.util.Collections;
25+
import java.util.concurrent.CompletableFuture;
26+
import java.util.concurrent.atomic.AtomicInteger;
2327
import java.util.concurrent.atomic.AtomicReference;
28+
import java.util.function.BooleanSupplier;
2429
import java.util.stream.Collectors;
2530

31+
import org.eclipse.core.filebuffers.FileBuffers;
32+
import org.eclipse.core.filebuffers.LocationKind;
2633
import org.eclipse.core.resources.IFile;
2734
import org.eclipse.core.resources.IMarker;
2835
import org.eclipse.core.resources.IResource;
@@ -34,11 +41,15 @@
3441
import org.eclipse.lsp4e.LanguageServerWrapper;
3542
import org.eclipse.lsp4e.LanguageServiceAccessor;
3643
import org.eclipse.lsp4e.operations.completion.LSContentAssistProcessor;
44+
import org.eclipse.lsp4j.DocumentDiagnosticParams;
45+
import org.eclipse.lsp4j.DocumentDiagnosticReport;
46+
import org.eclipse.lsp4j.services.LanguageServer;
3747
import org.eclipse.ui.PlatformUI;
3848
import org.eclipse.ui.editors.text.TextEditor;
3949
import org.eclipse.ui.ide.IDE;
4050
import org.eclipse.ui.tests.harness.util.DisplayHelper;
4151
import org.eclipse.wildwebdeveloper.Activator;
52+
import org.eclipse.wildwebdeveloper.markdown.MarkdownDiagnosticsManager;
4253
import org.junit.jupiter.api.Test;
4354
import org.junit.jupiter.api.extension.ExtendWith;
4455

@@ -48,6 +59,105 @@ record MarkdownTest(String markdown, String messagePattern, int severity) {
4859
@ExtendWith(AllCleanRule.class)
4960
class TestMarkdown {
5061

62+
private record DiagnosticSpy(AtomicInteger calls,
63+
AtomicReference<CompletableFuture<DocumentDiagnosticReport>> lastFuture, LanguageServer server) {
64+
}
65+
66+
private static DiagnosticSpy newDiagnosticSpy() {
67+
final var calls = new AtomicInteger();
68+
final var lastFuture = new AtomicReference<CompletableFuture<DocumentDiagnosticReport>>();
69+
70+
final Object textDocumentService = Proxy.newProxyInstance(TestMarkdown.class.getClassLoader(),
71+
new Class[] { org.eclipse.lsp4j.services.TextDocumentService.class }, (proxy, method, args) -> {
72+
if ("diagnostic".equals(method.getName()) && args != null && args.length == 1
73+
&& args[0] instanceof DocumentDiagnosticParams) {
74+
calls.incrementAndGet();
75+
final var fut = new CompletableFuture<DocumentDiagnosticReport>();
76+
lastFuture.set(fut);
77+
return fut;
78+
}
79+
if (CompletableFuture.class.isAssignableFrom(method.getReturnType())) {
80+
return CompletableFuture.completedFuture(null);
81+
}
82+
return null;
83+
});
84+
85+
final InvocationHandler serverHandler = (proxy, method, args) -> (switch (method.getName()) {
86+
case "getTextDocumentService" -> textDocumentService;
87+
case "getWorkspaceService" -> null;
88+
case "initialize", "shutdown" -> CompletableFuture.completedFuture(null);
89+
case "exit" -> null;
90+
default -> null;
91+
});
92+
93+
final var server = (LanguageServer) Proxy.newProxyInstance(TestMarkdown.class.getClassLoader(),
94+
new Class[] { LanguageServer.class }, serverHandler);
95+
return new DiagnosticSpy(calls, lastFuture, server);
96+
}
97+
98+
private static boolean waitUpTo(final long timeoutMs, final BooleanSupplier condition)
99+
throws InterruptedException {
100+
final long deadline = System.currentTimeMillis() + timeoutMs;
101+
while (System.currentTimeMillis() < deadline) {
102+
if (condition.getAsBoolean())
103+
return true;
104+
Thread.sleep(20);
105+
}
106+
return condition.getAsBoolean();
107+
}
108+
109+
@Test
110+
void refreshDiagnosticsDoesNothingWhenNoMarkdownBuffersOpen() throws Exception {
111+
final var project = ResourcesPlugin.getWorkspace().getRoot()
112+
.getProject(getClass().getName() + ".nobuf." + System.nanoTime());
113+
project.create(null);
114+
project.open(null);
115+
116+
final IFile file = project.getFile("doc.md");
117+
file.create("# Title\n".getBytes(StandardCharsets.UTF_8), true, false, null);
118+
119+
final var spy = newDiagnosticSpy();
120+
MarkdownDiagnosticsManager.refreshAllOpenMarkdownFiles(spy.server());
121+
122+
// Wait for debounce window + execution time; should still do nothing since no
123+
// Markdown buffer is open.
124+
assertTrue(waitUpTo(2_000, () -> spy.calls().get() == 0),
125+
"Diagnostic requests should not be made when no Markdown buffers are open");
126+
}
127+
128+
@Test
129+
void refreshDiagnosticsIsDedupedWhileInFlight() throws Exception {
130+
final var project = ResourcesPlugin.getWorkspace().getRoot()
131+
.getProject(getClass().getName() + ".dedupe." + System.nanoTime());
132+
project.create(null);
133+
project.open(null);
134+
135+
final IFile file = project.getFile("open.md");
136+
file.create("# Title\n".getBytes(StandardCharsets.UTF_8), true, false, null);
137+
138+
final var mgr = FileBuffers.getTextFileBufferManager();
139+
mgr.connect(file.getFullPath(), LocationKind.IFILE, null);
140+
try {
141+
final var spy = newDiagnosticSpy();
142+
143+
MarkdownDiagnosticsManager.refreshAllOpenMarkdownFiles(spy.server());
144+
assertTrue(waitUpTo(2_000, () -> spy.calls().get() == 1),
145+
"Expected exactly one diagnostic request for the open Markdown buffer");
146+
147+
// Trigger another refresh while the first diagnostic is still in-flight; should
148+
// not start a second diagnostic.
149+
MarkdownDiagnosticsManager.refreshAllOpenMarkdownFiles(spy.server());
150+
assertTrue(waitUpTo(2_000, () -> spy.calls().get() == 1), "Expected in-flight refresh to be de-duplicated");
151+
152+
final var fut = spy.lastFuture().get();
153+
if (fut != null && !fut.isDone()) {
154+
fut.complete(null);
155+
}
156+
} finally {
157+
mgr.disconnect(file.getFullPath(), LocationKind.IFILE, null);
158+
}
159+
}
160+
51161
@Test
52162
void diagnosticsCoverTypicalMarkdownIssues() throws Exception {
53163
var project = ResourcesPlugin.getWorkspace().getRoot().getProject(getClass().getName() + System.nanoTime());

0 commit comments

Comments
 (0)