diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/completions/ScopeNameCompletionProposal.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/completions/ScopeNameCompletionProposal.java index 8718dd0c7..cab7e72c8 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/completions/ScopeNameCompletionProposal.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/completions/ScopeNameCompletionProposal.java @@ -13,7 +13,6 @@ package org.springframework.ide.vscode.boot.java.completions; import org.eclipse.lsp4j.CompletionItemKind; import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; -import org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal; import org.springframework.ide.vscode.commons.util.Renderable; import org.springframework.ide.vscode.commons.util.text.IDocument; diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/ScoreableProposal.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/ScoreableProposal.java index f510c4303..0c4f3370d 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/ScoreableProposal.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/ScoreableProposal.java @@ -19,9 +19,10 @@ public abstract class ScoreableProposal implements ICompletionProposal { public static final double DEEMP_EXISTS = 0.1; public static final double DEEMP_DEPRECATION = 0.2; - public static final double DEEMP_DASH_PROPOSAL = 0.5; - public static final double DEEMP_INDENTED_PROPOSAL = 1.0; - public static final double DEEMP_DEDENTED_PROPOSAL = 1.5; + public static final double DEEMP_NEXT_CONTEXT = 0.0; + public static final double DEEMP_INDENTED_PROPOSAL = 0.4; + public static final double DEEMP_DASH_PROPOSAL = 0.6; + public static final double DEEMP_DEDENTED_PROPOSAL = 0.8; private static final double DEEMP_VALUE = 10_000; // should be large enough to move deemphasized stuff to bottom of list. @@ -53,7 +54,7 @@ public abstract class ScoreableProposal implements ICompletionProposal { } @Override public ScoreableProposal deemphasize(double howmuch) { - Assert.isLegal(howmuch>0.0); + Assert.isLegal(howmuch>=0.0); deemphasizedBy+= howmuch*DEEMP_VALUE; return this; } 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 69fe322ed..0d34ac348 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 @@ -10,12 +10,15 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.yaml.completion; +import static org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal.DEEMP_DASH_PROPOSAL; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -105,51 +108,71 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { int queryOffset = offset - query.length(); SNode contextNode = getContextNode(); DynamicSchemaContext dynamicCtxt = getSchemaContext(); - List properties = typeUtil.getProperties(type); - if (CollectionUtil.hasElements(properties)) { - ArrayList proposals = new ArrayList<>(properties.size()); + List allProperties = typeUtil.getProperties(type); + if (CollectionUtil.hasElements(allProperties)) { + List> tieredProperties = sortIntoTiers(allProperties); Set definedProps = dynamicCtxt.getDefinedProperties(); - for (YTypedProperty p : properties) { - String name = p.getName(); - double score = FuzzyMatcher.matchScore(query, name); - if (score!=0) { - YamlPath relativePath = YamlPath.fromSimpleProperty(name); - YamlPathEdits edits = new YamlPathEdits(doc); - if (!definedProps.contains(name)) { - //property not yet defined - YType YType = p.getType(); - edits.delete(queryOffset, query); - if (queryOffset>0 && !Character.isWhitespace(doc.getChar(queryOffset-1))) { - //See https://www.pivotaltracker.com/story/show/137722057 - edits.insert(queryOffset, " "); + for (List thisTier : tieredProperties) { + List undefinedProps = thisTier.stream() + .filter(p -> !definedProps.contains(p.getName())) + .collect(Collectors.toList()); + if (!undefinedProps.isEmpty()) { + List proposals = new ArrayList<>(); + for (YTypedProperty p : undefinedProps) { + String name = p.getName(); + double score = FuzzyMatcher.matchScore(query, name); + if (score!=0) { + YamlPath relativePath = YamlPath.fromSimpleProperty(name); + YamlPathEdits edits = new YamlPathEdits(doc); + YType YType = p.getType(); + edits.delete(queryOffset, query); + if (queryOffset>0 && !Character.isWhitespace(doc.getChar(queryOffset-1))) { + //See https://www.pivotaltracker.com/story/show/137722057 + edits.insert(queryOffset, " "); + } + edits.createPathInPlace(contextNode, relativePath, queryOffset, appendTextFor(YType)); + proposals.add(completionFactory().beanProperty(doc.getDocument(), + contextPath.toPropString(), getType(), + query, p, score, edits, typeUtil) + ); } - edits.createPathInPlace(contextNode, relativePath, queryOffset, appendTextFor(YType)); - proposals.add(completionFactory().beanProperty(doc.getDocument(), - contextPath.toPropString(), getType(), - query, p, score, edits, typeUtil) - ); - } else { - // This piece below deactivated becuase moving cursor like this doesn't work in vscode -// //property already defined -// // instead of filtering, navigate to the place where its defined. -// deleteQueryAndLine(doc, query, queryOffset, edits); -// //Cast to SChildBearingNode cannot fail because otherwise definedProps would be the empty set. -// edits.createPath((SChildBearingNode) contextNode, relativePath, ""); -// proposals.add( -// completionFactory().beanProperty(doc.getDocument(), -// contextPath.toPropString(), getType(), -// query, p, score, edits, typeUtil) -// .deemphasize(DEEMP_EXISTS) //deemphasize because it already exists -// ); } + return proposals; } } - return proposals; } return Collections.emptyList(); } - + /** + * Divides a given list of properties into tiers of decreasing significance. Property tiering + * is a mechanism to reduce 'noise' in content assist proposals. Only properties of the + * first tier that some still undefined properties will be used to generate proposals. + *

+ * This allows, for example, to only suggest a 'name' property when starting to define + * a new named entity. This is what a sane user would probably want, even though + * in theory they would be free to define the properties in any order they want. + */ + protected List> sortIntoTiers(List properties) { + if (properties.isEmpty()) { + //Nothing to sort + return ImmutableList.of(); + } else { + ImmutableList.Builder primary = ImmutableList.builder(); + ImmutableList.Builder required = ImmutableList.builder(); + ImmutableList.Builder other = ImmutableList.builder(); + for (YTypedProperty p : properties) { + if (p.isPrimary()) { + primary.add(p); + } else if (p.isRequired()) { + required.add(p); + } else { + other.add(p); + } + } + return ImmutableList.of(primary.build(), required.build(), other.build()); + } + } /** * Computes the text that should be appended at the end of a completion @@ -379,7 +402,7 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { protected String tranformLabel(String originalLabel) { return "- "+originalLabel; } - }.deemphasize(0.5) + }.deemphasize(DEEMP_DASH_PROPOSAL) ); } return dashedCompletions; diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java index 4a91634cd..3ec679dd4 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java @@ -10,9 +10,6 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.yaml.completion; -import static org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal.DEEMP_DEDENTED_PROPOSAL; -import static org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal.DEEMP_INDENTED_PROPOSAL; - import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -45,6 +42,8 @@ import org.springframework.ide.vscode.commons.yaml.util.YamlIndentUtil; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; +import static org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal.*; + /** * Implements {@link ICompletionEngine} for .yml file, based on a YamlAssistContextProvider * which has to to be injected into engine via its constructor. @@ -82,15 +81,17 @@ public class YamlCompletionEngine implements ICompletionEngine { if (!doc.isCommented(offset)) { SRootNode root = doc.getStructure(); SNode current = root.find(offset); - List contextNodes = getContextNodes(doc, current, offset); + int cursorIndent = doc.getColumn(offset); + int nodeIndent = current.getIndent(); + int baseIndent = YamlIndentUtil.minIndent(cursorIndent, nodeIndent); + List contextNodes = getContextNodes(doc, current, offset, baseIndent); if (current.getNodeType()==SNodeType.RAW) { //relaxed indentation List completions = new ArrayList<>(); - int cursorIndent = doc.getColumn(offset); - int nodeIndent = current.getIndent(); - int baseIndent = YamlIndentUtil.minIndent(cursorIndent, nodeIndent); + double deempasizeBy = 0.0; for (SNode contextNode : contextNodes) { - completions.addAll(getRelaxedCompletions(offset, doc, current, contextNode, baseIndent)); + completions.addAll(getRelaxedCompletions(offset, doc, current, contextNode, baseIndent, deempasizeBy)); + deempasizeBy += ScoreableProposal.DEEMP_NEXT_CONTEXT; } return completions; } else { @@ -104,29 +105,27 @@ public class YamlCompletionEngine implements ICompletionEngine { return Collections.emptyList(); } - protected Collection getRelaxedCompletions(int offset, YamlDocument doc, SNode current, SNode contextNode, int baseIndent) { + protected Collection getRelaxedCompletions(int offset, YamlDocument doc, SNode current, SNode contextNode, int baseIndent, double deempasizeBy) { try { return fixIndentations(getBaseCompletions(offset, doc, current, contextNode), - current, contextNode, baseIndent); + current, contextNode, baseIndent, deempasizeBy); } catch (Exception e) { Log.log(e); } return ImmutableList.of(); } - protected Collection fixIndentations(Collection completions, SNode currentNode, SNode contextNode, int baseIndent) { + protected Collection fixIndentations(Collection completions, SNode currentNode, + SNode contextNode, int baseIndent, double deempasizeBy) { if (!completions.isEmpty()) { int dashyIndent = getTargetIndent(contextNode, currentNode, true); int plainIndent = getTargetIndent(contextNode, currentNode, false); List transformed = new ArrayList<>(); for (ICompletionProposal p : completions) { - ICompletionProposal p_fixed = null; - if (p.getLabel().startsWith("- ")) { - p_fixed = indentFix(p, dashyIndent - baseIndent, currentNode, contextNode); - } else { - p_fixed = indentFix(p, plainIndent - baseIndent, currentNode, contextNode); - } + int targetIndent = p.getLabel().startsWith("- ") ? dashyIndent : plainIndent; + ScoreableProposal p_fixed = indentFix((ScoreableProposal)p, targetIndent - baseIndent, currentNode, contextNode); if (p_fixed!=null) { + p_fixed.deemphasize(deempasizeBy); transformed.add(p_fixed); } } @@ -135,11 +134,11 @@ public class YamlCompletionEngine implements ICompletionEngine { return Collections.emptyList(); } - protected ICompletionProposal indentFix(ICompletionProposal p, int fixIndentBy, SNode currentNode, SNode contextNode) { + protected ScoreableProposal indentFix(ScoreableProposal p, int fixIndentBy, SNode currentNode, SNode contextNode) { if (fixIndentBy==0) { return p; } else if (fixIndentBy>0) { - if (isExtraIndentRelaxable(contextNode)) { + if (isExtraIndentRelaxable(contextNode, fixIndentBy)) { return indented(p, Strings.repeat(" ", fixIndentBy)); } } else { // fixIndentBy < 0 @@ -182,7 +181,7 @@ public class YamlCompletionEngine implements ICompletionEngine { : contextNode.getIndent() + YamlIndentUtil.INDENT_BY; } - public ICompletionProposal dedented(ICompletionProposal proposal, int numSpacesToRemove, IDocument doc) { + public ScoreableProposal dedented(ICompletionProposal proposal, int numSpacesToRemove, IDocument doc) { Assert.isLegal(numSpacesToRemove>0); int spacesEnd = proposal.getTextEdit().getFirstEditStart(); int spacesStart = spacesEnd-numSpacesToRemove; @@ -214,23 +213,23 @@ public class YamlCompletionEngine implements ICompletionEngine { return null; } - - public ICompletionProposal indented(ICompletionProposal proposal, String indentStr) { + public ScoreableProposal indented(ICompletionProposal proposal, String indentStr) { + int numArrows = (indentStr.length()+1)/2; ScoreableProposal transformed = new TransformedCompletion(proposal) { @Override public String tranformLabel(String originalLabel) { - return Unicodes.RIGHT_ARROW+" " + originalLabel; + return Strings.repeat(Unicodes.RIGHT_ARROW+" ", numArrows) + originalLabel; } @Override public DocumentEdits transformEdit(DocumentEdits originalEdit) { originalEdit.indentFirstEdit(indentStr); return originalEdit; } }; - transformed.deemphasize(DEEMP_INDENTED_PROPOSAL*indentStr.length()/2); + transformed.deemphasize(numArrows * DEEMP_INDENTED_PROPOSAL); return transformed; } - private boolean isExtraIndentRelaxable(SNode contextNode) { - return contextNode!=null && ( + private boolean isExtraIndentRelaxable(SNode contextNode, int fixIndentBy) { + return contextNode!=null && /* fixIndentBy<=2 && */ ( isBarrenKey(contextNode) || isBarrenSeq(contextNode) ); @@ -308,8 +307,9 @@ public class YamlCompletionEngine implements ICompletionEngine { * 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 */ - protected List getContextNodes(YamlDocument doc, SNode node, int offset) { + protected List getContextNodes(YamlDocument doc, SNode node, int offset, int baseIndent) { if (node==null) { return null; } else if (node.getNodeType()==SNodeType.KEY) { @@ -334,8 +334,8 @@ 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 contextNodes = ImmutableList.builder(); while (node!=null ) { - //Any node that represents a 'step' between contexts must be kept. - if (node.getSegment()!=null) { + //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); } node = node.getParent(); 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 dc5493bc4..2fc7329b8 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 @@ -464,11 +464,8 @@ public class YTypeFactory { } /** - * If set to false (which is the default), then this type (when not yet inferred to a - * more specific version of itself) will be treated as if it can be anything (i.e atomic, map or sequence) - *

- * If set to true, then it is treated as strictly atomic type instead (i.e it isn't valid to - * use a map or sequence for its value). + * Treat this type as an atomic type (i.e. it can't be a map or sequence), when not yet inferred to a + * more specific version of itself). */ public AbstractType treatAsAtomic() { this.isAtomic = true; @@ -732,6 +729,7 @@ public class YTypeFactory { private Renderable description = Renderables.NO_DESCRIPTION; private boolean isRequired; private boolean isDeprecated; + private boolean isPrimary; private YTypedPropertyImpl(String name, YType type) { this.name = name; @@ -770,7 +768,7 @@ public class YTypeFactory { @Override public boolean isRequired() { - return isRequired; + return isRequired || isPrimary; } public void isDeprecated(boolean isDeprecated) { @@ -781,6 +779,16 @@ public class YTypeFactory { public boolean isDeprecated() { return this.isDeprecated; } + + public YTypedPropertyImpl isPrimary(boolean b) { + this.isPrimary = b; + return this; + } + + @Override + public boolean isPrimary() { + return isPrimary; + } } public YAtomicType yatomic(String name) { 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 0f3bb1b4e..b6c10556b 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 @@ -13,7 +13,6 @@ package org.springframework.ide.vscode.commons.yaml.schema; import java.util.List; import java.util.Map; -import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionEngine; import org.springframework.ide.vscode.commons.util.ValueParser; import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraint; diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypedProperty.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypedProperty.java index dbd7dbeef..36da3cd76 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypedProperty.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypedProperty.java @@ -19,6 +19,7 @@ public interface YTypedProperty { String getName(); YType getType(); Renderable getDescription(); - default boolean isRequired() { return false; } + default boolean isRequired() { return isPrimary() || false; } default boolean isDeprecated() { return false; } + default boolean isPrimary() { return false; } } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java index f1e098c2b..8d14f2bef 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java @@ -18,6 +18,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -656,7 +657,6 @@ public class YamlStructureParser { if (indent==-1) { createRawNode(parent, line); } else { - parent = dropTo(parent, indent); parent = parseLine(parent, line, true); } } @@ -668,15 +668,14 @@ public class YamlStructureParser { parent = createDocNode(parent.getRoot(), line); } else if (line.matches(SIMPLE_KEY_LINE)) { int currentIndent = line.getIndent(); - while (currentIndent==parent.getIndent() && parent.getNodeType()!=SNodeType.DOC) { - parent = parent.getParent(); - } + parent = dropToLevel(parent, (node) -> node.getIndent() { + int indent = node.getIndent(); + return indent < currentIndent || node.getNodeType()!=SNodeType.SEQ && indent<=currentIndent; + }); parent = createSeqNode(parent, line); parent = parseLine(parent, line.moveIndentMark(2), false); //parse from just after "- " for nested seq and key nodes } else if (createRawNode) { @@ -685,6 +684,13 @@ public class YamlStructureParser { return parent; } + private SChildBearingNode dropToLevel(SChildBearingNode parent, Predicate level) { + while (parent.getNodeType()!=SNodeType.DOC && parent.getSegment()!=null && !level.test(parent)) { + parent = parent.getParent(); + } + return parent; + } + private SChildBearingNode createDocNode(SRootNode parent, YamlLine line) { int start = line.getStart(); int end = line.getEnd(); @@ -713,13 +719,6 @@ public class YamlStructureParser { } - private SChildBearingNode dropTo(SChildBearingNode node, int indent) { - while (indent\n" + - " blah: blah" + " <*>" + , //================ + "<*>" + , // ==> + "uri: <*>" + ); + + assertContextualCompletions(PLAIN_COMPLETION, + "resources:\n" + + "- name: the-repo\n" + + " type: git\n" + + " source:\n" + + " uri:\n" + + " <*>" , //================ "<*>" , // ==> @@ -1021,8 +1035,6 @@ public class ConcourseEditorTest { , "tag_filter: <*>" , - "uri: <*>" - , "username: <*>" ); @@ -1156,6 +1168,22 @@ public class ConcourseEditorTest { } @Test public void gitResourcePutParamsCompletions() throws Exception { + assertContextualCompletions(PLAIN_COMPLETION, + "resources:\n" + + "- name: my-git\n" + + " type: git\n" + + "jobs:\n" + + "- name: do-stuff\n" + + " plan:\n" + + " - put: my-git\n" + + " params:\n" + + " <*>" + , + "<*>" + , // => + "repository: <*>" + ); + String context = "resources:\n" + "- name: my-git\n" + @@ -1165,10 +1193,10 @@ public class ConcourseEditorTest { " plan:\n" + " - put: my-git\n" + " params:\n" + - " <*>\n" + - " blah: blah"; + " repository: blah\n" + + " <*>"; - assertContextualCompletions(context, + assertContextualCompletions(PLAIN_COMPLETION, context, "<*>" , // ===> "annotate: <*>" @@ -1179,8 +1207,6 @@ public class ConcourseEditorTest { , "rebase: <*>" , - "repository: <*>" - , "tag: <*>" , "tag_prefix: <*>" @@ -2011,41 +2037,57 @@ public class ConcourseEditorTest { } @Test public void semverGitResourceSourceContentAssist() throws Exception { - String conText = + assertContextualCompletions(PLAIN_COMPLETION, "resources:\n" + "- name: version\n" + " type: semver\n" + " source:\n" + - "<*>\n" + - " blah: blah"; - assertContextualCompletions(conText, " driver: git\n" + " <*>" + , // =========== + "<*>" , // ==> + "uri: <*>" + ); + assertContextualCompletions(PLAIN_COMPLETION, + "resources:\n" + + "- name: version\n" + + " type: semver\n" + + " source:\n" + " driver: git\n" + + " uri: something\n" + + "<*>" + , // ============= + " <*>" + , // ==> " branch: <*>" , - " driver: git\n" + " file: <*>" - , + ); + assertContextualCompletions(PLAIN_COMPLETION, + "resources:\n" + + "- name: version\n" + + " type: semver\n" + + " source:\n" + " driver: git\n" + + " uri: something\n" + + " branch: master\n" + + " file: somefile\n" + + "<*>" + , // ============= + " <*>" + , // ==> " git_user: <*>" , - " driver: git\n" + " initial_version: <*>" , - " driver: git\n" + " password: <*>" , - " driver: git\n" + " private_key: <*>" , - " driver: git\n" + - " uri: <*>" - , - " driver: git\n" + " username: <*>" ); + } @Test public void semverGitResourceSourceReconcileAndHovers() throws Exception { @@ -2593,6 +2635,19 @@ public class ConcourseEditorTest { @Test public void contentAssistTaskFileToplevelProperties() throws Exception { assertTaskCompletions( + "<*>" + , // ==> + "platform: <*>" + , + "run:\n" + + " <*>" + ); + + assertContextualTaskCompletions( + "run: {}\n" + + "platform: linux\n" + + "<*>" + , "<*>" , // ==> "image: <*>" @@ -2608,11 +2663,6 @@ public class ConcourseEditorTest { , "params:\n" + " <*>" - , - "platform: <*>" - , - "run:\n" + - " <*>" ); assertTaskCompletions( @@ -2928,29 +2978,13 @@ public class ConcourseEditorTest { //"source", exists //"type", exists //For the nested context: - "→ branch", - "→ commit_verification_key_ids", - "→ commit_verification_keys", - "→ disable_ci_skip", - "→ git_config", - "→ gpg_keyserver", - "→ ignore_paths", - "→ password", - "→ paths", - "→ private_key", - "→ skip_ssl_verification", - "→ tag_filter", "→ uri", - "→ username", // For the top-level context: "← groups", "← jobs", "← resource_types", // For the 'next job' context: - "← - check_every", - "← - name", - "← - source", - "← - type" + "← - name" ); editor.assertCompletionWithLabel("check_every", @@ -2961,33 +2995,55 @@ public class ConcourseEditorTest { " check_every: <*>" ); - editor.assertCompletionWithLabel("→ branch", + editor.assertCompletionWithLabel("→ uri", "resources:\n" + "- name: foo\n" + " type: git\n" + " source:\n" + - " branch: <*>" + " uri: <*>" + ); + + editor = harness.newEditor( + "resources:\n" + + "- name: foo\n" + + " type: git\n" + + " source:\n" + + " uri: blah\n" + + " <*>" ); editor.assertCompletionWithLabel("→ commit_verification_key_ids", "resources:\n" + "- name: foo\n" + " type: git\n" + " source:\n" + + " uri: blah\n" + " commit_verification_key_ids:\n" + " - <*>" ); } @Test public void relaxedIndentContextMoreSpaces2() throws Exception { - - assertContextualCompletions( + assertContextualCompletions(INDENTED_COMPLETION, "resources:\n" + "- name: foo\n" + " type: git\n" + " source:\n" + " <*>" , // ========= - "bra<*>" + "ur" + , //=> + " uri: <*>" + ); + + assertContextualCompletions(INDENTED_COMPLETION, + "resources:\n" + + "- name: foo\n" + + " type: git\n" + + " source:\n" + + " uri: blah\n" + + " <*>" + , // ========= + "bra" , //=> " branch: <*>" ); @@ -2997,6 +3053,7 @@ public class ConcourseEditorTest { "- name: foo\n" + " type: git\n" + " source:\n" + + " uri: blah\n" + " <*>" , // ========= "comverids<*>" @@ -3027,13 +3084,6 @@ public class ConcourseEditorTest { //"name", exists //"plan", exists //"public", exists - //Completions with '-' - "- aggregate", - "- do", - "- get", - "- put", - "- task", - "- try", //Completions for nested context (i.e. task step) "→ attempts", "→ config", @@ -3048,18 +3098,18 @@ public class ConcourseEditorTest { "→ privileged", "→ tags", "→ timeout", - //"→ task" exists - "← groups\n" + - "← resource_types\n" + - "← resources\n" + - "← - build_logs_to_retain\n" + - "← - disable_manual_trigger\n" + - "← - max_in_flight\n" + - "← - name\n" + - "← - plan\n" + - "← - public\n" + - "← - serial\n" + - "← - serial_groups" + //Completions with '-' + "- aggregate", + "- do", + "- get", + "- put", + "- task", + "- try", + //Dedented completions + "← groups", + "← resource_types", + "← resources", + "← - name" ); } @@ -3445,6 +3495,24 @@ public class ConcourseEditorTest { ); } + @Test public void relaxedContentAssist_primary_properties() throws Exception{ + //See https://www.pivotaltracker.com/story/show/144584163 + Editor editor; + + editor = harness.newEditor( + "resources:\n" + + "- name: docker-git\n" + + "<*>" + ); + editor.assertCompletionLabels( + "groups", + "jobs", + "resource_types", + "→ type", + "- name" + ); + } + @Test public void relaxedContentAssistLessSpaces() throws Exception { Editor editor; @@ -3543,7 +3611,15 @@ public class ConcourseEditorTest { ////////////////////////////////////////////////////////////////////////////// private void assertContextualCompletions(String conText, String textBefore, String... textAfter) throws Exception { - Editor editor = harness.newEditor(conText); + assertContextualCompletions((c) -> true, conText, textBefore, textAfter); + } + + private void assertContextualCompletions(Predicate isInteresting, String conText, String textBefore, String... textAfter) throws Exception { + assertContextualCompletions(LanguageId.CONCOURSE_PIPELINE, isInteresting, conText, textBefore, textAfter); + } + + private void assertContextualCompletions(LanguageId language, Predicate isInteresting, String conText, String textBefore, String... textAfter) throws Exception { + Editor editor = harness.newEditor(language, conText); editor.reconcile(); //this ensures the conText is parsed and its AST is cached (will be used for //dynamic CA when the conText + textBefore is not parsable. assertContains(CURSOR, conText); @@ -3552,7 +3628,7 @@ public class ConcourseEditorTest { .map((String t) -> conText.replace(CURSOR, t)) .collect(Collectors.toList()).toArray(new String[0]); editor.setText(textBefore); - editor.assertCompletions(textAfter); + editor.assertCompletions(isInteresting, textAfter); } private void assertCompletions(String textBefore, String... textAfter) throws Exception { @@ -3565,5 +3641,18 @@ public class ConcourseEditorTest { editor.assertCompletions(textAfter); } + private void assertContextualTaskCompletions(String conText, String textBefore, String... textAfter) throws Exception { + assertContextualCompletions(LanguageId.CONCOURSE_TASK, c -> true, conText, textBefore, textAfter); + } + + public static final Predicate RELAXED_COMPLETION + = c -> c.getLabel().startsWith("- ") + || c.getLabel().startsWith(Unicodes.LEFT_ARROW+" ") + || c.getLabel().startsWith(Unicodes.RIGHT_ARROW+" ") + ; + + public static final Predicate PLAIN_COMPLETION = c -> !RELAXED_COMPLETION.test(c); + public static final Predicate DEDENTED_COMPLETION = c -> c.getLabel().startsWith(Unicodes.LEFT_ARROW+" "); + public static final Predicate INDENTED_COMPLETION = c -> c.getLabel().startsWith(Unicodes.RIGHT_ARROW+" "); } diff --git a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java index 3bf3bde01..12b596c10 100644 --- a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java +++ b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java @@ -194,6 +194,7 @@ public class ManifestYmlSchema implements YamlSchema { f.yprop("services", f.yseq(t_service)), f.yprop("stack", t_stack), f.yprop("timeout", t_pos_integer), + f.yprop(HEALTH_CHECK_TYPE_PROP, t_health_check_type), f.yprop(HEALTH_CHECK_HTTP_ENDPOINT_PROP, t_ne_string) }; diff --git a/headless-services/manifest-yaml-language-server/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlEditorTest.java b/headless-services/manifest-yaml-language-server/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlEditorTest.java index 0bc27aaa1..fb04920c1 100644 --- a/headless-services/manifest-yaml-language-server/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlEditorTest.java +++ b/headless-services/manifest-yaml-language-server/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlEditorTest.java @@ -32,6 +32,7 @@ import org.springframework.ide.vscode.commons.cloudfoundry.client.CFServiceInsta import org.springframework.ide.vscode.commons.cloudfoundry.client.CFStack; import org.springframework.ide.vscode.commons.cloudfoundry.client.ClientRequests; import org.springframework.ide.vscode.commons.cloudfoundry.client.cftarget.NoTargetsException; +import org.springframework.ide.vscode.commons.util.Unicodes; import org.springframework.ide.vscode.languageserver.testharness.CodeAction; import org.springframework.ide.vscode.languageserver.testharness.Editor; import org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness; @@ -316,74 +317,101 @@ public class ManifestYamlEditorTest { "- <*>" ); editor.assertCompletions( + "applications:\n" + + "- name: <*>" + ); + + editor = harness.newEditor( + "applications:\n" + + "- name: foo\n" + + " <*>" + ); + editor.assertCompletions((c) -> !(c.getLabel().startsWith("- ") || c.getLabel().startsWith(Unicodes.LEFT_ARROW+" ")), // --------------- "applications:\n" + - "- buildpack: <*>", + "- name: foo\n" + + " buildpack: <*>", // --------------- "applications:\n" + - "- command: <*>", + "- name: foo\n" + + " command: <*>", // --------------- "applications:\n" + - "- disk_quota: <*>", + "- name: foo\n" + + " disk_quota: <*>", // --------------- "applications:\n" + - "- domain: <*>", + "- name: foo\n" + + " domain: <*>", // --------------- "applications:\n" + - "- domains:\n"+ + "- name: foo\n" + + " domains:\n"+ " - <*>", // --------------- "applications:\n" + - "- env:\n"+ + "- name: foo\n" + + " env:\n"+ " <*>", // --------------- "applications:\n" + - "- health-check-http-endpoint: <*>", + "- name: foo\n" + + " health-check-http-endpoint: <*>", // --------------- "applications:\n" + - "- health-check-type: <*>", + "- name: foo\n" + + " health-check-type: <*>", // --------------- "applications:\n" + - "- host: <*>", + "- name: foo\n" + + " host: <*>", // --------------- "applications:\n" + - "- hosts:\n"+ + "- name: foo\n" + + " hosts:\n"+ " - <*>", // --------------- "applications:\n" + - "- instances: <*>", + "- name: foo\n" + + " instances: <*>", // --------------- "applications:\n" + - "- memory: <*>", + "- name: foo\n" + + " memory: <*>", // --------------- "applications:\n" + - "- name: <*>", + "- name: foo\n" + + " no-hostname: <*>", // --------------- "applications:\n" + - "- no-hostname: <*>", + "- name: foo\n" + + " no-route: <*>", // --------------- "applications:\n" + - "- no-route: <*>", + "- name: foo\n" + + " path: <*>", // --------------- "applications:\n" + - "- path: <*>", + "- name: foo\n" + + " random-route: <*>", // --------------- "applications:\n" + - "- random-route: <*>", - // --------------- - "applications:\n" + - "- routes:\n"+ + "- name: foo\n" + + " routes:\n"+ " - route: <*>", // --------------- "applications:\n" + - "- services:\n"+ + "- name: foo\n" + + " services:\n"+ " - <*>", // --------------- "applications:\n" + - "- stack: <*>", + "- name: foo\n" + + " stack: <*>", // --------------- "applications:\n" + - "- timeout: <*>" + "- name: foo\n" + + " timeout: <*>" ); } @@ -391,7 +419,8 @@ public class ManifestYamlEditorTest { public void completionDetailsAndDocs() throws Exception { Editor editor = harness.newEditor( "applications:\n" + - "- build<*>" + "- name: foo\n" + + " build<*>" ); editor.assertCompletionDetails("buildpack", "Buildpack", "If your application requires a custom buildpack"); } @@ -837,72 +866,7 @@ public class ManifestYamlEditorTest { "-<*>", // ===> "applications:\n" + - "- buildpack: <*>", - // --------------- - "applications:\n" + - "- command: <*>", - // --------------- - "applications:\n" + - "- disk_quota: <*>", - // --------------- - "applications:\n" + - "- domain: <*>", - // --------------- - "applications:\n" + - "- domains:\n"+ - " - <*>", - // --------------- - "applications:\n" + - "- env:\n"+ - " <*>", - // --------------- - "applications:\n" + - "- health-check-http-endpoint: <*>", - // --------------- - "applications:\n" + - "- health-check-type: <*>", - // --------------- - "applications:\n" + - "- host: <*>", - // --------------- - "applications:\n" + - "- hosts:\n"+ - " - <*>", - // --------------- - "applications:\n" + - "- instances: <*>", - // --------------- - "applications:\n" + - "- memory: <*>", - // --------------- - "applications:\n" + - "- name: <*>", - // --------------- - "applications:\n" + - "- no-hostname: <*>", - // --------------- - "applications:\n" + - "- no-route: <*>", - // --------------- - "applications:\n" + - "- path: <*>", - // --------------- - "applications:\n" + - "- random-route: <*>", - // --------------- - "applications:\n" + - "- routes:\n"+ - " - route: <*>", - // --------------- - "applications:\n" + - "- services:\n"+ - " - <*>", - // --------------- - "applications:\n" + - "- stack: <*>", - // --------------- - "applications:\n" + - "- timeout: <*>" + "- name: <*>" ); //Second example @@ -912,93 +876,8 @@ public class ManifestYamlEditorTest { "- name: test" , // ==> "applications:\n" + - "- buildpack: <*>\n" + - "- name: test" - , // --------------------- - "applications:\n" + - "- command: <*>\n" + - "- name: test" - , // --------------------- - "applications:\n" + - "- disk_quota: <*>\n" + - "- name: test" - , // --------------------- - "applications:\n" + - "- domain: <*>\n" + - "- name: test" - , // --------------------- - "applications:\n" + - "- domains:\n" + - " - <*>\n" + - "- name: test" - , // --------------------- - "applications:\n" + - "- env:\n" + - " <*>\n" + - "- name: test" - , // --------------------- - "applications:\n" + - "- health-check-http-endpoint: <*>\n" + - "- name: test" - , // --------------------- - "applications:\n" + - "- health-check-type: <*>\n" + - "- name: test" - , // --------------------- - "applications:\n" + - "- host: <*>\n" + - "- name: test" - , // --------------------- - "applications:\n" + - "- hosts:\n" + - " - <*>\n" + - "- name: test" - , // --------------------- - "applications:\n" + - "- instances: <*>\n" + - "- name: test" - , // --------------------- - "applications:\n" + - "- memory: <*>\n" + - "- name: test" - , // --------------------- - "applications:\n" + "- name: <*>\n" + "- name: test" - , // --------------------- - "applications:\n" + - "- no-hostname: <*>\n" + - "- name: test" - , // --------------------- - "applications:\n" + - "- no-route: <*>\n" + - "- name: test" - , // --------------------- - "applications:\n" + - "- path: <*>\n" + - "- name: test" - , // --------------------- - "applications:\n" + - "- random-route: <*>\n" + - "- name: test" - , // --------------------- - "applications:\n" + - "- routes:\n" + - " - route: <*>\n" + - "- name: test" - ,// --------------------- - "applications:\n" + - "- services:\n" + - " - <*>\n" + - "- name: test" - , // --------------------- - "applications:\n" + - "- stack: <*>\n" + - "- name: test" - , // --------------------- - "applications:\n" + - "- timeout: <*>\n" + - "- name: test" ); }