Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<CompletionItem>();
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);
}
}
13 changes: 13 additions & 0 deletions org.eclipse.lsp4e/src/org/eclipse/lsp4e/LanguageServerWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sebthom:
I think the blocks for case "workspace/executeCommand" and case "textDocument/completion" should be structured in the same way. With that in mind, should we not just re-raise the exception to the caller, as he provided a malformed Json as we would do in case "workspace/executeCommand" on line 1071?

Or alternative, what exceptions are we catching? A JsonSyntaxException? If so, could we be more specific and catch just that one and then do the same for the block case "workspace/executeCommand"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it was for the gson. I thought rather catch any error and not have that capability than not initialize the LS properly at all.

LanguageServerPlugin.logError(ex);
}
break;
}
case "workspace/symbol": //$NON-NLS-1$
final Either<Boolean, WorkspaceSymbolOptions> workspaceSymbolBeforeRegistration = serverCapabilities.getWorkspaceSymbolProvider();
serverCapabilities.setWorkspaceSymbolProvider(Boolean.TRUE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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$
Expand Down