diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/CompositeCompletionEngine.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/CompositeCompletionEngine.java new file mode 100644 index 000000000..04acfed99 --- /dev/null +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/CompositeCompletionEngine.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * 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.completion; + +import java.util.Collection; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.commons.languageserver.util.LanguageSpecific; +import org.springframework.ide.vscode.commons.util.Assert; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Multimap; + +/** + * A completion engine implementation that dispatches completion requests to + * one or more 'sub' completion engines based on language id of the document. + *

+ * If more than one sub-engine is applicable then completions are requested from + * all of them and their results are merged into a single list. + */ +public class CompositeCompletionEngine implements ICompletionEngine { + + private static Logger log = LoggerFactory.getLogger(CompositeCompletionEngine.class); + + Multimap subEngines = LinkedListMultimap.create(); + + public void add(LanguageId language, ICompletionEngine engine) { + subEngines.put(language, engine); + } + + public void add(ICompletionEngine engine) { + Assert.isLegal(engine instanceof LanguageSpecific, "Only LanguageSpecific completion engines are currently supported"); + Collection languages = ((LanguageSpecific)engine).supportedLanguages(); + Assert.isLegal(!languages.isEmpty(), "Completion engine that doesn't support any language"); + for (LanguageId languageId : languages) { + add(languageId, engine); + } + } + + @Override + public Collection getCompletions(TextDocument document, int offset) throws Exception { + LanguageId language = document.getLanguageId(); + log.info("languageId = {}", language); + Collection engines = subEngines.get(language); + if (engines.size()==1) { + //Special case to avoid some collection copying + ICompletionEngine engine = engines.iterator().next(); + return engine.getCompletions(document, offset); + } else { + ImmutableList.Builder completions = ImmutableList.builder(); + for (ICompletionEngine engine : engines) { + Collection c = engine.getCompletions(document, offset); + if (c!=null) { + completions.addAll(c); + } + } + return completions.build(); + } + }; + +} diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/composable/CompositeLanguageServerComponents.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/composable/CompositeLanguageServerComponents.java index b28c3ff66..a6e69913b 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/composable/CompositeLanguageServerComponents.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/composable/CompositeLanguageServerComponents.java @@ -10,19 +10,19 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.languageserver.composable; -import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.Set; -import java.util.concurrent.CompletableFuture; import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.TextDocumentPositionParams; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.commons.languageserver.completion.CompositeCompletionEngine; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionEngine; -import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine; import org.springframework.ide.vscode.commons.languageserver.util.HoverHandler; @@ -33,7 +33,6 @@ import org.springframework.ide.vscode.commons.util.text.IDocument; import org.springframework.ide.vscode.commons.util.text.LanguageId; import org.springframework.ide.vscode.commons.util.text.TextDocument; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; public class CompositeLanguageServerComponents implements LanguageServerComponents { @@ -42,6 +41,7 @@ public class CompositeLanguageServerComponents implements LanguageServerComponen public static class Builder { private Map componentsByLanguageId = new HashMap<>(); + private List completionEngines; public void add(LanguageServerComponents components) { for (LanguageId language : components.getInterestingLanguages()) { @@ -55,31 +55,32 @@ public class CompositeLanguageServerComponents implements LanguageServerComponen return new CompositeLanguageServerComponents(server, this); } + public void completionEngines(List completionEngines) { + Assert.isNull("completionEngines should only be set once", this.completionEngines); + this.completionEngines = completionEngines; + } } private final Map componentsByLanguageId; - private final ICompletionEngine completionEngine; + private final CompositeCompletionEngine completionEngine; private final IReconcileEngine reconcileEngine; private final HoverHandler hoverHandler; public CompositeLanguageServerComponents(SimpleLanguageServer server, Builder builder) { this.componentsByLanguageId = ImmutableMap.copyOf(builder.componentsByLanguageId); //Create composite Completion engine - this.completionEngine = new ICompletionEngine() { - @Override - public Collection getCompletions(TextDocument document, int offset) throws Exception { - LanguageId language = document.getLanguageId(); - log.info("languageId = {}", language); - LanguageServerComponents subComponents = componentsByLanguageId.get(language); - if (subComponents!=null) { - ICompletionEngine subEngine = subComponents.getCompletionEngine(); - if (subEngine!=null) { - return subEngine.getCompletions(document, offset); - } - } - return ImmutableList.of(); + this.completionEngine = new CompositeCompletionEngine(); + for (Entry entry : componentsByLanguageId.entrySet()) { + ICompletionEngine engine = entry.getValue().getCompletionEngine(); + if (engine!=null) { + completionEngine.add(entry.getKey(), entry.getValue().getCompletionEngine()); } - }; + } + if (builder.completionEngines!=null) { + for (ICompletionEngine engine : builder.completionEngines) { + this.completionEngine.add(engine); + } + } //Create composite Reconcile engine if (componentsByLanguageId.values().stream().map(LanguageServerComponents::getReconcileEngine).anyMatch(Optional::isPresent)) { this.reconcileEngine = new IReconcileEngine() { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java index c8e39a93c..b3e054801 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java @@ -10,6 +10,8 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.app; +import java.util.List; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; @@ -54,6 +56,7 @@ public class BootLanguageServerInitializer implements InitializingBean { @Autowired SpringProcessLiveDataProvider liveDataProvider; @Autowired BootJavaConfig config; @Autowired SpringSymbolIndex springIndexer; + @Autowired(required = false) List completionEngines; @Qualifier("adHocProperties") @Autowired ProjectBasedPropertyIndexProvider adHocProperties; @@ -83,6 +86,7 @@ public class BootLanguageServerInitializer implements InitializingBean { builder.add(new BootPropertiesLanguageServerComponents(server, params, javaElementLocationProvider, parser, yamlStructureProvider, yamlAssistContextProvider, sourceLinks)); builder.add(new BootJavaLanguageServerComponents(server, params, sourceLinks, cuCache, adHocProperties, symbolCache, liveDataProvider, config, springIndexer)); builder.add(new SpringXMLLanguageServerComponents(server, springIndexer, params, config)); + builder.completionEngines(completionEngines); components = builder.build(server); params.projectObserver.addListener(reconcileOpenDocuments(server, components, params.projectFinder)); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/DollarPropertyCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/DollarPropertyCompletionProvider.java new file mode 100644 index 000000000..674a9fbc8 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/DollarPropertyCompletionProvider.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * Copyright (c) 2020 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.boot.app; + +import java.util.Collection; +import java.util.List; + +import org.springframework.ide.vscode.boot.java.value.ValuePropertyKeyProposal; +import org.springframework.ide.vscode.boot.metadata.PropertyInfo; +import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndexProvider; +import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; +import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionEngine; +import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; +import org.springframework.ide.vscode.commons.languageserver.util.LanguageSpecific; +import org.springframework.ide.vscode.commons.languageserver.util.PrefixFinder; +import org.springframework.ide.vscode.commons.util.BadLocationException; +import org.springframework.ide.vscode.commons.util.FuzzyMap.Match; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.commons.util.text.TextDocument; +import org.springframework.stereotype.Component; + +import com.google.common.collect.ImmutableList; + +@Component +public class DollarPropertyCompletionProvider implements ICompletionEngine, LanguageSpecific { + + private static String DOLLAR = "${"; + + private static PrefixFinder PREFIX_FINDER = new PrefixFinder() { + @Override + protected boolean isPrefixChar(char c) { + return Character.isJavaIdentifierPart(c) || c=='-'; + } + }; + private static final Collection LANGUAGES = ImmutableList.of( + LanguageId.BOOT_PROPERTIES, + LanguageId.BOOT_PROPERTIES_YAML + ); + private SpringPropertyIndexProvider indexProvider; + + public DollarPropertyCompletionProvider(BootLanguageServerParams params) { + this.indexProvider = params.indexProvider; + } + + @Override + public Collection getCompletions(TextDocument doc, int offset) { + ImmutableList.Builder proposals = ImmutableList.builder(); + String prefix = PREFIX_FINDER.getPrefix(doc, offset); + int prefixStart = offset-prefix.length(); + try { + String dollar = doc.textBetween(prefixStart-DOLLAR.length(), prefixStart); + if (DOLLAR.equals(dollar)) { + String pattern = prefix.replaceAll("[^a-zA-Z0-9\\s+]", ""); + boolean hasCloseCurly = doc.getSafeChar(offset)=='}'; + List> matches = indexProvider.getIndex(doc).getProperties().find(pattern); + for (Match match : matches) { + DocumentEdits edits = new DocumentEdits(doc, false); + if (hasCloseCurly) { + edits.replace(prefixStart, offset, match.data.getId()); + } else { + edits.replace(prefixStart, offset, match.data.getId()+"}"); + } + proposals.add(new ValuePropertyKeyProposal(edits, match)); + } + } + } catch (BadLocationException e) { + //ignore. Didn't find the '${' + } + return proposals.build(); + } + + @Override + public Collection supportedLanguages() { + return LANGUAGES; + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationYamlEditorTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationYamlEditorTest.java index 559c24b9f..84d011eab 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationYamlEditorTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationYamlEditorTest.java @@ -122,7 +122,7 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest { @Test public void GH_427_completionInDollarReference() throws Exception { defaultTestData(); - assertCompletions( + Editor editor = newEditor( "server:\n" + " port: 8006\n" + "\n" + @@ -134,30 +134,23 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest { " host: localhost\n" + " port: 8500\n" + " discovery:\n" + - " service-name: ${appnam<*>}" - , //==> - "server:\n" + - " port: 8006\n" + - "\n" + - "spring:\n" + - " application:\n" + - " name: dadada\n" + - " cloud:\n" + - " consul:\n" + - " host: localhost\n" + - " port: 8500\n" + - " discovery:\n" + - " service-name: ${spring.application.name}" + " service-name: ${<*>}" + ); + + editor.assertContextualCompletions("appnam<*>", + "spring.application.name<*>", + "spring.data.rest.page-param-name<*>", + "spring.jackson.property-naming-strategy<*>" ); - assertCompletions( + editor = newEditor( "spring:\n" + " application:\n" + - " name: ${sport<*>}\n" - , //==> - "spring:\n" + - " application:\n" + - " name: ${server.port<*>}\n" + " name: ${<*>\n" + ); + editor.assertContextualCompletions("servport<*>", + "server.port}<*>", + "server.tomcat.port-header}<*>" ); }