diff --git a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/completion/DynamicCompletionRegistrationTest.java b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/completion/DynamicCompletionRegistrationTest.java new file mode 100644 index 000000000..60e8bf6ae --- /dev/null +++ b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/completion/DynamicCompletionRegistrationTest.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.completion; + +import static org.eclipse.lsp4e.test.utils.TestUtils.waitForAndAssertCondition; +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.eclipse.core.resources.IFile; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.contentassist.ICompletionProposal; +import org.eclipse.lsp4e.operations.completion.LSContentAssistProcessor; +import org.eclipse.lsp4e.test.utils.AbstractTestWithProject; +import org.eclipse.lsp4e.test.utils.TestUtils; +import org.eclipse.lsp4e.tests.mock.MockLanguageServer; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionItemKind; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.CompletionOptions; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.Registration; +import org.eclipse.lsp4j.RegistrationParams; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.services.LanguageClient; +import org.junit.Before; +import org.junit.Test; + +import com.google.gson.Gson; + +/** + * Verifies that dynamic registration of completion updates LSP4E server + * capabilities and enables content assist proposals. + */ +public class DynamicCompletionRegistrationTest extends AbstractTestWithProject { + + private LSContentAssistProcessor contentAssistProcessor; + + @Before + public void setup() { + contentAssistProcessor = new LSContentAssistProcessor(true, false); + } + + @Test + public void testDynamicCompletionRegistrationProvidesProposalsAndTriggers() throws Exception { + // Prepare a file and open a viewer + IFile file = TestUtils.createUniqueTestFile(project, ""); + ITextViewer viewer = TestUtils.openTextViewer(file); + + // Ensure the mock LS is up + waitForAndAssertCondition(5_000, () -> !MockLanguageServer.INSTANCE.getRemoteProxies().isEmpty()); + LanguageClient client = getMockClient(); + assertNotNull(client); + + // Provide a simple completion item in the mock LS + var items = new ArrayList(); + var item = new CompletionItem(); + item.setLabel("Alpha"); + item.setKind(CompletionItemKind.Text); + item.setTextEdit(Either.forLeft(new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), "Alpha"))); + items.add(item); + MockLanguageServer.INSTANCE.setCompletionList(new CompletionList(false, items)); + + // Dynamically register completion with trigger characters (server-like behavior) + var registration = new Registration(); + registration.setId("test-completion-reg"); + registration.setMethod("textDocument/completion"); + var opts = new CompletionOptions(); + opts.setTriggerCharacters(List.of(".", "/", "#")); + registration.setRegisterOptions(new Gson().toJsonTree(opts)); + client.registerCapability(new RegistrationParams(List.of(registration))).get(2, TimeUnit.SECONDS); + + // Compute proposals (manual invocation). Should return the mock item. + ICompletionProposal[] proposals = contentAssistProcessor.computeCompletionProposals(viewer, 0); + assertEquals(1, proposals.length); + assertEquals("Alpha", proposals[0].getDisplayString()); + + // Ask processor for auto-activation triggers and assert they include the registered ones + char[] triggers = contentAssistProcessor.getCompletionProposalAutoActivationCharacters(); + String trig = new String(triggers != null ? triggers : new char[0]); + assertTrue(trig.indexOf('.') >= 0); + assertTrue(trig.indexOf('/') >= 0); + assertTrue(trig.indexOf('#') >= 0); + } + + private LanguageClient getMockClient() { + var proxies = MockLanguageServer.INSTANCE.getRemoteProxies(); + assertEquals(1, proxies.size()); + return proxies.get(0); + } +} diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LanguageServerWrapper.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LanguageServerWrapper.java index b0a93e4a1..4dfbd8edd 100644 --- a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LanguageServerWrapper.java +++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LanguageServerWrapper.java @@ -83,6 +83,7 @@ import org.eclipse.lsp4j.ClientCapabilities; import org.eclipse.lsp4j.ClientInfo; import org.eclipse.lsp4j.CodeActionOptions; +import org.eclipse.lsp4j.CompletionOptions; import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams; import org.eclipse.lsp4j.DocumentFormattingOptions; import org.eclipse.lsp4j.DocumentOnTypeFormattingOptions; @@ -1107,6 +1108,18 @@ public void registerCapability(RegistrationParams params) { serverCapabilities.setCodeActionProvider(Boolean.TRUE); addRegistration(reg, () -> serverCapabilities.setCodeActionProvider(beforeRegistration)); break; + case "textDocument/completion": { //$NON-NLS-1$ + CompletionOptions previous = serverCapabilities.getCompletionProvider(); + try { + final var completionOpts = new Gson().fromJson((JsonObject) reg.getRegisterOptions(), + CompletionOptions.class); + serverCapabilities.setCompletionProvider(completionOpts); + addRegistration(reg, () -> serverCapabilities.setCompletionProvider(previous)); + } catch (final Exception ex) { + LanguageServerPlugin.logError(ex); + } + break; + } case "workspace/symbol": //$NON-NLS-1$ final Either workspaceSymbolBeforeRegistration = serverCapabilities.getWorkspaceSymbolProvider(); serverCapabilities.setWorkspaceSymbolProvider(Boolean.TRUE); diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/internal/SupportedFeatures.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/internal/SupportedFeatures.java index 5b4c02eff..6584b5b3f 100644 --- a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/internal/SupportedFeatures.java +++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/internal/SupportedFeatures.java @@ -96,6 +96,7 @@ public static TextDocumentClientCapabilities getTextDocumentClientCapabilities() "detail", //$NON-NLS-1$ "additionalTextEdits"))); //$NON-NLS-1$ final var completionCapabilities = new CompletionCapabilities(completionItemCapabilities); + completionCapabilities.setDynamicRegistration(Boolean.TRUE); completionCapabilities.setContextSupport(true); completionCapabilities.setCompletionList(new CompletionListCapabilities(List.of( // "commitCharacters", //$NON-NLS-1$