1616import static org .eclipse .wildwebdeveloper .markdown .MarkdownDiagnosticsManager .MARKDOWN_MARKER_TYPE ;
1717import static org .junit .jupiter .api .Assertions .assertTrue ;
1818
19+ import java .lang .reflect .InvocationHandler ;
20+ import java .lang .reflect .Proxy ;
1921import java .nio .charset .StandardCharsets ;
2022import java .util .ArrayList ;
2123import java .util .Arrays ;
2224import java .util .Collections ;
25+ import java .util .concurrent .CompletableFuture ;
26+ import java .util .concurrent .atomic .AtomicInteger ;
2327import java .util .concurrent .atomic .AtomicReference ;
28+ import java .util .function .BooleanSupplier ;
2429import java .util .stream .Collectors ;
2530
31+ import org .eclipse .core .filebuffers .FileBuffers ;
32+ import org .eclipse .core .filebuffers .LocationKind ;
2633import org .eclipse .core .resources .IFile ;
2734import org .eclipse .core .resources .IMarker ;
2835import org .eclipse .core .resources .IResource ;
3441import org .eclipse .lsp4e .LanguageServerWrapper ;
3542import org .eclipse .lsp4e .LanguageServiceAccessor ;
3643import 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 ;
3747import org .eclipse .ui .PlatformUI ;
3848import org .eclipse .ui .editors .text .TextEditor ;
3949import org .eclipse .ui .ide .IDE ;
4050import org .eclipse .ui .tests .harness .util .DisplayHelper ;
4151import org .eclipse .wildwebdeveloper .Activator ;
52+ import org .eclipse .wildwebdeveloper .markdown .MarkdownDiagnosticsManager ;
4253import org .junit .jupiter .api .Test ;
4354import org .junit .jupiter .api .extension .ExtendWith ;
4455
@@ -48,6 +59,105 @@ record MarkdownTest(String markdown, String messagePattern, int severity) {
4859@ ExtendWith (AllCleanRule .class )
4960class 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