diff --git a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/definition/HyperlinkDetectorErrorHandlingTest.java b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/definition/HyperlinkDetectorErrorHandlingTest.java new file mode 100644 index 000000000..558dde6f6 --- /dev/null +++ b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/definition/HyperlinkDetectorErrorHandlingTest.java @@ -0,0 +1,104 @@ +/******************************************************************************* + * 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.test.definition; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.core.resources.IFile; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.Region; +import org.eclipse.jface.text.hyperlink.IHyperlink; +import org.eclipse.lsp4e.operations.declaration.OpenDeclarationHyperlinkDetector; +import org.eclipse.lsp4e.test.utils.AbstractTestWithProject; +import org.eclipse.lsp4e.test.utils.TestUtils; +import org.eclipse.lsp4e.tests.mock.MockLanguageServer; +import org.eclipse.lsp4e.tests.mock.MockTextDocumentService; +import org.eclipse.lsp4j.DeclarationParams; +import org.eclipse.lsp4j.ImplementationParams; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.ServerCapabilities; +import org.eclipse.lsp4j.TypeDefinitionParams; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.junit.Test; + +public class HyperlinkDetectorErrorHandlingTest extends AbstractTestWithProject { + + private final OpenDeclarationHyperlinkDetector detector = new OpenDeclarationHyperlinkDetector(); + + @Override + protected ServerCapabilities getServerCapabilities() { + // Ensure providers are enabled to exercise all branches + var caps = MockLanguageServer.defaultServerCapabilities(); + caps.setDefinitionProvider(true); + caps.setTypeDefinitionProvider(true); + caps.setDeclarationProvider(true); + caps.setImplementationProvider(true); + return caps; + } + + @Test + public void testDefinitionRemainsWhenTypeDefinitionErrors() throws Exception { + MockLanguageServer.INSTANCE.setTextDocumentService( + // Simulate server error for typeDefinition (mirrors issue + // https://github.com/eclipse-lsp4e/lsp4e/issues/1169) + new MockTextDocumentService(MockLanguageServer.INSTANCE::buildMaybeDelayedFuture) { + @Override + public CompletableFuture, List>> typeDefinition( + TypeDefinitionParams params) { + var f = new CompletableFuture, List>>(); + f.completeExceptionally( + new RuntimeException("unexpected error during typeDefinition retrieval")); + return f; + } + + @Override + public CompletableFuture, List>> implementation( + ImplementationParams params) { + throw new RuntimeException("unexpected error during implementation retrieval"); + } + + @Override + public CompletableFuture, List>> declaration( + DeclarationParams params) { + throw new RuntimeException("unexpected error during declaration retrieval"); + } + }); + + // ensure TextDocumentService is faulty + assertThrows(RuntimeException.class, + () -> MockLanguageServer.INSTANCE.getTextDocumentService().declaration(null)); + assertThrows(RuntimeException.class, + () -> MockLanguageServer.INSTANCE.getTextDocumentService().implementation(null)); + assertTrue( + MockLanguageServer.INSTANCE.getTextDocumentService().typeDefinition(null).isCompletedExceptionally()); + + // Configure 1 good definition result + MockLanguageServer.INSTANCE.setDefinition(List.of( // + new Location("file://def", new Range(new Position(0, 0), new Position(0, 10))), // + new Location("file://def", new Range(new Position(1, 10), new Position(1, 20))))); + + IFile file = TestUtils.createUniqueTestFile(project, "Example Text"); + ITextViewer viewer = TestUtils.openTextViewer(file); + + IHyperlink[] links = detector.detectHyperlinks(viewer, new Region(0, 0), true); + + // Expected: 1 link (from definition) even if typeDefinition fails + assertNotNull("Hyperlinks should not be null when definition succeeds despite typeDefinition error", links); + assertEquals(2, links.length); + } +} 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 e08998172..4b18d4388 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 @@ -74,24 +74,29 @@ private static record LabeledLocations(String label, final int offset = region.getOffset(); final CompletableFuture> request = CACHE.computeIfAbsent(document, offset, () -> { - var definitions = LanguageServers.forDocument(document) + final var definitions = LanguageServers.forDocument(document) .withCapability(ServerCapabilities::getDefinitionProvider) .collectAll(ls -> ls.getTextDocumentService().definition(LSPEclipseUtils.toDefinitionParams(params)) - .thenApply(l -> new LabeledLocations(Messages.definitionHyperlinkLabel, l))); + .thenApply(l -> new LabeledLocations(Messages.definitionHyperlinkLabel, l)) + .exceptionally(err -> new LabeledLocations(Messages.definitionHyperlinkLabel, null))); final var declarations = LanguageServers.forDocument(document) - .withCapability(ServerCapabilities::getDeclarationProvider).collectAll( - ls -> ls.getTextDocumentService().declaration(LSPEclipseUtils.toDeclarationParams(params)) - .thenApply(l -> new LabeledLocations(Messages.declarationHyperlinkLabel, l))); + .withCapability(ServerCapabilities::getDeclarationProvider) + .collectAll(ls -> ls.getTextDocumentService() + .declaration(LSPEclipseUtils.toDeclarationParams(params)) + .thenApply(l -> new LabeledLocations(Messages.declarationHyperlinkLabel, l)) + .exceptionally(err -> new LabeledLocations(Messages.declarationHyperlinkLabel, null))); final var typeDefinitions = LanguageServers.forDocument(document) .withCapability(ServerCapabilities::getTypeDefinitionProvider) .collectAll(ls -> ls.getTextDocumentService() .typeDefinition(LSPEclipseUtils.toTypeDefinitionParams(params)) - .thenApply(l -> new LabeledLocations(Messages.typeDefinitionHyperlinkLabel, l))); + .thenApply(l -> new LabeledLocations(Messages.typeDefinitionHyperlinkLabel, l)) + .exceptionally(err -> new LabeledLocations(Messages.typeDefinitionHyperlinkLabel, null))); final var implementations = LanguageServers.forDocument(document) .withCapability(ServerCapabilities::getImplementationProvider) .collectAll(ls -> ls.getTextDocumentService() .implementation(LSPEclipseUtils.toImplementationParams(params)) - .thenApply(l -> new LabeledLocations(Messages.implementationHyperlinkLabel, l))); + .thenApply(l -> new LabeledLocations(Messages.implementationHyperlinkLabel, l)) + .exceptionally(err -> new LabeledLocations(Messages.implementationHyperlinkLabel, null))); CompletableFuture> combined = LanguageServers.addAll( LanguageServers.addAll(LanguageServers.addAll(definitions, declarations), typeDefinitions),