From 099c19bba35398c5a1625b7a6cd4881acf89f3f4 Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Tue, 24 Jan 2017 12:05:15 -0800 Subject: [PATCH] Goto definition iin pipeline editor, working for resources --- .../definition/SimpleDefinitionFinder.java | 87 ++++++++++++++++++ .../util/DefinitionHandler.java | 22 +++++ .../languageserver/util/DocumentRegion.java | 7 ++ .../util/SimpleTextDocumentService.java | 20 ++++- .../ide/vscode/commons/util/Log.java | 12 ++- .../vscode/commons/yaml/path/YamlPath.java | 13 ++- .../yaml/reconcile/ITypeCollector.java | 28 ++++++ .../SchemaBasedYamlASTReconciler.java | 54 +++++++----- .../yaml/reconcile/YamlReconcileEngine.java | 2 +- .../YamlSchemaBasedReconcileEngine.java | 16 +++- .../commons/yaml/schema/YTypeFactory.java | 1 - .../languageserver/testharness/Editor.java | 42 +++++++++ .../testharness/LanguageServerHarness.java | 6 ++ .../vscode-concourse/lib/Main.ts | 3 - .../ide/vscode/concourse/ASTTypeCache.java | 88 +++++++++++++++++++ .../concourse/ConcourseDefinitionFinder.java | 76 ++++++++++++++++ .../concourse/ConcourseLanguageServer.java | 29 +++--- .../ide/vscode/concourse/ConcourseModel.java | 41 ++++++--- .../vscode/concourse/PipelineYmlSchema.java | 12 +-- .../concourse/PipelineYamlEditorTest.java | 25 ++++++ 20 files changed, 515 insertions(+), 69 deletions(-) create mode 100644 vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/definition/SimpleDefinitionFinder.java create mode 100644 vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DefinitionHandler.java create mode 100644 vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/ITypeCollector.java create mode 100644 vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ASTTypeCache.java create mode 100644 vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ConcourseDefinitionFinder.java diff --git a/vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/definition/SimpleDefinitionFinder.java b/vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/definition/SimpleDefinitionFinder.java new file mode 100644 index 000000000..360455bd1 --- /dev/null +++ b/vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/definition/SimpleDefinitionFinder.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * Copyright (c) 2017 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.languageserver.definition; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.TextDocumentPositionParams; +import org.springframework.ide.vscode.commons.languageserver.util.DefinitionHandler; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +import reactor.core.publisher.Flux; + +/** + * {@link SimpleDefinitionFinder} provides a 'dummy' implementation of + * @author Kris De Volder + */ +public class SimpleDefinitionFinder implements DefinitionHandler { + + protected final T server; + + public SimpleDefinitionFinder(T server) { + this.server = server; + } + + @Override + public CompletableFuture> handle(TextDocumentPositionParams position) { + return findDefinitions(position) + .collect(Collectors.toList()) + .toFuture(); + } + + /** + * This is meant to be overridden by subclass. This method provides a simple implementation + * of 'goto definition' (which is not one you probably want to use in practice, but it + * might be usful just to test whether things are wired up correctly to make the + * 'goto definition' action in vscode work. + *

+ * The implementation provided here simply looks for the first occurrence of the word + * currently pointed at in the current document using String.indexOf. + */ + protected Flux findDefinitions(TextDocumentPositionParams params) { + try { + TextDocument doc = server.getTextDocumentService().get(params); + int offset = doc.toOffset(params.getPosition()); + int start = offset; + while (Character.isLetter(doc.getSafeChar(start))) { + start--; + } + start = start+1; + int end = offset; + while (Character.isLetter(doc.getSafeChar(end))) { + end++; + } + String word = doc.textBetween(start, end); + Log.log("Looking for definition of '"+word+"'"); + String text = doc.get(); + int def = text.indexOf(word); + if (def>=0) { + return Flux.just( + new Location(params.getTextDocument().getUri(), + doc.toRange(def, word.length()) + ) + ) + .doOnNext((Location loc) -> { + Log.log("definition: "+loc); + }); + } + } catch (Exception e) { + Log.log(e); + } + return Flux.empty(); + } + +} diff --git a/vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DefinitionHandler.java b/vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DefinitionHandler.java new file mode 100644 index 000000000..927eef4eb --- /dev/null +++ b/vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DefinitionHandler.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2017 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.languageserver.util; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.TextDocumentPositionParams; + +@FunctionalInterface +public interface DefinitionHandler { + CompletableFuture> handle(TextDocumentPositionParams position); +} diff --git a/vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DocumentRegion.java b/vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DocumentRegion.java index 37d0665ee..093587a0f 100644 --- a/vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DocumentRegion.java +++ b/vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DocumentRegion.java @@ -65,6 +65,13 @@ public class DocumentRegion implements CharSequence { this.end = limitRange(end, start, doc.getLength()); } + /** + * Create {@link DocumentRegion} covering the whole document. + */ + public DocumentRegion(IDocument doc) { + this(doc, 0, doc.getLength()); + } + private int limitRange(int offset, int min, int max) { if (offset> definition(TextDocumentPositionParams position) { - return Futures.of(Collections.emptyList()); + DefinitionHandler h = this.definitionHandler; + if (h!=null) { + Object r = h.handle(position); //YUCK! + return (CompletableFuture>) r; + } + return CompletableFuture.completedFuture(Collections.emptyList()); } @Override @@ -294,4 +307,9 @@ public class SimpleTextDocumentService implements TextDocumentService { return Futures.of(Collections.emptyList()); } + public void onDefinition(TextDocumentPositionParams h) { + // TODO Auto-generated method stub + + } + } diff --git a/vscode-extensions/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/Log.java b/vscode-extensions/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/Log.java index 29484b6a8..59e01bbd1 100644 --- a/vscode-extensions/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/Log.java +++ b/vscode-extensions/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/Log.java @@ -15,23 +15,21 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Deprecated, this class is here to make porting old STS code easier. Code should - * avoid using this as much as possible and replaces calls to this by using - * {@link java.util.logging.Logger} directly + * This class is here to make porting old STS code easier. Instead of using this, + * consider using {@link java.util.logging.Logger} directly */ -@Deprecated public class Log { - + final static Logger logger = LoggerFactory.getLogger(Log.class); public static void log(Throwable e) { logger.error("Error", e); } - + public static void log(String message, Throwable t) { logger.error(message, t); } - + public static void log(String message) { logger.error(message); } diff --git a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPath.java b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPath.java index 1a26aa600..e22755b08 100644 --- a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPath.java +++ b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPath.java @@ -16,11 +16,11 @@ import java.util.List; import java.util.stream.Stream; import org.springframework.ide.vscode.commons.yaml.ast.NodeRef; -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.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; @@ -142,6 +142,14 @@ public class YamlPath { return traverseAmbiguously(startNode).findFirst().orElse(null); } + public Stream traverseAmbiguously(YamlFileAST ast) { + if (ast!=null) { + return traverseAmbiguously(new ASTRootCursor(ast)) + .map((ASTCursor cursor) -> (Node)cursor.getNode()); + } + return Stream.empty(); + } + public Stream traverseAmbiguously(Node startNode) { if (startNode!=null) { return traverseAmbiguously(new NodeCursor(startNode)) @@ -293,5 +301,4 @@ public class YamlPath { return new YamlPath(common); } - } diff --git a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/ITypeCollector.java b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/ITypeCollector.java new file mode 100644 index 000000000..b74c3b526 --- /dev/null +++ b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/ITypeCollector.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2017 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 org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST; +import org.springframework.ide.vscode.commons.yaml.schema.YType; +import org.yaml.snakeyaml.nodes.Node; + +/** + * A type collector can optionally be added to a {@link YamlASTReconciler}. + * It is notified of the types the reconciler infers for + * any AST nodes it visits during reconciling. + * + * @author Kris De Volder + */ +public interface ITypeCollector { + void beginCollecting(YamlFileAST ast); + void accept(Node node, YType type); + void endCollecting(YamlFileAST ast); +} diff --git a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java index 581baef8f..4379e98d8 100644 --- a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java +++ b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java @@ -21,11 +21,9 @@ import java.util.Set; import java.util.TreeSet; import java.util.regex.Pattern; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion; -import org.springframework.ide.vscode.commons.util.CollectionUtil; import org.springframework.ide.vscode.commons.util.ExceptionUtil; import org.springframework.ide.vscode.commons.util.IntegerRange; import org.springframework.ide.vscode.commons.util.Log; @@ -54,34 +52,43 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler { private final IProblemCollector problems; private final YamlSchema schema; private final YTypeUtil typeUtil; + private final ITypeCollector typeCollector; - public SchemaBasedYamlASTReconciler(IProblemCollector problems, YamlSchema schema) { + public SchemaBasedYamlASTReconciler(IProblemCollector problems, YamlSchema schema, ITypeCollector typeCollector) { this.problems = problems; this.schema = schema; + this.typeCollector = typeCollector; this.typeUtil = schema.getTypeUtil(); } @Override public void reconcile(YamlFileAST ast) { - List nodes = ast.getNodes(); - IntegerRange expectedDocs = schema.expectedNumberOfDocuments(); - if (!expectedDocs.isInRange(nodes.size())) { - //wrong number of documents in the file. Figure out a good error message. - if (nodes.isEmpty()) { - problem(allOf(ast.getDocument()), "'"+schema.getName()+"' must have at least some Yaml content"); - } else if (expectedDocs.isTooLarge(nodes.size())) { - int upperBound = expectedDocs.getUpperBound(); - Node extraNode = nodes.get(upperBound); - problem(dashesAtStartOf(ast, extraNode), "'"+schema.getName()+"' should not have more than "+upperBound+" Yaml Documents"); - } else if (expectedDocs.isTooSmall(nodes.size())) { - int lowerBound = expectedDocs.getLowerBound(); - problem(endOf(ast.getDocument()), "'"+schema.getName()+"' should have at least "+lowerBound+" Yaml Documents"); + if (typeCollector!=null) typeCollector.beginCollecting(ast); + try { + List nodes = ast.getNodes(); + IntegerRange expectedDocs = schema.expectedNumberOfDocuments(); + if (!expectedDocs.isInRange(nodes.size())) { + //wrong number of documents in the file. Figure out a good error message. + if (nodes.isEmpty()) { + problem(allOf(ast.getDocument()), "'"+schema.getName()+"' must have at least some Yaml content"); + } else if (expectedDocs.isTooLarge(nodes.size())) { + int upperBound = expectedDocs.getUpperBound(); + Node extraNode = nodes.get(upperBound); + problem(dashesAtStartOf(ast, extraNode), "'"+schema.getName()+"' should not have more than "+upperBound+" Yaml Documents"); + } else if (expectedDocs.isTooSmall(nodes.size())) { + int lowerBound = expectedDocs.getLowerBound(); + problem(endOf(ast.getDocument()), "'"+schema.getName()+"' should have at least "+lowerBound+" Yaml Documents"); + } } - } - if (nodes!=null && !nodes.isEmpty()) { - for (int i = 0; i < nodes.size(); i++) { - Node node = nodes.get(i); - reconcile(ast.getDocument(), new YamlPath(YamlPathSegment.valueAt(i)), node, schema.getTopLevelType()); + if (nodes!=null && !nodes.isEmpty()) { + for (int i = 0; i < nodes.size(); i++) { + Node node = nodes.get(i); + reconcile(ast.getDocument(), new YamlPath(YamlPathSegment.valueAt(i)), node, schema.getTopLevelType()); + } + } + } finally { + if (typeCollector!=null) { + typeCollector.endCollecting(ast); } } } @@ -89,8 +96,6 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler { private DocumentRegion dashesAtStartOf(YamlFileAST ast, Node node) { try { int start = node.getStartMark().getIndex(); - int end = node.getEndMark().getIndex(); - String text = ast.getDocument().textBetween(start, end); DocumentRegion textBefore = new DocumentRegion(ast.getDocument(), 0, start) .trimEnd(Pattern.compile("\\s*")); DocumentRegion dashes = textBefore.subSequence(textBefore.getLength()-3); @@ -108,6 +113,9 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler { if (type!=null) { DynamicSchemaContext schemaContext = new ASTDynamicSchemaContext(doc, path, node); type = typeUtil.inferMoreSpecificType(type, schemaContext); + if (typeCollector!=null) { + typeCollector.accept(node, type); + } switch (getNodeId(node)) { case mapping: MappingNode map = (MappingNode) node; diff --git a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlReconcileEngine.java b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlReconcileEngine.java index 57646088b..6c123930d 100644 --- a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlReconcileEngine.java +++ b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlReconcileEngine.java @@ -27,7 +27,7 @@ import org.yaml.snakeyaml.scanner.ScannerException; * @author Kris De Volder */ public abstract class YamlReconcileEngine implements IReconcileEngine { - + final static Logger logger = LoggerFactory.getLogger(YamlReconcileEngine.class); protected final YamlASTProvider parser; diff --git a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlSchemaBasedReconcileEngine.java b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlSchemaBasedReconcileEngine.java index 819b9d2d8..27dd3c197 100644 --- a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlSchemaBasedReconcileEngine.java +++ b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlSchemaBasedReconcileEngine.java @@ -23,6 +23,12 @@ import org.springframework.ide.vscode.commons.yaml.schema.YamlSchema; public final class YamlSchemaBasedReconcileEngine extends YamlReconcileEngine { private final YamlSchema schema; + /** + * An optional type collector can be added. It will notified about all the types + * the reconciler infers when reconciling an AST. + */ + private ITypeCollector typeCollector; + public YamlSchemaBasedReconcileEngine(YamlASTProvider parser, YamlSchema schema) { super(parser); this.schema = schema; @@ -35,6 +41,14 @@ public final class YamlSchemaBasedReconcileEngine extends YamlReconcileEngine { @Override protected YamlASTReconciler getASTReconciler(IDocument doc, IProblemCollector problems) { - return new SchemaBasedYamlASTReconciler(problems, schema); + return new SchemaBasedYamlASTReconciler(problems, schema, typeCollector); + } + + public ITypeCollector getTypeCollector() { + return typeCollector; + } + + public void setTypeCollector(ITypeCollector typeCollector) { + this.typeCollector = typeCollector; } } \ No newline at end of file diff --git a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java index f3001c43e..3ad6c1c32 100644 --- a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java +++ b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java @@ -277,7 +277,6 @@ public class YTypeFactory { private ValueParser getParser(DynamicSchemaContext dc) { return parser == null ? null : parser.withContext(dc); } - } /** 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 b478fa532..fed25236a 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 @@ -31,9 +31,12 @@ import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.CompletionList; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TextDocumentPositionParams; import org.eclipse.lsp4j.TextEdit; import org.junit.Assert; @@ -498,4 +501,43 @@ public class Editor { ignoredTypes.add(type.toString()); } + public void assertGotoDefinition(Position pos, Range expectedTarget) throws Exception { + TextDocumentIdentifier textDocumentId = document.getId(); + TextDocumentPositionParams params = new TextDocumentPositionParams(textDocumentId, textDocumentId.getUri(), pos); + List defs = harness.getDefinitions(params); + assertEquals(1, defs.size()); + assertEquals(new Location(textDocumentId.getUri(), expectedTarget), defs.get(0)); + } + + /** + * Determines the position of (the middle of) a snippet of text in the document. + * + * @param contextSnippet A larger snippet containing the actual snippet to look for. + * This larger snippet is used to narrow the section of the document + * where we look for the actual snippet. This is useful when the snippet + * occurs multiple times in the document. + * @param focusSnippet The snippet to look for + */ + public Position positionOf(String longSnippet, String focusSnippet) throws Exception { + Range r = rangeOf(longSnippet, focusSnippet); + return r==null?null:r.getStart(); + } + + /** + * Determines the range of a snippet of text in the document. + * + * @param contextSnippet A larger snippet containing the actual snippet to look for. + * This larger snippet is used to narrow the section of the document + * where we look for the actual snippet. This is useful when the snippet + * occurs multiple times in the document. + * @param focusSnippet The snippet to look for + */ + public Range rangeOf(String longSnippet, String focusSnippet) throws Exception { + int relativeOffset = longSnippet.indexOf(focusSnippet); + int contextStart = getRawText().indexOf(longSnippet); + Assert.assertTrue("'"+longSnippet+"' not found in editor", contextStart>=0); + int start = contextStart+relativeOffset; + return new Range(document.toPosition(start), document.toPosition(start+focusSnippet.length())); + } + } diff --git a/vscode-extensions/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java b/vscode-extensions/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java index 5c3ef5415..85dc2d12c 100644 --- a/vscode-extensions/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java +++ b/vscode-extensions/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java @@ -25,6 +25,7 @@ import java.util.Map; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import org.assertj.core.api.Condition; @@ -38,6 +39,7 @@ import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.MessageParams; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.PublishDiagnosticsParams; @@ -375,4 +377,8 @@ public class LanguageServerHarness { assertEquals(expected, completion.getLabel()); } + public List getDefinitions(TextDocumentPositionParams params) throws Exception { + return server.getTextDocumentService().definition(params).get(); + } + } diff --git a/vscode-extensions/vscode-concourse/lib/Main.ts b/vscode-extensions/vscode-concourse/lib/Main.ts index aea08a13d..599d90bf1 100644 --- a/vscode-extensions/vscode-concourse/lib/Main.ts +++ b/vscode-extensions/vscode-concourse/lib/Main.ts @@ -11,9 +11,6 @@ import * as ChildProcess from 'child_process'; import {LanguageClient, LanguageClientOptions, SettingMonitor, ServerOptions, StreamInfo} from 'vscode-languageclient'; import {TextDocument, OutputChannel} from 'vscode'; -var DEBUG = false; -const DEBUG_ARG = '-agentlib:jdwp=transport=dt_socket,server=y,address=8000,suspend=y'; - var log_output : OutputChannel = null; function log(msg : string) { diff --git a/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ASTTypeCache.java b/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ASTTypeCache.java new file mode 100644 index 000000000..74fe9586a --- /dev/null +++ b/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ASTTypeCache.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright (c) 2017 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.concourse; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.ide.vscode.commons.util.Assert; +import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST; +import org.springframework.ide.vscode.commons.yaml.reconcile.ITypeCollector; +import org.springframework.ide.vscode.commons.yaml.schema.YType; +import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YAtomicType; +import org.yaml.snakeyaml.nodes.Node; + +import com.google.common.collect.ImmutableMap; + +/** + * An implementation of {@link ITypeCollector} which keeps track of the + * types of 'interesting' nodes in the ast. + * + * @author Kris De Volder + */ +public class ASTTypeCache implements ITypeCollector { + + /** + * Set upon commencing a reconciler session. + */ + private YamlFileAST currentAst = null; + + /** + * Collects types for the current session. + */ + private ImmutableMap.Builder currentTypes = null; + + private final Set interestingTypes = new HashSet<>(); + private final Map> typeIndex = new HashMap<>(); + + @Override + public void beginCollecting(YamlFileAST ast) { + Assert.isNull("A session is already active. Concurrency isn't supported by ITypeCollector protocol", currentTypes); + this.currentAst = ast; + this.currentTypes = ImmutableMap.builder(); + } + + @Override + public void endCollecting(YamlFileAST ast) { + Assert.isLegal(currentAst==ast); + String uri = ast.getDocument().getUri(); + typeIndex.put(uri, currentTypes.build()); + this.currentAst = null; + this.currentTypes = null; + } + + @Override + public void accept(Node node, YType type) { + if (interestingTypes.contains(type)) { + currentTypes.put(node, type); + } + } + + public YType getType(YamlFileAST ast, Node node) { + ImmutableMap types = typeIndex.get(ast.getDocument().getUri()); + if (types!=null) { + return types.get(node); + } + return null; + } + + /** + * Declares a given YType as 'interesting'. This means that nodes of this type will be + * added to the index. + */ + public void addInterestingType(YAtomicType type) { + this.interestingTypes.add(type); + } + + +} diff --git a/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ConcourseDefinitionFinder.java b/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ConcourseDefinitionFinder.java new file mode 100644 index 000000000..6fc95aa2e --- /dev/null +++ b/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ConcourseDefinitionFinder.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2017 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.concourse; + +import java.util.Optional; + +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.TextDocumentPositionParams; +import org.springframework.ide.vscode.commons.languageserver.definition.SimpleDefinitionFinder; +import org.springframework.ide.vscode.commons.util.BadLocationException; +import org.springframework.ide.vscode.commons.util.Log; +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.schema.YType; +import org.yaml.snakeyaml.nodes.Node; + +import reactor.core.publisher.Flux; + +public class ConcourseDefinitionFinder extends SimpleDefinitionFinder { + + private final ConcourseModel models; + private final PipelineYmlSchema schema; + private ASTTypeCache astTypes; + + public ConcourseDefinitionFinder(ConcourseLanguageServer server, ConcourseModel models, PipelineYmlSchema schema) { + super(server); + this.models = models; + this.schema = schema; + this.astTypes = models.getAstTypeCache(); + astTypes.addInterestingType(schema.t_resource_name); + } + + @Override + protected Flux findDefinitions(TextDocumentPositionParams params) { + try { + TextDocument doc = server.getTextDocumentService().get(params); + if (doc!=null) { + YamlFileAST ast = models.getSafeAst(doc, false); + Node refNode = ast.findNode(doc.toOffset(params.getPosition())); + if (refNode!=null) { + YType type = astTypes.getType(ast, refNode); + if (schema.t_resource_name==type) { + String name = NodeUtil.asScalar(refNode); + return Flux.fromStream(models.getResourceDefinitionNodes(ast, name)) + .map((node) -> toLocation(doc, node)) + .filter(Optional::isPresent) + .map(Optional::get); + } + } + } + } catch (Exception e) { + return Flux.error(e); + } + return Flux.empty(); + } + + Optional toLocation(TextDocument doc, Node node) { + int start = node.getStartMark().getIndex(); + int end = node.getEndMark().getIndex(); + try { + return Optional.of(new Location(doc.getUri(), doc.toRange(start, end-start))); + } catch (BadLocationException e) { + Log.log(e); + return Optional.empty(); + } + } +} diff --git a/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServer.java b/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServer.java index 334ec3782..1b6e16285 100644 --- a/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServer.java +++ b/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServer.java @@ -18,20 +18,16 @@ import org.springframework.ide.vscode.commons.languageserver.completion.VscodeCo import org.springframework.ide.vscode.commons.languageserver.hover.HoverInfoProvider; import org.springframework.ide.vscode.commons.languageserver.hover.VscodeHoverEngine; import org.springframework.ide.vscode.commons.languageserver.hover.VscodeHoverEngineAdapter; -import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine; 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.text.TextDocument; import org.springframework.ide.vscode.commons.yaml.ast.YamlASTProvider; -import org.springframework.ide.vscode.commons.yaml.ast.YamlParser; import org.springframework.ide.vscode.commons.yaml.completion.SchemaBasedYamlAssistContextProvider; import org.springframework.ide.vscode.commons.yaml.completion.YamlAssistContextProvider; import org.springframework.ide.vscode.commons.yaml.completion.YamlCompletionEngine; import org.springframework.ide.vscode.commons.yaml.hover.YamlHoverInfoProvider; import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaBasedReconcileEngine; -import org.springframework.ide.vscode.commons.yaml.schema.YamlSchema; import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureProvider; -import org.yaml.snakeyaml.Yaml; public class ConcourseLanguageServer extends SimpleLanguageServer { @@ -42,20 +38,22 @@ public class ConcourseLanguageServer extends SimpleLanguageServer { YamlASTProvider currentAsts = models.getAstProvider(false); YamlStructureProvider structureProvider = YamlStructureProvider.DEFAULT; - YamlSchema schema = new PipelineYmlSchema(models); + PipelineYmlSchema schema = new PipelineYmlSchema(models); YamlAssistContextProvider contextProvider = new SchemaBasedYamlAssistContextProvider(schema); YamlCompletionEngine yamlCompletionEngine = new YamlCompletionEngine(structureProvider, contextProvider); VscodeCompletionEngine completionEngine = new VscodeCompletionEngineAdapter(this, yamlCompletionEngine); HoverInfoProvider infoProvider = new YamlHoverInfoProvider(currentAsts, structureProvider, contextProvider); VscodeHoverEngine hoverEngine = new VscodeHoverEngineAdapter(this, infoProvider); - IReconcileEngine engine = new YamlSchemaBasedReconcileEngine(currentAsts, schema); + YamlSchemaBasedReconcileEngine reconcileEngine = new YamlSchemaBasedReconcileEngine(currentAsts, schema); + ConcourseDefinitionFinder definitionFinder = new ConcourseDefinitionFinder(this, models, schema); + reconcileEngine.setTypeCollector(models.getAstTypeCache()); // SimpleWorkspaceService workspace = getWorkspaceService(); documents.onDidChangeContent(params -> { TextDocument doc = params.getDocument(); - validateWith(doc, engine); + validateWith(doc, reconcileEngine); }); - + // workspace.onDidChangeConfiguraton(settings -> { // System.out.println("Config changed: "+params); // Integer val = settings.getInt("languageServerExample", "maxNumberOfProblems"); @@ -66,24 +64,27 @@ public class ConcourseLanguageServer extends SimpleLanguageServer { // } // } // }); - + documents.onCompletion(completionEngine::getCompletions); documents.onCompletionResolve(completionEngine::resolveCompletion); - documents.onHover(hoverEngine ::getHover); + documents.onHover(hoverEngine::getHover); + documents.onDefinition(definitionFinder); } - + @Override protected ServerCapabilities getServerCapabilities() { ServerCapabilities c = new ServerCapabilities(); - + c.setTextDocumentSync(TextDocumentSyncKind.Incremental); c.setHoverProvider(true); - + CompletionOptions completionProvider = new CompletionOptions(); completionProvider.setResolveProvider(false); c.setCompletionProvider(completionProvider); - + + c.setDefinitionProvider(true); + return c; } } 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 00642746b..6df4fd28f 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 @@ -14,6 +14,7 @@ import static org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment.a import static org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment.valueAt; import java.util.function.Function; +import java.util.stream.Stream; import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; import org.springframework.ide.vscode.commons.languageserver.util.TextDocumentContentChange; @@ -54,7 +55,7 @@ public class ConcourseModel { valueAt("name") ); - private static final YamlPath RESOURCES_FROM_ROOT_PATH = new YamlPath( + private static final YamlPath RESOURCES_PATH = new YamlPath( anyChild(), // skip over the root node which contains multiple doces valueAt("resources"), anyChild() @@ -63,6 +64,9 @@ public class ConcourseModel { private final YamlParser parser; private final StaleFallbackCache asts = new StaleFallbackCache<>(); + private final ASTTypeCache astTypes = new ASTTypeCache(); + + public ConcourseModel(SimpleTextDocumentService documents) { Yaml yaml = new Yaml(); this.parser = new YamlParser(yaml); @@ -101,7 +105,7 @@ public class ConcourseModel { */ public String getResourceType(IDocument doc, String resourceName) { return getFromAst(doc, (ast) -> { - Node resource = RESOURCES_FROM_ROOT_PATH.traverseAmbiguously(new ASTRootCursor(ast)) + Node resource = RESOURCES_PATH.traverseAmbiguously(new ASTRootCursor(ast)) .map((cursor) -> ((NodeCursor)cursor).getNode()) .filter((resourceNode) -> resourceName.equals(NodeUtil.getScalarProperty(resourceNode, "name"))) .findFirst().orElse(null); @@ -141,7 +145,7 @@ public class ConcourseModel { if (doc!=null) { String uri = doc.getUri(); if (uri!=null) { - YamlFileAST ast = getAst(doc); + YamlFileAST ast = getAst(doc, true); return astFunction.apply(ast); } } @@ -154,15 +158,11 @@ public class ConcourseModel { } public YamlFileAST getSafeAst(IDocument doc) { - try { - return getAst(doc); - } catch (Exception e) { - return null; - } + return getSafeAst(doc, true); } - public YamlFileAST getAst(IDocument doc) throws Exception { - return getAstProvider(true).getAST(doc); + public YamlFileAST getAst(IDocument doc, boolean allowStaleAst) throws Exception { + return getAstProvider(allowStaleAst).getAST(doc); } public YamlASTProvider getAstProvider(boolean allowStaleAsts) { @@ -177,4 +177,25 @@ public class ConcourseModel { }; } + public YamlFileAST getSafeAst(IDocument doc, boolean allowStaleAst) { + if (doc!=null) { + try { + return getAst(doc, allowStaleAst); + } catch (Exception e) { + //ignored + } + } + return null; + } + + public ASTTypeCache getAstTypeCache() { + return astTypes; + } + + public Stream getResourceDefinitionNodes(YamlFileAST ast, String name) { + return RESOURCE_NAMES_PATH.prepend(YamlPathSegment.anyChild()) + .traverseAmbiguously(ast) + .filter(node -> name.equals(NodeUtil.asScalar(node))); + } + } diff --git a/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java b/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java index ae41c0efa..5babe0b9c 100644 --- a/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java +++ b/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java @@ -57,6 +57,8 @@ public class PipelineYmlSchema implements YamlSchema { public final YType t_strictly_pos_integer = f.yatomic("Strictly Positive Integer") .parseWith(ValueParsers.integerAtLeast(1)); + public final YAtomicType t_resource_name; + private final ResourceTypeRegistry resourceTypes = new ResourceTypeRegistry(); public PipelineYmlSchema(ConcourseModel models) { @@ -102,7 +104,7 @@ public class PipelineYmlSchema implements YamlSchema { // The vagrant-cloud r ); - YType resourceName = f.yenum("Resource Name", + this.t_resource_name = f.yenum("Resource Name", (parseString, validValues) -> { return "The '"+parseString+"' resource does not exist. Existing resources: "+validValues; }, @@ -126,7 +128,7 @@ public class PipelineYmlSchema implements YamlSchema { jobNameDef.parseWith(ConcourseValueParsers.jobNameDef(models)); YBeanType getStep = f.ybean("GetStep"); - addProp(getStep, "get", resourceName); + addProp(getStep, "get", t_resource_name); addProp(getStep, "resource", t_string); addProp(getStep, "version", t_version); addProp(getStep, "passed", f.yseq(jobName)); @@ -136,7 +138,7 @@ public class PipelineYmlSchema implements YamlSchema { addProp(getStep, "trigger", t_boolean); YBeanType putStep = f.ybean("PutStep"); - addProp(putStep, "put", resourceName); + addProp(putStep, "put", t_resource_name); addProp(putStep, "resource", jobName); addProp(putStep, "params", f.contextAware("PutParams", (dc) -> resourceTypes.getOutParamsType(getResourceType("put", models, dc)) @@ -151,7 +153,7 @@ public class PipelineYmlSchema implements YamlSchema { addProp(taskStep, "privileged", t_boolean); addProp(taskStep, "params", t_params); addProp(taskStep, "image", t_ne_string); - addProp(taskStep, "input_mapping", f.ymap(t_ne_string, resourceName)); + addProp(taskStep, "input_mapping", f.ymap(t_ne_string, t_resource_name)); addProp(taskStep, "output_mapping", t_string_params); YBeanType aggregateStep = f.ybean("AggregateStep"); @@ -208,7 +210,7 @@ public class PipelineYmlSchema implements YamlSchema { YBeanType group = f.ybean("Group"); addProp(group, "name", t_ne_string).isRequired(true); - addProp(group, "resources", f.yseq(resourceName)); + addProp(group, "resources", f.yseq(t_resource_name)); addProp(group, "jobs", f.yseq(jobName)); addProp(TOPLEVEL_TYPE, "resources", f.yseq(resource)); diff --git a/vscode-extensions/vscode-concourse/src/test/java/org/springframework/ide/vscode/concourse/PipelineYamlEditorTest.java b/vscode-extensions/vscode-concourse/src/test/java/org/springframework/ide/vscode/concourse/PipelineYamlEditorTest.java index e61919b0d..c4862e173 100644 --- a/vscode-extensions/vscode-concourse/src/test/java/org/springframework/ide/vscode/concourse/PipelineYamlEditorTest.java +++ b/vscode-extensions/vscode-concourse/src/test/java/org/springframework/ide/vscode/concourse/PipelineYamlEditorTest.java @@ -1281,6 +1281,31 @@ public class PipelineYamlEditorTest { editor.assertHoverContains("skip_download", "Skip `docker pull`"); } + @Test + public void gotoResourceDefinition() throws Exception { + Editor editor = harness.newEditor( + "resources:\n" + + "- name: my-git\n" + + " type: git\n" + + "- name: build-env\n" + + " type: docker-image\n" + + "jobs:\n" + + "- name: do-stuff\n" + + " plan:\n" + + " - get: my-git\n" + + " params:\n" + + " rootfs: true\n" + + " save: true\n" + + " - put: build-env\n" + + " build: my-git/docker\n" + ); + + editor.assertGotoDefinition(editor.positionOf("get: my-git", "my-git"), + editor.rangeOf("- name: my-git", "my-git") + ); + } + + ////////////////////////////////////////////////////////////////////////////// private void assertContextualCompletions(String conText, String textBefore, String... textAfter) throws Exception {