From d9f2047bcf97f28a6eadc8e34dc432e6c4463038 Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Tue, 1 Aug 2017 12:05:25 -0700 Subject: [PATCH] Snippet generator mostly working, still needs some tweaks / bug fixes. --- .../bosh/BoshDeploymentManifestSchema.java | 5 +- .../ide/vscode/bosh/BoshLanguageServer.java | 14 +- .../snippets/SchemaBasedSnippetGenerator.java | 152 ++++++++++++++++++ .../ide/vscode/bosh/BoshEditorTest.java | 9 +- .../bosh/SchemaBasedSnippetGeneratorTest.java | 36 +++++ .../languageserver/util/SnippetBuilder.java | 26 ++- .../yaml/completion/YTypeAssistContext.java | 25 +++ .../YamlCompletionEngineOptions.java | 8 +- .../commons/yaml/schema/YTypeFactory.java | 16 +- .../vscode/commons/yaml/schema/YTypeUtil.java | 11 ++ .../vscode/commons/yaml/snippet/Snippet.java | 40 +++++ .../snippet/TypeBasedSnippetProvider.java | 21 +++ .../testharness/LanguageServerHarness.java | 60 +++---- 13 files changed, 374 insertions(+), 49 deletions(-) create mode 100644 headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/snippets/SchemaBasedSnippetGenerator.java create mode 100644 headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/SchemaBasedSnippetGeneratorTest.java create mode 100644 headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/Snippet.java create mode 100644 headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/TypeBasedSnippetProvider.java diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java index 1f84b62ea..c1aeec5e1 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java @@ -25,13 +25,13 @@ import org.springframework.ide.vscode.bosh.models.ReleasesModel; import org.springframework.ide.vscode.bosh.models.StemcellData; import org.springframework.ide.vscode.bosh.models.StemcellModel; import org.springframework.ide.vscode.bosh.models.StemcellsModel; +import org.springframework.ide.vscode.bosh.snippets.SchemaBasedSnippetGenerator; import org.springframework.ide.vscode.commons.util.Assert; import org.springframework.ide.vscode.commons.util.CollectorUtil; import org.springframework.ide.vscode.commons.util.PartialCollection; import org.springframework.ide.vscode.commons.util.Renderable; import org.springframework.ide.vscode.commons.util.Renderables; import org.springframework.ide.vscode.commons.util.StringUtil; -import org.springframework.ide.vscode.commons.util.ValueParser; import org.springframework.ide.vscode.commons.util.ValueParsers; import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil; import org.springframework.ide.vscode.commons.yaml.ast.YamlAstCache; @@ -40,7 +40,6 @@ import org.springframework.ide.vscode.commons.yaml.path.YamlPath; import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache; import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems; import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; -import org.springframework.ide.vscode.commons.yaml.schema.SchemaContextAware; import org.springframework.ide.vscode.commons.yaml.schema.YType; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.AbstractType; @@ -53,7 +52,6 @@ import org.springframework.ide.vscode.commons.yaml.schema.YTypedProperty; import org.springframework.ide.vscode.commons.yaml.schema.YamlSchema; import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraints; -import com.fasterxml.jackson.databind.type.TypeFactory; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -328,7 +326,6 @@ public class BoshDeploymentManifestSchema implements YamlSchema { return new StemcellModel(path.dropLast().traverseToNode(ast)); } - private String getCurrentEntityProperty(DynamicSchemaContext dc, String propName) { YamlPath path = dc.getPath(); YamlFileAST ast = asts.getSafeAst(dc.getDocument(), true); diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java index fbc704e19..b53c9e078 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java @@ -14,6 +14,7 @@ import org.springframework.ide.vscode.bosh.models.CloudConfigModel; import org.springframework.ide.vscode.bosh.models.DynamicModelProvider; import org.springframework.ide.vscode.bosh.models.ReleasesModel; import org.springframework.ide.vscode.bosh.models.StemcellsModel; +import org.springframework.ide.vscode.bosh.snippets.SchemaBasedSnippetGenerator; import org.springframework.ide.vscode.commons.languageserver.completion.VscodeCompletionEngineAdapter; import org.springframework.ide.vscode.commons.languageserver.hover.HoverInfoProvider; import org.springframework.ide.vscode.commons.languageserver.hover.VscodeHoverEngine; @@ -23,7 +24,6 @@ import org.springframework.ide.vscode.commons.languageserver.util.Settings; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; import org.springframework.ide.vscode.commons.languageserver.util.SimpleWorkspaceService; -import org.springframework.ide.vscode.commons.util.Log; import org.springframework.ide.vscode.commons.util.text.LanguageId; import org.springframework.ide.vscode.commons.util.text.TextDocument; import org.springframework.ide.vscode.commons.yaml.ast.YamlAstCache; @@ -41,6 +41,7 @@ import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureProvid public class BoshLanguageServer extends SimpleLanguageServer { private final VscodeCompletionEngineAdapter completionEngine; + private BoshDeploymentManifestSchema schema; public BoshLanguageServer(BoshCliConfig cliConfig, DynamicModelProvider cloudConfigProvider, @@ -52,10 +53,11 @@ public class BoshLanguageServer extends SimpleLanguageServer { SimpleTextDocumentService documents = getTextDocumentService(); ASTTypeCache astTypeCache = new ASTTypeCache(); - BoshDeploymentManifestSchema schema = new BoshDeploymentManifestSchema(asts, astTypeCache, cloudConfigProvider, stemcellsProvider, releasesProvider); + schema = new BoshDeploymentManifestSchema(asts, astTypeCache, cloudConfigProvider, stemcellsProvider, releasesProvider); YamlStructureProvider structureProvider = YamlStructureProvider.DEFAULT; YamlAssistContextProvider contextProvider = new SchemaBasedYamlAssistContextProvider(schema); + enableSnippets(true); YamlCompletionEngine yamlCompletionEngine = new YamlCompletionEngine(structureProvider, contextProvider, YamlCompletionEngineOptions.DEFAULT); completionEngine = createCompletionEngineAdapter(this, yamlCompletionEngine); HoverInfoProvider infoProvider = new YamlHoverInfoProvider(asts.getAstProvider(true), structureProvider, contextProvider); @@ -87,6 +89,14 @@ public class BoshLanguageServer extends SimpleLanguageServer { } } + public void enableSnippets(boolean enable) { + if (enable) { + schema.f.setSnippetProvider(new SchemaBasedSnippetGenerator(schema.getTypeUtil(), this::createSnippetBuilder)); + } else { + schema.f.setSnippetProvider(null); + } + } + public BoshLanguageServer setMaxCompletions(int maxCompletions) { completionEngine.setMaxCompletions(maxCompletions); return this; diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/snippets/SchemaBasedSnippetGenerator.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/snippets/SchemaBasedSnippetGenerator.java new file mode 100644 index 000000000..47d19606a --- /dev/null +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/snippets/SchemaBasedSnippetGenerator.java @@ -0,0 +1,152 @@ +/******************************************************************************* + * Copyright (c) 2017 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.bosh.snippets; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import org.springframework.ide.vscode.commons.languageserver.util.SnippetBuilder; +import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.yaml.schema.YType; +import org.springframework.ide.vscode.commons.yaml.schema.YTypeUtil; +import org.springframework.ide.vscode.commons.yaml.schema.YTypedProperty; +import org.springframework.ide.vscode.commons.yaml.snippet.Snippet; +import org.springframework.ide.vscode.commons.yaml.snippet.TypeBasedSnippetProvider; +import org.springframework.ide.vscode.commons.yaml.util.YamlIndentUtil; + +import com.google.common.base.Supplier; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +/** + * An implementation of {@link TypeBasedSnippetProvider} that generates snippets + * automatically from schema types. + */ +public class SchemaBasedSnippetGenerator implements TypeBasedSnippetProvider { + + private YTypeUtil typeUtil; + private Supplier snippetBuilderFactory; + + public SchemaBasedSnippetGenerator(YTypeUtil typeUtil, Supplier snippetBuilderFactory) { + super(); + this.typeUtil = typeUtil; + this.snippetBuilderFactory = snippetBuilderFactory; + } + + private Cache> cache = CacheBuilder.newBuilder().build(); + private int maxNesting = Integer.MAX_VALUE; + + @Override + public Collection getSnippets(YType type) { + try { + return cache.get(type, () -> generateSnippets(type)); + } catch (ExecutionException e) { + Log.log(e); + return ImmutableList.of(); + } + } + + private Collection generateSnippets(YType type) { + ImmutableList.Builder snippets = ImmutableList.builder(); + //Generate a 'full' snippet that defines all required properties of the current type. + Snippet snippet = generateFullSnippet(type, 0); + if (snippet!=null) { + snippets.add(snippet); + } + //Generate single property snippets that only define a single properties (with 'mega snippets' for nested types) + for (YTypedProperty p : typeUtil.getProperties(type)) { + String propName = p.getName(); + SnippetBuilder builder = snippetBuilderFactory.get(); + generateBeanSnippet(ImmutableList.of(p), builder, 0, maxNesting); + if (builder.getPlaceholderCount()>=2) { + snippets.add(new Snippet(p.getName()+" Snippet", builder.toString(), (dc) -> + !dc.getDefinedProperties().contains(propName) + )); + } + } + return snippets.build(); + } + + private Snippet generateFullSnippet(YType type, int indent) { + if (typeUtil.isBean(type)) { + SnippetBuilder builder = snippetBuilderFactory.get(); + List requiredProps = typeUtil.getProperties(type).stream() + .filter(p -> p.isPrimary() || p.isRequired()) + .collect(Collectors.toList()); + if (!requiredProps.isEmpty()) { + generateBeanSnippet(requiredProps, builder, indent, maxNesting); + } + if (builder.getPlaceholderCount()>=2) { + //place holder count is a good indicator of snippet complexity and allows us to + // avoid creating trivial snippets (which aren't very useful). + ImmutableSet vetoProps = ImmutableSet.copyOf(requiredProps); + // Do not suggest snippet if it contains properties that are already defined. + return new Snippet(typeUtil.niceTypeName(type)+" Snippet", builder.toString(), (dc) -> + dc.getDefinedProperties().stream().noneMatch(vetoProps::contains) + ); + } + } + return null; + } + + private void generateBeanSnippet(List props, SnippetBuilder builder, int indent, int nestingLimit) { + if (nestingLimit>0) { + boolean first = true; + for (YTypedProperty p : props) { + if (!first) { + builder.newline(indent); + } + builder.text(p.getName()); + builder.text(":"); + generateNestedSnippet(false, p.getType(), builder, indent, nestingLimit-1); + first = false; + } + } else { + //reached the limit of bean number of nested property expansions allowed. + builder.placeHolder(); + } + } + + + private void generateNestedSnippet(boolean parentIsSeq, YType type, SnippetBuilder builder, int indent, int nestingLimit) { + if (type==null) { + //Assume its some kind of pojo bean + builder.newline(indent+YamlIndentUtil.INDENT_BY); + builder.placeHolder(); + } else if (typeUtil.isBean(type) || typeUtil.isMap(type)) { + if (!parentIsSeq) { + //ready to enter nested keys on next line + indent += YamlIndentUtil.INDENT_BY; + builder.newline(indent); + } + //Insert required keys + List requiredProps = typeUtil.getProperties(type).stream() + .filter(p -> p.isPrimary() || p.isRequired()) + .collect(Collectors.toList()); + generateBeanSnippet(requiredProps, builder, indent, nestingLimit); + } else if (typeUtil.isSequencable(type)) { + //ready to enter sequence element on next line + builder.newline(indent); + builder.text("- "); + indent += YamlIndentUtil.INDENT_BY; + generateNestedSnippet(true, typeUtil.getDomainType(type), builder, indent, nestingLimit); + } else { //Treat like atomic + //ready to enter whatever on the same line + builder.text(" "); + builder.placeHolder(); + } + } + +} diff --git a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshEditorTest.java b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshEditorTest.java index 32dafad58..2e59c7058 100644 --- a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshEditorTest.java +++ b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshEditorTest.java @@ -45,7 +45,7 @@ import com.google.common.collect.ImmutableSet; public class BoshEditorTest { - LanguageServerHarness harness; + LanguageServerHarness harness; private BoshCliConfig cliConfig = new BoshCliConfig(); private MockCloudConfigProvider cloudConfigProvider = new MockCloudConfigProvider(cliConfig); @@ -53,7 +53,7 @@ public class BoshEditorTest { private DynamicModelProvider releasesProvider = mock(DynamicModelProvider.class); @Before public void setup() throws Exception { - harness = new LanguageServerHarness(() -> { + harness = new LanguageServerHarness(() -> { return new BoshLanguageServer(cliConfig, cloudConfigProvider, (dc) -> stemcellsProvider.getModel(dc), (dc) -> releasesProvider.getModel(dc) @@ -156,6 +156,7 @@ public class BoshEditorTest { } @Test public void toplevelPropertyCompletions() throws Exception { + harness.getServer().enableSnippets(false); Editor editor = harness.newEditor( "<*>" ); @@ -200,6 +201,7 @@ public class BoshEditorTest { } @Test public void stemcellCompletions() throws Exception { + harness.getServer().enableSnippets(false); Editor editor = harness.newEditor( "stemcells:\n" + "- <*>" @@ -253,6 +255,7 @@ public class BoshEditorTest { @Test public void releasesBlockCompletions() throws Exception { + harness.getServer().enableSnippets(false); Editor editor = harness.newEditor( "releases:\n" + "- <*>" @@ -518,6 +521,7 @@ public class BoshEditorTest { } @Test public void updateBlockCompletions() throws Exception { + harness.getServer().enableSnippets(false); Editor editor = harness.newEditor( "update:\n" + " <*>" @@ -557,6 +561,7 @@ public class BoshEditorTest { } @Test public void variablesBlockCompletions() throws Exception { + harness.getServer().enableSnippets(false); Editor editor = harness.newEditor( "variables:\n" + "- <*>" diff --git a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/SchemaBasedSnippetGeneratorTest.java b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/SchemaBasedSnippetGeneratorTest.java new file mode 100644 index 000000000..b743995ec --- /dev/null +++ b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/SchemaBasedSnippetGeneratorTest.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2017 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.bosh; + +import org.junit.Test; +import org.springframework.ide.vscode.bosh.snippets.SchemaBasedSnippetGenerator; +import org.springframework.ide.vscode.commons.languageserver.util.SnippetBuilder; +import org.springframework.ide.vscode.commons.yaml.ast.YamlAstCache; +import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache; +import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; +import org.springframework.ide.vscode.commons.yaml.schema.YType; +import org.springframework.ide.vscode.commons.yaml.schema.YTypeUtil; + +public class SchemaBasedSnippetGeneratorTest { + + private ASTTypeCache astTypes = new ASTTypeCache(); + private YamlAstCache asts = new YamlAstCache(); + private BoshDeploymentManifestSchema schema = new BoshDeploymentManifestSchema(asts, astTypes, (dc) -> null, (dc) -> null, (dc) -> null); + private YTypeUtil typeUtil = schema.getTypeUtil(); + private SchemaBasedSnippetGenerator generator = new SchemaBasedSnippetGenerator(typeUtil, SnippetBuilder::new); + + @Test + public void toplevelSnippet() throws Exception { + YType v2Schema = typeUtil.inferMoreSpecificType(schema.getTopLevelType(), DynamicSchemaContext.NULL); + System.out.println(generator.getSnippets(v2Schema).iterator().next()); + } + +} diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SnippetBuilder.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SnippetBuilder.java index ce57b8eb8..850d152a0 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SnippetBuilder.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SnippetBuilder.java @@ -35,11 +35,7 @@ public class SnippetBuilder { * be overridden by subclasses to support other formats. *

* The default implementation creates place holder strings that - * match the undocumented format vscode currently supports. - *

- * Note: this format is explicitly different from what the LSP - * specifies. So it is very likely we should change this implementation - * in the near future. + * match format specified by LSP 3.0. */ protected String createPlaceHolder(int id) { return "$"+id; @@ -50,4 +46,24 @@ public class SnippetBuilder { return buf.toString(); } + public void newline(int indent) { + buf.append("\n"); + for (int i = 0; i < indent; i++) { + buf.append(' '); + } + } + + public void ensureSpace() { + if (buf.length()>0 && !Character.isWhitespace(buf.charAt(buf.length()-1))) { + buf.append(' '); + } + } + + /** + * @return The number of placeholder that where inserted in the snippet. + */ + public int getPlaceholderCount() { + return nextPlaceHolderId-1; + } + } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java index 45ede6c40..433eed681 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java @@ -45,6 +45,8 @@ import org.springframework.ide.vscode.commons.yaml.schema.YType; import org.springframework.ide.vscode.commons.yaml.schema.YTypeUtil; import org.springframework.ide.vscode.commons.yaml.schema.YTypedProperty; import org.springframework.ide.vscode.commons.yaml.schema.YValueHint; +import org.springframework.ide.vscode.commons.yaml.snippet.Snippet; +import org.springframework.ide.vscode.commons.yaml.snippet.TypeBasedSnippetProvider; import org.springframework.ide.vscode.commons.yaml.structure.YamlDocument; import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SNode; import org.springframework.ide.vscode.commons.yaml.util.YamlIndentUtil; @@ -98,6 +100,29 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { List completions = getValueCompletions(doc, node, offset, query); if (completions.isEmpty()) { completions = getKeyCompletions(doc, offset, query); + TypeBasedSnippetProvider snippetProvider = typeUtil.getSnippetProvider(); + if (snippetProvider!=null) { + Collection snippets = snippetProvider.getSnippets(type); + YamlIndentUtil indenter = new YamlIndentUtil(doc); + for (Snippet snippet : snippets) { + String snippetName = snippet.getName(); + double score = FuzzyMatcher.matchScore(query, snippetName); + if (score!=0.0) { + String textBeforeQuery = doc.getLineTextBefore(offset); + DocumentEdits edits = new DocumentEdits(doc.getDocument()); + int start = offset - query.length(); + edits.delete(start, query); + int referenceIndent = textBeforeQuery.length(); + boolean needsSpace = start > 0 && !Character.isWhitespace(doc.getChar(offset-1)); + if (needsSpace) { + referenceIndent++; + edits.insert(start, " "); + } + edits.insert(start, indenter.applyIndentation(snippet.getSnippet(), referenceIndent)); + completions.add(completionFactory().valueProposal(snippetName, query, snippetName, type, null, score, edits, typeUtil)); + } + } + } } if (typeUtil.isSequencable(type)) { completions = new ArrayList<>(completions); diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngineOptions.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngineOptions.java index d5c74d5a2..d4a1a537a 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngineOptions.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngineOptions.java @@ -13,15 +13,15 @@ package org.springframework.ide.vscode.commons.yaml.completion; public interface YamlCompletionEngineOptions { /** * Whether the completion engine includes 'less indented' proposals (i.e. proposals - * that aren't valid at the current CA position, but are valid if we delete - * some spaces in front of the cursor first. + * that aren't valid at the current CA position, but are valid if we delete + * some spaces in front of the cursor first. */ - default boolean includeDeindentedProposals() { + default boolean includeDeindentedProposals() { //Disabled by default for now because of bug introduced in VSCode 1.12: //https://github.com/Microsoft/vscode/issues/26096 return true; } - + YamlCompletionEngineOptions DEFAULT = new YamlCompletionEngineOptions() {}; YamlCompletionEngineOptions TEST_DEFAULT = new YamlCompletionEngineOptions() { @Override public boolean includeDeindentedProposals() { return true; } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java index 54227bd14..7a937cae5 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java @@ -16,7 +16,6 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -31,8 +30,6 @@ import org.springframework.ide.vscode.commons.languageserver.reconcile.Replaceme import org.springframework.ide.vscode.commons.util.Assert; import org.springframework.ide.vscode.commons.util.CollectorUtil; import org.springframework.ide.vscode.commons.util.EnumValueParser; -import org.springframework.ide.vscode.commons.util.ExceptionUtil; -import org.springframework.ide.vscode.commons.util.Log; import org.springframework.ide.vscode.commons.util.PartialCollection; import org.springframework.ide.vscode.commons.util.Renderable; import org.springframework.ide.vscode.commons.util.Renderables; @@ -40,8 +37,8 @@ import org.springframework.ide.vscode.commons.util.ValueParser; import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems; import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraint; import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraints; +import org.springframework.ide.vscode.commons.yaml.snippet.TypeBasedSnippetProvider; -import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; import com.google.common.collect.ImmutableMap; @@ -60,6 +57,7 @@ public class YTypeFactory { private boolean enableTieredOptionalPropertyProposals = true; private boolean suggestDeprecatedProperties = true; + private TypeBasedSnippetProvider snippetProvider = null; private static class Deprecation { final String errorMsg; @@ -239,6 +237,11 @@ public class YTypeFactory { return ((AbstractType)type).getCustomContentAssistant(); } + @Override + public TypeBasedSnippetProvider getSnippetProvider() { + return snippetProvider; + } + @Override public boolean tieredOptionalPropertyProposals() { return enableTieredOptionalPropertyProposals; @@ -980,5 +983,10 @@ public class YTypeFactory { return this; } + public YTypeFactory setSnippetProvider(TypeBasedSnippetProvider snippetProvider) { + this.snippetProvider = snippetProvider; + return this; + } + } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeUtil.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeUtil.java index 4a572ba61..5517a3c8f 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeUtil.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeUtil.java @@ -10,12 +10,15 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.yaml.schema; +import java.util.Collection; import java.util.List; import java.util.Map; import org.springframework.ide.vscode.commons.util.PartialCollection; import org.springframework.ide.vscode.commons.util.ValueParser; import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraint; +import org.springframework.ide.vscode.commons.yaml.snippet.Snippet; +import org.springframework.ide.vscode.commons.yaml.snippet.TypeBasedSnippetProvider; /** * An implementation of YTypeUtil provides implementations of various @@ -51,6 +54,14 @@ public interface YTypeUtil { ISubCompletionEngine getCustomContentAssistant(YType type); + /** + * Config option for type-bases complection enging. Snippets can be + * associated with schema types. These snippets will be suggested as + * additional completions based on the type of value expected in + * a context. + */ + TypeBasedSnippetProvider getSnippetProvider(); + /** * Config option for type-based completion engine. This enables the * 'tiered' proposals feature (so that optional properties are not diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/Snippet.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/Snippet.java new file mode 100644 index 000000000..5946114ca --- /dev/null +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/Snippet.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) 2017 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.yaml.snippet; + +import java.util.function.Predicate; + +import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; + +public class Snippet { + + private final String name; + private final String snippet; + private final Predicate applicability; + + public Snippet(String name, String snippet, Predicate applicability) { + super(); + this.name = name; + this.snippet = snippet; + this.applicability = applicability; + } + public String getName() { + return name; + } + public String getSnippet() { + return snippet; + } + + @Override + public String toString() { + return "Snippet [ name="+name+",\n" +snippet +"\n]"; + } +} diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/TypeBasedSnippetProvider.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/TypeBasedSnippetProvider.java new file mode 100644 index 000000000..235730073 --- /dev/null +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/snippet/TypeBasedSnippetProvider.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2017 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.yaml.snippet; + +import java.util.Collection; + +import org.springframework.ide.vscode.commons.yaml.schema.YType; + +public interface TypeBasedSnippetProvider { + + Collection getSnippets(YType contextType); + +} diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java index 05e5f6fc4..07830f3b0 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java @@ -92,16 +92,16 @@ import com.google.common.collect.ImmutableList; import reactor.core.publisher.Mono; -public class LanguageServerHarness { +public class LanguageServerHarness { //Warning this 'harness' is incomplete. Growing it as needed. private Random random = new Random(); - private Callable factory; + private Callable factory; private LanguageId defaultLanguageId; - private SimpleLanguageServer server; + private S server; private InitializeResult initResult; @@ -110,12 +110,12 @@ public class LanguageServerHarness { private List activeEditors = new ArrayList<>(); - public LanguageServerHarness(Callable factory, LanguageId defaultLanguageId) { + public LanguageServerHarness(Callable factory, LanguageId defaultLanguageId) { this.factory = factory; this.defaultLanguageId = defaultLanguageId; } - public LanguageServerHarness(Callable factory) throws Exception { + public LanguageServerHarness(Callable factory) throws Exception { this(factory, LanguageId.PLAINTEXT); } @@ -184,9 +184,9 @@ public class LanguageServerHarness { workspaceCap.setExecuteCommand(exeCap); clientCap.setWorkspace(workspaceCap); initParams.setCapabilities(clientCap); - initResult = server.initialize(initParams).get(); - if (server instanceof LanguageClientAware) { - ((LanguageClientAware) server).connect(new STS4LanguageClient() { + initResult = getServer().initialize(initParams).get(); + if (getServer() instanceof LanguageClientAware) { + ((LanguageClientAware) getServer()).connect(new STS4LanguageClient() { @Override public void telemetryEvent(Object object) { // TODO Auto-generated method stub @@ -248,15 +248,15 @@ public class LanguageServerHarness { }); } - server.initialized(); + getServer().initialized(); return initResult; } public TextDocumentInfo openDocument(TextDocumentInfo documentInfo) throws Exception { DidOpenTextDocumentParams didOpen = new DidOpenTextDocumentParams(); didOpen.setTextDocument(documentInfo.getDocument()); - if (server!=null) { - server.getTextDocumentService().didOpen(didOpen); + if (getServer()!=null) { + getServer().getTextDocumentService().didOpen(didOpen); } return documentInfo; } @@ -295,8 +295,8 @@ public class LanguageServerHarness { default: throw new IllegalStateException("Unkown SYNC mode: "+getDocumentSyncMode()); } - if (server!=null) { - server.getTextDocumentService().didChange(didChange); + if (getServer()!=null) { + getServer().getTextDocumentService().didChange(didChange); } return documents.get(uri); } @@ -320,8 +320,8 @@ public class LanguageServerHarness { default: throw new IllegalStateException("Unkown SYNC mode: "+getDocumentSyncMode()); } - if (server!=null) { - server.getTextDocumentService().didChange(didChange); + if (getServer()!=null) { + getServer().getTextDocumentService().didChange(didChange); } return documents.get(uri); } @@ -339,7 +339,7 @@ public class LanguageServerHarness { } public PublishDiagnosticsParams getDiagnostics(TextDocumentInfo doc) throws Exception { - this.server.waitForReconcile(); + this.getServer().waitForReconcile(); return diagnostics.get(doc.getUri()); } @@ -376,8 +376,8 @@ public class LanguageServerHarness { TextDocumentPositionParams params = new TextDocumentPositionParams(); params.setPosition(cursor); params.setTextDocument(doc.getId()); - server.waitForReconcile(); - Either, CompletionList> completions = server.getTextDocumentService().completion(params).get(); + getServer().waitForReconcile(); + Either, CompletionList> completions = getServer().getTextDocumentService().completion(params).get(); if (completions.isLeft()) { List list = completions.getLeft(); return new CompletionList(false, list); @@ -391,14 +391,14 @@ public class LanguageServerHarness { TextDocumentPositionParams params = new TextDocumentPositionParams(); params.setPosition(cursor); params.setTextDocument(document.getId()); - return server.getTextDocumentService().hover(params ).get(); + return getServer().getTextDocumentService().hover(params ).get(); } public CompletionItem resolveCompletionItem(CompletionItem maybeUnresolved) { - if (server.hasLazyCompletionResolver()) { + if (getServer().hasLazyCompletionResolver()) { try { - return server.getTextDocumentService().resolveCompletionItem(maybeUnresolved).get(); + return getServer().getTextDocumentService().resolveCompletionItem(maybeUnresolved).get(); } catch (Exception e) { throw new RuntimeException(e); } @@ -477,14 +477,14 @@ public class LanguageServerHarness { } public List getDefinitions(TextDocumentPositionParams params) throws Exception { - server.waitForReconcile(); //goto definitions relies on reconciler infos! Must wait or race condition breaking tests occasionally. - return server.getTextDocumentService().definition(params).get(); + getServer().waitForReconcile(); //goto definitions relies on reconciler infos! Must wait or race condition breaking tests occasionally. + return getServer().getTextDocumentService().definition(params).get(); } public List getCodeActions(TextDocumentInfo doc, Diagnostic problem) throws Exception { CodeActionContext context = new CodeActionContext(ImmutableList.of(problem)); List actions = - server.getTextDocumentService().codeAction(new CodeActionParams(doc.getId(), problem.getRange(), context)).get(); + getServer().getTextDocumentService().codeAction(new CodeActionParams(doc.getId(), problem.getRange(), context)).get(); return actions.stream() .map((command) -> new CodeAction(this, command)) .collect(Collectors.toList()); @@ -498,7 +498,7 @@ public class LanguageServerHarness { //Note convert the params to a 'typeless' Object because that is more representative on how it will be // received when we get it in a real client/server setting (i.e. parsed from json). List untypedParams = mapper.convertValue(args, List.class); - server.getWorkspaceService() + getServer().getWorkspaceService() .executeCommand(new ExecuteCommandParams(command.getCommand(), untypedParams)) .get(); } @@ -544,9 +544,9 @@ public class LanguageServerHarness { } public List getDocumentSymbols(TextDocumentInfo document) throws Exception { - server.waitForReconcile(); //TODO: if the server works properly this shouldn't be needed it should do that internally itself somehow. + getServer().waitForReconcile(); //TODO: if the server works properly this shouldn't be needed it should do that internally itself somehow. DocumentSymbolParams params = new DocumentSymbolParams(document.getId()); - return server.getTextDocumentService().documentSymbol(params).get(); + return getServer().getTextDocumentService().documentSymbol(params).get(); } /** @@ -554,7 +554,7 @@ public class LanguageServerHarness { */ public SynchronizationPoint reconcilerThreadStart() { CompletableFuture blocker = new CompletableFuture<>(); - server.setTestListener(new LanguageServerTestListener() { + getServer().setTestListener(new LanguageServerTestListener() { @Override public void reconcileStarted(String uri, int version) { try { @@ -585,4 +585,8 @@ public class LanguageServerHarness { return newEditor(IOUtil.toString(is)); } } + + public S getServer() { + return server; + } }