diff --git a/build.gradle b/build.gradle index 0e33051..4ab9609 100644 --- a/build.gradle +++ b/build.gradle @@ -157,9 +157,9 @@ publishing { dependencies { implementation "org.eclipse.lsp4j:org.eclipse.lsp4j:0.14.0" - implementation "software.amazon.smithy:smithy-model:[1.30.0, 2.0[" + implementation "software.amazon.smithy:smithy-model:[1.31.0, 2.0[" implementation 'io.get-coursier:interface:1.0.4' - implementation 'com.disneystreaming.smithy:smithytranslate-formatter-jvm-java-api:0.3.3' + implementation 'com.disneystreaming.smithy:smithytranslate-formatter-jvm-java-api:0.3.4' // Use JUnit test framework testImplementation "junit:junit:4.13" diff --git a/build.sc b/build.sc index d6d6ac1..3834416 100644 --- a/build.sc +++ b/build.sc @@ -9,9 +9,9 @@ object lsp extends MavenModule with PublishModule { def ivyDeps = Agg( ivy"org.eclipse.lsp4j:org.eclipse.lsp4j:0.14.0", - ivy"software.amazon.smithy:smithy-model:1.30.0", + ivy"software.amazon.smithy:smithy-model:1.31.0", ivy"io.get-coursier:interface:1.0.4", - ivy"com.disneystreaming.smithy:smithytranslate-formatter-jvm-java-api:0.3.3" + ivy"com.disneystreaming.smithy:smithytranslate-formatter-jvm-java-api:0.3.4" ) def publishVersion = T { gitVersion() } diff --git a/src/main/java/software/amazon/smithy/lsp/ProtocolAdapter.java b/src/main/java/software/amazon/smithy/lsp/ProtocolAdapter.java index 7f3baff..fc738b1 100644 --- a/src/main/java/software/amazon/smithy/lsp/ProtocolAdapter.java +++ b/src/main/java/software/amazon/smithy/lsp/ProtocolAdapter.java @@ -15,10 +15,13 @@ package software.amazon.smithy.lsp; +import java.util.Optional; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.DiagnosticSeverity; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SymbolKind; +import software.amazon.smithy.model.shapes.ShapeType; import software.amazon.smithy.model.validation.Severity; import software.amazon.smithy.model.validation.ValidationEvent; @@ -61,4 +64,64 @@ public static DiagnosticSeverity toDiagnosticSeverity(Severity severity) { return DiagnosticSeverity.Hint; } } + + /** + * @param shapeType The type to be converted to a SymbolKind + * @param parentType An optional type of the shape's enclosing definition + * @return An lsp4j SymbolKind + */ + public static SymbolKind toSymbolKind(ShapeType shapeType, Optional parentType) { + switch (shapeType) { + case BYTE: + case BIG_INTEGER: + case DOUBLE: + case BIG_DECIMAL: + case FLOAT: + case LONG: + case INTEGER: + case SHORT: + return SymbolKind.Number; + case BLOB: + // technically a sequence of bytes, so due to the lack of a better alternative, an array + case LIST: + case SET: + return SymbolKind.Array; + case BOOLEAN: + return SymbolKind.Boolean; + case STRING: + return SymbolKind.String; + case TIMESTAMP: + case UNION: + return SymbolKind.Interface; + + case DOCUMENT: + return SymbolKind.Class; + case ENUM: + case INT_ENUM: + return SymbolKind.Enum; + case MAP: + return SymbolKind.Object; + case STRUCTURE: + return SymbolKind.Struct; + case MEMBER: + if (!parentType.isPresent()) { + return SymbolKind.Field; + } + switch (parentType.get()) { + case ENUM: + return SymbolKind.EnumMember; + case UNION: + return SymbolKind.Class; + default: return SymbolKind.Field; + } + case SERVICE: + case RESOURCE: + return SymbolKind.Module; + case OPERATION: + return SymbolKind.Method; + default: + // This case shouldn't be reachable + return SymbolKind.Key; + } + } } diff --git a/src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java b/src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java index d7b6c9e..09d13af 100644 --- a/src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java +++ b/src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java @@ -16,7 +16,6 @@ package software.amazon.smithy.lsp; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.net.URI; import java.nio.file.Files; @@ -45,7 +44,6 @@ import org.eclipse.lsp4j.services.WorkspaceService; import software.amazon.smithy.lsp.codeactions.SmithyCodeActions; import software.amazon.smithy.lsp.ext.LspLog; -import software.amazon.smithy.lsp.ext.ValidationException; import software.amazon.smithy.utils.ListUtils; public class SmithyLanguageServer implements LanguageServer, LanguageClientAware, SmithyProtocolExtensions { @@ -59,7 +57,7 @@ public CompletableFuture shutdown() { return Utils.completableFuture(new Object()); } - private void loadSmithyBuild(File root) throws ValidationException, FileNotFoundException { + private void loadSmithyBuild(File root) { this.tds.ifPresent(tds -> tds.createProject(root)); } @@ -108,6 +106,7 @@ public CompletableFuture initialize(InitializeParams params) { capabilities.setCompletionProvider(new CompletionOptions(true, null)); capabilities.setHoverProvider(true); capabilities.setDocumentFormattingProvider(true); + capabilities.setDocumentSymbolProvider(true); return Utils.completableFuture(new InitializeResult(capabilities)); } diff --git a/src/main/java/software/amazon/smithy/lsp/SmithyTextDocumentService.java b/src/main/java/software/amazon/smithy/lsp/SmithyTextDocumentService.java index 5af6e14..4493a5c 100644 --- a/src/main/java/software/amazon/smithy/lsp/SmithyTextDocumentService.java +++ b/src/main/java/software/amazon/smithy/lsp/SmithyTextDocumentService.java @@ -19,8 +19,10 @@ import com.google.common.hash.Hashing; import java.io.File; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; +import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -49,6 +51,8 @@ import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.DidSaveTextDocumentParams; import org.eclipse.lsp4j.DocumentFormattingParams; +import org.eclipse.lsp4j.DocumentSymbol; +import org.eclipse.lsp4j.DocumentSymbolParams; import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.HoverParams; import org.eclipse.lsp4j.Location; @@ -59,6 +63,8 @@ import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.SymbolKind; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentItem; import org.eclipse.lsp4j.TextEdit; @@ -85,6 +91,7 @@ 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.shapes.ShapeType; import software.amazon.smithy.model.shapes.SmithyIdlModelSerializer; import software.amazon.smithy.model.validation.ValidatedResult; import software.amazon.smithy.model.validation.ValidationEvent; @@ -322,6 +329,9 @@ private File designatedTemporaryFile(File source) { return new File(this.temporaryFolder, hashed + Constants.SMITHY_EXTENSION); } + /** + * @return lines in the file or buffer + */ private List textBufferContents(String path) throws IOException { List contents; if (Utils.isSmithyJarFile(path)) { @@ -388,6 +398,58 @@ private String getLine(List lines, Position position) { return lines.get(position.getLine()); } + @Override + public CompletableFuture>> documentSymbol( + DocumentSymbolParams params + ) { + try { + Map locations = project.getLocations(); + Model model = project.getModel().unwrap(); + + List symbols = new ArrayList<>(); + + URI documentUri = documentIdentifierToUri(params.getTextDocument()); + + locations.forEach((shapeId, loc) -> { + String[] locSegments = loc.getUri().replace("\\", "/").split(":"); + boolean matchesDocument = documentUri.toString().endsWith(locSegments[locSegments.length - 1]); + + if (!matchesDocument) { + return; + } + + Shape shape = model.expectShape(shapeId); + + Optional parentType = shape.isMemberShape() + ? Optional.of(model.expectShape(shapeId.withoutMember()).getType()) + : Optional.empty(); + + SymbolKind kind = ProtocolAdapter.toSymbolKind(shape.getType(), parentType); + + String symbolName = shapeId.getMember().orElse(shapeId.getName()); + + symbols.add(new DocumentSymbol(symbolName, kind, loc.getRange(), loc.getRange())); + }); + + return Utils.completableFuture( + symbols + .stream() + .map(Either::forRight) + .collect(Collectors.toList()) + ); + } catch (Exception e) { + e.printStackTrace(System.err); + + return Utils.completableFuture(Collections.emptyList()); + } + } + + private URI documentIdentifierToUri(TextDocumentIdentifier ident) throws UnsupportedEncodingException { + return Utils.isSmithyJarFile(ident.getUri()) + ? URI.create(URLDecoder.decode(ident.getUri(), StandardCharsets.UTF_8.name())) + : this.fileUri(ident).toURI(); + } + @Override public CompletableFuture, List>> definition( DefinitionParams params) { @@ -489,21 +551,21 @@ private Optional getTargetShape(Shape initialShape, String token, Model m } private String getHoverContentsForShape(Shape shape, Model model) { - try { - String serializedShape = serializeShape(shape, model); + List validationEvents = getValidationEventsForShape(shape); + String serializedShape = serializeShape(shape, model); + if (validationEvents.isEmpty()) { return "```smithy\n" + serializedShape + "\n```"; - } catch (Exception e) { - List validationEvents = getValidationEventsForShape(shape); - StringBuilder contents = new StringBuilder(); - contents.append("Can't display shape ").append("`").append(shape.getId().toString()).append("`:"); - for (ValidationEvent event : validationEvents) { - contents.append(System.lineSeparator()).append(event.getMessage()); - } - if (validationEvents.isEmpty()) { - contents.append(System.lineSeparator()).append(e); - } - return contents.toString(); } + StringBuilder contents = new StringBuilder(); + contents.append("```smithy\n"); + contents.append(serializedShape); + contents.append("\n"); + contents.append("---\n"); + for (ValidationEvent event : validationEvents) { + contents.append(event.getSeverity() + ": " + event.getMessage() + "\n"); + } + contents.append("```"); + return contents.toString(); } private String serializeShape(Shape shape, Model model) { diff --git a/src/main/java/software/amazon/smithy/lsp/Utils.java b/src/main/java/software/amazon/smithy/lsp/Utils.java index cb89964..f79a203 100644 --- a/src/main/java/software/amazon/smithy/lsp/Utils.java +++ b/src/main/java/software/amazon/smithy/lsp/Utils.java @@ -56,7 +56,7 @@ public static CompletableFuture completableFuture(U value) { * @return Returns whether the uri points to a file in jar. * @throws IOException when rawUri cannot be URL-decoded */ - public static boolean isSmithyJarFile(String rawUri) throws IOException { + public static boolean isSmithyJarFile(String rawUri) { try { String uri = java.net.URLDecoder.decode(rawUri, StandardCharsets.UTF_8.name()); return uri.startsWith("smithyjar:"); diff --git a/src/test/java/software/amazon/smithy/lsp/SmithyTextDocumentServiceTest.java b/src/test/java/software/amazon/smithy/lsp/SmithyTextDocumentServiceTest.java index 7d7a685..5c0e477 100644 --- a/src/test/java/software/amazon/smithy/lsp/SmithyTextDocumentServiceTest.java +++ b/src/test/java/software/amazon/smithy/lsp/SmithyTextDocumentServiceTest.java @@ -39,6 +39,8 @@ import org.eclipse.lsp4j.DidChangeTextDocumentParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.DocumentSymbol; +import org.eclipse.lsp4j.DocumentSymbolParams; import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.HoverParams; import org.eclipse.lsp4j.Location; @@ -49,6 +51,8 @@ import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.ShowMessageRequestParams; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.SymbolKind; import org.eclipse.lsp4j.TextDocumentContentChangeEvent; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentItem; @@ -210,7 +214,7 @@ public void allowsHoverWhenThereAreUnknownTraits() throws Exception { } @Test - public void hoverOnBrokenShapeShowsErrorMessage() throws Exception { + public void hoverOnBrokenShapeAppendsValidations() throws Exception { Path baseDir = Paths.get(getClass().getResource("ext/models").toURI()); String modelFilename = "unknown-trait.smithy"; Path modelFilePath = baseDir.resolve(modelFilename); @@ -223,7 +227,10 @@ public void hoverOnBrokenShapeShowsErrorMessage() throws Exception { TextDocumentIdentifier tdi = new TextDocumentIdentifier(hs.file(modelFilename).toString()); Hover hover = tds.hover(hoverParams(tdi, 10, 13)).get(); MarkupContent hoverContent = hover.getContents().getRight(); - assertTrue(hoverContent.getValue().startsWith("Can't display shape")); + assertEquals(hoverContent.getKind(),"markdown"); + assertTrue(hoverContent.getValue().startsWith("```smithy")); + assertTrue(hoverContent.getValue().contains("structure Foo {}")); + assertTrue(hoverContent.getValue().contains("WARNING: Unable to resolve trait `com.external#unknownTrait`")); } } @@ -549,9 +556,9 @@ public void hoverV1() throws Exception { // Resolves via resource read. Hover readHover = tds.hover(hoverParams(mainTdi, 76, 12)).get(); - correctHover(mainHoverPrefix, "@http(\n method: \"PUT\"\n uri: \"/bar\"\n code: 200\n)\n@readonly\n" - + "operation MyOperation {\n input: MyOperationInput\n output: MyOperationOutput\n" - + " errors: [\n MyError\n ]\n}", readHover); + assertTrue(readHover.getContents().getRight().getValue().contains("@http(\n method: \"PUT\"\n " + + "uri: \"/bar\"\n code: 200\n)\n@readonly\noperation MyOperation {\n input: " + + "MyOperationInput\n output: MyOperationOutput\n errors: [\n MyError\n ]\n}")); // Does not correspond to shape. Hover noMatchHover = tds.hover(hoverParams(mainTdi, 0, 0)).get(); @@ -662,9 +669,9 @@ public void hoverV2() throws Exception { // Resolves via resource read. Hover readHover = tds.hover(hoverParams(mainTdi, 78, 12)).get(); - correctHover(mainHoverPrefix, "@http(\n method: \"PUT\"\n uri: \"/bar\"\n code: 200\n)\n@readonly\n" - + "operation MyOperation {\n input: MyOperationInput\n output: MyOperationOutput\n" - + " errors: [\n MyError\n ]\n}", readHover); + assertTrue(readHover.getContents().getRight().getValue().contains("@http(\n method: \"PUT\"\n " + + "uri: \"/bar\"\n code: 200\n)\n@readonly\noperation MyOperation {\n input: " + + "MyOperationInput\n output: MyOperationOutput\n errors: [\n MyError\n ]\n}")); // Does not correspond to shape. Hover noMatchHover = tds.hover(hoverParams(mainTdi, 0, 0)).get(); @@ -863,6 +870,35 @@ public void ensureVersionDiagnostic() throws Exception { } + @Test + public void documentSymbols() throws Exception { + Path baseDir = Paths.get(SmithyProjectTest.class.getResource("models/document-symbols").toURI()); + + String currentFile = "current.smithy"; + String anotherFile = "another.smithy"; + + List files = ListUtils.of(baseDir.resolve(currentFile),baseDir.resolve(anotherFile)); + + try (Harness hs = Harness.create(SmithyBuildExtensions.builder().build(), files)) { + SmithyTextDocumentService tds = new SmithyTextDocumentService(Optional.empty(), hs.getTempFolder()); + tds.createProject(hs.getConfig(), hs.getRoot()); + + TextDocumentIdentifier currentDocumentIdent = new TextDocumentIdentifier(uri(hs.file(currentFile))); + + List> symbols = + tds.documentSymbol(new DocumentSymbolParams(currentDocumentIdent)).get(); + + assertEquals(2, symbols.size()); + + assertEquals("city", symbols.get(0).getRight().getName()); + assertEquals(SymbolKind.Field, symbols.get(0).getRight().getKind()); + + assertEquals("Weather", symbols.get(1).getRight().getName()); + assertEquals(SymbolKind.Struct, symbols.get(1).getRight().getKind()); + } + + } + private static class StubClient implements LanguageClient { public List diagnostics = new ArrayList<>(); public List shown = new ArrayList<>(); diff --git a/src/test/resources/software/amazon/smithy/lsp/ext/models/document-symbols/another.smithy b/src/test/resources/software/amazon/smithy/lsp/ext/models/document-symbols/another.smithy new file mode 100644 index 0000000..9228eaa --- /dev/null +++ b/src/test/resources/software/amazon/smithy/lsp/ext/models/document-symbols/another.smithy @@ -0,0 +1,3 @@ +$version: "2" +namespace test +structure City { } diff --git a/src/test/resources/software/amazon/smithy/lsp/ext/models/document-symbols/current.smithy b/src/test/resources/software/amazon/smithy/lsp/ext/models/document-symbols/current.smithy new file mode 100644 index 0000000..9a69918 --- /dev/null +++ b/src/test/resources/software/amazon/smithy/lsp/ext/models/document-symbols/current.smithy @@ -0,0 +1,5 @@ +$version: "2" +namespace test +structure Weather { + @required city: City +}