This commit is contained in:
Kris De Volder
2020-04-23 15:44:57 -07:00
parent 6d99b2b14e
commit 3b65e1f511
5 changed files with 196 additions and 40 deletions

View File

@@ -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();
}
};
}

View File

@@ -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() {

View File

@@ -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));

View File

@@ -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;
}
}

View File

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