Make snippets fully subsume property completions in bosh editor

This commit is contained in:
Kris De Volder
2017-08-10 11:53:15 -07:00
parent aedab760d6
commit 94fa2cf498
7 changed files with 156 additions and 120 deletions

View File

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

View File

@@ -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<YType, Collection<Snippet>> cache = CacheBuilder.newBuilder().build();
private Cache<YType, Collection<Snippet>> cache = CacheBuilder.newBuilder()
.weakKeys()
.build();
private int maxNesting = Integer.MAX_VALUE;
@Override
@@ -60,24 +61,9 @@ public class SchemaBasedSnippetGenerator implements TypeBasedSnippetProvider {
}
private Collection<Snippet> generateSnippets(YType type) {
ImmutableList.Builder<Snippet> 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) {

View File

@@ -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<*>"

View File

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

View File

@@ -101,7 +101,6 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
String query = getPrefix(doc, node, offset);
List<ICompletionProposal> completions = getValueCompletions(doc, node, offset, query);
if (completions.isEmpty()) {
completions = getKeyCompletions(doc, node, offset, query);
TypeBasedSnippetProvider snippetProvider = typeUtil.getSnippetProvider();
if (snippetProvider!=null) {
Collection<Snippet> 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<ICompletionProposal> 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);

View File

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

View File

@@ -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<Snippet> getSnippets(YType contextType);
Snippet getSnippet(YType contextType, YTypedProperty p);
}