From bc86170ae022ab8741f5facb28fbb7a0d1650099 Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Tue, 12 Feb 2019 13:44:27 -0800 Subject: [PATCH] Hierchical document symbols for concourse pipeline --- .../ide/vscode/commons/yaml/ast/NodeUtil.java | 4 + .../vscode/commons/yaml/path/YamlPath.java | 30 +++ .../commons/yaml/path/YamlPathSegment.java | 1 - ...ypeBasedYamlHierarchicalSymbolHandler.java | 202 ++++++++++++++++++ .../reconcile/TypeBasedYamlSymbolHandler.java | 2 + .../ConcourseLanguageServerBootApp.java | 7 +- .../ConcourseLanguageServerInitializer.java | 2 +- .../vscode/concourse/PipelineYmlSchema.java | 56 +++-- 8 files changed, 270 insertions(+), 34 deletions(-) create mode 100644 headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/TypeBasedYamlHierarchicalSymbolHandler.java diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/ast/NodeUtil.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/ast/NodeUtil.java index 1ab1fecc2..c7cc3504e 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/ast/NodeUtil.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/ast/NodeUtil.java @@ -144,4 +144,8 @@ public class NodeUtil { return new DocumentRegion(doc, node.getStartMark().getIndex(), node.getEndMark().getIndex()); } + public static DocumentRegion region(IDocument doc, NodeTuple tup) { + return new DocumentRegion(doc, tup.getKeyNode().getStartMark().getIndex(), tup.getValueNode().getEndMark().getIndex()); + } + } 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 a9333226e..d2f62bfa2 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 @@ -310,4 +310,34 @@ public class YamlPath extends AbstractYamlTraversal { return isEmpty(); } + public boolean startsWith(YamlPath prefix) { + if (this.size()>=prefix.size()) { + YamlPath chopped = this.dropLast(this.size() - prefix.size()); + return chopped.equals(prefix); + } + return false; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(segments); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + YamlPath other = (YamlPath) obj; + if (!Arrays.equals(segments, other.segments)) + return false; + return true; + } + } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPathSegment.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPathSegment.java index b119c11c1..7f931e183 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPathSegment.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPathSegment.java @@ -231,7 +231,6 @@ public abstract class YamlPathSegment extends AbstractYamlTraversal { protected char getTypeCode() { return '&'; } - } @Override 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 new file mode 100644 index 000000000..d6e5a57c5 --- /dev/null +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/TypeBasedYamlHierarchicalSymbolHandler.java @@ -0,0 +1,202 @@ +/******************************************************************************* + * Copyright (c) 2019 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.yaml.reconcile; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; + +import org.eclipse.lsp4j.DocumentSymbol; +import org.eclipse.lsp4j.DocumentSymbolParams; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.SymbolKind; +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.IDocument; +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.YamlPath; +import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment; +import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment.YamlPathSegmentType; +import org.springframework.ide.vscode.commons.yaml.schema.YType; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeTuple; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap.Builder; + +public class TypeBasedYamlHierarchicalSymbolHandler implements HierarchicalDocumentSymbolHandler, ITypeCollector { + + private static final Logger log = LoggerFactory.getLogger(TypeBasedYamlHierarchicalSymbolHandler.class); + + public static class HierarchicalDefType { + /** + * A yaml node of this type constitutes a definion. This should identify the 'whole' definition not just the + * part of the node that contains the name of the defined entity. + */ + public final YType defType; + + /** + * A yaml optional path that points to the part of the node where the defined entity's name can be found. + */ + public final YamlPath namePath; + + public final SymbolKind kind; + + public final String detail; + + public HierarchicalDefType(YType defType, YamlPath namePath, SymbolKind kind, String detail) { + super(); + this.defType = defType; + this.namePath = namePath; + this.kind = kind; + this.detail = detail; + } + @Override + public String toString() { + return "HierarchicalDefType [defType=" + defType + ", namePath=" + namePath + "]"; + } + public DocumentSymbol createSymbol(YamlFileAST currentAst, Node node, YType type, YamlPath path) { + try { + IDocument doc = currentAst.getDocument(); + if (namePath!=null) { + Node nameNode = namePath.traverseNode(node); + return new DocumentSymbol(NodeUtil.asScalar(nameNode), kind, + NodeUtil.region(doc, node).asRange(), + NodeUtil.region(doc, nameNode).asRange(), + detail + ); + } else { + //If there's no 'namePath' then we will assume the node we found is the value of a map entry... + //and use the map's key as the symbol's name + MappingNode map = NodeUtil.asMapping(path.dropLast().traverseToNode(currentAst)); + if (map!=null) { + YamlPathSegment segment = path.getLastSegment(); + if (segment.getType()==YamlPathSegmentType.VAL_AT_KEY) { + String key = segment.toPropString(); + for (NodeTuple entry : map.getValue()) { + if (key.equals(NodeUtil.asScalar(entry.getKeyNode()))) { + return new DocumentSymbol(key, kind, + NodeUtil.region(doc, entry).asRange(), + NodeUtil.region(doc, entry.getKeyNode()).asRange(), + detail + ); + } + } + } + } + } + } catch (Exception e) { + log.error("", e); + } + return null; + } + } + + private static class Item { + YamlPath path; + DocumentSymbol symbol; + Item(YamlPath path, DocumentSymbol symbol) { + super(); + this.path = path; + this.symbol = symbol; + } + public void addChild(DocumentSymbol sym) { + if (symbol.getChildren()==null) { + symbol.setChildren(new ArrayList()); + } + symbol.getChildren().add(sym); + } + } + + private TypeBasedYamlSymbolHandler baseHandler; + Map hierarchicalDefinitionTypes; + + Map> outlineByUri = new HashMap<>(); + private YamlFileAST currentAst; + + private Stack stack = new Stack<>(); + private ImmutableList.Builder rootSymbols; + + public TypeBasedYamlHierarchicalSymbolHandler(TypeBasedYamlSymbolHandler baseHandler, + List hierarchicalDefinitionTypes) { + this.baseHandler = baseHandler; + Builder builder = ImmutableMap.builder(); + for (HierarchicalDefType hdt : hierarchicalDefinitionTypes) { + builder.put(hdt.defType, hdt); + } + this.hierarchicalDefinitionTypes = builder.build(); + } + + @Override + public List handle(DocumentSymbolParams params) { + return baseHandler.handle(params); + } + + @Override + public List handleHierarchic(DocumentSymbolParams params) { + return outlineByUri.get(params.getTextDocument().getUri()); + } + + @Override + public void beginCollecting(YamlFileAST ast) { + Assert.isNull("Session already active", currentAst); + this.currentAst = ast; + this.rootSymbols = ImmutableList.builder(); + this.stack = new Stack<>(); + } + + @Override + public void accept(Node node, YType type, YamlPath path) { + HierarchicalDefType def = hierarchicalDefinitionTypes.get(type); + 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); + } + stack.push(new Item(path, sym)); + } + } + + private Item findParent(YamlPath path) { + if (stack.isEmpty()) { + return null; + } + Item item = stack.peek(); + while (!path.startsWith(item.path)) { + stack.pop(); + if (stack.isEmpty()) { + return null; + } + item = stack.peek(); + } + return item; + } + + @Override + public void endCollecting(YamlFileAST ast) { + Assert.isLegal(this.currentAst == ast); + String uri = currentAst.getDocument().getUri(); + this.outlineByUri.put(uri, rootSymbols.build()); + this.rootSymbols = null; + this.currentAst = null; + this.stack = null; + } +} diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/TypeBasedYamlSymbolHandler.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/TypeBasedYamlSymbolHandler.java index c5622258f..ce15f5077 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/TypeBasedYamlSymbolHandler.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/TypeBasedYamlSymbolHandler.java @@ -28,6 +28,8 @@ import org.springframework.ide.vscode.commons.util.BadLocationException; import org.springframework.ide.vscode.commons.util.text.DocumentRegion; import org.springframework.ide.vscode.commons.util.text.TextDocument; 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.YamlPath; import org.springframework.ide.vscode.commons.yaml.schema.YType; import org.yaml.snakeyaml.nodes.Node; diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServerBootApp.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServerBootApp.java index d3b15d8b2..a5f2f0570 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServerBootApp.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServerBootApp.java @@ -13,10 +13,12 @@ package org.springframework.ide.vscode.concourse; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; +import org.springframework.ide.vscode.commons.languageserver.util.HierarchicalDocumentSymbolHandler; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; import org.springframework.ide.vscode.commons.util.LogRedirect; import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache; +import org.springframework.ide.vscode.commons.yaml.reconcile.TypeBasedYamlHierarchicalSymbolHandler; import org.springframework.ide.vscode.commons.yaml.reconcile.TypeBasedYamlSymbolHandler; import org.springframework.ide.vscode.concourse.github.DefaultGithubInfoProvider; import org.springframework.ide.vscode.concourse.github.GithubInfoProvider; @@ -47,8 +49,9 @@ public class ConcourseLanguageServerBootApp { return new ASTTypeCache(); } - @Bean TypeBasedYamlSymbolHandler documentSymbolHandler(SimpleTextDocumentService documents, ASTTypeCache astTypeCache, PipelineYmlSchema schema) { - return new TypeBasedYamlSymbolHandler(documents, astTypeCache, schema.getDefinitionTypes()); + @Bean HierarchicalDocumentSymbolHandler documentSymbolHandler(SimpleTextDocumentService documents, ASTTypeCache astTypeCache, PipelineYmlSchema schema) { + TypeBasedYamlSymbolHandler baseHandler = new TypeBasedYamlSymbolHandler(documents, astTypeCache, schema.getDefinitionTypes()); + return new TypeBasedYamlHierarchicalSymbolHandler(baseHandler, schema.getHierarchicalDefinitionTypes()); } @Bean PipelineYmlSchema pipelineYmlSchema(ConcourseModel models, GithubInfoProvider github) { diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServerInitializer.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServerInitializer.java index 1c509dc1c..08e4cc443 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServerInitializer.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServerInitializer.java @@ -37,11 +37,11 @@ import org.springframework.ide.vscode.commons.yaml.quickfix.YamlQuickfixes; import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache; import org.springframework.ide.vscode.commons.yaml.reconcile.TypeBasedYamlSymbolHandler; import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaBasedReconcileEngine; +import org.springframework.ide.vscode.commons.yaml.reconcile.TypeBasedYamlHierarchicalSymbolHandler.HierarchicalDefType; import org.springframework.ide.vscode.commons.yaml.schema.YType; import org.springframework.ide.vscode.commons.yaml.schema.YamlSchema; import org.springframework.ide.vscode.commons.yaml.snippet.SchemaBasedSnippetGenerator; import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureProvider; -import org.springframework.ide.vscode.concourse.PipelineYmlSchema.HierarchicalDefType; import org.springframework.ide.vscode.concourse.github.GithubInfoProvider; import org.springframework.stereotype.Component; diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java index 779f0049f..6f2d008f5 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.eclipse.lsp4j.SymbolKind; import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; import org.springframework.ide.vscode.commons.util.MimeTypes; import org.springframework.ide.vscode.commons.util.PartialCollection; @@ -32,6 +33,7 @@ import org.springframework.ide.vscode.commons.yaml.path.YamlPath; import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment; import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems; import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaValueParsers; +import org.springframework.ide.vscode.commons.yaml.reconcile.TypeBasedYamlHierarchicalSymbolHandler.HierarchicalDefType; import org.springframework.ide.vscode.commons.yaml.schema.BasicYValueHint; import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; import org.springframework.ide.vscode.commons.yaml.schema.YType; @@ -40,6 +42,7 @@ import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.AbstractT import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YAtomicType; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YBeanType; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YBeanUnionType; +import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YSeqType; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YTypedPropertyImpl; import org.springframework.ide.vscode.commons.yaml.schema.YTypeUtil; import org.springframework.ide.vscode.commons.yaml.schema.YTypedProperty; @@ -61,30 +64,6 @@ import com.google.common.collect.ImmutableList; */ public class PipelineYmlSchema implements YamlSchema { - public static class HierarchicalDefType { - /** - * A yaml node of this type constitutes a definion. This should identify the 'whole' definition not just the - * part of the node that contains the name of the defined entity. - */ - public final YType defType; - - /** - * A yaml path that points to the part of the node where the defined entity's name can be found. - */ - public final YamlPath nameNode; - - public HierarchicalDefType(YType defType, YamlPath nameNode) { - super(); - this.defType = defType; - this.nameNode = nameNode; - } - @Override - public String toString() { - return "HierarchicalDefType [defType=" + defType + ", nameNode=" + nameNode + "]"; - } - - } - //TODO: the infos for composing this should probably be integrated somehow in the ResourceTypeRegistry so // we only have a list of built-in resource types in a single place. public static final YValueHint[] BUILT_IN_RESOURCE_TYPES = { @@ -189,6 +168,7 @@ public class PipelineYmlSchema implements YamlSchema { ); private List definitionTypes = new ArrayList<>(); + private List hierarchicDefinitions; private GithubInfoProvider github; @@ -436,10 +416,14 @@ public class PipelineYmlSchema implements YamlSchema { addProp(group, "resources", f.yseq(t_resource_name)); addProp(group, "jobs", f.yseq(t_job_name)); - addProp(TOPLEVEL_TYPE, "resources", f.yseq(t_resource)); - addProp(TOPLEVEL_TYPE, "jobs", f.yseq(job)); - addProp(TOPLEVEL_TYPE, "resource_types", f.yseq(resourceType)); - addProp(TOPLEVEL_TYPE, "groups", f.yseq(group).require(models::jobAssignmentIsComplete)); + YSeqType t_resources = f.yseq(t_resource); + YSeqType t_jobs = f.yseq(job); + YSeqType t_resourceTypes = f.yseq(resourceType); + AbstractType t_groups = f.yseq(group).require(models::jobAssignmentIsComplete); + addProp(TOPLEVEL_TYPE, "resources", t_resources); + addProp(TOPLEVEL_TYPE, "jobs", t_jobs); + addProp(TOPLEVEL_TYPE, "resource_types", t_resourceTypes); + addProp(TOPLEVEL_TYPE, "groups", t_groups); definitionTypes = ImmutableList.of( jobNameDef, @@ -447,6 +431,19 @@ public class PipelineYmlSchema implements YamlSchema { t_resource_name_def, t_group_name_def ); + hierarchicDefinitions = ImmutableList.of( + new HierarchicalDefType(t_resources, null, SymbolKind.File, "Resources"), + new HierarchicalDefType(t_resource, YamlPath.fromSimpleProperty("name"), SymbolKind.File, "Resource"), + + new HierarchicalDefType(t_jobs, null, SymbolKind.Method, "Jobs"), + new HierarchicalDefType(job, YamlPath.fromSimpleProperty("name"), SymbolKind.Method, "Job"), + + new HierarchicalDefType(t_resourceTypes, null, SymbolKind.Interface, "Resource Types"), + new HierarchicalDefType(resourceType, YamlPath.fromSimpleProperty("name"), SymbolKind.Interface, "Resource Type"), + + new HierarchicalDefType(t_groups, null, SymbolKind.Package, "Groups"), + new HierarchicalDefType(group, YamlPath.fromSimpleProperty("name"), SymbolKind.Package, "Groups") + ); initializeDefaultResourceTypes(); } @@ -833,8 +830,7 @@ public class PipelineYmlSchema implements YamlSchema { } public List getHierarchicalDefinitionTypes() { - // TODO Auto-generated method stub - return null; + return hierarchicDefinitions; } }