diff --git a/vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java b/vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java index 40186eba8..1e7613ac3 100644 --- a/vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java +++ b/vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java @@ -404,4 +404,15 @@ public class DocumentEdits implements ProposalApplier { insert(start, newText); } + /** + * Adds extra indentation at the position of the first edit in this {@link DocumentEdits} + */ + public void indentFirstEdit(String indentString) { + if (edits.size()>0) { + Edit firstEdit = edits.get(0); + int offset = firstEdit.getStart(); + edits.add(0, new Insertion(offset, indentString)); + } + } + } diff --git a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java index 20eb0fafe..31999218f 100644 --- a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java +++ b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java @@ -10,14 +10,20 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.yaml.completion; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.List; +import org.eclipse.lsp4j.CompletionItemKind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionEngine; 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.Assert; +import org.springframework.ide.vscode.commons.util.Renderable; import org.springframework.ide.vscode.commons.util.text.IDocument; import org.springframework.ide.vscode.commons.yaml.path.YamlPath; import org.springframework.ide.vscode.commons.yaml.structure.YamlDocument; @@ -65,16 +71,116 @@ public class YamlCompletionEngine implements ICompletionEngine { SRootNode root = doc.getStructure(); SNode current = root.find(offset); SNode contextNode = getContextNode(doc, current, offset); - if (contextNode!=null) { - YamlAssistContext context = getContext(doc, contextNode); - if (context==null && isDubiousKey(contextNode, offset)) { - current = current.getParent(); - contextNode = contextNode.getParent(); - context = getContext(doc, contextNode); + List all = new ArrayList<>(getPreciseCompletions(offset, doc, current, contextNode)); + all.addAll(addIndentations(getRelaxedCompletions(offset, doc, contextNode, current))); + return all; + } + return Collections.emptyList(); + } + + private Collection addIndentations( + Collection completions) { + if (!completions.isEmpty()) { + List transformed = new ArrayList<>(); + for (ICompletionProposal p : completions) { + transformed.add(indented(p)); + } + return transformed; + } + return Collections.emptyList(); + } + + public ICompletionProposal indented(ICompletionProposal proposal) { + ScoreableProposal transformed = new ScoreableProposal() { + + DocumentEdits indentedEdit = null; + + @Override + public synchronized DocumentEdits getTextEdit() { + if (indentedEdit==null) { + indentedEdit = proposal.getTextEdit(); + indentedEdit.indentFirstEdit(YamlIndentUtil.INDENT_STR); } - if (context!=null) { - return context.getCompletions(doc, current, offset); + return indentedEdit; + } + + @Override + public String getLabel() { + return "➔ "+proposal.getLabel(); + } + + @Override + public CompletionItemKind getKind() { + return proposal.getKind(); + } + + @Override + public Renderable getDocumentation() { + return proposal.getDocumentation(); + } + + @Override + public String getDetail() { + return proposal.getDetail(); + } + + @Override + public double getBaseScore() { + if (proposal instanceof ScoreableProposal) { + return ((ScoreableProposal) proposal).getBaseScore(); } + return 0; + } + }; + transformed.deemphasize(); + return transformed; + } + + private Collection getRelaxedCompletions(int offset, YamlDocument doc, SNode preciseContextNode, SNode currentNode) throws Exception { + if (preciseContextNode!=null) { + SNode contextNode = getRelaxedContextNode(preciseContextNode, currentNode); + YamlAssistContext context = getContext(doc, contextNode); + if (context!=null) { + return context.getCompletions(doc, currentNode, offset); + } + } + return Collections.emptyList(); + } + + private boolean isBarrenKey(SNode node) throws Exception { + if (node.getNodeType()==SNodeType.KEY) { + SKeyNode keyNode = (SKeyNode) node; + String value = keyNode.getSimpleValue(); + return value.trim().isEmpty(); + } + return false; + } + + private SNode getRelaxedContextNode(SNode preciseContextNode, SNode currentNode) throws Exception { + while (currentNode!=null) { + if (currentNode.getParent()==preciseContextNode) { + if (isBarrenKey(currentNode)) { + return currentNode; + } else { + return null; + } + } + currentNode = currentNode.getParent(); + } + return currentNode; + } + + protected Collection getPreciseCompletions(int offset, YamlDocument doc, SNode current, SNode contextNode) + throws Exception { + if (contextNode!=null) { + YamlAssistContext context = getContext(doc, contextNode); + if (context==null && isDubiousKey(contextNode, offset)) { + current = current.getParent(); + contextNode = contextNode.getParent(); + context = getContext(doc, contextNode); + } + if (context!=null) { + return context.getCompletions(doc, current, offset); } } return Collections.emptyList(); diff --git a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPathSegment.java b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPathSegment.java index 73906f99d..def73d205 100644 --- a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPathSegment.java +++ b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPathSegment.java @@ -27,7 +27,7 @@ public abstract class YamlPathSegment { } public static class AnyChild extends YamlPathSegment { - + private static AnyChild INSTANCE = new AnyChild(); private AnyChild() {} @@ -61,10 +61,12 @@ public abstract class YamlPathSegment { this.index = index; } + @Override public String toNavString() { return "["+index+"]"; } + @Override public String toPropString() { return "["+index+"]"; } @@ -175,6 +177,7 @@ public abstract class YamlPathSegment { } + @Override public String toString() { return toNavString(); } @@ -194,7 +197,7 @@ public abstract class YamlPathSegment { public static YamlPathSegment keyAt(String key) { return new KeyAtKey(key); } - + public static YamlPathSegment anyChild() { return AnyChild.INSTANCE; } diff --git a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java index f080f9a71..844a9f4b9 100644 --- a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java +++ b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java @@ -757,6 +757,10 @@ public class YamlStructureParser { return doc.textBetween(getStart(), getColonOffset()); } + public String getSimpleValue() { + return doc.textBetween(getColonOffset()+1, getNodeEnd()); + } + /** * Get the offset of the ':' character that separates the 'key' from the 'value' area. * @return Absolute offset (from beginning of document). @@ -789,6 +793,7 @@ public class YamlStructureParser { return indentedText; } + } private Iterable getKeyAliases(String key) { diff --git a/vscode-extensions/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java b/vscode-extensions/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java index af1ac93d8..da98b1648 100644 --- a/vscode-extensions/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java +++ b/vscode-extensions/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java @@ -518,6 +518,14 @@ public class Editor { .get(); } + public void assertCompletionWithLabel(String expectLabel, String expectedResult) throws Exception { + CompletionItem completion = assertCompletionWithLabel(expectLabel); + String saveText = getText(); + apply(completion); + assertEquals(expectedResult, getText()); + setText(saveText); + } + public void setSelection(int start, int end) { Assert.assertTrue(start>=0); @@ -618,4 +626,5 @@ public class Editor { public String getLanguageId() { return languageId; } + } diff --git a/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ConcourseModel.java b/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ConcourseModel.java index 0c43abe85..cd3f395a8 100644 --- a/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ConcourseModel.java +++ b/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ConcourseModel.java @@ -46,7 +46,6 @@ import com.google.common.collect.Multiset; * and completion engine). */ public class ConcourseModel { - public static final YamlPath JOB_NAMES_PATH = new YamlPath( anyChild(), valueAt("jobs"), diff --git a/vscode-extensions/vscode-concourse/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java b/vscode-extensions/vscode-concourse/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java index 777103352..091e8d2ac 100644 --- a/vscode-extensions/vscode-concourse/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java +++ b/vscode-extensions/vscode-concourse/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java @@ -2618,7 +2618,7 @@ public class ConcourseEditorTest { } } - @Ignore @Test public void relaxedIndentContextMoreSpaces() throws Exception { + @Test public void relaxedIndentContextMoreSpaces() throws Exception { Editor editor; editor = harness.newEditor( @@ -2635,20 +2635,72 @@ public class ConcourseEditorTest { "source", "type", //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" + "➔ 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" + ); + + editor.assertCompletionWithLabel("check_every", + "resources:\n" + + "- name: foo\n" + + " type: git\n" + + " source:\n" + + " check_every: <*>" + ); + + editor.assertCompletionWithLabel("➔ branch", + "resources:\n" + + "- name: foo\n" + + " type: git\n" + + " source:\n" + + " branch: <*>" + ); + editor.assertCompletionWithLabel("➔ commit_verification_key_ids", + "resources:\n" + + "- name: foo\n" + + " type: git\n" + + " source:\n" + + " commit_verification_key_ids:\n" + + " - <*>" + ); + } + + @Test public void relaxedIndentContextMoreSpaces2() throws Exception { + + assertContextualCompletions( + "resources:\n" + + "- name: foo\n" + + " type: git\n" + + " source:\n" + + " <*>" + , // ========= + "bra<*>" + , //=> + " branch: <*>" + ); + + assertContextualCompletions( + "resources:\n" + + "- name: foo\n" + + " type: git\n" + + " source:\n" + + " <*>" + , // ========= + "comverids<*>" + , //=> + " commit_verification_key_ids:\n" + + " - <*>" ); }