From 6841b5713a71bead83b1da232f8c4142cd2e6995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kroi=C3=9F=2C=20Florian?= Date: Thu, 26 Feb 2026 20:04:07 +0100 Subject: [PATCH] feat: Inlay hints with text edits can be applied when clicking on them --- org.eclipse.lsp4e.test/META-INF/MANIFEST.MF | 2 +- org.eclipse.lsp4e.test/pom.xml | 2 +- .../LSPLineContentCodeMiningTest.java | 26 ++++++ org.eclipse.lsp4e/META-INF/MANIFEST.MF | 2 +- org.eclipse.lsp4e/pom.xml | 2 +- .../inlayhint/LSPLineContentCodeMining.java | 88 +++++++++++++------ 6 files changed, 91 insertions(+), 31 deletions(-) diff --git a/org.eclipse.lsp4e.test/META-INF/MANIFEST.MF b/org.eclipse.lsp4e.test/META-INF/MANIFEST.MF index 7313dba46..b671b9949 100644 --- a/org.eclipse.lsp4e.test/META-INF/MANIFEST.MF +++ b/org.eclipse.lsp4e.test/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Tests for language server bundle (Incubation) Bundle-SymbolicName: org.eclipse.lsp4e.test;singleton:=true -Bundle-Version: 0.16.12.qualifier +Bundle-Version: 0.16.13.qualifier Fragment-Host: org.eclipse.lsp4e Bundle-Vendor: Eclipse LSP4E Bundle-RequiredExecutionEnvironment: JavaSE-21 diff --git a/org.eclipse.lsp4e.test/pom.xml b/org.eclipse.lsp4e.test/pom.xml index d8254b27c..96bc55b37 100644 --- a/org.eclipse.lsp4e.test/pom.xml +++ b/org.eclipse.lsp4e.test/pom.xml @@ -8,7 +8,7 @@ org.eclipse.lsp4e.test eclipse-test-plugin - 0.16.12-SNAPSHOT + 0.16.13-SNAPSHOT diff --git a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/operations/inlayhint/LSPLineContentCodeMiningTest.java b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/operations/inlayhint/LSPLineContentCodeMiningTest.java index 591035d1e..8a6099b0f 100644 --- a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/operations/inlayhint/LSPLineContentCodeMiningTest.java +++ b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/operations/inlayhint/LSPLineContentCodeMiningTest.java @@ -34,6 +34,9 @@ import org.eclipse.lsp4j.InlayHint; import org.eclipse.lsp4j.InlayHintLabelPart; import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.swt.SWT; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.widgets.Display; @@ -76,6 +79,29 @@ public void singleLabelPartCommand(MockLanguageServerFactory factory) throws Exc assertEquals(MockLanguageServer.SUPPORTED_COMMAND_ID, executedCommand.getCommand()); assertEquals(command.getArguments(), executedCommand.getArguments()); } + + @Test + void inlayHintWithTextEdit() throws Exception { + IFile file = TestUtils.createUniqueTestFile(project, "lspt", "x = [1, 2]"); + ITextViewer textViewer = TestUtils.openTextViewer(file); + IDocument document = textViewer.getDocument(); + + final var provider = new InlayHintProvider(); + + // Create inlayhint with text edit + InlayHint inlayHint = new InlayHint(new Position(0, 0), Either.forLeft(": list[int]")); + inlayHint.setTextEdits(List.of(new TextEdit(new Range(new Position(0, 1), new Position(0, 1)), ": list[int]"))); + + // Simulate user clicking on inlayhint + LanguageServerWrapper wrapper = LanguageServiceAccessor.getLSWrapper(project, + LanguageServersRegistry.getInstance().getDefinition(MOCK_SERVER_ID)); + final var sut = new LSPLineContentCodeMining(inlayHint, document, wrapper, provider); + MouseEvent mouseEvent = createMouseEvent(); + sut.getAction().accept(mouseEvent); + + // Text edit should be applied. + assertEquals("x: list[int] = [1, 2]", document.get()); + } private static InlayHintLabelPart createInlayLabelPart(String text, String commandID) { final var labelPart = new InlayHintLabelPart(text); diff --git a/org.eclipse.lsp4e/META-INF/MANIFEST.MF b/org.eclipse.lsp4e/META-INF/MANIFEST.MF index 1168ef015..ebf5953a4 100644 --- a/org.eclipse.lsp4e/META-INF/MANIFEST.MF +++ b/org.eclipse.lsp4e/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Language Server Protocol client for Eclipse IDE (Incubation) Bundle-SymbolicName: org.eclipse.lsp4e;singleton:=true -Bundle-Version: 0.19.13.qualifier +Bundle-Version: 0.19.14.qualifier Bundle-RequiredExecutionEnvironment: JavaSE-21 Require-Bundle: org.eclipse.core.runtime;bundle-version="3.12.0", org.eclipse.equinox.common;bundle-version="3.8.0", diff --git a/org.eclipse.lsp4e/pom.xml b/org.eclipse.lsp4e/pom.xml index 00bc8584b..a3acac8b6 100644 --- a/org.eclipse.lsp4e/pom.xml +++ b/org.eclipse.lsp4e/pom.xml @@ -10,7 +10,7 @@ org.eclipse.lsp4e eclipse-plugin - 0.19.13-SNAPSHOT + 0.19.14-SNAPSHOT diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/inlayhint/LSPLineContentCodeMining.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/inlayhint/LSPLineContentCodeMining.java index f67fee6d5..2003103a1 100644 --- a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/inlayhint/LSPLineContentCodeMining.java +++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/inlayhint/LSPLineContentCodeMining.java @@ -26,6 +26,8 @@ import org.eclipse.lsp4e.LanguageServerWrapper; import org.eclipse.lsp4e.LanguageServers; import org.eclipse.lsp4e.command.CommandExecutor; +import org.eclipse.lsp4e.internal.NullSafetyHelper; +import org.eclipse.lsp4j.Command; import org.eclipse.lsp4j.ExecuteCommandOptions; import org.eclipse.lsp4j.ExecuteCommandParams; import org.eclipse.lsp4j.InlayHint; @@ -111,7 +113,7 @@ private static boolean canResolveInlayHint(@Nullable ServerCapabilities capabili Either inlayProvider = capabilities.getInlayHintProvider(); if (inlayProvider != null && inlayProvider.isRight()) { InlayHintRegistrationOptions options = inlayProvider.getRight(); - return options.getResolveProvider() != null && options.getResolveProvider().booleanValue(); + return Boolean.TRUE.equals(options.getResolveProvider()); } return false; } @@ -134,35 +136,67 @@ private static org.eclipse.jface.text.Position toPosition(Position position, IDo @Override public final @Nullable Consumer getAction() { - return inlayHint.getLabel().map(l -> null, this::labelPartAction); + List parts = inlayHint.getLabel().map(l -> null, r -> r); + boolean hasPartWithCommand = parts != null + && parts.stream().map(InlayHintLabelPart::getCommand).anyMatch(Objects::nonNull); + boolean hasTextEdits = inlayHint.getTextEdits() != null && !inlayHint.getTextEdits().isEmpty(); + + if (!(hasPartWithCommand || hasTextEdits)) { + // Neither command nor text edits -> no action + return null; + } + + return evt -> { + /* + * LSP does not define if commands or text edits take precedence. So for the + * sake of backwards compatibility, we first check for commands and then for + * text edits + */ + if (hasPartWithCommand && handlePartWithCommand(evt, NullSafetyHelper.castNonNull(parts))) { + return; + } + if (hasTextEdits) { + // Apply text edits + try { + LSPEclipseUtils.applyEdits(document, inlayHint.getTextEdits()); + // The text of the inlay hint is integrated into the document. + // To avoid a brief moment where both the inserted text and the inlay hint are + // rendered, we set an empty label for the hint. + setLabel(""); //$NON-NLS-1$ + } catch (BadLocationException e) { + // Location to modify document was no longer valid -> Ignore + } + } + }; } - private @Nullable Consumer labelPartAction(List labelParts) { - String title = getLabel(); - if (title != null && !title.isEmpty() - && labelParts.stream().map(InlayHintLabelPart::getCommand).anyMatch(Objects::nonNull)) { - return me -> findLabelPart(me, labelParts) // - .map(InlayHintLabelPart::getCommand) // - .filter(Objects::nonNull) // - .ifPresent(command -> { - ServerCapabilities serverCapabilities = wrapper.getServerCapabilities(); - ExecuteCommandOptions provider = serverCapabilities == null ? null - : serverCapabilities.getExecuteCommandProvider(); - String commandId = command.getCommand(); - if (provider != null && provider.getCommands().contains(commandId)) { - LanguageServers.forDocument(document).computeAll((w, ls) -> { - if (w == wrapper) { - return ls.getWorkspaceService().executeCommand( - new ExecuteCommandParams(commandId, command.getArguments())); - } - return CompletableFuture.completedFuture(null); - }); - } else { - CommandExecutor.executeCommandClientSide(command, document); - } - }); + /** + * Returns true if the mouse event hit a label part with a command. + */ + private boolean handlePartWithCommand(MouseEvent evt, List parts) { + // Figure out if the user clicked on a specific label part which has a command + Optional clickedPart = findLabelPart(evt, NullSafetyHelper.castNonNull(parts)); + Optional commandOpt = clickedPart.map(InlayHintLabelPart::getCommand); + if (commandOpt.isPresent()) { + Command command = commandOpt.get(); + ServerCapabilities serverCapabilities = wrapper.getServerCapabilities(); + ExecuteCommandOptions provider = serverCapabilities == null ? null + : serverCapabilities.getExecuteCommandProvider(); + String commandId = command.getCommand(); + if (provider != null && provider.getCommands().contains(commandId)) { + LanguageServers.forDocument(document).computeAll((w, ls) -> { + if (w == wrapper) { + return ls.getWorkspaceService() + .executeCommand(new ExecuteCommandParams(commandId, command.getArguments())); + } + return CompletableFuture.completedFuture(null); + }); + } else { + CommandExecutor.executeCommandClientSide(command, document); + } + return true; } - return null; + return false; } private Optional findLabelPart(MouseEvent me, List labelParts) {