diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/internal/DocumentOffsetAsyncCache.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/internal/DocumentOffsetAsyncCache.java
new file mode 100644
index 000000000..d519b8d16
--- /dev/null
+++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/internal/DocumentOffsetAsyncCache.java
@@ -0,0 +1,141 @@
+/*******************************************************************************
+ * Copyright (c) 2025 Vegard IT GmbH and others.
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Sebastian Thomschke (Vegard IT GmbH) - initial implementation.
+ *******************************************************************************/
+package org.eclipse.lsp4e.internal;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IDocumentExtension4;
+
+/**
+ * Generic, per-document+offset cache for asynchronous results that avoids
+ * starting the same work twice by sharing a single running task.
+ *
+ *
+ * Features:
+ *
Weakly keys by {@link IDocument} to avoid memory leaks.
+ * Per-document concurrent maps for thread-safe access from UI and
+ * background.
+ * Eviction: TTL-based using {@link System#nanoTime()} and document-change
+ * invalidation when a stable modification stamp is available.
+ * In-flight de-duplication: only one running task per document+offset.
+ * Stale-result protection: if the document changes while a value is being
+ * computed, the result is delivered to callers but is not cached.
+ */
+public final class DocumentOffsetAsyncCache {
+
+ private record Entry(V value, long createdNanos, long docModStamp) {
+ boolean stale(final long ttlNanos, final long currentDocStamp) {
+ return System.nanoTime() - createdNanos > ttlNanos //
+ // Invalidate when we can confidently detect a document change
+ || (docModStamp != IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP
+ && currentDocStamp != IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP
+ && docModStamp != currentDocStamp);
+ }
+ }
+
+ private final Map>> cache = Collections
+ .synchronizedMap(new WeakHashMap<>());
+ private final Map>> inFlight = Collections
+ .synchronizedMap(new WeakHashMap<>());
+
+ private final long ttlNanos;
+
+ public DocumentOffsetAsyncCache(final Duration ttl) {
+ this.ttlNanos = TimeUnit.MILLISECONDS.toNanos(ttl.toMillis());
+ }
+
+ /**
+ * Returns cached value if present and valid; otherwise returns the single
+ * running task or starts one via {@code supplier}. A value is valid if it has
+ * not expired by TTL and (when stamps are available) matches the current
+ * document stamp. Results computed for an older stamp are not cached.
+ */
+ public CompletableFuture computeIfAbsent(final IDocument doc, final int offset,
+ final Supplier> supplier) {
+ // Fast path: return a completed future if a fresh value is already cached
+ final @Nullable V cachedNow = getNow(doc, offset);
+ if (cachedNow != null)
+ return CompletableFuture.completedFuture(cachedNow);
+
+ final ConcurrentMap> byOffset = inFlight.computeIfAbsent(doc,
+ d -> new ConcurrentHashMap<>());
+ return byOffset.computeIfAbsent(offset, k -> {
+ final long startStamp = DocumentUtil.getDocumentModificationStamp(doc);
+ final CompletableFuture cf = supplier.get();
+ cf.whenComplete((v, t) -> {
+ // Always clean up the in-flight entry by key. Only one future exists
+ // per offset due to computeIfAbsent, so this is safe and avoids capturing
+ // a specific future instance.
+ byOffset.remove(offset);
+ if (t == null && v != null) {
+ final long nowStamp = DocumentUtil.getDocumentModificationStamp(doc);
+ if (startStamp == IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP
+ || nowStamp == IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP || nowStamp == startStamp) {
+ put(doc, offset, v);
+ }
+ }
+ });
+ return cf;
+ });
+ }
+
+ /**
+ * @return the cached value if present and valid; removes and returns null if
+ * TTL expired or the document stamp changed.
+ */
+ public @Nullable V getNow(final IDocument doc, final int offset) {
+ final ConcurrentMap> byOffset = cache.get(doc);
+ if (byOffset == null)
+ return null;
+
+ final Entry e = byOffset.get(offset);
+ if (e == null)
+ return null;
+
+ final long nowStamp = DocumentUtil.getDocumentModificationStamp(doc);
+ if (e.stale(ttlNanos, nowStamp)) {
+ byOffset.remove(offset, e);
+ return null;
+ }
+ return e.value;
+ }
+
+ public void invalidate(final IDocument doc) {
+ cache.remove(doc); // synchronizedMap handles its own locking
+ final var map = inFlight.remove(doc); // remove returns the per-doc map, if any
+ if (map != null) {
+ map.values().forEach(f -> f.cancel(true));
+ }
+ }
+
+ /**
+ * Stores a value tagged with the current document modification stamp.
+ */
+ public void put(final IDocument doc, final int offset, final V value) {
+ cache.compute(doc, (d, byOffset) -> {
+ final ConcurrentMap> map = byOffset != null ? byOffset : new ConcurrentHashMap<>();
+ final long stamp = DocumentUtil.getDocumentModificationStamp(doc);
+ map.put(offset, new Entry<>(value, System.nanoTime(), stamp));
+ return map;
+ });
+ }
+}
diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/declaration/DeferredOpenDeclarationHyperlink.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/declaration/DeferredOpenDeclarationHyperlink.java
new file mode 100644
index 000000000..bae1e314b
--- /dev/null
+++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/declaration/DeferredOpenDeclarationHyperlink.java
@@ -0,0 +1,118 @@
+/*******************************************************************************
+ * Copyright (c) 2025 Vegard IT GmbH and others.
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Sebastian Thomschke (Vegard IT GmbH) - initial implementation.
+ *******************************************************************************/
+package org.eclipse.lsp4e.operations.declaration;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IDocumentExtension4;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.hyperlink.IHyperlink;
+import org.eclipse.lsp4e.LanguageServerPlugin;
+import org.eclipse.lsp4e.internal.DocumentUtil;
+
+/**
+ * An implementation of {@link IHyperlink} which asynchronously opens the link
+ * once the language server has responded. Opening is dismissed if the editor
+ * was closed in the meantime, the document was modified, or the response took
+ * longer than a given timeout.
+ */
+final class DeferredOpenDeclarationHyperlink implements IHyperlink {
+
+ private static final long DEFERRED_OPEN_TIMEOUT_NANOS = TimeUnit.SECONDS.toNanos(5);
+
+ private final ITextViewer viewer;
+ private final IDocument document;
+ private final long documentInitialModificationStamp;
+ private final IRegion region;
+ private final CompletableFuture<@Nullable IHyperlink> future;
+ private final long createdNanos = System.nanoTime();
+
+ DeferredOpenDeclarationHyperlink(final ITextViewer viewer, final IDocument document, final IRegion region,
+ final CompletableFuture<@Nullable IHyperlink> future) {
+ this.viewer = viewer;
+ this.document = document;
+ this.region = region;
+ this.future = future;
+ this.documentInitialModificationStamp = DocumentUtil.getDocumentModificationStamp(document);
+ }
+
+ @Override
+ public IRegion getHyperlinkRegion() {
+ return region;
+ }
+
+ @Override
+ public @Nullable String getTypeLabel() {
+ final var link = getResolvedLink();
+ return link != null ? link.getTypeLabel() : null;
+ }
+
+ @Override
+ public @Nullable String getHyperlinkText() {
+ final var link = getResolvedLink();
+ return link != null ? link.getHyperlinkText() : null;
+ }
+
+ @Override
+ public void open() {
+ future.whenComplete((link, ex) -> {
+ if (ex != null) {
+ LanguageServerPlugin.logError(ex.getLocalizedMessage(), ex);
+ return;
+ }
+ final var widget = viewer.getTextWidget();
+ if (widget == null)
+ return;
+ if (link == null) {
+ LanguageServerPlugin.logWarning("No hyperlink target resolved for Open Declaration"); //$NON-NLS-1$
+ return;
+ }
+ widget.getDisplay().asyncExec(() -> {
+ if (isStale())
+ return;
+ link.open();
+ });
+ });
+ }
+
+ private @Nullable IHyperlink getResolvedLink() {
+ try {
+ return future.getNow(null);
+ } catch (CompletionException ex) {
+ LanguageServerPlugin.logError(ex.getLocalizedMessage(), ex);
+ return null;
+ }
+ }
+
+ private boolean isStale() {
+ // LS response came too late?
+ if (System.nanoTime() - createdNanos > DEFERRED_OPEN_TIMEOUT_NANOS)
+ return true;
+
+ // Editor was closed?
+ final var widget = viewer.getTextWidget();
+ if (widget == null || widget.isDisposed())
+ return true;
+
+ // Document was modified?
+ if (documentInitialModificationStamp != IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP
+ && DocumentUtil.getDocumentModificationStamp(document) != documentInitialModificationStamp)
+ return true;
+
+ return false;
+ }
+}
diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/declaration/DeferredOpenMultiDeclarationHyperlink.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/declaration/DeferredOpenMultiDeclarationHyperlink.java
new file mode 100644
index 000000000..6517de57e
--- /dev/null
+++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/declaration/DeferredOpenMultiDeclarationHyperlink.java
@@ -0,0 +1,138 @@
+/*******************************************************************************
+ * Copyright (c) 2025 Vegard IT GmbH and others.
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Sebastian Thomschke (Vegard IT GmbH) - initial implementation.
+ ******************************************************************************/
+package org.eclipse.lsp4e.operations.declaration;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IDocumentExtension4;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.hyperlink.IHyperlink;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.window.Window;
+import org.eclipse.lsp4e.LanguageServerPlugin;
+import org.eclipse.lsp4e.internal.DocumentUtil;
+import org.eclipse.lsp4e.ui.Messages;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.dialogs.ElementListSelectionDialog;
+
+/**
+ * An implementation of {@link IHyperlink} which asynchronously opens a chooser
+ * of links once the language server has responded. Opening is dismissed if the
+ * editor was closed in the meantime, the document was modified, or the response
+ * took longer than a given timeout.
+ */
+final class DeferredOpenMultiDeclarationHyperlink implements IHyperlink {
+
+ private static final long DEFERRED_OPEN_TIMEOUT_NANOS = TimeUnit.SECONDS.toNanos(5);
+
+ private final ITextViewer viewer;
+ private final IDocument document;
+ private final long documentInitialModificationStamp;
+ private final IRegion region;
+ private final CompletableFuture extends List extends IHyperlink>> future;
+ private final long createdNanos = System.nanoTime();
+
+ DeferredOpenMultiDeclarationHyperlink(final ITextViewer viewer, final IDocument document, final IRegion region,
+ final CompletableFuture extends List extends IHyperlink>> future) {
+ this.viewer = viewer;
+ this.document = document;
+ this.region = region;
+ this.future = future;
+ this.documentInitialModificationStamp = DocumentUtil.getDocumentModificationStamp(document);
+ }
+
+ @Override
+ public IRegion getHyperlinkRegion() {
+ return region;
+ }
+
+ @Override
+ public @Nullable String getTypeLabel() {
+ return "Open Declaration (resolving...)"; //$NON-NLS-1$
+ }
+
+ @Override
+ public @Nullable String getHyperlinkText() {
+ return "Open Declaration (resolving...)"; //$NON-NLS-1$
+ }
+
+ @Override
+ public void open() {
+ future.whenComplete((links, ex) -> {
+ if (ex != null) {
+ LanguageServerPlugin.logError(ex.getLocalizedMessage(), ex);
+ return;
+ }
+ final var widget = viewer.getTextWidget();
+ if (widget == null)
+ return;
+ if (links.isEmpty()) {
+ LanguageServerPlugin.logWarning("No hyperlink targets resolved for Open Declaration"); //$NON-NLS-1$
+ return;
+ }
+ widget.getDisplay().asyncExec(() -> {
+ if (isStale())
+ return;
+
+ if (links.size() == 1) {
+ links.get(0).open();
+ return;
+ }
+
+ final Shell shell = widget.getShell();
+ final var dialog = new ElementListSelectionDialog(shell, new LabelProvider() {
+ @Override
+ public String getText(final @Nullable Object element) {
+ if (element instanceof final IHyperlink link) {
+ final String text = link.getHyperlinkText();
+ return text != null ? text : link.getTypeLabel();
+ }
+ return element == null ? "" : element.toString(); //$NON-NLS-1$
+ }
+ });
+ dialog.setTitle(Messages.declarationHyperlinkLabel);
+ dialog.setMessage("Select a target:"); //$NON-NLS-1$
+ dialog.setElements(links.toArray());
+ dialog.setMultipleSelection(false);
+ if (dialog.open() == Window.OK) {
+ Object result = dialog.getFirstResult();
+ if (result instanceof IHyperlink link) {
+ link.open();
+ }
+ }
+ });
+ });
+ }
+
+ private boolean isStale() {
+ // LS response came too late?
+ if (System.nanoTime() - createdNanos > DEFERRED_OPEN_TIMEOUT_NANOS)
+ return true;
+
+ // Editor was closed?
+ final var widget = viewer.getTextWidget();
+ if (widget == null || widget.isDisposed())
+ return true;
+
+ // Document was modified?
+ if (documentInitialModificationStamp != IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP
+ && DocumentUtil.getDocumentModificationStamp(document) != documentInitialModificationStamp)
+ return true;
+
+ return false;
+ }
+}
diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/declaration/OpenDeclarationHyperlinkDetector.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/declaration/OpenDeclarationHyperlinkDetector.java
index b53aef239..e08998172 100644
--- a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/declaration/OpenDeclarationHyperlinkDetector.java
+++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/declaration/OpenDeclarationHyperlinkDetector.java
@@ -10,14 +10,15 @@
* Mickael Istria (Red Hat Inc.) - initial implementation
* Michał Niewrzał (Rogue Wave Software Inc.) - hyperlink range detection
* Lucas Bullen (Red Hat Inc.) - [Bug 517428] Requests sent before initialization
+ * Sebastian Thomschke (Vegard IT GmbH) - avoid UI freeze when working with slow language servers
*******************************************************************************/
package org.eclipse.lsp4e.operations.declaration;
-import java.util.Collection;
-import java.util.Collections;
+import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -34,7 +35,7 @@
import org.eclipse.lsp4e.LSPEclipseUtils;
import org.eclipse.lsp4e.LanguageServerPlugin;
import org.eclipse.lsp4e.LanguageServers;
-import org.eclipse.lsp4e.internal.Pair;
+import org.eclipse.lsp4e.internal.DocumentOffsetAsyncCache;
import org.eclipse.lsp4e.ui.Messages;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.LocationLink;
@@ -45,8 +46,19 @@
public class OpenDeclarationHyperlinkDetector extends AbstractHyperlinkDetector {
+ private static final long UI_BLOCKING_BUDGET_MS = 200;
+
+ @NonNullByDefault({})
+ private static record LabeledLocations(String label,
+ @Nullable Either, List extends LocationLink>> locations) {
+ }
+
+ private static final DocumentOffsetAsyncCache> CACHE = new DocumentOffsetAsyncCache<>(
+ Duration.ofSeconds(10));
+
@Override
- public IHyperlink @Nullable [] detectHyperlinks(ITextViewer textViewer, IRegion region, boolean canShowMultipleHyperlinks) {
+ public IHyperlink @Nullable [] detectHyperlinks(ITextViewer textViewer, IRegion region,
+ boolean canShowMultipleHyperlinks) {
final IDocument document = textViewer.getDocument();
if (document == null) {
return null;
@@ -58,32 +70,57 @@ public class OpenDeclarationHyperlinkDetector extends AbstractHyperlinkDetector
LanguageServerPlugin.logError(e);
return null;
}
- final var allLinks = new LinkedHashMap,LSBasedHyperlink>();
+
+ final int offset = region.getOffset();
+
+ final CompletableFuture> request = CACHE.computeIfAbsent(document, offset, () -> {
+ var definitions = LanguageServers.forDocument(document)
+ .withCapability(ServerCapabilities::getDefinitionProvider)
+ .collectAll(ls -> ls.getTextDocumentService().definition(LSPEclipseUtils.toDefinitionParams(params))
+ .thenApply(l -> new LabeledLocations(Messages.definitionHyperlinkLabel, l)));
+ final var declarations = LanguageServers.forDocument(document)
+ .withCapability(ServerCapabilities::getDeclarationProvider).collectAll(
+ ls -> ls.getTextDocumentService().declaration(LSPEclipseUtils.toDeclarationParams(params))
+ .thenApply(l -> new LabeledLocations(Messages.declarationHyperlinkLabel, l)));
+ final var typeDefinitions = LanguageServers.forDocument(document)
+ .withCapability(ServerCapabilities::getTypeDefinitionProvider)
+ .collectAll(ls -> ls.getTextDocumentService()
+ .typeDefinition(LSPEclipseUtils.toTypeDefinitionParams(params))
+ .thenApply(l -> new LabeledLocations(Messages.typeDefinitionHyperlinkLabel, l)));
+ final var implementations = LanguageServers.forDocument(document)
+ .withCapability(ServerCapabilities::getImplementationProvider)
+ .collectAll(ls -> ls.getTextDocumentService()
+ .implementation(LSPEclipseUtils.toImplementationParams(params))
+ .thenApply(l -> new LabeledLocations(Messages.implementationHyperlinkLabel, l)));
+
+ CompletableFuture> combined = LanguageServers.addAll(
+ LanguageServers.addAll(LanguageServers.addAll(definitions, declarations), typeDefinitions),
+ implementations);
+ return combined.thenApply(locations -> toHyperlinks(document, region, locations));
+ });
+
try {
- var definitions = LanguageServers.forDocument(document).withCapability(ServerCapabilities::getDefinitionProvider)
- .collectAll(ls -> ls.getTextDocumentService().definition(LSPEclipseUtils.toDefinitionParams(params)).thenApply(l -> Pair.of(Messages.definitionHyperlinkLabel, l)));
- var declarations = LanguageServers.forDocument(document).withCapability(ServerCapabilities::getDeclarationProvider)
- .collectAll(ls -> ls.getTextDocumentService().declaration(LSPEclipseUtils.toDeclarationParams(params)).thenApply(l -> Pair.of(Messages.declarationHyperlinkLabel, l)));
- var typeDefinitions = LanguageServers.forDocument(document).withCapability(ServerCapabilities::getTypeDefinitionProvider)
- .collectAll(ls -> ls.getTextDocumentService().typeDefinition(LSPEclipseUtils.toTypeDefinitionParams(params)).thenApply(l -> Pair.of(Messages.typeDefinitionHyperlinkLabel, l)));
- var implementations = LanguageServers.forDocument(document).withCapability(ServerCapabilities::getImplementationProvider)
- .collectAll(ls -> ls.getTextDocumentService().implementation(LSPEclipseUtils.toImplementationParams(params)).thenApply(l -> Pair.of(Messages.implementationHyperlinkLabel, l)));
- LanguageServers.addAll(LanguageServers.addAll(LanguageServers.addAll(definitions, declarations), typeDefinitions), implementations)
- .get(800, TimeUnit.MILLISECONDS)
- .stream().flatMap(locations -> toHyperlinks(document, region, locations.first(), locations.second()).stream())
- .forEach(link -> allLinks.putIfAbsent(link.getLocation(), link));
- } catch (ExecutionException e) {
- LanguageServerPlugin.logError(e);
- } catch (InterruptedException e) {
- LanguageServerPlugin.logError(e);
+ // Try to get a quick result within the UI budget; keep UI responsive.
+ final List links = request.get(UI_BLOCKING_BUDGET_MS, TimeUnit.MILLISECONDS);
+ return links.isEmpty() ? null : links.toArray(IHyperlink[]::new);
+ } catch (final ExecutionException ex) {
+ LanguageServerPlugin.logError(ex);
+ } catch (final InterruptedException ex) {
+ LanguageServerPlugin.logError(ex);
Thread.currentThread().interrupt();
- } catch (TimeoutException e) {
- LanguageServerPlugin.logWarning("Could not detect hyperlinks due to timeout after 800 milliseconds"); //$NON-NLS-1$
- }
- if (allLinks.isEmpty()) {
- return null;
+ } catch (final TimeoutException ex) {
+ if (canShowMultipleHyperlinks) {
+ return new IHyperlink[] { new DeferredOpenMultiDeclarationHyperlink(textViewer, document,
+ findWord(document, region), request) };
+ } else {
+ final CompletableFuture<@Nullable IHyperlink> firstLink = request
+ .thenApply(links -> !links.isEmpty() ? links.get(0) : null);
+ return new IHyperlink[] { new DeferredOpenDeclarationHyperlink(textViewer, document,
+ findWord(document, region), firstLink) };
+ }
}
- return allLinks.values().toArray(IHyperlink[]::new);
+
+ return null;
}
/**
@@ -93,22 +130,27 @@ public class OpenDeclarationHyperlinkDetector extends AbstractHyperlinkDetector
* the document
* @param linkRegion
* the region
- * @param locationType
- * the location type
* @param locations
* the LSP locations
*/
- private static Collection toHyperlinks(IDocument document, IRegion region,
- String locationType, @NonNullByDefault({}) Either, List extends LocationLink>> locations) {
- if (locations == null) {
- return Collections.emptyList();
+ private static List toHyperlinks(final IDocument doc, final IRegion region,
+ final List locations) {
+ final var allLinks = new LinkedHashMap, LSBasedHyperlink>();
+ for (final LabeledLocations locs : locations) {
+ final var either = locs.locations();
+ if (either == null)
+ continue;
+ if (either.isLeft()) {
+ either.getLeft().stream().filter(Objects::nonNull)
+ .map(loc -> new LSBasedHyperlink(loc, findWord(doc, region), locs.label()))
+ .forEach(h -> allLinks.putIfAbsent(h.getLocation(), h));
+ } else {
+ either.getRight().stream().filter(Objects::nonNull).map(
+ locLink -> new LSBasedHyperlink(locLink, getSelectedRegion(doc, region, locLink), locs.label()))
+ .forEach(h -> allLinks.putIfAbsent(h.getLocation(), h));
+ }
}
- return locations.map(//
- l -> l.stream().filter(Objects::nonNull)
- .map(location -> new LSBasedHyperlink(location, findWord(document, region), locationType))
- .toList(),
- r -> r.stream().filter(Objects::nonNull).map(locationLink -> new LSBasedHyperlink(locationLink,
- getSelectedRegion(document, region, locationLink), locationType)).toList());
+ return allLinks.values().stream().toList();
}
/**