Implementation for https://github.com/spring-projects/sts4/issues/427
This commit is contained in:
@@ -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.
|
||||
* <p>
|
||||
* 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<LanguageId, ICompletionEngine> 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<LanguageId> 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<ICompletionProposal> getCompletions(TextDocument document, int offset) throws Exception {
|
||||
LanguageId language = document.getLanguageId();
|
||||
log.info("languageId = {}", language);
|
||||
Collection<ICompletionEngine> 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<ICompletionProposal> completions = ImmutableList.builder();
|
||||
for (ICompletionEngine engine : engines) {
|
||||
Collection<ICompletionProposal> c = engine.getCompletions(document, offset);
|
||||
if (c!=null) {
|
||||
completions.addAll(c);
|
||||
}
|
||||
}
|
||||
return completions.build();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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<LanguageId, LanguageServerComponents> componentsByLanguageId = new HashMap<>();
|
||||
private List<ICompletionEngine> 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<ICompletionEngine> completionEngines) {
|
||||
Assert.isNull("completionEngines should only be set once", this.completionEngines);
|
||||
this.completionEngines = completionEngines;
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<LanguageId, LanguageServerComponents> 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<ICompletionProposal> 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<LanguageId, LanguageServerComponents> 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() {
|
||||
|
||||
@@ -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<ICompletionEngine> 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));
|
||||
|
||||
|
||||
@@ -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<LanguageId> LANGUAGES = ImmutableList.of(
|
||||
LanguageId.BOOT_PROPERTIES,
|
||||
LanguageId.BOOT_PROPERTIES_YAML
|
||||
);
|
||||
private SpringPropertyIndexProvider indexProvider;
|
||||
|
||||
public DollarPropertyCompletionProvider(BootLanguageServerParams params) {
|
||||
this.indexProvider = params.indexProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ICompletionProposal> getCompletions(TextDocument doc, int offset) {
|
||||
ImmutableList.Builder<ICompletionProposal> 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<Match<PropertyInfo>> matches = indexProvider.getIndex(doc).getProperties().find(pattern);
|
||||
for (Match<PropertyInfo> 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<LanguageId> supportedLanguages() {
|
||||
return LANGUAGES;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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}<*>"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user