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}<*>"
);
}