Goto definition iin pipeline editor, working for resources

This commit is contained in:
Kris De Volder
2017-01-24 12:05:15 -08:00
parent 46818d3462
commit 099c19bba3
20 changed files with 515 additions and 69 deletions

View File

@@ -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<T extends SimpleLanguageServer> implements DefinitionHandler {
protected final T server;
public SimpleDefinitionFinder(T server) {
this.server = server;
}
@Override
public CompletableFuture<List<Location>> 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.
* <p>
* 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<Location> 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();
}
}

View File

@@ -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<List<Location>> handle(TextDocumentPositionParams position);
}

View File

@@ -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<min) {
return min;

View File

@@ -72,6 +72,8 @@ public class SimpleTextDocumentService implements TextDocumentService {
private CompletionResolveHandler completionResolveHandler = null;
private HoverHandler hoverHandler = null;
private DefinitionHandler definitionHandler;
public SimpleTextDocumentService(SimpleLanguageServer server) {
this.server = server;
}
@@ -91,6 +93,11 @@ public class SimpleTextDocumentService implements TextDocumentService {
this.completionResolveHandler = h;
}
public synchronized void onDefinition(DefinitionHandler h) {
Assert.isNull("A defintion handler is already set, multiple handlers not supported yet", definitionHandler);
this.definitionHandler = h;
}
/**
* Gets all documents this service is tracking, generally these are the documents that have been opened / changed,
* and not yet closed.
@@ -221,9 +228,15 @@ public class SimpleTextDocumentService implements TextDocumentService {
return Futures.of(null);
}
@SuppressWarnings({ "unchecked"})
@Override
public CompletableFuture<List<? extends Location>> definition(TextDocumentPositionParams position) {
return Futures.of(Collections.emptyList());
DefinitionHandler h = this.definitionHandler;
if (h!=null) {
Object r = h.handle(position); //YUCK!
return (CompletableFuture<List<? extends Location>>) 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
}
}

View File

@@ -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);
}

View File

@@ -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<Node> traverseAmbiguously(YamlFileAST ast) {
if (ast!=null) {
return traverseAmbiguously(new ASTRootCursor(ast))
.map((ASTCursor cursor) -> (Node)cursor.getNode());
}
return Stream.empty();
}
public Stream<Node> traverseAmbiguously(Node startNode) {
if (startNode!=null) {
return traverseAmbiguously(new NodeCursor(startNode))
@@ -293,5 +301,4 @@ public class YamlPath {
return new YamlPath(common);
}
}

View File

@@ -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);
}

View File

@@ -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<Node> 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<Node> 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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -277,7 +277,6 @@ public class YTypeFactory {
private ValueParser getParser(DynamicSchemaContext dc) {
return parser == null ? null : parser.withContext(dc);
}
}
/**

View File

@@ -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<? extends Location> 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()));
}
}

View File

@@ -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<? extends Location> getDefinitions(TextDocumentPositionParams params) throws Exception {
return server.getTextDocumentService().definition(params).get();
}
}