Skip to content

Commit 90347ed

Browse files
authored
feat: auto cancel superseded codemining requests per document (#1410)
Adresses #236
1 parent 1dc6959 commit 90347ed

5 files changed

Lines changed: 219 additions & 106 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Vegard IT GmbH and others.
3+
* This program and the accompanying materials are made
4+
* available under the terms of the Eclipse Public License 2.0
5+
* which is available at https://www.eclipse.org/legal/epl-2.0/
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Sebastian Thomschke (Vegard IT GmbH) - initial implementation
11+
*******************************************************************************/
12+
package org.eclipse.lsp4e.test.internal;
13+
14+
import static org.junit.jupiter.api.Assertions.*;
15+
16+
import java.util.List;
17+
import java.util.concurrent.CompletableFuture;
18+
19+
import org.eclipse.core.resources.IFile;
20+
import org.eclipse.core.runtime.NullProgressMonitor;
21+
import org.eclipse.jdt.annotation.Nullable;
22+
import org.eclipse.jface.text.IDocument;
23+
import org.eclipse.jface.text.ITextViewer;
24+
import org.eclipse.jface.text.codemining.ICodeMining;
25+
import org.eclipse.lsp4e.internal.AbstractLSPCodeMiningProvider;
26+
import org.eclipse.lsp4e.test.utils.AbstractTestWithProject;
27+
import org.eclipse.lsp4e.test.utils.TestUtils;
28+
import org.eclipse.lsp4j.TextDocumentIdentifier;
29+
import org.junit.jupiter.api.Test;
30+
31+
/**
32+
* Verifies that {@link AbstractLSPCodeMiningProvider} cancels the previous
33+
* in-flight request for the same document when a new one starts.
34+
*/
35+
class AbstractLSPCodeMiningProviderTest extends AbstractTestWithProject {
36+
37+
private static final class StubProvider extends AbstractLSPCodeMiningProvider {
38+
@Override
39+
protected @Nullable CompletableFuture<List<? extends ICodeMining>> doProvideCodeMinings(IDocument doc,
40+
TextDocumentIdentifier docId) {
41+
return new CompletableFuture<>();
42+
}
43+
}
44+
45+
@Test
46+
void cancelsPreviousRequestForSameDocument() throws Exception {
47+
IFile file = TestUtils.createUniqueTestFile(project, "txt", "content");
48+
ITextViewer viewer = TestUtils.openTextViewer(file);
49+
50+
var provider = new StubProvider();
51+
52+
var first = provider.provideCodeMinings(viewer, new NullProgressMonitor());
53+
assertNotNull(first);
54+
assertFalse(first.isCancelled(), "first request should start uncancelled");
55+
56+
var second = provider.provideCodeMinings(viewer, new NullProgressMonitor());
57+
assertNotNull(second);
58+
59+
assertTrue(first.isCancelled(), "previous request should be cancelled when a new one starts");
60+
assertFalse(second.isCancelled(), "new request should remain active");
61+
}
62+
63+
@Test
64+
void keepsRequestsIndependentAcrossDocuments() throws Exception {
65+
IFile file1 = TestUtils.createUniqueTestFile(project, "txt", "one");
66+
IFile file2 = TestUtils.createUniqueTestFile(project, "txt", "two");
67+
ITextViewer viewer1 = TestUtils.openTextViewer(file1);
68+
ITextViewer viewer2 = TestUtils.openTextViewer(file2);
69+
70+
var provider = new StubProvider();
71+
72+
var first = provider.provideCodeMinings(viewer1, new NullProgressMonitor());
73+
var second = provider.provideCodeMinings(viewer2, new NullProgressMonitor());
74+
75+
assertNotNull(first);
76+
assertNotNull(second);
77+
assertFalse(first.isCancelled(), "request for doc1 must remain active when doc2 starts");
78+
assertFalse(second.isCancelled(), "request for doc2 must start active");
79+
}
80+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Vegard IT GmbH and others.
3+
* This program and the accompanying materials are made
4+
* available under the terms of the Eclipse Public License 2.0
5+
* which is available at https://www.eclipse.org/legal/epl-2.0/
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Sebastian Thomschke (Vegard IT GmbH) - initial implementation.
11+
*******************************************************************************/
12+
package org.eclipse.lsp4e.internal;
13+
14+
import java.net.URI;
15+
import java.util.List;
16+
import java.util.concurrent.CompletableFuture;
17+
import java.util.concurrent.ConcurrentHashMap;
18+
import java.util.concurrent.ConcurrentMap;
19+
20+
import org.eclipse.core.runtime.IProgressMonitor;
21+
import org.eclipse.jdt.annotation.Nullable;
22+
import org.eclipse.jface.text.IDocument;
23+
import org.eclipse.jface.text.ITextViewer;
24+
import org.eclipse.jface.text.codemining.AbstractCodeMiningProvider;
25+
import org.eclipse.jface.text.codemining.ICodeMining;
26+
import org.eclipse.lsp4e.LSPEclipseUtils;
27+
import org.eclipse.lsp4j.TextDocumentIdentifier;
28+
29+
/**
30+
* Base class for LSP-backed code mining providers that:
31+
* <ul>
32+
* <li>compute code minings asynchronously per document using LSP requests</li>
33+
* <li>track at most one in-flight request per document and cancel the previous
34+
* one when a new computation starts</li>
35+
* </ul>
36+
*/
37+
public abstract class AbstractLSPCodeMiningProvider extends AbstractCodeMiningProvider {
38+
39+
private final ConcurrentMap<IDocument, CompletableFuture<List<? extends ICodeMining>>> pendingRequests = new ConcurrentHashMap<>();
40+
41+
/**
42+
* Computes code minings for the given document.
43+
*
44+
* @return a future producing the list of code minings, or {@code null} if no
45+
* code minings are available
46+
*/
47+
protected abstract @Nullable CompletableFuture<List<? extends ICodeMining>> doProvideCodeMinings(IDocument doc,
48+
TextDocumentIdentifier docId);
49+
50+
@Override
51+
public final @Nullable CompletableFuture<List<? extends ICodeMining>> provideCodeMinings(final ITextViewer viewer,
52+
final IProgressMonitor monitor) {
53+
final IDocument document = viewer.getDocument();
54+
if (document == null) {
55+
return null;
56+
}
57+
58+
final URI docURI = LSPEclipseUtils.toUri(document);
59+
if (docURI == null)
60+
return null;
61+
62+
final TextDocumentIdentifier docId = LSPEclipseUtils.toTextDocumentIdentifier(docURI);
63+
64+
final var current = doProvideCodeMinings(document, docId);
65+
final CompletableFuture<List<? extends ICodeMining>> previous;
66+
if (current == null) {
67+
previous = pendingRequests.remove(document);
68+
} else {
69+
previous = pendingRequests.put(document, current);
70+
}
71+
if (previous != null && !previous.isDone()) {
72+
previous.cancel(true);
73+
}
74+
75+
return current;
76+
}
77+
}

org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/codelens/CodeLensProvider.java

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,36 @@
88
*******************************************************************************/
99
package org.eclipse.lsp4e.operations.codelens;
1010

11-
import java.net.URI;
1211
import java.util.List;
1312
import java.util.Objects;
1413
import java.util.concurrent.CompletableFuture;
1514

16-
import org.eclipse.core.runtime.IProgressMonitor;
1715
import org.eclipse.jdt.annotation.Nullable;
1816
import org.eclipse.jface.text.BadLocationException;
1917
import org.eclipse.jface.text.IDocument;
20-
import org.eclipse.jface.text.ITextViewer;
21-
import org.eclipse.jface.text.codemining.AbstractCodeMiningProvider;
2218
import org.eclipse.jface.text.codemining.ICodeMining;
23-
import org.eclipse.lsp4e.LSPEclipseUtils;
2419
import org.eclipse.lsp4e.LanguageServerPlugin;
2520
import org.eclipse.lsp4e.LanguageServerWrapper;
2621
import org.eclipse.lsp4e.LanguageServers;
2722
import org.eclipse.lsp4e.LanguageServers.LanguageServerDocumentExecutor;
23+
import org.eclipse.lsp4e.internal.AbstractLSPCodeMiningProvider;
2824
import org.eclipse.lsp4j.CodeLens;
2925
import org.eclipse.lsp4j.CodeLensParams;
26+
import org.eclipse.lsp4j.TextDocumentIdentifier;
3027

31-
public class CodeLensProvider extends AbstractCodeMiningProvider {
28+
public class CodeLensProvider extends AbstractLSPCodeMiningProvider {
3229

33-
private @Nullable CompletableFuture<List<? extends ICodeMining>> provideCodeMinings(IDocument document) {
34-
URI docURI = LSPEclipseUtils.toUri(document);
35-
if (docURI != null) {
36-
final var param = new CodeLensParams(LSPEclipseUtils.toTextDocumentIdentifier(docURI));
37-
LanguageServerDocumentExecutor executor = LanguageServers.forDocument(document)
38-
.withFilter(sc -> sc.getCodeLensProvider() != null);
39-
return executor
40-
.collectAll((w, ls) -> ls.getTextDocumentService().codeLens(param)
41-
.thenApply(codeLenses -> LanguageServers.streamSafely(codeLenses)
42-
.map(codeLens -> toCodeMining(document, w, codeLens)).filter(Objects::nonNull)))
43-
.thenApply(result -> result.stream().flatMap(s -> s).toList());
44-
} else {
45-
return null;
46-
}
30+
@Override
31+
protected @Nullable CompletableFuture<List<? extends ICodeMining>> doProvideCodeMinings(IDocument document,
32+
TextDocumentIdentifier docId) {
33+
final var param = new CodeLensParams(docId);
34+
LanguageServerDocumentExecutor executor = LanguageServers.forDocument(document)
35+
.withFilter(sc -> sc.getCodeLensProvider() != null);
36+
return executor
37+
.collectAll((w, ls) -> ls.getTextDocumentService().codeLens(param)
38+
.thenApply(codeLenses -> LanguageServers.streamSafely(codeLenses)
39+
.map(codeLens -> toCodeMining(document, w, codeLens)).filter(Objects::nonNull)))
40+
.thenApply(result -> result.stream().flatMap(s -> s).toList());
4741
}
4842

4943
private @Nullable LSPCodeMining toCodeMining(IDocument document, LanguageServerWrapper languageServerWrapper,
@@ -58,12 +52,4 @@ public class CodeLensProvider extends AbstractCodeMiningProvider {
5852
return null;
5953
}
6054
}
61-
62-
@Override
63-
public @Nullable CompletableFuture<List<? extends ICodeMining>> provideCodeMinings(ITextViewer viewer,
64-
IProgressMonitor monitor) {
65-
IDocument document = viewer.getDocument();
66-
return document != null ? provideCodeMinings(document) : null;
67-
}
68-
6955
}

org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/color/DocumentColorProvider.java

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,21 @@
1111
*/
1212
package org.eclipse.lsp4e.operations.color;
1313

14-
import java.net.URI;
1514
import java.util.HashMap;
1615
import java.util.List;
1716
import java.util.Map;
1817
import java.util.Objects;
1918
import java.util.concurrent.CompletableFuture;
2019
import java.util.function.Function;
2120

22-
import org.eclipse.core.runtime.IProgressMonitor;
2321
import org.eclipse.jdt.annotation.Nullable;
2422
import org.eclipse.jface.text.BadLocationException;
2523
import org.eclipse.jface.text.IDocument;
26-
import org.eclipse.jface.text.ITextViewer;
27-
import org.eclipse.jface.text.codemining.AbstractCodeMiningProvider;
2824
import org.eclipse.jface.text.codemining.ICodeMining;
29-
import org.eclipse.lsp4e.LSPEclipseUtils;
3025
import org.eclipse.lsp4e.LanguageServerPlugin;
3126
import org.eclipse.lsp4e.LanguageServerWrapper;
3227
import org.eclipse.lsp4e.LanguageServers;
28+
import org.eclipse.lsp4e.internal.AbstractLSPCodeMiningProvider;
3329
import org.eclipse.lsp4j.ColorInformation;
3430
import org.eclipse.lsp4j.DocumentColorParams;
3531
import org.eclipse.lsp4j.ServerCapabilities;
@@ -41,34 +37,28 @@
4137
/**
4238
* Consume the 'textDocument/documentColor' request to decorate color references
4339
* in the editor.
44-
*
4540
*/
46-
public class DocumentColorProvider extends AbstractCodeMiningProvider {
41+
public class DocumentColorProvider extends AbstractLSPCodeMiningProvider {
4742

4843
private final Map<RGBA, Color> colorTable;
4944

5045
public DocumentColorProvider() {
5146
colorTable = new HashMap<>();
5247
}
5348

54-
private @Nullable CompletableFuture<List<? extends ICodeMining>> provideCodeMinings(IDocument document) {
55-
URI docURI = LSPEclipseUtils.toUri(document);
56-
57-
if (docURI != null) {
58-
final var textDocumentIdentifier = LSPEclipseUtils.toTextDocumentIdentifier(docURI);
59-
final var param = new DocumentColorParams(textDocumentIdentifier);
60-
return LanguageServers.forDocument(document)
61-
.withCapability(ServerCapabilities::getColorProvider)
62-
.collectAll(
63-
// Need to do some of the result processing inside the function we supply to collectAll(...)
64-
// as need the LSW to construct the ColorInformationMining
65-
(wrapper, ls) -> ls.getTextDocumentService().documentColor(param)
66-
.thenApply(colors -> LanguageServers.streamSafely(colors)
67-
.map(color -> toMining(color, document, textDocumentIdentifier, wrapper))))
68-
.thenApply(res -> res.stream().flatMap(Function.identity()).filter(Objects::nonNull).toList());
69-
} else {
70-
return null;
71-
}
49+
@Override
50+
protected @Nullable CompletableFuture<List<? extends ICodeMining>> doProvideCodeMinings(IDocument document,
51+
TextDocumentIdentifier docId) {
52+
final var param = new DocumentColorParams(docId);
53+
return LanguageServers.forDocument(document)
54+
.withCapability(ServerCapabilities::getColorProvider)
55+
.collectAll(
56+
// Need to do some of the result processing inside the function we supply to collectAll(...)
57+
// as need the LSW to construct the ColorInformationMining
58+
(wrapper, ls) -> ls.getTextDocumentService().documentColor(param)
59+
.thenApply(colors -> LanguageServers.streamSafely(colors)
60+
.map(color -> toMining(color, document, docId, wrapper))))
61+
.thenApply(res -> res.stream().flatMap(Function.identity()).filter(Objects::nonNull).toList());
7262
}
7363

7464
private @Nullable ColorInformationMining toMining(ColorInformation color, IDocument document, TextDocumentIdentifier textDocumentIdentifier, LanguageServerWrapper wrapper) {
@@ -82,13 +72,6 @@ public DocumentColorProvider() {
8272
return null;
8373
}
8474

85-
@Override
86-
public @Nullable CompletableFuture<List<? extends ICodeMining>> provideCodeMinings(ITextViewer viewer,
87-
IProgressMonitor monitor) {
88-
IDocument document = viewer.getDocument();
89-
return document != null ? provideCodeMinings(document) : null;
90-
}
91-
9275
/**
9376
* Returns the color from the given rgba.
9477
*

0 commit comments

Comments
 (0)