From 5c5d535556e6c333a48cdfd0f96fa0ef1b61b4e3 Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Wed, 22 May 2019 10:36:48 -0700 Subject: [PATCH] Trigger characters per language --- .../config/LanguageServerProperties.java | 8 +- ...ompletionServerCapabilityRegistration.java | 105 ++++++++++++++++++ .../util/ServerCapabilityInitializer.java | 23 ++++ .../util/SimpleLanguageServer.java | 26 ++--- .../starter/LanguageServerAutoConf.java | 17 +-- .../completion/DocumentEditsTest.java | 2 +- .../src/main/resources/application.properties | 3 - .../src/main/resources/application.yml | 9 ++ 8 files changed, 163 insertions(+), 30 deletions(-) create mode 100644 headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/CompletionServerCapabilityRegistration.java create mode 100644 headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/ServerCapabilityInitializer.java delete mode 100644 headless-services/spring-boot-language-server/src/main/resources/application.properties create mode 100644 headless-services/spring-boot-language-server/src/main/resources/application.yml diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/config/LanguageServerProperties.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/config/LanguageServerProperties.java index 8d8e7a804..e92de1e47 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/config/LanguageServerProperties.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/config/LanguageServerProperties.java @@ -1,5 +1,7 @@ package org.springframework.ide.vscode.commons.languageserver.config; +import java.util.Map; + import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties("languageserver") @@ -31,7 +33,7 @@ public class LanguageServerProperties { * being ignored when the client is vscode to avoid breaking Java completion * triggers. */ - private String completionTriggerCharacters; + private Map completionTriggerCharacters; public boolean isStandalone() { return standalone; @@ -57,11 +59,11 @@ public class LanguageServerProperties { this.extensionId = extensionId; } - public String getCompletionTriggerCharacters() { + public Map getCompletionTriggerCharacters() { return completionTriggerCharacters; } - public void setCompletionTriggerCharacters(String completionTriggerCharacters) { + public void setCompletionTriggerCharacters(Map completionTriggerCharacters) { this.completionTriggerCharacters = completionTriggerCharacters; } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/CompletionServerCapabilityRegistration.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/CompletionServerCapabilityRegistration.java new file mode 100644 index 000000000..223167077 --- /dev/null +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/CompletionServerCapabilityRegistration.java @@ -0,0 +1,105 @@ +package org.springframework.ide.vscode.commons.languageserver.util; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4j.CompletionOptions; +import org.eclipse.lsp4j.CompletionRegistrationOptions; +import org.eclipse.lsp4j.DocumentFilter; +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.Registration; +import org.eclipse.lsp4j.RegistrationParams; +import org.eclipse.lsp4j.ServerCapabilities; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.commons.languageserver.config.LanguageServerProperties; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; + +import reactor.core.publisher.Mono; + +public class CompletionServerCapabilityRegistration implements ServerCapabilityInitializer { + + private static Logger log = LoggerFactory.getLogger(CompletionServerCapabilityRegistration.class); + + private final SimpleLanguageServer server; + private final LanguageServerProperties props; + + private boolean hasDynamicCompletionRegistration; + + public CompletionServerCapabilityRegistration(SimpleLanguageServer server, LanguageServerProperties props) { + this.server = server; + this.props = props; + } + + private List getMergedTriggerCharacters() { + Set allTriggerChars = new TreeSet<>(); + Map triggersByLanguage = props.getCompletionTriggerCharacters(); + if (triggersByLanguage!=null) { + for (String triggers : triggersByLanguage.values()) { + for (int i = 0; i < triggers.length(); i++) { + String c = triggers.substring(i, i+1); + allTriggerChars.add(c); + } + } + } + if (!allTriggerChars.isEmpty()) { + return ImmutableList.copyOf(allTriggerChars); + } + return null; + } + + @Override + public void initialize(InitializeParams params, ServerCapabilities cap) { + this.hasDynamicCompletionRegistration = SimpleLanguageServer.safeGet(false, () -> params.getCapabilities().getTextDocument().getCompletion().getDynamicRegistration()); + log.info("hasDynamicCompletionRegistration = "+hasDynamicCompletionRegistration); + List allTiggerChars = getMergedTriggerCharacters(); + if (!hasDynamicCompletionRegistration || allTiggerChars==null) { + //Register completion provider and triggerCharacters statically + CompletionOptions completionProvider = new CompletionOptions(); + completionProvider.setResolveProvider(server.hasLazyCompletionResolver()); + completionProvider.setTriggerCharacters(getMergedTriggerCharacters()); + cap.setCompletionProvider(completionProvider); + log.info("Registering Completion Capability Statically {}", completionProvider); + } else { + //Register completion provider and triggerCharacters dynamically, one registration per + //language + server.onInitialized(Mono.fromCallable(() -> { + List registrations = new ArrayList<>(); + for (Entry entry : props.getCompletionTriggerCharacters().entrySet()) { + String languageId = entry.getKey(); + String triggerCharsString = entry.getValue(); + Registration r = new Registration(UUID.randomUUID().toString(), "textDocument/completion"); + CompletionRegistrationOptions registerOptions = new CompletionRegistrationOptions(); + registerOptions.setResolveProvider(server.hasLazyCompletionResolver()); + registerOptions.setTriggerCharacters(toTriggerChars(triggerCharsString)); + DocumentFilter df = new DocumentFilter(); + df.setLanguage(languageId); + registerOptions.setDocumentSelector(ImmutableList.of(df)); + r.setRegisterOptions(registerOptions); + registrations.add(r); + } + RegistrationParams regParams = new RegistrationParams(registrations); + log.info("Registering Dynamic Completion Capabality {}", regParams); + return Mono.fromFuture(server.getClient().registerCapability(regParams)); + })); + } + } + + private List toTriggerChars(String triggerCharsString) { + ImmutableList.Builder builder = ImmutableList.builder(); + for (int i = 0; i < triggerCharsString.length(); i++) { + builder.add(triggerCharsString.substring(i, i+1)); + } + return builder.build(); + } + +} diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/ServerCapabilityInitializer.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/ServerCapabilityInitializer.java new file mode 100644 index 000000000..95f1b9a3f --- /dev/null +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/ServerCapabilityInitializer.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * Copyright (c) 2019 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.languageserver.util; + +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.ServerCapabilities; + +/** + * {@link SimpleLanguageServer} will automatically find any {@link ServerCapabilityInitializer} beans + * in the application context and call on them during language server initialization to allow + * it to participate in initializing the ServerCapability the server returns to the client. + */ +public interface ServerCapabilityInitializer { + void initialize(InitializeParams params, ServerCapabilities cap); +} diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java index eee8254de..07d0be022 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java @@ -55,6 +55,7 @@ import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.LanguageClientAware; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContext; import org.springframework.ide.vscode.commons.languageserver.DiagnosticService; import org.springframework.ide.vscode.commons.languageserver.ProgressService; import org.springframework.ide.vscode.commons.languageserver.Sts4LanguageServer; @@ -111,9 +112,7 @@ public final class SimpleLanguageServer implements Sts4LanguageServer, LanguageC public final LazyCompletionResolver completionResolver = createCompletionResolver(); private SimpleTextDocumentService tds; - private SimpleWorkspaceService workspace; - private STS4LanguageClient client; private ProgressService progressService = (String taskId, String statusMsg) -> { @@ -155,6 +154,7 @@ public final class SimpleLanguageServer implements Sts4LanguageServer, LanguageC private String completionTriggerCharacters = null; + final private ApplicationContext appContext; @Override public void connect(LanguageClient _client) { @@ -187,7 +187,8 @@ public final class SimpleLanguageServer implements Sts4LanguageServer, LanguageC } } - public SimpleLanguageServer(String extensionId) { + public SimpleLanguageServer(String extensionId, ApplicationContext appContext) { + this.appContext = appContext; Assert.isNotNull(extensionId); this.EXTENSION_ID = extensionId; this.CODE_ACTION_COMMAND_ID = "sts."+EXTENSION_ID+".codeAction"; @@ -270,6 +271,12 @@ public final class SimpleLanguageServer implements Sts4LanguageServer, LanguageC getWorkspaceService().onExecuteCommand(this::executeCommand); } ServerCapabilities cap = getServerCapabilities(); + if (appContext!=null) { + Map extraCaps = appContext.getBeansOfType(ServerCapabilityInitializer.class); + for (ServerCapabilityInitializer capIniter : extraCaps.values()) { + capIniter.initialize(params, cap); + } + } result.setCapabilities(cap); Consumer ih = this.initializeHandler; if (ih!=null){ @@ -324,7 +331,7 @@ public final class SimpleLanguageServer implements Sts4LanguageServer, LanguageC * Get some info safely. If there's any kind of exception, ignore it * and retutn default value instead. */ - private static T safeGet(T deflt, Callable getter) { + public static T safeGet(T deflt, Callable getter) { try { T x = getter.call(); if (x!=null) { @@ -370,17 +377,6 @@ public final class SimpleLanguageServer implements Sts4LanguageServer, LanguageC c.setTextDocumentSync(TextDocumentSyncKind.Incremental); c.setHoverProvider(true); - CompletionOptions completionProvider = new CompletionOptions(); - completionProvider.setResolveProvider(hasLazyCompletionResolver()); - if (this.completionTriggerCharacters!=null) { - String[] chars = new String[this.completionTriggerCharacters.length()]; - for (int i = 0; i < chars.length; i++) { - chars[i] = this.completionTriggerCharacters.substring(i, i+1); - } - completionProvider.setTriggerCharacters(ImmutableList.copyOf(chars)); - } - c.setCompletionProvider(completionProvider); - if (hasQuickFixes()) { c.setCodeActionProvider(true); } diff --git a/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerAutoConf.java b/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerAutoConf.java index d9940b6ba..0af23ac0e 100644 --- a/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerAutoConf.java +++ b/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerAutoConf.java @@ -19,17 +19,17 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.ide.vscode.commons.languageserver.completion.VscodeCompletionEngineAdapter.CompletionFilter; import org.springframework.ide.vscode.commons.languageserver.config.LanguageServerInitializer; import org.springframework.ide.vscode.commons.languageserver.config.LanguageServerProperties; import org.springframework.ide.vscode.commons.languageserver.reconcile.DiagnosticSeverityProvider; +import org.springframework.ide.vscode.commons.languageserver.util.CompletionServerCapabilityRegistration; import org.springframework.ide.vscode.commons.languageserver.util.DefinitionHandler; import org.springframework.ide.vscode.commons.languageserver.util.DocumentSymbolHandler; import org.springframework.ide.vscode.commons.languageserver.util.LanguageSpecific; -import org.springframework.ide.vscode.commons.languageserver.util.LspClient; -import org.springframework.ide.vscode.commons.languageserver.util.LspClient.Client; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; import org.springframework.ide.vscode.commons.util.text.LanguageId; @@ -46,13 +46,10 @@ public class LanguageServerAutoConf { @Bean public SimpleLanguageServer languageServer( LanguageServerProperties props, Optional severities, - Optional completionFilter + Optional completionFilter, + ApplicationContext appContext ) throws Exception { - SimpleLanguageServer server = new SimpleLanguageServer(props.getExtensionId()); - if (LspClient.currentClient()!=Client.VSCODE) { - //Vscode excluded. See https://github.com/spring-projects/sts4/issues/193 - server.setCompletionTriggerCharacters(props.getCompletionTriggerCharacters()); - } + SimpleLanguageServer server = new SimpleLanguageServer(props.getExtensionId(), appContext); server.setCompletionFilter(completionFilter); severities.ifPresent(server::setDiagnosticSeverityProvider); return server; @@ -65,6 +62,10 @@ public class LanguageServerAutoConf { serverInit.initialize(server); }; } + + @Bean CompletionServerCapabilityRegistration completionCapabilities(SimpleLanguageServer server, LanguageServerProperties props) { + return new CompletionServerCapabilityRegistration(server, props); + } @Bean SimpleTextDocumentService documents(SimpleLanguageServer ls) { return ls.getTextDocumentService(); diff --git a/headless-services/commons/language-server-test-harness/src/test/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEditsTest.java b/headless-services/commons/language-server-test-harness/src/test/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEditsTest.java index 070253c07..0ceba200c 100644 --- a/headless-services/commons/language-server-test-harness/src/test/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEditsTest.java +++ b/headless-services/commons/language-server-test-harness/src/test/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEditsTest.java @@ -36,7 +36,7 @@ public class DocumentEditsTest { @Before public void setup() throws Exception { - SimpleLanguageServer server = new SimpleLanguageServer("dont-care"); + SimpleLanguageServer server = new SimpleLanguageServer("dont-care", null); harness = new LanguageServerHarness(server, LanguageId.PLAINTEXT); } diff --git a/headless-services/spring-boot-language-server/src/main/resources/application.properties b/headless-services/spring-boot-language-server/src/main/resources/application.properties deleted file mode 100644 index 144abc886..000000000 --- a/headless-services/spring-boot-language-server/src/main/resources/application.properties +++ /dev/null @@ -1,3 +0,0 @@ -spring.main.banner-mode=off -languageserver.extension-id=vscode-spring-boot -languageserver.completion-trigger-characters=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ. diff --git a/headless-services/spring-boot-language-server/src/main/resources/application.yml b/headless-services/spring-boot-language-server/src/main/resources/application.yml new file mode 100644 index 000000000..fbe3d0350 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/resources/application.yml @@ -0,0 +1,9 @@ +languageserver: + extension-id: vscode-spring-boot + completion-trigger-characters: + java: "" + spring-boot-properties-yaml: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ. + spring-boot-properties: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ. +spring: + main: + banner-mode: 'off'