diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPath.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPath.java index ec29323a3..a9333226e 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPath.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPath.java @@ -13,7 +13,6 @@ package org.springframework.ide.vscode.commons.yaml.path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.springframework.ide.vscode.commons.yaml.ast.NodeRef; @@ -21,9 +20,7 @@ import org.springframework.ide.vscode.commons.yaml.ast.NodeRef.RootRef; import org.springframework.ide.vscode.commons.yaml.ast.NodeRef.SeqRef; import org.springframework.ide.vscode.commons.yaml.ast.NodeRef.TupleValueRef; import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil; -import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST; import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment.YamlPathSegmentType; -import org.yaml.snakeyaml.nodes.Node; import reactor.core.publisher.Flux; 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 92cbe47b1..021b08cbd 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 @@ -443,7 +443,8 @@ public class YamlStructureParser { public abstract class SChildBearingNode extends SNode { private List children = null; - private Multimap keyMap = null; //lazily constructed index of children. + private Multimap keyMap = null; //lazily constructed index of children. + private int seqChildren = 0; //Keeps a tally of number of children of type Seq public SChildBearingNode(SChildBearingNode parent, YamlDocument doc, int indent, int start, int end) { super(parent, doc, indent, start, end); @@ -460,6 +461,9 @@ public class YamlStructureParser { children = new ArrayList(); } children.add(c); + if (c instanceof SSeqNode) { + seqChildren++; + } } public SNode getLastChild() { List cs = getChildren(); @@ -519,9 +523,9 @@ public class YamlStructureParser { private SSeqNode getSeqChildWithIndex(int index) { if (index>=0) { - List children = getChildren(); - if (index children = keyMap().get(index); + if (!children.isEmpty()) { + SNode child = children.iterator().next(); if (child instanceof SSeqNode) { return (SSeqNode) child; } @@ -555,15 +559,19 @@ public class YamlStructureParser { return (SKeyNode)getChildrenWithKey(key).findFirst().orElse(null); } - private Multimap keyMap() { + private Multimap keyMap() { if (keyMap==null) { - ListMultimap index = MultimapBuilder.hashKeys().arrayListValues().build(); + ListMultimap index = MultimapBuilder.hashKeys().arrayListValues().build(); for (SNode node: getChildren()) { try { if (node.getNodeType()==SNodeType.KEY) { SKeyNode keyNode = (SKeyNode)node; - String key = ((SKeyNode)node).getKey(); + String key = keyNode.getKey(); index.put(key, keyNode); + } else if (node.getNodeType()==SNodeType.SEQ) { + SSeqNode seqNode = (SSeqNode) node; + int key = seqNode.index; + index.put(key, seqNode); } } catch (Exception e) { Log.log(e); @@ -592,6 +600,10 @@ public class YamlStructureParser { return null; } + public int seqChildrenCount() { + return seqChildren; + } + } public abstract class SLeafNode extends SNode { @@ -729,7 +741,7 @@ public class YamlStructureParser { public SSeqNode(SChildBearingNode parent, YamlDocument doc, int indent, int start, int end) throws Exception { super(parent, doc, indent, start, end); - this.index = parent.getChildren().size()-1; + this.index = parent.seqChildrenCount() - 1; } public int getIndex() { diff --git a/headless-services/commons/commons-yaml/src/test/java/org/springframework/ide/vscode/yaml/structure/YamlStructureParserTest.java b/headless-services/commons/commons-yaml/src/test/java/org/springframework/ide/vscode/yaml/structure/YamlStructureParserTest.java index 214517eda..67d3bbf0c 100644 --- a/headless-services/commons/commons-yaml/src/test/java/org/springframework/ide/vscode/yaml/structure/YamlStructureParserTest.java +++ b/headless-services/commons/commons-yaml/src/test/java/org/springframework/ide/vscode/yaml/structure/YamlStructureParserTest.java @@ -17,6 +17,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment.*; import java.util.ArrayList; import java.util.regex.Pattern; @@ -610,20 +611,20 @@ public class YamlStructureParserTest { " moonstone\n" ); assertParseOneDoc(editor, //////////////// - "DOC(0): ", - " KEY(0): world:", - " KEY(2): europe:", - " KEY(4): france:", - " RAW(6): cheese", - " KEY(4): belgium:", - " RAW(2): beer", - " KEY(2): canada:", - " KEY(4): montreal: poutine", - " KEY(4): vancouver:", - " RAW(6): salmon", - " KEY(0): moon:", - " KEY(2): moonbase-alfa:", - " RAW(4): moonstone", + "DOC(0): ", + " KEY(0): world:", + " KEY(2): europe:", + " KEY(4): france:", + " RAW(6): cheese", + " KEY(4): belgium:", + " RAW(2): beer", + " KEY(2): canada:", + " KEY(4): montreal: poutine", + " KEY(4): vancouver:", + " RAW(6): salmon", + " KEY(0): moon:", + " KEY(2): moonbase-alfa:", + " RAW(4): moonstone", " RAW(-1):" ); } @@ -835,6 +836,61 @@ public class YamlStructureParserTest { assertValueRange(editor, root, "foo:", null); } + @Test public void testEmptyLines() throws Exception { + //Attempt to pin down issue in YamlStructureParser related to + //https://www.pivotaltracker.com/story/show/163752179 + String[] insertion = { + "", + "\n" + }; + + YamlPath expect = new YamlPath( + valueAt(0), //document index + valueAt("jobs"), + valueAt(0), + valueAt("plan"), + valueAt(1), + valueAt("params") + ); + + for (String maybeEmptyLine : insertion) { + MockYamlEditor editor = new MockYamlEditor( +// "resources:\n" + +// "\n" + +// "- name: banana-img.git\n" + +// " type: docker-image\n" + +// " source:\n" + +// " repository: repo/banana-scratch-build\n" + +// "\n" + +// "- name: repo.git\n" + +// " type: git\n" + +// " source:\n" + +// " uri: https://example.com/repo.git\n" + +// "\n" + + "jobs:\n" + + "- name: work-img\n" + + " plan:\n" + + maybeEmptyLine + + " - get: repo.git\n" + + " - put: banana-img.git\n" + + " params:\n" + + " " + ); + + int cursor = editor.getRawText().length()-1; + SRootNode root = editor.parseStructure(); + SNode node = root.find(cursor); + System.out.println("node = "+node); + YamlPath path = node.getPath(); + System.out.println("path = "+path); + assertEquals(expect.toString(), path.toString()); + + SNode traversed = path.traverse(root); + //Note: We might expact path of a node should lead to that node, but actually it doesn't for historic reasons. + assertEquals(node.getParent(), traversed); + } + } + private void assertValueRange(MockYamlEditor editor, SRootNode root, String nodeText, String expectedValue) throws Exception { int start = editor.getText().indexOf(nodeText); SKeyNode node = (SKeyNode) root.find(start); diff --git a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java index 47b0ffcea..9930a27e1 100644 --- a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java +++ b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java @@ -5159,6 +5159,41 @@ public class ConcourseEditorTest { editor.assertProblems(/*None*/); } + @Test + public void PT_163752179_completions_confused_by_empty_lines() throws Exception { + //See: https://www.pivotaltracker.com/story/show/163752179 + String[] insertion = { + "", + "\n" + }; + + for (String maybeEmptyLine : insertion) { + Editor editor = harness.newEditor( + "resources:\n" + + "\n" + + "- name: banana-img.git\n" + + " type: docker-image\n" + + " source:\n" + + " repository: repo/banana-scratch-build\n" + + "\n" + + "- name: repo.git\n" + + " type: git\n" + + " source:\n" + + " uri: https://example.com/repo.git\n" + + "\n" + + "jobs:\n" + + "- name: work-img\n" + + " plan:\n" + + maybeEmptyLine + + " - get: repo.git\n" + + " - put: banana-img.git\n" + + " params:\n" + + " <*>" + ); + editor.assertCompletionLabels(c -> c.getLabel().startsWith("cache"), "cache", "cache_from", "cache_tag"); + } + } + ////////////////////////////////////////////////////////////////////////////// private void assertContextualCompletions(String conText, String textBefore, String... textAfter) throws Exception {