diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/DocumentRegion.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/DocumentRegion.java index 9f0e83f0e..0477d0d22 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/DocumentRegion.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/DocumentRegion.java @@ -359,4 +359,8 @@ public class DocumentRegion implements CharSequence, IRegion { return charBefore.equals("\n"); //This should work on windows too because windows uses "\r\n" but that still ends with "\n". } + public boolean contains(DocumentRegion inner) { + return start<=inner.start && inner.end <= end; + } + } \ No newline at end of file diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/NodeCursor.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/NodeCursor.java index 6e9d030d3..72b682ea4 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/NodeCursor.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/NodeCursor.java @@ -12,7 +12,9 @@ package org.springframework.ide.vscode.commons.yaml.path; import java.util.stream.Stream; +import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; import org.springframework.ide.vscode.commons.util.Assert; +import org.springframework.ide.vscode.commons.yaml.ast.NodeMergeSupport; import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil; import org.yaml.snakeyaml.nodes.MappingNode; import org.yaml.snakeyaml.nodes.Node; @@ -29,6 +31,8 @@ public class NodeCursor extends ASTCursor { private final Node currentNode; + private NodeMergeSupport nodeMerger = new NodeMergeSupport(IProblemCollector.NULL); + public NodeCursor(Node node) { Assert.isNotNull(node); this.currentNode = node; @@ -41,7 +45,7 @@ public class NodeCursor extends ASTCursor { switch (s.getType()) { case KEY_AT_KEY: { String key = s.toPropString(); - MappingNode mappingNode = NodeUtil.asMapping(getNode()); + MappingNode mappingNode = asMapping(getNode()); if (mappingNode!=null) { return mappingNode.getValue().stream() .filter((c) -> key.equals(NodeUtil.asScalar(c.getKeyNode()))) @@ -51,7 +55,7 @@ public class NodeCursor extends ASTCursor { return Stream.empty(); } case ANY_CHILD: { - MappingNode mappingNode = NodeUtil.asMapping(getNode()); + MappingNode mappingNode = asMapping(getNode()); if (mappingNode!=null) { return mappingNode.getValue().stream() .map((c) -> new NodeCursor(c.getValueNode())); @@ -72,7 +76,7 @@ public class NodeCursor extends ASTCursor { return Stream.empty(); } case VAL_AT_KEY: { - MappingNode mappingNode = NodeUtil.asMapping(getNode()); + MappingNode mappingNode = asMapping(getNode()); if (mappingNode!=null) { String key = s.toPropString(); return mappingNode.getValue().stream() @@ -87,6 +91,16 @@ public class NodeCursor extends ASTCursor { } } + + + private MappingNode asMapping(Node node) { + MappingNode mapping = NodeUtil.asMapping(node); + if (mapping!=null) { + nodeMerger.flattenMapping(mapping); + } + return mapping; + } + @Override public Node getNode() { return currentNode; diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/TypeBasedYamlHierarchicalSymbolHandler.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/TypeBasedYamlHierarchicalSymbolHandler.java index e7cd18cf3..1f9ae0657 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/TypeBasedYamlHierarchicalSymbolHandler.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/TypeBasedYamlHierarchicalSymbolHandler.java @@ -24,6 +24,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.commons.languageserver.util.HierarchicalDocumentSymbolHandler; import org.springframework.ide.vscode.commons.util.Assert; +import org.springframework.ide.vscode.commons.util.text.DocumentRegion; import org.springframework.ide.vscode.commons.util.text.IDocument; import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil; import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST; @@ -76,9 +77,16 @@ public class TypeBasedYamlHierarchicalSymbolHandler implements HierarchicalDocum if (namePath!=null) { Node nameNode = namePath.traverseNode(node); if (nameNode!=null) { + DocumentRegion nodeRegion = NodeUtil.region(doc, node); + DocumentRegion nameRegion = NodeUtil.region(doc, nameNode); + if (!nodeRegion.contains(nameRegion)) { + //This violates the expectation that most clients have. + //vscode, for example drops the entire symbols hierarchy as 'invalid' if this happens + nodeRegion = nameRegion; + } return new DocumentSymbol(NodeUtil.asScalar(nameNode), kind, - NodeUtil.region(doc, node).asRange(), - NodeUtil.region(doc, nameNode).asRange(), + nodeRegion.asRange(), + nameRegion.asRange(), detail ); } @@ -151,7 +159,9 @@ public class TypeBasedYamlHierarchicalSymbolHandler implements HierarchicalDocum @Override public List handleHierarchic(DocumentSymbolParams params) { - return outlineByUri.get(params.getTextDocument().getUri()); + List symbols = outlineByUri.get(params.getTextDocument().getUri()); + log.info("hierarchical symbols: {}", symbols); + return symbols; } @Override @@ -168,10 +178,12 @@ public class TypeBasedYamlHierarchicalSymbolHandler implements HierarchicalDocum if (def!=null) { Item parent = findParent(path); DocumentSymbol sym = def.createSymbol(currentAst, node, type, path); - if (parent!=null) { - parent.addChild(sym); - } else { - rootSymbols.add(sym); + if (sym!=null) { + if (parent!=null) { + parent.addChild(sym); + } else { + rootSymbols.add(sym); + } } stack.push(new Item(path, sym)); } diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java index f50664126..ca53dcc61 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java @@ -55,6 +55,7 @@ import org.junit.Assert; import org.springframework.ide.vscode.commons.protocol.HighlightParams; import org.springframework.ide.vscode.commons.util.StringUtil; import org.springframework.ide.vscode.commons.util.Unicodes; +import org.springframework.ide.vscode.commons.util.text.DocumentRegion; import org.springframework.ide.vscode.commons.util.text.LanguageId; import com.google.common.collect.ImmutableList; @@ -991,12 +992,36 @@ public class Editor { symbolDump.append(s.getDetail()); symbolDump.append("\n"); assertEquals(s.getName(), getText(s.getSelectionRange())); + + assertNestedRange("Invalid selection range for "+s.getName(), s.getRange(), s.getSelectionRange()); List children = s.getChildren(); if (children!=null) { dumpSymbols(children, indent+1, symbolDump); } } + private void assertNestedRange(String msg, Range outerRange, Range innerRange) { + boolean startOk = compare(outerRange.getStart(), innerRange.getStart()) <= 0; + boolean endOk = compare(outerRange.getEnd(), innerRange.getEnd()) >= 0; + if (startOk && endOk) { + //it's fine! + } else { + fail(msg + "\n" + + "outer: '"+this.getText(outerRange)+"'\n" + + "does not contain\n" + + "inner: '"+this.getText(innerRange)+"'" + ); + } + } + + private static int compare(Position p1, Position p2) { + if (p1.getLine()==p2.getLine()) { + return p1.getCharacter() - p2.getCharacter(); + } else { + return p1.getLine() - p2.getLine(); + } + } + private List getHierarchicalDocumentSymbols() throws Exception { return harness.getHierarchicalDocumentSymbols(this.doc); } 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 c04cb1b99..03f16e45d 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 @@ -27,6 +27,7 @@ import java.util.stream.Collectors; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.DiagnosticSeverity; +import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.InsertTextFormat; import org.junit.Before; import org.junit.Ignore; @@ -240,6 +241,31 @@ public class ConcourseEditorTest { InputStream stream = ConcourseEditorTest.class.getClassLoader().getResourceAsStream(resourceName); return IOUtil.toString(stream); } + + @Test + public void outlineWithAnchors() throws Exception { + harness.enableHierarchicalDocumentSymbols(true); + //See: https://github.com/spring-projects/sts4/issues/483 + Editor editor = harness.newEditor( + "def: &stuff\n" + + " name: git\n" + + " type: git\n" + + " source:\n" + + " uri: \n" + + "resources:\n" + + "- <<: *stuff\n" + + "- name: git2\n" + + " type: git\n" + + " source:\n" + + " uri: git@github.com:kdvolder/7zip.git" + ); + + editor.assertHierarchicalDocumentSymbols( + "resources::Resources\n" + + " git::Resource\n" + + " git2::Resource\n" + ); + } @Test public void reconcileStructuralProblems() throws Exception {