Snippets now mostly working all tests passing.

This commit is contained in:
Kris De Volder
2017-08-02 09:11:13 -07:00
parent e0d9effc14
commit 88c703e9e5
8 changed files with 225 additions and 41 deletions

View File

@@ -16,6 +16,7 @@ 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.CollectorUtil;
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;
@@ -84,17 +85,13 @@ public class SchemaBasedSnippetGenerator implements TypeBasedSnippetProvider {
SnippetBuilder builder = snippetBuilderFactory.get();
List<YTypedProperty> requiredProps = typeUtil.getProperties(type).stream()
.filter(p -> p.isPrimary() || p.isRequired())
.collect(Collectors.toList());
.collect(CollectorUtil.toImmutableList());
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<YTypedProperty> 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)
requiredProps.stream().noneMatch(p -> dc.getDefinedProperties().contains(p.getName()))
);
}
}

View File

@@ -14,7 +14,7 @@ import static org.junit.Assert.assertEquals;
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.PLAIN_COMPLETION;
import static org.springframework.ide.vscode.languageserver.testharness.Editor.*;
import static org.springframework.ide.vscode.languageserver.testharness.TestAsserts.assertContains;
import java.io.IOException;
@@ -25,6 +25,7 @@ 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;
@@ -1609,4 +1610,159 @@ public class BoshEditorTest {
);
}
@Test public void snippet_toplevel() throws Exception {
Editor editor = harness.newEditor("<*>");
editor.assertCompletions(SNIPPET_COMPLETION,
"name: $1\n" +
"releases:\n" +
"- name: $2\n" +
" version: $3\n" +
"stemcells:\n" +
"- alias: $4\n" +
" version: $5\n" +
"update:\n" +
" canaries: $6\n" +
" max_in_flight: $7\n" +
" canary_watch_time: $8\n" +
" update_watch_time: $9\n" +
"instance_groups:\n" +
"- name: $10\n" +
" azs:\n" +
" - $11\n" +
" instances: $12\n" +
" jobs:\n" +
" - name: $13\n" +
" release: $14\n" +
" vm_type: $15\n" +
" 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<*>"
);
}
@Test public void snippet_disabledWhenPropertiesAlreadyDefined() throws Exception {
Editor editor = harness.newEditor(
"name:\n" +
"releases:\n" +
"stemcells:\n" +
"<*>"
);
editor.assertCompletionLabels(SNIPPET_COMPLETION,
//"BoshDeploymentManifest Snippet",
"instance_groups Snippet",
// "releases Snippet",
// "stemcells Snippet"
"update Snippet",
"variables Snippet",
"- Stemcell Snippet"
);
}
@Test public void snippet_nested_plain() throws Exception {
Editor editor;
//Plain exact completion
editor = harness.newEditor(
"instance_groups:\n" +
"- name: blah\n" +
" <*>"
);
editor.assertContextualCompletions(LanguageId.BOSH_DEPLOYMENT, c -> c.getLabel().equals("jobs Snippet"),
"jo<*>"
, // ------
"jobs:\n" +
" - name: $1\n" +
" release: $2<*>"
);
editor.assertCompletionWithLabel("jobs Snippet",
"instance_groups:\n" +
"- name: blah\n" +
" jobs:\n" +
" - name: $1\n" +
" release: $2<*>"
);
}
@Test public void snippet_nested_indenting() throws Exception {
Editor editor;
//With extra indent:
editor = harness.newEditor(
"instance_groups:\n" +
"- name: blah\n" +
"<*>"
);
editor.assertCompletionWithLabel("→ jobs Snippet",
"instance_groups:\n" +
"- name: blah\n" +
" jobs:\n" +
" - name: $1\n" +
" release: $2<*>"
);
editor.assertContextualCompletions(LanguageId.BOSH_DEPLOYMENT, c -> c.getLabel().equals("→ jobs Snippet"),
"jo<*>"
, // ------
" jobs:\n" +
" - name: $1\n" +
" release: $2<*>"
);
}
@Test public void relaxedCAmoreSpaces() throws Exception {
Editor editor = harness.newEditor(
"name: foo\n" +
"instance_groups:\n" +
"- name: \n" +
"<*>"
);
editor.assertContextualCompletions(LanguageId.BOSH_DEPLOYMENT, c -> c.getLabel().equals("→ jobs"),
"jo<*>"
, // ==>
" jobs:\n" +
" - <*>"
);
}
@Test @Ignore public void keyCompletionThatNeedANewline() throws Exception {
Editor editor = harness.newEditor(
"name: foo\n" +
"update: canwa<*>"
);
editor.assertCompletions(
"name: foo\n" +
"update: \n" +
" canary_watch_time: <*>"
);
}
}

