Skip to content

Commit 969d796

Browse files
committed
perf(highlight): dedupe LSP requests via cache and 75ms debounce
1 parent d142f58 commit 969d796

2 files changed

Lines changed: 80 additions & 10 deletions

File tree

org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/declaration/OpenDeclarationHyperlinkDetector.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,11 @@ private static record LabeledLocations(String label,
7171
return null;
7272
}
7373

74-
final int offset = region.getOffset();
74+
// Normalize cache key to the start of the word to avoid cache misses when the
75+
// mouse moves within the same symbol.
76+
final int cacheKeyOffset = findWord(document, region).getOffset();
7577

76-
final CompletableFuture<List<LSBasedHyperlink>> request = CACHE.computeIfAbsent(document, offset, () -> {
78+
final CompletableFuture<List<LSBasedHyperlink>> request = CACHE.computeIfAbsent(document, cacheKeyOffset, () -> {
7779
final var definitions = LanguageServers.forDocument(document)
7880
.withCapability(ServerCapabilities::getDefinitionProvider)
7981
.collectAll(ls -> ls.getTextDocumentService().definition(LSPEclipseUtils.toDefinitionParams(params))
@@ -98,7 +100,7 @@ private static record LabeledLocations(String label,
98100
.thenApply(l -> new LabeledLocations(Messages.implementationHyperlinkLabel, l))
99101
.exceptionally(err -> new LabeledLocations(Messages.implementationHyperlinkLabel, null)));
100102

101-
CompletableFuture<List<LabeledLocations>> combined = LanguageServers.addAll(
103+
final CompletableFuture<List<LabeledLocations>> combined = LanguageServers.addAll(
102104
LanguageServers.addAll(LanguageServers.addAll(definitions, declarations), typeDefinitions),
103105
implementations);
104106
return combined.thenApply(locations -> toHyperlinks(document, region, locations));

org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/highlight/HighlightReconcilingStrategy.java

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import static org.eclipse.lsp4e.internal.NullSafetyHelper.lateNonNull;
1717

1818
import java.net.URI;
19+
import java.time.Duration;
20+
import java.util.ArrayList;
1921
import java.util.HashMap;
2022
import java.util.List;
2123
import java.util.Map.Entry;
@@ -51,6 +53,7 @@
5153
import org.eclipse.lsp4e.LSPEclipseUtils;
5254
import org.eclipse.lsp4e.LanguageServerPlugin;
5355
import org.eclipse.lsp4e.LanguageServers;
56+
import org.eclipse.lsp4e.internal.DocumentOffsetAsyncCache;
5457
import org.eclipse.lsp4j.DocumentHighlight;
5558
import org.eclipse.lsp4j.DocumentHighlightKind;
5659
import org.eclipse.lsp4j.DocumentHighlightParams;
@@ -77,6 +80,16 @@ public class HighlightReconcilingStrategy
7780
private @Nullable IDocument document;
7881
private @Nullable Job highlightJob;
7982

83+
// Debounce to avoid flooding requests while the user moves the caret/mouse.
84+
private static final int HIGHLIGHT_DEBOUNCE_MS = 75;
85+
86+
// Short-lived cache for highlights per document+normalized offset.
87+
private static final DocumentOffsetAsyncCache<List<DocumentHighlight>> HIGHLIGHT_CACHE =
88+
new DocumentOffsetAsyncCache<>(Duration.ofSeconds(10));
89+
90+
// Track the last normalized cache key to avoid canceling identical in-flight work.
91+
private int lastCacheKeyOffset = -1;
92+
8093
/**
8194
* Holds the current occurrence annotations.
8295
*/
@@ -119,7 +132,8 @@ private void updateHighlights(ISelection selection) {
119132
}
120133
highlightJob = Job.createSystem("LSP4E Highlight", //$NON-NLS-1$
121134
(ICoreRunnable)(monitor -> collectHighlights(textSelection.getOffset(), monitor)));
122-
highlightJob.schedule();
135+
// Debounce scheduling slightly to coalesce rapid selection changes
136+
highlightJob.schedule(HIGHLIGHT_DEBOUNCE_MS);
123137
}
124138
}
125139

@@ -188,9 +202,20 @@ private void collectHighlights(int caretOffset, @Nullable IProgressMonitor monit
188202
if (sourceViewer == null || document == null || !enabled || monitor != null && monitor.isCanceled()) {
189203
return;
190204
}
191-
cancel();
205+
206+
// Normalize the cache key to the start of the word/symbol.
207+
final int cacheKeyOffset = normalizedOffset(document, caretOffset);
208+
209+
// Only cancel previous requests if the target symbol actually changed.
210+
if (cacheKeyOffset != lastCacheKeyOffset) {
211+
cancel();
212+
lastCacheKeyOffset = cacheKeyOffset;
213+
}
214+
192215
Position position;
193216
try {
217+
// Send the original caret offset to the LS to preserve behavior
218+
// expected by tests and servers that distinguish positions within a word.
194219
position = LSPEclipseUtils.toPosition(caretOffset, document);
195220
} catch (BadLocationException e) {
196221
LanguageServerPlugin.logError(e);
@@ -202,14 +227,36 @@ private void collectHighlights(int caretOffset, @Nullable IProgressMonitor monit
202227
}
203228
final var identifier = LSPEclipseUtils.toTextDocumentIdentifier(uri);
204229
final var params = new DocumentHighlightParams(identifier, position);
205-
requests = LanguageServers.forDocument(document)
206-
.withCapability(ServerCapabilities::getDocumentHighlightProvider)
207-
.computeAll(languageServer -> languageServer.getTextDocumentService().documentHighlight(params));
208-
requests.forEach(request -> request.thenAcceptAsync(highlights -> {
230+
231+
// Use cache to deduplicate requests to the same symbol for a short period.
232+
final CompletableFuture<List<DocumentHighlight>> request = HIGHLIGHT_CACHE.computeIfAbsent(document,
233+
cacheKeyOffset, () -> {
234+
final var reqs = requests = LanguageServers.forDocument(document)
235+
.withCapability(ServerCapabilities::getDocumentHighlightProvider)
236+
.computeAll(ls -> ls.getTextDocumentService().documentHighlight(params));
237+
final CompletableFuture<?> all = CompletableFuture
238+
.allOf(reqs.stream().map(f -> (CompletableFuture<?>) f).toArray(CompletableFuture[]::new));
239+
return all.thenApply(unused -> {
240+
final var allHighlights = new ArrayList<DocumentHighlight>();
241+
for (final var req : reqs) {
242+
try {
243+
final List<? extends DocumentHighlight> highlight = req.join();
244+
if (highlight != null) {
245+
allHighlights.addAll(highlight);
246+
}
247+
} catch (Exception ex) {
248+
// ignore single-server failures
249+
}
250+
}
251+
return allHighlights;
252+
});
253+
});
254+
255+
request.thenAcceptAsync(highlights -> {
209256
if (monitor == null || !monitor.isCanceled()) {
210257
updateAnnotations(highlights, sourceViewer.getAnnotationModel());
211258
}
212-
}));
259+
});
213260
}
214261

215262
/**
@@ -273,6 +320,27 @@ private Object getLockObject(IAnnotationModel annotationModel) {
273320
return annotationModel;
274321
}
275322

323+
/**
324+
* Compute a stable cache key for the symbol at the given caret offset by
325+
* normalizing to the start of the Unicode identifier under the caret.
326+
*/
327+
private static int normalizedOffset(final IDocument document, final int offset) {
328+
try {
329+
// Walk left to the first non-identifier part, then move right one.
330+
int pos = Math.max(0, offset - 1);
331+
final int docLen = document.getLength();
332+
while (pos >= 0 && pos < docLen) {
333+
if (!Character.isUnicodeIdentifierPart(document.getChar(pos))) {
334+
break;
335+
}
336+
pos--;
337+
}
338+
return Math.min(docLen, pos + 1);
339+
} catch (final BadLocationException ex) {
340+
return offset;
341+
}
342+
}
343+
276344
void removeOccurrenceAnnotations() {
277345
final var sourceViewer = this.sourceViewer;
278346
if(sourceViewer == null)

0 commit comments

Comments
 (0)