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 b9451cc54..37dc4035a 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 @@ -257,7 +257,7 @@ public class BoshDeploymentManifestSchema extends SchemaSupport implements YamlS } ); - addProp(t_stemcell, "alias", t_stemcell_alias_def).isRequired(true); + addProp(t_stemcell, "alias", t_stemcell_alias_def).isPrimary(true); addProp(t_stemcell, "version", t_stemcell_version_ref).isRequired(true); addProp(t_stemcell, "name", t_stemcell_name_ref); addProp(t_stemcell, "os", t_stemcell_os_ref); 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 index 31587b244..89c99f30f 100644 --- 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 @@ -29,7 +29,6 @@ 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 @@ -46,7 +45,9 @@ public class SchemaBasedSnippetGenerator implements TypeBasedSnippetProvider { this.snippetBuilderFactory = snippetBuilderFactory; } - private Cache> cache = CacheBuilder.newBuilder().build(); + private Cache> cache = CacheBuilder.newBuilder() + .weakKeys() + .build(); private int maxNesting = Integer.MAX_VALUE; @Override @@ -60,24 +61,9 @@ public class SchemaBasedSnippetGenerator implements TypeBasedSnippetProvider { } 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(); + return snippet==null ? ImmutableList.of() : ImmutableList.of(snippet); } private Snippet generateFullSnippet(YType type, int indent) { @@ -116,6 +102,16 @@ public class SchemaBasedSnippetGenerator implements TypeBasedSnippetProvider { } } + @Override + public Snippet getSnippet(YType contextType, YTypedProperty p) { + //TODO: cache? + SnippetBuilder builder = snippetBuilderFactory.get(); + generateBeanSnippet(ImmutableList.of(p), builder, 0, maxNesting); + String propName = p.getName(); + return new Snippet(propName, builder.toString(), (dc) -> + !dc.getDefinedProperties().contains(propName) + ); + } private void generateNestedSnippet(boolean parentIsSeq, YType type, SnippetBuilder builder, int indent, int nestingLimit) { if (type==null) { 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 24ed747b3..ed96cd463 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 @@ -11,11 +11,12 @@ package org.springframework.ide.vscode.bosh; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.springframework.ide.vscode.languageserver.testharness.Editor.*; +import static org.springframework.ide.vscode.languageserver.testharness.Editor.DEDENTED_COMPLETION; +import static org.springframework.ide.vscode.languageserver.testharness.Editor.PLAIN_COMPLETION; +import static org.springframework.ide.vscode.languageserver.testharness.Editor.SNIPPET_COMPLETION; import static org.springframework.ide.vscode.languageserver.testharness.TestAsserts.assertContains; import java.io.IOException; @@ -26,7 +27,6 @@ import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.DiagnosticSeverity; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.springframework.ide.vscode.bosh.mocks.MockCloudConfigProvider; import org.springframework.ide.vscode.bosh.models.BoshCommandReleasesProvider; @@ -158,11 +158,10 @@ public class BoshEditorTest { } @Test public void toplevelPropertyCompletions() throws Exception { - harness.getServer().enableSnippets(false); Editor editor = harness.newEditor( "<*>" ); - editor.assertCompletions( + editor.assertCompletions(SNIPPET_COMPLETION.negate(), "name: <*>" ); @@ -174,24 +173,42 @@ public class BoshEditorTest { editor.assertCompletions( "name: blah\n" + "instance_groups:\n" + - "- name: <*>" + "- name: $1\n" + + " azs:\n" + + " - $2\n" + + " instances: $3\n" + + " jobs:\n" + + " - name: $4\n" + + " release: $5\n" + + " vm_type: $6\n" + + " stemcell: $7\n" + + " networks:\n" + + " - name: $8<*>" , // ============ "name: blah\n" + "releases:\n" + - "- name: <*>" + "- name: $1\n" + + " version: $2<*>" , // ============ "name: blah\n" + - "stemcells:\n- <*>" + "stemcells:\n" + + "- alias: $1\n" + + " version: $2<*>" , // ============ "name: blah\n" + "tags:\n <*>" , // ============ "name: blah\n" + - "update:\n <*>" + "update:\n" + + " canaries: $1\n" + + " max_in_flight: $2\n" + + " canary_watch_time: $3\n" + + " update_watch_time: $4<*>" , // ============ "name: blah\n" + "variables:\n" + - "- name: <*>" + "- name: $1\n" + + " type: $2<*>" // Below completions are suppressed because they are deprecated // , // ============ // "name: blah\n" + @@ -203,29 +220,32 @@ public class BoshEditorTest { } @Test public void stemcellCompletions() throws Exception { - harness.getServer().enableSnippets(false); Editor editor = harness.newEditor( "stemcells:\n" + "- <*>" ); - editor.assertCompletions( + editor.assertCompletions(PLAIN_COMPLETION, + "stemcells:\n" + + "- alias: $1\n" + + " version: $2<*>" + , // ========== "stemcells:\n" + "- alias: <*>" - , // ========== - "stemcells:\n" + - "- name: <*>" - , // ========== - "stemcells:\n" + - "- os: <*>" - , // ========== - "stemcells:\n" + - "- version: <*>" ); editor = harness.newEditor( "stemcells:\n" + - "- alias<*>" + "- alias: blah\n" + + " <*>" ); + editor.assertContextualCompletions(PLAIN_COMPLETION, + "<*>" + , // => + "name: <*>", + "os: <*>", + "version: <*>" + ); + } @Test public void stemcellReconciling() throws Exception { @@ -1636,37 +1656,6 @@ public class BoshEditorTest { " stemcell: $16\n" + " networks:\n" + " - name: $17<*>" - , // ------------------ - "instance_groups:\n" + - "- name: $1\n" + - " azs:\n" + - " - $2\n" + - " instances: $3\n" + - " jobs:\n" + - " - name: $4\n" + - " release: $5\n" + - " vm_type: $6\n" + - " stemcell: $7\n" + - " networks:\n" + - " - name: $8<*>" - , // ---------------- - "releases:\n" + - "- name: $1\n" + - " version: $2<*>" - , // ---------------- - "stemcells:\n" + - "- alias: $1\n" + - " version: $2<*>" - , // ---------------- - "update:\n" + - " canaries: $1\n" + - " max_in_flight: $2\n" + - " canary_watch_time: $3\n" + - " update_watch_time: $4<*>" - , // ----------------- - "variables:\n" + - "- name: $1\n" + - " type: $2<*>" ); } @@ -1678,14 +1667,16 @@ public class BoshEditorTest { "<*>" ); - editor.assertCompletionLabels(SNIPPET_COMPLETION, + editor.assertCompletionLabels( //"BoshDeploymentManifest Snippet", - "instance_groups Snippet", + "instance_groups", // "releases Snippet", // "stemcells Snippet" - "update Snippet", - "variables Snippet", - "- Stemcell Snippet" + "tags", + "update", + "variables", + "- Stemcell Snippet", + "- alias" ); } @@ -1697,14 +1688,14 @@ public class BoshEditorTest { "- name: blah\n" + " <*>" ); - editor.assertContextualCompletions(c -> c.getLabel().equals("jobs Snippet"), + editor.assertContextualCompletions(c -> c.getLabel().equals("jobs"), "jo<*>" , // ------ "jobs:\n" + " - name: $1\n" + " release: $2<*>" ); - editor.assertCompletionWithLabel("jobs Snippet", + editor.assertCompletionWithLabel("jobs", "instance_groups:\n" + "- name: blah\n" + " jobs:\n" + @@ -1721,14 +1712,14 @@ public class BoshEditorTest { "- name: blah\n" + "<*>" ); - editor.assertCompletionWithLabel("→ jobs Snippet", + editor.assertCompletionWithLabel("→ jobs", "instance_groups:\n" + "- name: blah\n" + " jobs:\n" + " - name: $1\n" + " release: $2<*>" ); - editor.assertContextualCompletions(c -> c.getLabel().equals("→ jobs Snippet"), + editor.assertContextualCompletions(c -> c.getLabel().equals("→ jobs"), "jo<*>" , // ------ " jobs:\n" + @@ -1746,7 +1737,7 @@ public class BoshEditorTest { " type: aaa\n" + "<*>" ); - editor.assertContextualCompletions(DEDENTED_COMPLETION.and(SNIPPET_COMPLETION), + editor.assertContextualCompletions(DEDENTED_COMPLETION, " <*>" , // ==> "instance_groups:\n" + @@ -1770,6 +1761,9 @@ public class BoshEditorTest { "- alias: $1\n" + " version: $2<*>" , //======== + "tags:\n" + + " <*>" + , //======== "update:\n" + " canaries: $1\n" + " max_in_flight: $2\n" + @@ -1778,6 +1772,8 @@ public class BoshEditorTest { , //======== "- name: $1\n" + " type: $2<*>" + , //======== + "- name: <*>" ); } @@ -1794,19 +1790,34 @@ public class BoshEditorTest { " <*>" , //==> "instance_groups:\n" + - "- name: <*>" + "- name: $1\n" + + " azs:\n" + + " - $2\n" + + " instances: $3\n" + + " jobs:\n" + + " - name: $4\n" + + " release: $5\n" + + " vm_type: $6\n" + + " stemcell: $7\n" + + " networks:\n" + + " - name: $8<*>" , //---- "releases:\n" + - "- name: <*>" + "- name: $1\n" + + " version: $2<*>" , //---- "stemcells:\n" + - "- <*>" + "- alias: $1\n" + + " version: $2<*>" , //--- "tags:\n" + " <*>" , //--- "update:\n" + - " <*>" + " canaries: $1\n" + + " max_in_flight: $2\n" + + " canary_watch_time: $3\n" + + " update_watch_time: $4<*>" , //--- "- name: <*>" ); @@ -1823,7 +1834,8 @@ public class BoshEditorTest { "jo<*>" , // ==> " jobs:\n" + - " - <*>" + " - name: $1\n" + + " release: $2<*>" ); } @@ -2121,9 +2133,6 @@ public class BoshEditorTest { editor.assertContextualCompletions(PLAIN_COMPLETION, "<*>" , // => - "subnets:\n" + //non-snippet - " - <*>" - , "subnets:\n" + //snippet " - range: $1\n" + " gateway: $2<*>" 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 850d152a0..26ca9aa35 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 @@ -12,7 +12,8 @@ package org.springframework.ide.vscode.commons.languageserver.util; public class SnippetBuilder { - private int nextPlaceHolderId = 1; + private static final int FIRST_PLACE_HOLDER_ID = 1; + private int nextPlaceHolderId = FIRST_PLACE_HOLDER_ID; private StringBuilder buf = new StringBuilder(); public SnippetBuilder text(String text) { @@ -43,7 +44,14 @@ public class SnippetBuilder { @Override public String toString() { - return buf.toString(); + String str = buf.toString(); + if (getPlaceholderCount()==1 ) { + String placeHolder = createPlaceHolder(FIRST_PLACE_HOLDER_ID); + if (str.endsWith(placeHolder)) { + str = str.substring(0, str.length()-placeHolder.length()); + } + } + return str; } public void newline(int indent) { 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 66963f72b..2b282318b 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 @@ -101,7 +101,6 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { String query = getPrefix(doc, node, offset); List completions = getValueCompletions(doc, node, offset, query); if (completions.isEmpty()) { - completions = getKeyCompletions(doc, node, offset, query); TypeBasedSnippetProvider snippetProvider = typeUtil.getSnippetProvider(); if (snippetProvider!=null) { Collection snippets = snippetProvider.getSnippets(type); @@ -110,20 +109,12 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { String snippetName = snippet.getName(); double score = FuzzyMatcher.matchScore(query, snippetName); if (score!=0.0 && snippet.isApplicable(getSchemaContext())) { - DocumentEdits edits = new DocumentEdits(doc.getDocument()); - int start = offset - query.length(); - edits.delete(start, query); - int referenceIndent = doc.getColumn(start); - boolean needsSpace = start > 0 && !Character.isWhitespace(doc.getChar(start-1)); - if (needsSpace) { - referenceIndent++; - edits.insert(start, " "); - } - edits.insert(start, indenter.applyIndentation(snippet.getSnippet(), referenceIndent)); + DocumentEdits edits = createEditFromSnippet(doc, node, offset, query, indenter, snippet); completions.add(completionFactory().valueProposal(snippetName, query, snippetName, type, null, score, edits, typeUtil)); } } } + completions.addAll(getKeyCompletions(doc, node, offset, query)); } if (typeUtil.isSequencable(type)) { completions = new ArrayList<>(completions); @@ -132,6 +123,26 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { return completions; } + private DocumentEdits createEditFromSnippet(YamlDocument doc, SNode node, int offset, String query, YamlIndentUtil indenter, + Snippet _snippet) throws Exception { + DocumentEdits edits = new DocumentEdits(doc.getDocument()); + int start = offset - query.length(); + edits.delete(start, query); + int referenceIndent = doc.getColumn(start); + + boolean needNewline = node.getNodeType()==SNodeType.KEY; + String snippet = _snippet.getSnippet(); + if (needNewline) { + snippet = "\n"+snippet; + referenceIndent = YamlIndentUtil.getNewChildKeyIndent(node); + } else if (start > 0 && !Character.isWhitespace(doc.getChar(start-1))) { + referenceIndent++; + edits.insert(start, " "); + } + edits.insert(start, indenter.applyIndentation(snippet, referenceIndent)); + return edits; + } + public List getKeyCompletions(YamlDocument doc, SNode node, int offset, String query) throws Exception { int queryOffset = offset - query.length(); DynamicSchemaContext dynamicCtxt = getSchemaContext(); @@ -151,24 +162,33 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { String name = p.getName(); double score = FuzzyMatcher.matchScore(query, name); if (score!=0) { - DocumentEdits edits = new DocumentEdits(doc.getDocument()); - YType YType = p.getType(); - edits.delete(queryOffset, query); - int referenceIndent = doc.getColumn(queryOffset); - boolean needNewline = node.getNodeType()==SNodeType.KEY; - StringBuilder snippet = new StringBuilder(); - if (needNewline) { - snippet.append("\n"); - referenceIndent = YamlIndentUtil.getNewChildKeyIndent(node); - } else if (queryOffset>0 && !Character.isWhitespace(doc.getChar(queryOffset-1))) { - //See https://www.pivotaltracker.com/story/show/137722057 - snippet.append(" "); - referenceIndent++; + TypeBasedSnippetProvider snippetProvider = typeUtil.getSnippetProvider(); + DocumentEdits edits; + if (snippetProvider!=null) { + // Generate edits from snippet + Snippet snippet = snippetProvider.getSnippet(type, p); + edits = createEditFromSnippet(doc, node, offset, query, indenter, snippet); + } else { + //Generate edits the old-fashioned way + edits = new DocumentEdits(doc.getDocument()); + YType YType = p.getType(); + edits.delete(queryOffset, query); + int referenceIndent = doc.getColumn(queryOffset); + boolean needNewline = node.getNodeType()==SNodeType.KEY; + StringBuilder snippet = new StringBuilder(); + if (needNewline) { + snippet.append("\n"); + referenceIndent = YamlIndentUtil.getNewChildKeyIndent(node); + } else if (queryOffset>0 && !Character.isWhitespace(doc.getChar(queryOffset-1))) { + //See https://www.pivotaltracker.com/story/show/137722057 + snippet.append(" "); + referenceIndent++; + } + snippet.append(p.getName()); + snippet.append(":"); + snippet.append(appendTextFor(YType)); + edits.insert(queryOffset, indenter.applyIndentation(snippet.toString(), referenceIndent)); } - snippet.append(p.getName()); - snippet.append(":"); - snippet.append(appendTextFor(YType)); - edits.insert(queryOffset, indenter.applyIndentation(snippet.toString(), referenceIndent)); ICompletionProposal completion = completionFactory().beanProperty(doc.getDocument(), contextPath.toPropString(), getType(), query, p, score, edits, typeUtil); 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 index e5a074434..11acd5aeb 100644 --- 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 @@ -13,6 +13,7 @@ package org.springframework.ide.vscode.commons.yaml.snippet; import java.util.function.Predicate; import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; +import org.springframework.ide.vscode.commons.yaml.schema.YTypedProperty; public class Snippet { 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 index 235730073..49acad072 100644 --- 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 @@ -13,9 +13,11 @@ package org.springframework.ide.vscode.commons.yaml.snippet; import java.util.Collection; import org.springframework.ide.vscode.commons.yaml.schema.YType; +import org.springframework.ide.vscode.commons.yaml.schema.YTypedProperty; public interface TypeBasedSnippetProvider { Collection getSnippets(YType contextType); + Snippet getSnippet(YType contextType, YTypedProperty p); }