View File

@@ -47,6 +47,8 @@ public class DocumentEdits implements ProposalApplier {
private static final Pattern NON_WS_CHAR = Pattern.compile("\\S");
private boolean isRelativeIndent = false;
// Note: for small number of edits this implementation is okay.
// for large number of edits it is potentially slow because of the
// way it transforms edit coordinates (a growing chain of

View File

@@ -10,7 +10,8 @@
*******************************************************************************/
package org.springframework.ide.vscode.commons.yaml.completion;
import static org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal.*;
import static org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal.DEEMP_DASH_PROPOSAL;
import static org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal.DEEMP_DEPRECATION;
import java.util.ArrayList;
import java.util.Collection;
@@ -33,7 +34,6 @@ 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.ValueParseException;
import org.springframework.ide.vscode.commons.yaml.completion.DefaultCompletionFactory.ValueProposal;
import org.springframework.ide.vscode.commons.yaml.hover.YPropertyInfoTemplates;
import org.springframework.ide.vscode.commons.yaml.path.YamlPath;
import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment;
@@ -107,13 +107,12 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
for (Snippet snippet : snippets) {
String snippetName = snippet.getName();
double score = FuzzyMatcher.matchScore(query, snippetName);
if (score!=0.0) {
String textBeforeQuery = doc.getLineTextBefore(offset);
if (score!=0.0 && snippet.isApplicable(getSchemaContext())) {
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));
int referenceIndent = doc.getColumn(start);
boolean needsSpace = start > 0 && !Character.isWhitespace(doc.getChar(start-1));
if (needsSpace) {
referenceIndent++;
edits.insert(start, " ");
@@ -133,7 +132,6 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
public List<ICompletionProposal> getKeyCompletions(YamlDocument doc, int offset, String query) throws Exception {
int queryOffset = offset - query.length();
SNode contextNode = getContextNode();
DynamicSchemaContext dynamicCtxt = getSchemaContext();
List<YTypedProperty> allProperties = typeUtil.getProperties(type);
if (CollectionUtil.hasElements(allProperties)) {
@@ -141,6 +139,7 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
Set<String> definedProps = dynamicCtxt.getDefinedProperties();
List<ICompletionProposal> proposals = new ArrayList<>();
boolean suggestDeprecated = typeUtil.suggestDeprecatedProperties();
YamlIndentUtil indenter = new YamlIndentUtil(doc);
for (List<YTypedProperty> thisTier : tieredProperties) {
List<YTypedProperty> undefinedProps = thisTier.stream()
.filter(p -> !definedProps.contains(p.getName()) && (suggestDeprecated || !p.isDeprecated()))
@@ -150,15 +149,17 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
String name = p.getName();
double score = FuzzyMatcher.matchScore(query, name);
if (score!=0) {
YamlPath relativePath = YamlPath.fromSimpleProperty(name);
YamlPathEdits edits = new YamlPathEdits(doc);
DocumentEdits edits = new DocumentEdits(doc.getDocument());
YType YType = p.getType();
edits.delete(queryOffset, query);
int referenceIndent = doc.getColumn(queryOffset);
if (queryOffset>0 && !Character.isWhitespace(doc.getChar(queryOffset-1))) {
//See https://www.pivotaltracker.com/story/show/137722057
edits.insert(queryOffset, " ");
referenceIndent++;
}
edits.createPathInPlace(contextNode, relativePath, queryOffset, appendTextFor(YType));
String snippet = p.getName()+":" +appendTextFor(YType);
edits.insert(queryOffset, indenter.applyIndentation(snippet, referenceIndent));
ICompletionProposal completion = completionFactory().beanProperty(doc.getDocument(),
contextPath.toPropString(), getType(),
query, p, score, edits, typeUtil);
@@ -405,18 +406,20 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
@Override
protected DocumentEdits transformEdit(DocumentEdits textEdit) {
textEdit.transformFirstNonWhitespaceEdit((Integer offset, String insertText) -> {
YamlIndentUtil indenter = new YamlIndentUtil("\n");
if (needNewline(textEdit)) {
return insertText.substring(0, offset)
+ "\n" +Strings.repeat(" ", node.getIndent())+"- "
+ insertText.substring(offset);
+ indenter.applyIndentation(insertText.substring(offset), YamlIndentUtil.INDENT_BY);
} else if (offset > 2) {
String prefix = insertText.substring(offset-2, offset);
if (" ".equals(prefix)) {
//special case don't add the "- " in front, but replace the inserted spaces instead.
return insertText.substring(0, offset-2)+"- "+insertText.substring(offset);
return insertText.substring(0, offset-2)
+ "- "+ insertText.substring(offset);
}
}
return insertText.substring(0, offset) + "- "+insertText.substring(offset);
return insertText.substring(0, offset) + "- "+indenter.applyIndentation(insertText.substring(offset), YamlIndentUtil.INDENT_BY);
});
return textEdit;
}
@@ -425,11 +428,9 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
//value proposals which are inserted right after a key will not automatically include a newline, as
// its not required for them. So we should add it along with the dash.
try {
if (original instanceof ValueProposal) {
Integer insertAt = textEdit.getFirstEditStart();
if (insertAt!=null) {
return !"".equals(doc.getLineTextBefore(insertAt).trim());
}
Integer insertAt = textEdit.getFirstEditStart();
if (insertAt!=null) {
return !"".equals(doc.getLineTextBefore(insertAt).trim());
}
} catch (Exception e) {
Log.log(e);

View File

@@ -52,7 +52,7 @@ import com.google.common.collect.ImmutableList;
* @author Kris De Volder
*/
public class YamlCompletionEngine implements ICompletionEngine {
Pattern SPACES = Pattern.compile("[ ]+");
final static Logger logger = LoggerFactory.getLogger(YamlCompletionEngine.class);
@@ -110,7 +110,7 @@ public class YamlCompletionEngine implements ICompletionEngine {
protected Collection<? extends ICompletionProposal> getRelaxedCompletions(int offset, YamlDocument doc, SNode current, SNode contextNode, int baseIndent, double deempasizeBy) {
try {
return fixIndentations(getBaseCompletions(offset, doc, current, contextNode),
return fixIndentations(getBaseCompletions(offset, doc, current, contextNode),
current, contextNode, baseIndent, deempasizeBy);
} catch (Exception e) {
Log.log(e);
@@ -118,7 +118,7 @@ public class YamlCompletionEngine implements ICompletionEngine {
return ImmutableList.of();
}
protected Collection<? extends ICompletionProposal> fixIndentations(Collection<ICompletionProposal> completions, SNode currentNode,
protected Collection<? extends ICompletionProposal> fixIndentations(Collection<ICompletionProposal> completions, SNode currentNode,
SNode contextNode, int baseIndent, double deempasizeBy) {
if (!completions.isEmpty()) {
int dashyIndent = getTargetIndent(contextNode, currentNode, true);
@@ -144,7 +144,7 @@ public class YamlCompletionEngine implements ICompletionEngine {
if (isExtraIndentRelaxable(contextNode, fixIndentBy)) {
return indented(p, Strings.repeat(" ", fixIndentBy));
}
} else { // fixIndentBy < 0
} else { // fixIndentBy < 0
if (isLesserIndentRelaxable(currentNode, contextNode)) {
return dedented(p, -fixIndentBy, contextNode.getDocument());
}
@@ -168,7 +168,7 @@ public class YamlCompletionEngine implements ICompletionEngine {
}
/**
* Determine the indentation level needed to line up with other contextNode children.
* Determine the indentation level needed to line up with other contextNode children.
* If the contextNode has no children, then compute a proper default indentation where
* a new child could be added.
*/
@@ -182,8 +182,8 @@ public class YamlCompletionEngine implements ICompletionEngine {
if (child.isPresent()) {
return child.get().getIndent();
}
return (dashy || contextNode.getNodeType()==SNodeType.DOC)
? contextNode.getIndent()
return (dashy || contextNode.getNodeType()==SNodeType.DOC)
? contextNode.getIndent()
: contextNode.getIndent() + YamlIndentUtil.INDENT_BY;
}
@@ -212,7 +212,7 @@ public class YamlCompletionEngine implements ICompletionEngine {
};
transformed.deemphasize(DEEMP_DEDENTED_PROPOSAL*numArrows);
return transformed;
}
}
// we can't dedent the proposal by the requested amount of space. So err on the safe
// side and ignore the proposal. (Otherwise me might end up deleting non-space chars
// in our attempt to de-dent.)
@@ -226,7 +226,13 @@ public class YamlCompletionEngine implements ICompletionEngine {
return Strings.repeat(Unicodes.RIGHT_ARROW+" ", numArrows) + originalLabel;
}
@Override public DocumentEdits transformEdit(DocumentEdits originalEdit) {
originalEdit.indentFirstEdit(indentStr);
// originalEdit.indentFirstEdit(indentStr);
YamlIndentUtil indenter = new YamlIndentUtil("\n");
originalEdit.transformFirstNonWhitespaceEdit((Integer offset, String insertText) -> {
String prefix = insertText.substring(0, offset);
String target = insertText.substring(offset);
return prefix + indentStr + indenter.applyIndentation(target, indentStr);
});
return originalEdit;
}
};
@@ -306,14 +312,14 @@ public class YamlCompletionEngine implements ICompletionEngine {
}
return null;
}
/**
* Get context node candidates taking into account that we want to have a 'relaxed' interpretation
* of the context node with respect to the current indentation where we ask for a completion.
* To allow for the ambiguity in indentation a list of context nodes is returned instead of a
* To allow for the ambiguity in indentation a list of context nodes is returned instead of a
* single node. (Note we may still return a singleton list for cases where relaxed indentation
* doesn't seem desirable).
* @param baseIndent
* @param baseIndent
*/
protected List<SNode> getContextNodes(YamlDocument doc, SNode node, int offset, int baseIndent) {
if (node==null) {
@@ -340,7 +346,7 @@ public class YamlCompletionEngine implements ICompletionEngine {
//This node has flexibility around indentation. So this is where me need to build a list of candidates!
ImmutableList.Builder<SNode> contextNodes = ImmutableList.builder();
while (node!=null ) {
//Any node that represents a 'step' between contexts and is not too deeply nested is kept.
//Any node that represents a 'step' between contexts and is not too deeply nested is kept.
if (node.getSegment()!=null && node.getIndent()<=baseIndent) {
contextNodes.add(node);
}

View File

@@ -37,4 +37,10 @@ public class Snippet {
public String toString() {
return "Snippet [ name="+name+",\n" +snippet +"\n]";
}
public Predicate<DynamicSchemaContext> getApplicability() {
return applicability;
}
public boolean isApplicable(DynamicSchemaContext dc) {
return applicability==null || applicability.test(dc);
}
}

View File

@@ -11,6 +11,7 @@
package org.springframework.ide.vscode.commons.yaml.util;
import org.springframework.ide.vscode.commons.util.Assert;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.ide.vscode.commons.yaml.structure.YamlDocument;
import com.google.common.base.Strings;
@@ -42,7 +43,11 @@ public class YamlIndentUtil {
}
public YamlIndentUtil(YamlDocument doc) {
this(doc.getDocument().getDefaultLineDelimiter());
this(doc.getDocument());
}
public YamlIndentUtil(IDocument doc) {
this(doc.getDefaultLineDelimiter());
}
/**
@@ -89,6 +94,10 @@ public class YamlIndentUtil {
return text.replaceAll("\\n", newlineWithIndent(indentBy));
}
public String applyIndentation(String text, String indentStr) {
return text.replaceAll("\\n", "\n"+indentStr);
}
/**
* Increase offset by indentation. Take care when 'indent' is -1 (unkownn) to
* just return offset unmodified.

View File

@@ -63,6 +63,7 @@ public class Editor {
public static final Predicate<CompletionItem> PLAIN_COMPLETION = c -> !RELAXED_COMPLETION.test(c);
public static final Predicate<CompletionItem> DEDENTED_COMPLETION = c -> c.getLabel().startsWith(Unicodes.LEFT_ARROW+" ");
public static final Predicate<CompletionItem> INDENTED_COMPLETION = c -> c.getLabel().startsWith(Unicodes.RIGHT_ARROW+" ");
public static final Predicate<CompletionItem> SNIPPET_COMPLETION = c -> c.getLabel().endsWith("Snippet");
static class EditorState {
String documentContents;
@@ -327,6 +328,10 @@ public class Editor {
}
public List<CompletionItem> assertCompletionLabels(String... expectedLabels) throws Exception {
return assertCompletionLabels(c -> true, expectedLabels);
}
public List<CompletionItem> assertCompletionLabels(Predicate<CompletionItem> isInteresting, String... expectedLabels) throws Exception {
StringBuilder expect = new StringBuilder();
StringBuilder actual = new StringBuilder();
for (String label : expectedLabels) {
@@ -336,8 +341,10 @@ public class Editor {
List<CompletionItem> completions;
for (CompletionItem completion : completions = getCompletions()) {
actual.append(completion.getLabel());
actual.append("\n");
if (isInteresting.test(completion)) {
actual.append(completion.getLabel());
actual.append("\n");
}
}
assertEquals(expect.toString(), actual.toString());
return completions;