From 6cd1f26e2a8654161e673079c4f4fffb02e3c07d Mon Sep 17 00:00:00 2001 From: Chase Coalwell Date: Fri, 29 Apr 2022 11:52:56 -0600 Subject: [PATCH] Determine end of definition location (#35) * Determine end of definition location * Add TODOs for using Comparator, update jar loading * Use ShapeId for Locations map --- .gitignore | 1 + .../smithy/lsp/SmithyTextDocumentService.java | 49 +++- .../amazon/smithy/lsp/ext/SmithyProject.java | 222 +++++++++++++++--- .../amazon/smithy/lsp/ext/Harness.java | 32 ++- .../smithy/lsp/ext/SmithyProjectTest.java | 90 ++++++- .../ext/SmithyTextDocumentServiceTest.java | 86 +++++++ .../amazon/smithy/lsp/ext/models/main.smithy | 80 +++++++ .../amazon/smithy/lsp/ext/models/test.smithy | 12 + 8 files changed, 517 insertions(+), 55 deletions(-) create mode 100644 src/test/resources/software/amazon/smithy/lsp/ext/models/main.smithy create mode 100644 src/test/resources/software/amazon/smithy/lsp/ext/models/test.smithy diff --git a/.gitignore b/.gitignore index 820042a2..d47eb117 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ bin .java-version *.smithy +!/src/test/resources/**/*.smithy .ammonite \ No newline at end of file diff --git a/src/main/java/software/amazon/smithy/lsp/SmithyTextDocumentService.java b/src/main/java/software/amazon/smithy/lsp/SmithyTextDocumentService.java index 35c83aad..b652bd5c 100644 --- a/src/main/java/software/amazon/smithy/lsp/SmithyTextDocumentService.java +++ b/src/main/java/software/amazon/smithy/lsp/SmithyTextDocumentService.java @@ -63,6 +63,10 @@ import software.amazon.smithy.lsp.ext.model.SmithyBuildExtensions; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.knowledge.NeighborProviderIndex; +import software.amazon.smithy.model.neighbor.Walker; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.validation.ValidatedResult; import software.amazon.smithy.model.validation.ValidationEvent; @@ -202,7 +206,11 @@ private List textBufferContents(String path) throws IOException { LspLog.println("Path " + path + " was found in temporary buffer"); contents = Arrays.stream(tempContents.split("\n")).collect(Collectors.toList()); } else { - contents = readAll(new File(URI.create(path))); + try { + contents = readAll(new File(URI.create(path))); + } catch (IllegalArgumentException e) { + contents = readAll(new File(path)); + } } } @@ -254,8 +262,39 @@ private String findToken(String path, Position p) throws IOException { public CompletableFuture, List>> definition( DefinitionParams params) { try { + // This attempts to return the definition location that corresponds to a position within a text document. + // First, the position is used to find any shapes in the model that are defined at that location. Next, + // a token is extracted from the raw text document. The model is walked from the starting shapeId and any + // the locations of neighboring shapes that match the token are returned. For example, if the position + // is the input of an operation, the token will be the name of the input structure, and the operation will + // be walked to return the location of where the input structure is defined. This allows go-to-definition + // to jump from the input of the operation, to where the input structure is actually defined. + List locations; + Optional initialShapeId = project.getShapeIdFromLocation(params.getTextDocument().getUri(), + params.getPosition()); String found = findToken(params.getTextDocument().getUri(), params.getPosition()); - return Utils.completableFuture(Either.forLeft(project.getLocations().getOrDefault(found, noLocations))); + if (initialShapeId.isPresent()) { + Model model = project.getModel().unwrap(); + Shape initialShape = model.getShape(initialShapeId.get()).get(); + // Find first neighbor (non-member) with name that matches token. + Walker shapeWalker = new Walker(NeighborProviderIndex.of(model).getProvider()); + Optional target = shapeWalker.walkShapes(initialShape).stream() + .filter(shape -> !shape.isMemberShape()) + .map(shape -> shape.getId()) + .filter(shape -> shape.getName().equals(found)) + .findFirst(); + // Use location on target, or else default to initial shape. + locations = Collections.singletonList(project.getLocations().get(target.orElse(initialShapeId.get()))); + } else { + // If the definition params do not have a matching shape at that location, return locations of all + // shapes that match token by shape name. This makes it possible link the shape name in a line + // comment to its definition. + locations = project.getLocations().entrySet().stream() + .filter(entry -> entry.getKey().getName().equals(found)) + .map(entry -> entry.getValue()) + .collect(Collectors.toList()); + } + return Utils.completableFuture(Either.forLeft(locations)); } catch (Exception e) { // TODO: handle exception @@ -330,7 +369,11 @@ private File fileUri(TextDocumentItem tdi) { } private File fileFromUri(String uri) { - return new File(URI.create(uri)); + try { + return new File(URI.create(uri)); + } catch (IllegalArgumentException e) { + return new File(uri); + } } /** diff --git a/src/main/java/software/amazon/smithy/lsp/ext/SmithyProject.java b/src/main/java/software/amazon/smithy/lsp/ext/SmithyProject.java index 63ed514f..507dafc0 100644 --- a/src/main/java/software/amazon/smithy/lsp/ext/SmithyProject.java +++ b/src/main/java/software/amazon/smithy/lsp/ext/SmithyProject.java @@ -17,15 +17,18 @@ import java.io.File; import java.io.IOException; +import java.io.Serializable; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.lsp4j.Location; @@ -35,17 +38,21 @@ import software.amazon.smithy.lsp.SmithyInterface; import software.amazon.smithy.lsp.Utils; import software.amazon.smithy.lsp.ext.model.SmithyBuildExtensions; +import software.amazon.smithy.model.FromSourceLocation; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; +import software.amazon.smithy.model.traits.Trait; import software.amazon.smithy.model.validation.ValidatedResult; public final class SmithyProject { private final List imports; private final List smithyFiles; private final List externalJars; - private Map> locations = Collections.emptyMap(); - private ValidatedResult model; + private Map locations = Collections.emptyMap(); + private final ValidatedResult model; private final File root; private SmithyProject(List imports, List smithyFiles, List externalJars, File root, @@ -55,9 +62,7 @@ private SmithyProject(List imports, List smithyFiles, List ext this.model = model; this.smithyFiles = smithyFiles; this.externalJars = externalJars; - model.getResult().ifPresent(m -> { - this.locations = collectLocations(m); - }); + model.getResult().ifPresent(m -> this.locations = collectLocations(m)); } /** @@ -71,7 +76,7 @@ private SmithyProject(List imports, List smithyFiles, List ext * @return either an error, or a loaded project */ public Either recompile(File changed, File exclude) { - List newFiles = new ArrayList(); + List newFiles = new ArrayList<>(); for (File existing : onlyExistingFiles(this.smithyFiles)) { if (exclude != null && !existing.equals(exclude)) { @@ -102,14 +107,10 @@ public List getCompletions(String token) { return this.model.getResult().map(model -> Completions.find(model, token)).orElse(Collections.emptyList()); } - public Map> getLocations() { + public Map getLocations() { return this.locations; } - public Either reload(SmithyBuildExtensions config) { - return load(config, this.root); - } - /** * Load the project using a SmithyBuildExtensions configuration and workspace * root. @@ -145,7 +146,7 @@ private static Either load(List imports, List LspLog.println(event)); + model.getRight().getValidationEvents().forEach(LspLog::println); return Either.forRight(new SmithyProject(imports, smithyFiles, externalJars, root, model.getRight())); } } @@ -159,37 +160,164 @@ public File getRoot() { return this.root; } - private static Map> collectLocations(Model model) { - Map> locations = new HashMap<>(); - model.shapes().forEach(shape -> { - SourceLocation sourceLocation = shape.getSourceLocation(); - String fileName = sourceLocation.getFilename(); - String uri = Utils.isJarFile(fileName) - ? Utils.toSmithyJarFile(fileName) - : !fileName.startsWith("file:") ? "file:" + fileName - : fileName; - - Position pos = new Position(sourceLocation.getLine() - 1, sourceLocation.getColumn() - 1); - Location location = new Location(uri, new Range(pos, pos)); - - String shapeName = shape.getId().getName(); - // Members get the same shapeName as their parent structure - // so we ignore them, to avoil producing a location per-member - // TODO: index members somehow as well? - if (shape.getType() != ShapeType.MEMBER) { - if (locations.containsKey(shapeName)) { - locations.get(shapeName).add(location); + private static Map collectLocations(Model model) { + Map locations = new HashMap<>(); + List modelFiles = model.shapes() + .map(shape -> shape.getSourceLocation().getFilename()) + .distinct() + .collect(Collectors.toList()); + for (String modelFile : modelFiles) { + List lines = getFileLines(modelFile); + int endMarker = getInitialEndMarker(lines); + int memberEndMarker = getInitialEndMarker(lines); + + // Get shapes reverse-sorted by source location to work from bottom of file to top. + List shapes = model.shapes() + .filter(shape -> shape.getSourceLocation().getFilename().equals(modelFile)) + // TODO: Once the change in https://github.com/awslabs/smithy/pull/1192 lands, replace with with + // `.sorted(Comparator.comparing(Shape::getSourceLocation).reversed())`. + .sorted(new SourceLocationSorter().reversed()) + .collect(Collectors.toList()); + + + for (Shape shape : shapes) { + SourceLocation sourceLocation = shape.getSourceLocation(); + Position startPosition = new Position(sourceLocation.getLine() - 1, sourceLocation.getColumn() - 1); + Position endPosition; + if (endMarker < sourceLocation.getLine()) { + endPosition = new Position(sourceLocation.getLine() - 1, sourceLocation.getColumn() - 1); } else { - List locList = new ArrayList(); - locList.add(location); - locations.put(shapeName, locList); + endPosition = getEndPosition(endMarker, lines); } - } - }); + // Find the end of a member's location by first trimming trailing commas, empty lines and closing + // structure braces. + if (shape.getType() == ShapeType.MEMBER) { + int currentMemberEndMarker = memberEndMarker < endMarker ? memberEndMarker : endMarker; + String currentLine = lines.get(currentMemberEndMarker - 1).trim(); + while (currentLine.startsWith("//") || currentLine.equals("") || currentLine.equals("}")) { + currentMemberEndMarker = currentMemberEndMarker - 1; + currentLine = lines.get(currentMemberEndMarker - 1).trim(); + } + // Set the member's end position. + endPosition = getEndPosition(currentMemberEndMarker, lines); + // Advance the member end marker on any traits on the current member, so that the next member + // location starts in the right place. + List traits = new ArrayList<>(shape.getAllTraits().values()); + if (!traits.isEmpty()) { + traits.sort(new SourceLocationSorter()); + currentMemberEndMarker = traits.get(0).getSourceLocation().getLine(); + } + memberEndMarker = currentMemberEndMarker - 1; + } else { + endMarker = advanceMarkerOnNonMemberShapes(startPosition, shape, lines); + } + Location location = new Location(getUri(modelFile), new Range(startPosition, endPosition)); + locations.put(shape.getId(), location); + } + } return locations; } + private static int advanceMarkerOnNonMemberShapes(Position startPosition, Shape shape, List fileLines) { + // When handling non-member shapes, advance the end marker for traits and comments above the current + // shape. + int marker = startPosition.getLine(); + List traits = new ArrayList<>(shape.getAllTraits().values()); + // If the shape has traits, advance the end marker again. + if (!traits.isEmpty()) { + // TODO: Replace with Comparator when this class is removed. + traits.sort(new SourceLocationSorter()); + marker = traits.get(0).getSourceLocation().getLine() - 1; + } + // Move the end marker when encountering line comments or empty lines. + if (fileLines.size() > marker) { + while (fileLines.get(marker - 1).trim().startsWith("//") + || fileLines.get(marker - 1).trim().equals("")) { + marker = marker - 1; + } + } + return marker; + } + + /** + * Returns the shapeId of the shape that corresponds to the file uri and position within the model. + * + * @param uri String uri of model file + * @param position Cursor position within model file + * @return ShapeId of corresponding shape defined at location. + */ + public Optional getShapeIdFromLocation(String uri, Position position) { + Comparator> rangeSize = Comparator.comparing(entry -> + entry.getValue().getRange().getEnd().getLine() - entry.getValue().getRange().getStart().getLine()); + + return locations.entrySet().stream() + .filter(entry -> entry.getValue().getUri().endsWith(Paths.get(uri).toString())) + .filter(entry -> isPositionInRange(entry.getValue().getRange(), position)) + // Since the position is in each of the overlapping shapes, return the location with the smallest range. + .sorted(rangeSize) + .map(entry -> entry.getKey()) + .findFirst(); + } + + private boolean isPositionInRange(Range range, Position position) { + if (range.getStart().getLine() > position.getLine()) { + return false; + } + if (range.getEnd().getLine() < position.getLine()) { + return false; + } + if (range.getStart().getLine() == position.getLine()) { + return range.getStart().getCharacter() <= position.getCharacter(); + } else if (range.getEnd().getLine() == position.getLine()) { + return range.getEnd().getCharacter() >= position.getCharacter(); + } + return true; + } + + private static int getInitialEndMarker(List lines) { + int endMarker = lines.size(); + // Remove empty lines from the end of the file. + if (lines.size() > 0) { + while (lines.get(endMarker - 1).trim().equals("")) { + endMarker = endMarker - 1; + } + } + return endMarker; + } + + // If the lines of the model were successfully loaded, return the end position of the actual shape line, + // otherwise set it to the start of the next line. + private static Position getEndPosition(int endMarker, List lines) { + if (lines.size() >= endMarker) { + return new Position(endMarker - 1, lines.get(endMarker - 1).length()); + } + return new Position(endMarker, 0); + } + + private static List getFileLines(String file) { + try { + if (Utils.isSmithyJarFile(file) || Utils.isJarFile(file)) { + return Utils.jarFileContents(Utils.toSmithyJarFile(file)); + } else { + return Files.readAllLines(Paths.get(file)); + } + } catch (IOException e) { + LspLog.println("File " + file + " could not be loaded."); + } + return Collections.emptyList(); + } + + private static String getUri(String fileName) { + return Utils.isJarFile(fileName) + ? Utils.toSmithyJarFile(fileName) + : addFilePrefix(fileName); + } + + private static String addFilePrefix(String fileName) { + return !fileName.startsWith("file:") ? "file:" + fileName : fileName; + } + private static Boolean isValidSmithyFile(Path file) { String fName = file.getFileName().toString(); return fName.endsWith(Constants.SMITHY_EXTENSION); @@ -232,4 +360,24 @@ private static List downloadExternalDependencies(SmithyBuildExtensions ext private static List onlyExistingFiles(Collection files) { return files.stream().filter(File::isFile).collect(Collectors.toList()); } + + // TODO: Remove this Class once the change in https://github.com/awslabs/smithy/pull/1192 is available. + private static class SourceLocationSorter implements Comparator, Serializable { + @Override + public int compare(FromSourceLocation s1, FromSourceLocation s2) { + SourceLocation sourceLocation = s1.getSourceLocation(); + SourceLocation otherSourceLocation = s2.getSourceLocation(); + + if (!sourceLocation.getFilename().equals(otherSourceLocation.getFilename())) { + return sourceLocation.getFilename().compareTo(otherSourceLocation.getFilename()); + } + + int lineComparison = Integer.compare(sourceLocation.getLine(), otherSourceLocation.getLine()); + if (lineComparison != 0) { + return lineComparison; + } + + return Integer.compare(sourceLocation.getColumn(), otherSourceLocation.getColumn()); + } + } } diff --git a/src/test/java/software/amazon/smithy/lsp/ext/Harness.java b/src/test/java/software/amazon/smithy/lsp/ext/Harness.java index ea58fa31..9bf60ad0 100644 --- a/src/test/java/software/amazon/smithy/lsp/ext/Harness.java +++ b/src/test/java/software/amazon/smithy/lsp/ext/Harness.java @@ -16,16 +16,18 @@ package software.amazon.smithy.lsp.ext; import com.google.common.io.Files; -import org.eclipse.lsp4j.jsonrpc.messages.Either; - import java.io.File; import java.io.FileWriter; import java.nio.charset.Charset; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import software.amazon.smithy.lsp.Utils; import software.amazon.smithy.lsp.ext.model.SmithyBuildExtensions; +import software.amazon.smithy.utils.IoUtils; public class Harness implements AutoCloseable { private File root; @@ -95,12 +97,7 @@ public static Harness create(SmithyBuildExtensions ext) throws Exception { // TODO: How to make this safe? File hs = Files.createTempDir(); File tmp = Files.createTempDir(); - - Either loaded = SmithyProject.load(ext, hs); - if (loaded.isRight()) - return new Harness(hs, tmp, loaded.getRight(), ext); - else - throw loaded.getLeft(); + return loadHarness(ext, hs, tmp); } public static Harness create(SmithyBuildExtensions ext, Map files) throws Exception { @@ -110,11 +107,28 @@ public static Harness create(SmithyBuildExtensions ext, Map file for (Entry entry : files.entrySet()) { safeCreateFile(entry.getKey(), entry.getValue(), hs); } + return loadHarness(ext, hs, tmp); + } + + public static Harness create(SmithyBuildExtensions ext, List files) throws Exception { + File hs = Files.createTempDir(); + File tmp = Files.createTempDir(); + for (Path path : files) { + if (Utils.isJarFile(path.toString())) { + String contents = String.join(System.lineSeparator(), Utils.jarFileContents(path.toString())); + safeCreateFile(path.getFileName().toString(), contents, hs); + } else { + safeCreateFile(path.getFileName().toString(), IoUtils.readUtf8File(path), hs); + } + } + return loadHarness(ext, hs, tmp); + } + + private static Harness loadHarness(SmithyBuildExtensions ext, File hs, File tmp) throws Exception { Either loaded = SmithyProject.load(ext, hs); if (loaded.isRight()) return new Harness(hs, tmp, loaded.getRight(), ext); else throw loaded.getLeft(); } - } diff --git a/src/test/java/software/amazon/smithy/lsp/ext/SmithyProjectTest.java b/src/test/java/software/amazon/smithy/lsp/ext/SmithyProjectTest.java index da835d1f..e9676a1b 100644 --- a/src/test/java/software/amazon/smithy/lsp/ext/SmithyProjectTest.java +++ b/src/test/java/software/amazon/smithy/lsp/ext/SmithyProjectTest.java @@ -15,20 +15,26 @@ package software.amazon.smithy.lsp.ext; -import org.junit.Test; -import software.amazon.smithy.lsp.ext.model.SmithyBuildExtensions; -import software.amazon.smithy.utils.ListUtils; -import software.amazon.smithy.utils.MapUtils; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import com.google.common.collect.ImmutableList; import java.io.File; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; - -import static org.junit.Assert.assertEquals; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.junit.Test; +import software.amazon.smithy.lsp.ext.model.SmithyBuildExtensions; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.MapUtils; public class SmithyProjectTest { @@ -71,4 +77,76 @@ public void respectingEmptyConfig() throws Exception { } + @Test + public void definitionLocations() throws Exception { + Path baseDir = Paths.get(SmithyProjectTest.class.getResource("models").toURI()); + Path modelMain = baseDir.resolve("main.smithy"); + Path modelTest = baseDir.resolve("test.smithy"); + List modelFiles = ImmutableList.of(modelMain, modelTest); + + try (Harness hs = Harness.create(SmithyBuildExtensions.builder().build(), modelFiles)) { + Map locationMap = hs.getProject().getLocations(); + + correctLocation(locationMap, "com.foo#SingleLine", 4, 0, 4, 23); + correctLocation(locationMap, "com.foo#MultiLine", 6, 8,13, 9); + correctLocation(locationMap, "com.foo#SingleTrait", 16, 4, 16, 22); + correctLocation(locationMap, "com.foo#MultiTrait", 20, 0,21, 14); + correctLocation(locationMap, "com.foo#MultiTraitAndLineComments", 35, 0,37, 1); + correctLocation(locationMap,"com.foo#MultiTraitAndDocComments", 46, 0,48, 1); + correctLocation(locationMap, "com.example#OtherStructure", 4, 0, 8, 1); + } + } + + @Test + public void shapeIdFromLocation() throws Exception { + Path baseDir = Paths.get(SmithyProjectTest.class.getResource("models").toURI()); + Path modelMain = baseDir.resolve("main.smithy"); + Path modelTest = baseDir.resolve("test.smithy"); + List modelFiles = ImmutableList.of(modelMain, modelTest); + + try (Harness hs = Harness.create(SmithyBuildExtensions.builder().build(), modelFiles)) { + SmithyProject project = hs.getProject(); + String uri = hs.file("main.smithy").toString(); + String testUri = hs.file("test.smithy").toString(); + + assertFalse(project.getShapeIdFromLocation("empty.smithy", new Position(0, 0)).isPresent()); + assertFalse(project.getShapeIdFromLocation(uri, new Position(0, 0)).isPresent()); + // Position on shape start line, but before char start + assertFalse(project.getShapeIdFromLocation(uri, new Position(17, 0)).isPresent()); + // Position on shape end line, but after char end + assertFalse(project.getShapeIdFromLocation(uri, new Position(14, 10)).isPresent()); + // Position on shape start line + assertEquals(ShapeId.from("com.foo#SingleLine"), project.getShapeIdFromLocation(uri, + new Position(4, 10)).get()); + // Position on multi-line shape start line + assertEquals(ShapeId.from("com.foo#MultiLine"), project.getShapeIdFromLocation(uri, + new Position(6, 8)).get()); + // Position on multi-line shape end line + assertEquals(ShapeId.from("com.foo#MultiLine"), project.getShapeIdFromLocation(uri, + new Position(13, 6)).get()); + // Member positions + assertEquals(ShapeId.from("com.foo#MultiLine$a"), project.getShapeIdFromLocation(uri, + new Position(7,14)).get()); + assertEquals(ShapeId.from("com.foo#MultiLine$b"), project.getShapeIdFromLocation(uri, + new Position(10,14)).get()); + assertEquals(ShapeId.from("com.foo#MultiLine$c"), project.getShapeIdFromLocation(uri, + new Position(12,14)).get()); + // Member positions on target + assertEquals(ShapeId.from("com.foo#MultiLine$a"), project.getShapeIdFromLocation(uri, + new Position(7,18)).get()); + assertEquals(ShapeId.from("com.foo#MultiLine$b"), project.getShapeIdFromLocation(uri, + new Position(10,18)).get()); + assertEquals(ShapeId.from("com.foo#MultiLine$c"), project.getShapeIdFromLocation(uri, + new Position(12,18)).get()); + assertEquals(ShapeId.from("com.example#OtherStructure"), project.getShapeIdFromLocation(testUri, + new Position(4, 15)).get()); + } + } + + private void correctLocation(Map locationMap, String shapeId, int startLine, + int startColumn, int endLine, int endColumn) { + Location location = locationMap.get(ShapeId.from(shapeId)); + Range range = new Range(new Position(startLine, startColumn), new Position(endLine, endColumn)); + assertEquals(range, location.getRange()); + } } diff --git a/src/test/java/software/amazon/smithy/lsp/ext/SmithyTextDocumentServiceTest.java b/src/test/java/software/amazon/smithy/lsp/ext/SmithyTextDocumentServiceTest.java index a3cfc5e5..90166ea1 100644 --- a/src/test/java/software/amazon/smithy/lsp/ext/SmithyTextDocumentServiceTest.java +++ b/src/test/java/software/amazon/smithy/lsp/ext/SmithyTextDocumentServiceTest.java @@ -16,8 +16,12 @@ package software.amazon.smithy.lsp.ext; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import com.google.common.collect.ImmutableList; import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -26,12 +30,15 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; +import org.eclipse.lsp4j.DefinitionParams; import org.eclipse.lsp4j.DiagnosticSeverity; import org.eclipse.lsp4j.DidChangeTextDocumentParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.MessageActionItem; import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.ShowMessageRequestParams; import org.eclipse.lsp4j.TextDocumentContentChangeEvent; @@ -158,6 +165,73 @@ public void handlingChanges() throws Exception { } + @Test + public void definitions() throws Exception { + Path baseDir = Paths.get(SmithyProjectTest.class.getResource("models").toURI()); + String modelFilename = "main.smithy"; + Path modelMain = baseDir.resolve(modelFilename); + List modelFiles = ImmutableList.of(modelMain); + + try (Harness hs = Harness.create(SmithyBuildExtensions.builder().build(), modelFiles)) { + SmithyTextDocumentService tds = new SmithyTextDocumentService(Optional.empty(), hs.getTempFolder()); + StubClient client = new StubClient(); + tds.createProject(hs.getConfig(), hs.getRoot()); + tds.setClient(client); + TextDocumentIdentifier mainTdi = new TextDocumentIdentifier(hs.file(modelFilename).toString()); + + // Resolves via token => shape name. + DefinitionParams commentParams = definitionParams(mainTdi, 43, 37); + Location commentLocation = tds.definition(commentParams).get().getLeft().get(0); + + // Resolves via shape target location in model. + DefinitionParams memberParams = definitionParams(mainTdi, 12, 18); + Location memberTargetLocation = tds.definition(memberParams).get().getLeft().get(0); + + // Resolves via member shape target location in prelude. + DefinitionParams preludeTargetParams = definitionParams(mainTdi, 36, 12); + Location preludeTargetLocation = tds.definition(preludeTargetParams).get().getLeft().get(0); + + // Resolves to current location. + DefinitionParams selfParams = definitionParams(mainTdi, 36, 0); + Location selfLocation = tds.definition(selfParams).get().getLeft().get(0); + + // Resolves via operation input. + DefinitionParams inputParams = definitionParams(mainTdi, 52, 16); + Location inputLocation = tds.definition(inputParams).get().getLeft().get(0); + + // Resolves via operation output. + DefinitionParams outputParams = definitionParams(mainTdi, 53, 17); + Location outputLocation = tds.definition(outputParams).get().getLeft().get(0); + + // Resolves via operation error. + DefinitionParams errorParams = definitionParams(mainTdi, 54, 14); + Location errorLocation = tds.definition(errorParams).get().getLeft().get(0); + + // Resolves via resource ids. + DefinitionParams idParams = definitionParams(mainTdi, 75, 29); + Location idLocation = tds.definition(idParams).get().getLeft().get(0); + + // Resolves via resource read. + DefinitionParams readParams = definitionParams(mainTdi, 76, 12); + Location readLocation = tds.definition(readParams).get().getLeft().get(0); + + // Does not correspond to shape. + DefinitionParams noMatchParams = definitionParams(mainTdi, 0, 0); + List noMatchLocationList = (List) tds.definition(noMatchParams).get().getLeft(); + + correctLocation(commentLocation, modelFilename, 20, 0, 21, 14); + correctLocation(memberTargetLocation, modelFilename, 4, 0, 4, 23); + correctLocation(selfLocation, modelFilename, 35, 0, 37, 1); + correctLocation(inputLocation, modelFilename, 57, 0, 61, 1); + correctLocation(outputLocation, modelFilename, 63, 0, 66, 1); + correctLocation(errorLocation, modelFilename, 69, 0, 72, 1); + correctLocation(idLocation, modelFilename, 79, 0, 79, 11); + correctLocation(readLocation, modelFilename, 51, 0, 55, 1); + assertTrue(preludeTargetLocation.getUri().endsWith("prelude.smithy")); + assertTrue(noMatchLocationList.isEmpty()); + } + } + private class StubClient implements LanguageClient { public List diagnostics = new ArrayList<>(); public List shown = new ArrayList<>(); @@ -220,4 +294,16 @@ private TextDocumentItem textDocumentItem(File f, String text) { private String uri(File f) { return f.toURI().toString(); } + + private DefinitionParams definitionParams(TextDocumentIdentifier tdi, int line, int character) { + return new DefinitionParams(tdi, new Position(line, character)); + } + + private void correctLocation(Location location, String uri, int startLine, int startCol, int endLine, int endCol) { + assertEquals(startLine, location.getRange().getStart().getLine()); + assertEquals(startCol, location.getRange().getStart().getCharacter()); + assertEquals(endLine, location.getRange().getEnd().getLine()); + assertEquals(endCol, location.getRange().getEnd().getCharacter()); + assertTrue(location.getUri().endsWith(uri)); + } } diff --git a/src/test/resources/software/amazon/smithy/lsp/ext/models/main.smithy b/src/test/resources/software/amazon/smithy/lsp/ext/models/main.smithy new file mode 100644 index 00000000..3a21871b --- /dev/null +++ b/src/test/resources/software/amazon/smithy/lsp/ext/models/main.smithy @@ -0,0 +1,80 @@ +$version: "1.0" + +namespace com.foo + +structure SingleLine {} + + structure MultiLine { + a: String, + + + b: String, + @required + c: SingleLine + } + + @pattern("^[A-Za-z0-9 ]+$") + string SingleTrait + +@input +@tags(["foo"]) +structure MultiTrait { + a: String} + +// Line comments +// comments +@input + // comments + @tags(["a", + "b", + "c", + "d", + "e", + "f" + ] +) +structure MultiTraitAndLineComments { + a: String +} + + + + +/// Doc comments +/// Comment about corresponding MultiTrait shape +@input +@tags(["foo"]) +structure MultiTraitAndDocComments { + a: String +} + +@readonly +operation MyOperation { + input: MyInput, + output: MyOutput, + errors: [MyError] +} + +structure MyInput { + foo: String, + @required + myId: MyId +} + +structure MyOutput { + corge: String, + qux: String +} + +@error("client") +structure MyError { + blah: String, + blahhhh: Integer +} + +resource MyResource { + identifiers: { myId: MyId }, + read: MyOperation +} + +string MyId diff --git a/src/test/resources/software/amazon/smithy/lsp/ext/models/test.smithy b/src/test/resources/software/amazon/smithy/lsp/ext/models/test.smithy new file mode 100644 index 00000000..1c013002 --- /dev/null +++ b/src/test/resources/software/amazon/smithy/lsp/ext/models/test.smithy @@ -0,0 +1,12 @@ +$version: "1.0" + +namespace com.example + +structure OtherStructure { + foo: String, + bar: String, + baz: Integer +} + + +