From a95e6c77b9da1c51a97be8db1c3b98446eb1eab9 Mon Sep 17 00:00:00 2001 From: Miles Ziemer Date: Tue, 17 Dec 2024 15:23:42 -0500 Subject: [PATCH] Refactoring This commit keeps the functionality added to the language features in the previous commits, but does some broad refactoring of those changes to clean up the APIs, get rid of some footguns, and reduce the chance of some concurrency/parallelism issues. The main changes are: - Syntax.Ident/Syntax.Node.Str produced by the new parser now copy the actual string value. Previously, they only stored the start/end positions, and required you to copy the value out of the Document on-demand. This reduced the memory footprint of parsing, but I was concerned about the Document being changed at the same time another thread is trying to copy a value out of it. Copying eagerly avoids this. Plus, we can avoid most of the memory issues by doing partial reparsing (more on that later). - Project now stores an index of files -> shapes defined in that file, instead of storing the shapes on the SmithyFile. This index is only needed to help determine which shapes need to be removed when rebuilding the model, so it doesn't make sense for SmithyFile to know about it. This also ties into the next change... - Multiple changes to SmithyFile. SmithyFile now has a subclass, IdlFile, which stores its parse result. With the addition of the parser, and the changes to make DocumentVersion/DocumentImports/DocumentNamespace be computed from the parse result, SmithyFile can't represent both IDL and AST files. Arguably, it never really did because AST files don't have namespaces/imports. Either way, IdlFile now provides access to the parse result, which contains DocumentNamespace/Version/Imports, as well as the parsed statements. I also added synchronization to handle access to the parse result, since it will be mutated on every change. I don't really like how this works, but I'm going to address that in a future update (which I will describe below). - Added StatementView, which wraps a list of parsed statements and a specific index in that list, providing methods to look "around" that index. This replaces the error-prone and unreadable SyntaxSearch, which required you to pass around int indicies everywhere. Some more minor changes to note: - Moved diagnostics computation into SmithyDiagnostics. It already belonged there probably, but especially with the addition of IdlFile I just had to do it. - Moved document symbols into a 'handler' like definition, etc. - Added `uri` and `isDetached` properties to ProjectAndFile, for convenience. There are still some rough edges with this code, but I plan on making a follow up PR to address them, so I this one doesn't become even larger. Specifically, I want to only parse opened/managed files. This could let us get rid of the whole ProjectFile thing, or at least not require going through a project to find a file (it would be stored directly on ServerState). This also makes the synchronization story much simpler, improves initialization time, and should make it easier to eventually load projects async. --- .../amazon/smithy/lsp/ServerState.java | 7 +- .../smithy/lsp/SmithyLanguageServer.java | 182 +---- .../lsp/diagnostics/SmithyDiagnostics.java | 161 ++++- .../amazon/smithy/lsp/document/Document.java | 87 +-- .../smithy/lsp/document/DocumentImports.java | 5 +- .../lsp/document/DocumentNamespace.java | 5 +- .../smithy/lsp/document/DocumentParser.java | 123 +--- .../smithy/lsp/document/DocumentShape.java | 42 -- .../smithy/lsp/document/DocumentVersion.java | 5 +- .../smithy/lsp/language/CompleterContext.java | 92 +++ .../lsp/language/CompletionCandidates.java | 2 - .../lsp/language/CompletionHandler.java | 159 ++--- .../lsp/language/DefinitionHandler.java | 15 +- .../lsp/language/DocumentSymbolHandler.java | 63 ++ .../lsp/language/DynamicMemberTarget.java | 39 +- .../smithy/lsp/language/HoverHandler.java | 18 +- .../smithy/lsp/language/IdlPosition.java | 121 ++-- .../smithy/lsp/language/NodeSearch.java | 35 + ...peCompletions.java => ShapeCompleter.java} | 137 ++-- .../smithy/lsp/language/ShapeSearch.java | 143 ++-- ...eCompletions.java => SimpleCompleter.java} | 125 ++-- .../amazon/smithy/lsp/project/IdlFile.java | 46 ++ .../amazon/smithy/lsp/project/Project.java | 160 ++--- .../smithy/lsp/project/ProjectAndFile.java | 4 +- .../smithy/lsp/project/ProjectLoader.java | 214 +++--- .../amazon/smithy/lsp/project/SmithyFile.java | 232 +----- .../smithy/lsp/protocol/LspAdapter.java | 31 + .../amazon/smithy/lsp/syntax/NodeCursor.java | 12 +- .../amazon/smithy/lsp/syntax/Parser.java | 129 ++-- .../smithy/lsp/syntax/StatementView.java | 228 ++++++ .../amazon/smithy/lsp/syntax/Syntax.java | 110 +-- .../smithy/lsp/syntax/SyntaxSearch.java | 214 ------ .../smithy/lsp/SmithyLanguageServerTest.java | 670 +----------------- .../lsp/SmithyVersionRefactoringTest.java | 12 +- .../lsp/document/DocumentParserTest.java | 135 +--- .../lsp/language/CompletionHandlerTest.java | 36 +- .../lsp/language/DefinitionHandlerTest.java | 35 +- .../lsp/language/DocumentSymbolTest.java | 62 ++ .../smithy/lsp/language/HoverHandlerTest.java | 22 +- .../smithy/lsp/project/ProjectTest.java | 52 +- .../smithy/lsp/syntax/IdlParserTest.java | 6 +- .../smithy/lsp/syntax/NodeParserTest.java | 37 +- .../smithy/lsp/syntax/SyntaxSearchTest.java | 4 +- 43 files changed, 1580 insertions(+), 2437 deletions(-) delete mode 100644 src/main/java/software/amazon/smithy/lsp/document/DocumentShape.java create mode 100644 src/main/java/software/amazon/smithy/lsp/language/CompleterContext.java create mode 100644 src/main/java/software/amazon/smithy/lsp/language/DocumentSymbolHandler.java rename src/main/java/software/amazon/smithy/lsp/language/{ShapeCompletions.java => ShapeCompleter.java} (69%) rename src/main/java/software/amazon/smithy/lsp/language/{SimpleCompletions.java => SimpleCompleter.java} (67%) create mode 100644 src/main/java/software/amazon/smithy/lsp/project/IdlFile.java create mode 100644 src/main/java/software/amazon/smithy/lsp/syntax/StatementView.java delete mode 100644 src/main/java/software/amazon/smithy/lsp/syntax/SyntaxSearch.java create mode 100644 src/test/java/software/amazon/smithy/lsp/language/DocumentSymbolTest.java diff --git a/src/main/java/software/amazon/smithy/lsp/ServerState.java b/src/main/java/software/amazon/smithy/lsp/ServerState.java index 20f30733..9481d38d 100644 --- a/src/main/java/software/amazon/smithy/lsp/ServerState.java +++ b/src/main/java/software/amazon/smithy/lsp/ServerState.java @@ -96,7 +96,7 @@ ProjectAndFile findProjectAndFile(String uri) { String path = LspAdapter.toPath(uri); ProjectFile projectFile = detachedProject.getProjectFile(path); if (projectFile != null) { - return new ProjectAndFile(detachedProject, projectFile); + return new ProjectAndFile(uri, detachedProject, projectFile, true); } } @@ -133,17 +133,16 @@ private ProjectAndFile findAttachedAndRemoveDetached(String uri) { ProjectFile projectFile = project.getProjectFile(path); if (projectFile != null) { detachedProjects.remove(uri); - return new ProjectAndFile(project, projectFile); + return new ProjectAndFile(uri, project, projectFile, false); } } return null; } - Project createDetachedProject(String uri, String text) { + void createDetachedProject(String uri, String text) { Project project = ProjectLoader.loadDetached(uri, text); detachedProjects.put(uri, project); - return project; } List tryInitProject(Path root) { diff --git a/src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java b/src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java index 16c73f07..073525c7 100644 --- a/src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java +++ b/src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java @@ -45,7 +45,6 @@ import org.eclipse.lsp4j.CompletionParams; import org.eclipse.lsp4j.DefinitionParams; import org.eclipse.lsp4j.Diagnostic; -import org.eclipse.lsp4j.DiagnosticSeverity; import org.eclipse.lsp4j.DidChangeConfigurationParams; import org.eclipse.lsp4j.DidChangeTextDocumentParams; import org.eclipse.lsp4j.DidChangeWatchedFilesParams; @@ -73,7 +72,6 @@ import org.eclipse.lsp4j.RegistrationParams; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.SymbolInformation; -import org.eclipse.lsp4j.SymbolKind; import org.eclipse.lsp4j.TextDocumentChangeRegistrationOptions; import org.eclipse.lsp4j.TextDocumentContentChangeEvent; import org.eclipse.lsp4j.TextDocumentIdentifier; @@ -98,26 +96,25 @@ import software.amazon.smithy.lsp.codeactions.SmithyCodeActions; import software.amazon.smithy.lsp.diagnostics.SmithyDiagnostics; import software.amazon.smithy.lsp.document.Document; -import software.amazon.smithy.lsp.document.DocumentParser; -import software.amazon.smithy.lsp.document.DocumentShape; import software.amazon.smithy.lsp.ext.OpenProject; import software.amazon.smithy.lsp.ext.SelectorParams; import software.amazon.smithy.lsp.ext.ServerStatus; import software.amazon.smithy.lsp.ext.SmithyProtocolExtensions; import software.amazon.smithy.lsp.language.CompletionHandler; import software.amazon.smithy.lsp.language.DefinitionHandler; +import software.amazon.smithy.lsp.language.DocumentSymbolHandler; import software.amazon.smithy.lsp.language.HoverHandler; import software.amazon.smithy.lsp.project.BuildFile; +import software.amazon.smithy.lsp.project.IdlFile; import software.amazon.smithy.lsp.project.Project; import software.amazon.smithy.lsp.project.ProjectAndFile; import software.amazon.smithy.lsp.project.SmithyFile; import software.amazon.smithy.lsp.protocol.LspAdapter; -import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.lsp.syntax.Syntax; import software.amazon.smithy.model.loader.IdlTokenizer; import software.amazon.smithy.model.selector.Selector; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.validation.Severity; -import software.amazon.smithy.model.validation.ValidationEvent; import software.amazon.smithy.syntax.Formatter; import software.amazon.smithy.syntax.TokenTree; import software.amazon.smithy.utils.IoUtils; @@ -161,6 +158,10 @@ ServerState getState() { return state; } + Severity getMinimumSeverity() { + return minimumSeverity; + } + @Override public void connect(LanguageClient client) { LOGGER.finest("Connect"); @@ -513,7 +514,7 @@ public void didChange(DidChangeTextDocumentParams params) { // Report any parse/shape/trait loading errors CompletableFuture future = CompletableFuture .runAsync(() -> project.updateModelWithoutValidating(uri)) - .thenComposeAsync(unused -> sendFileDiagnostics(uri)); + .thenComposeAsync(unused -> sendFileDiagnostics(projectAndFile)); state.lifecycleManager().putTask(uri, future); } } @@ -533,9 +534,10 @@ public void didOpen(DidOpenTextDocumentParams params) { projectAndFile.file().document().applyEdit(null, text); } else { state.createDetachedProject(uri, text); + projectAndFile = state.findProjectAndFile(uri); // Note: This will always be present } - state.lifecycleManager().putTask(uri, sendFileDiagnostics(uri)); + state.lifecycleManager().putTask(uri, sendFileDiagnostics(projectAndFile)); } @Override @@ -581,7 +583,7 @@ public void didSave(DidSaveTextDocumentParams params) { } else { CompletableFuture future = CompletableFuture .runAsync(() -> project.updateAndValidateModel(uri)) - .thenCompose(unused -> sendFileDiagnostics(uri)); + .thenCompose(unused -> sendFileDiagnostics(projectAndFile)); state.lifecycleManager().putTask(uri, future); } } @@ -597,15 +599,13 @@ public CompletableFuture, CompletionList>> completio return completedFuture(Either.forLeft(Collections.emptyList())); } - if (!(projectAndFile.file() instanceof SmithyFile smithyFile)) { + if (!(projectAndFile.file() instanceof IdlFile smithyFile)) { return completedFuture(Either.forLeft(List.of())); } Project project = projectAndFile.project(); - return CompletableFutures.computeAsync((cc) -> { - CompletionHandler handler = new CompletionHandler(project, smithyFile); - return Either.forLeft(handler.handle(params, cc)); - }); + var handler = new CompletionHandler(project, smithyFile); + return CompletableFutures.computeAsync((cc) -> Either.forLeft(handler.handle(params, cc))); } @Override @@ -627,54 +627,13 @@ public CompletableFuture resolveCompletionItem(CompletionItem un return completedFuture(Collections.emptyList()); } - if (!(projectAndFile.file() instanceof SmithyFile smithyFile)) { + if (!(projectAndFile.file() instanceof IdlFile idlFile)) { return completedFuture(List.of()); } - return CompletableFutures.computeAsync((cc) -> { - Collection documentShapes = smithyFile.documentShapes(); - if (documentShapes.isEmpty()) { - return Collections.emptyList(); - } - - if (cc.isCanceled()) { - return Collections.emptyList(); - } - - List> documentSymbols = new ArrayList<>(documentShapes.size()); - for (DocumentShape documentShape : documentShapes) { - SymbolKind symbolKind; - switch (documentShape.kind()) { - case Inline: - // No shape name in the document text, so no symbol - continue; - case DefinedMember: - case Elided: - symbolKind = SymbolKind.Property; - break; - case DefinedShape: - case Targeted: - default: - symbolKind = SymbolKind.Class; - break; - } - - // Check before copying shapeName, which is actually a reference to the underlying document, and may - // be changed. - cc.checkCanceled(); - - String symbolName = documentShape.shapeName().toString(); - if (symbolName.isEmpty()) { - LOGGER.warning("[DocumentSymbols] Empty shape name for " + documentShape); - continue; - } - Range symbolRange = documentShape.range(); - DocumentSymbol symbol = new DocumentSymbol(symbolName, symbolKind, symbolRange, symbolRange); - documentSymbols.add(Either.forRight(symbol)); - } - - return documentSymbols; - }); + List statements = idlFile.getParse().statements(); + var handler = new DocumentSymbolHandler(idlFile.document(), statements); + return CompletableFuture.supplyAsync(handler::handle); } @Override @@ -689,13 +648,13 @@ public CompletableFuture resolveCompletionItem(CompletionItem un return completedFuture(null); } - if (!(projectAndFile.file() instanceof SmithyFile smithyFile)) { + if (!(projectAndFile.file() instanceof IdlFile smithyFile)) { return completedFuture(null); } Project project = projectAndFile.project(); - List locations = new DefinitionHandler(project, smithyFile).handle(params); - return completedFuture(Either.forLeft(locations)); + var handler = new DefinitionHandler(project, smithyFile); + return CompletableFuture.supplyAsync(() -> Either.forLeft(handler.handle(params))); } @Override @@ -709,15 +668,15 @@ public CompletableFuture hover(HoverParams params) { return completedFuture(null); } - if (!(projectAndFile.file() instanceof SmithyFile smithyFile)) { + if (!(projectAndFile.file() instanceof IdlFile smithyFile)) { return completedFuture(null); } Project project = projectAndFile.project(); // TODO: Abstract away passing minimum severity - Hover hover = new HoverHandler(project, smithyFile, minimumSeverity).handle(params); - return completedFuture(hover); + var handler = new HoverHandler(project, smithyFile, minimumSeverity); + return CompletableFuture.supplyAsync(() -> handler.handle(params)); } @Override @@ -756,99 +715,16 @@ public CompletableFuture> formatting(DocumentFormatting private void sendFileDiagnosticsForManagedDocuments() { for (String managedDocumentUri : state.managedUris()) { - state.lifecycleManager().putOrComposeTask(managedDocumentUri, sendFileDiagnostics(managedDocumentUri)); + ProjectAndFile projectAndFile = state.findProjectAndFile(managedDocumentUri); + state.lifecycleManager().putOrComposeTask(managedDocumentUri, sendFileDiagnostics(projectAndFile)); } } - private CompletableFuture sendFileDiagnostics(String uri) { + private CompletableFuture sendFileDiagnostics(ProjectAndFile projectAndFile) { return CompletableFuture.runAsync(() -> { - List diagnostics = getFileDiagnostics(uri); - PublishDiagnosticsParams publishDiagnosticsParams = new PublishDiagnosticsParams(uri, diagnostics); + List diagnostics = SmithyDiagnostics.getFileDiagnostics(projectAndFile, minimumSeverity); + var publishDiagnosticsParams = new PublishDiagnosticsParams(projectAndFile.uri(), diagnostics); client.publishDiagnostics(publishDiagnosticsParams); }); } - - List getFileDiagnostics(String uri) { - if (LspAdapter.isJarFile(uri) || LspAdapter.isSmithyJarFile(uri)) { - // Don't send diagnostics to jar files since they can't be edited - // and diagnostics could be misleading. - return Collections.emptyList(); - } - - ProjectAndFile projectAndFile = state.findProjectAndFile(uri); - if (projectAndFile == null) { - client.unknownFileError(uri, "diagnostics"); - return List.of(); - } - - if (!(projectAndFile.file() instanceof SmithyFile smithyFile)) { - return List.of(); - } - - Project project = projectAndFile.project(); - String path = LspAdapter.toPath(uri); - - List diagnostics = project.modelResult().getValidationEvents().stream() - .filter(validationEvent -> validationEvent.getSeverity().compareTo(minimumSeverity) >= 0) - .filter(validationEvent -> validationEvent.getSourceLocation().getFilename().equals(path)) - .map(validationEvent -> toDiagnostic(validationEvent, smithyFile)) - .collect(Collectors.toCollection(ArrayList::new)); - - Diagnostic versionDiagnostic = SmithyDiagnostics.versionDiagnostic(smithyFile); - if (versionDiagnostic != null) { - diagnostics.add(versionDiagnostic); - } - - if (state.isDetached(uri)) { - diagnostics.add(SmithyDiagnostics.detachedDiagnostic(smithyFile)); - } - - return diagnostics; - } - - private static Diagnostic toDiagnostic(ValidationEvent validationEvent, SmithyFile smithyFile) { - DiagnosticSeverity severity = toDiagnosticSeverity(validationEvent.getSeverity()); - SourceLocation sourceLocation = validationEvent.getSourceLocation(); - Range range = determineRange(validationEvent, sourceLocation, smithyFile); - String message = validationEvent.getId() + ": " + validationEvent.getMessage(); - return new Diagnostic(range, message, severity, "Smithy"); - } - - private static Range determineRange(ValidationEvent validationEvent, - SourceLocation sourceLocation, - SmithyFile smithyFile) { - final Range defaultRange = LspAdapter.lineOffset(LspAdapter.toPosition(sourceLocation)); - - if (smithyFile == null) { - return defaultRange; - } - - DocumentParser parser = DocumentParser.forDocument(smithyFile.document()); - - // Case where we have shapes present - if (validationEvent.getShapeId().isPresent()) { - // Event is (probably) on a member target - if (validationEvent.containsId("Target")) { - DocumentShape documentShape = smithyFile.documentShapesByStartPosition() - .get(LspAdapter.toPosition(sourceLocation)); - if (documentShape != null && documentShape.hasMemberTarget()) { - return documentShape.targetReference().range(); - } - } else { - // Check if the event location is on a trait application - return Objects.requireNonNullElse(parser.traitIdRange(sourceLocation), defaultRange); - } - } - - return Objects.requireNonNullElse(parser.findContiguousRange(sourceLocation), defaultRange); - } - - private static DiagnosticSeverity toDiagnosticSeverity(Severity severity) { - return switch (severity) { - case ERROR, DANGER -> DiagnosticSeverity.Error; - case WARNING -> DiagnosticSeverity.Warning; - case NOTE -> DiagnosticSeverity.Information; - default -> DiagnosticSeverity.Hint; - }; - } } diff --git a/src/main/java/software/amazon/smithy/lsp/diagnostics/SmithyDiagnostics.java b/src/main/java/software/amazon/smithy/lsp/diagnostics/SmithyDiagnostics.java index 2f4452d8..54459b17 100644 --- a/src/main/java/software/amazon/smithy/lsp/diagnostics/SmithyDiagnostics.java +++ b/src/main/java/software/amazon/smithy/lsp/diagnostics/SmithyDiagnostics.java @@ -5,17 +5,28 @@ package software.amazon.smithy.lsp.diagnostics; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.DiagnosticCodeDescription; import org.eclipse.lsp4j.DiagnosticSeverity; +import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; -import software.amazon.smithy.lsp.document.DocumentVersion; +import software.amazon.smithy.lsp.document.DocumentParser; +import software.amazon.smithy.lsp.project.IdlFile; +import software.amazon.smithy.lsp.project.Project; +import software.amazon.smithy.lsp.project.ProjectAndFile; import software.amazon.smithy.lsp.project.SmithyFile; import software.amazon.smithy.lsp.protocol.LspAdapter; +import software.amazon.smithy.lsp.syntax.StatementView; +import software.amazon.smithy.lsp.syntax.Syntax; +import software.amazon.smithy.model.validation.Severity; +import software.amazon.smithy.model.validation.ValidationEvent; /** - * Utility class for creating different kinds of file diagnostics, that aren't - * necessarily connected to model validation events. + * Creates diagnostics for Smithy files. */ public final class SmithyDiagnostics { public static final String UPDATE_VERSION = "migrating-idl-1-to-2"; @@ -29,38 +40,65 @@ private SmithyDiagnostics() { } /** - * Creates a diagnostic for when a $version control statement hasn't been defined, - * or when it has been defined for IDL 1.0. - * - * @param smithyFile The Smithy file to get a version diagnostic for - * @return The version diagnostic associated with the Smithy file, or null - * if one doesn't exist + * @param projectAndFile Project and file to get diagnostics for + * @param minimumSeverity Minimum severity of validation events to diagnose + * @return A list of diagnostics for the given project and file */ - public static Diagnostic versionDiagnostic(SmithyFile smithyFile) { - if (smithyFile.documentVersion().isPresent()) { - DocumentVersion documentVersion = smithyFile.documentVersion().get(); - if (!documentVersion.version().startsWith("2")) { - Diagnostic diagnostic = createDiagnostic( - documentVersion.range(), "You can upgrade to idl version 2.", UPDATE_VERSION); - diagnostic.setCodeDescription(UPDATE_VERSION_DESCRIPTION); - return diagnostic; - } - } else if (smithyFile.document() != null) { + public static List getFileDiagnostics(ProjectAndFile projectAndFile, Severity minimumSeverity) { + if (LspAdapter.isJarFile(projectAndFile.uri()) || LspAdapter.isSmithyJarFile(projectAndFile.uri())) { + // Don't send diagnostics to jar files since they can't be edited + // and diagnostics could be misleading. + return List.of(); + } + + if (!(projectAndFile.file() instanceof SmithyFile smithyFile)) { + return List.of(); + } + + Project project = projectAndFile.project(); + String path = projectAndFile.file().path(); + + EventToDiagnostic eventToDiagnostic = eventToDiagnostic(smithyFile); + + List diagnostics = project.modelResult().getValidationEvents().stream() + .filter(event -> event.getSeverity().compareTo(minimumSeverity) >= 0 + && event.getSourceLocation().getFilename().equals(path)) + .map(eventToDiagnostic::toDiagnostic) + .collect(Collectors.toCollection(ArrayList::new)); + + Diagnostic versionDiagnostic = versionDiagnostic(smithyFile); + if (versionDiagnostic != null) { + diagnostics.add(versionDiagnostic); + } + + if (projectAndFile.isDetached()) { + diagnostics.add(detachedDiagnostic(smithyFile)); + } + + return diagnostics; + } + + private static Diagnostic versionDiagnostic(SmithyFile smithyFile) { + if (!(smithyFile instanceof IdlFile idlFile)) { + return null; + } + + Syntax.IdlParseResult syntaxInfo = idlFile.getParse(); + if (syntaxInfo.version().version().startsWith("2")) { + return null; + } else if (!LspAdapter.isEmpty(syntaxInfo.version().range())) { + var diagnostic = createDiagnostic( + syntaxInfo.version().range(), "You can upgrade to idl version 2.", UPDATE_VERSION); + diagnostic.setCodeDescription(UPDATE_VERSION_DESCRIPTION); + return diagnostic; + } else { int end = smithyFile.document().lineEnd(0); Range range = LspAdapter.lineSpan(0, 0, end); return createDiagnostic(range, "You should define a version for your Smithy file", DEFINE_VERSION); } - return null; } - /** - * Creates a diagnostic for when a Smithy file is not connected to a - * Smithy project via smithy-build.json or other build file. - * - * @param smithyFile The Smithy file to get a detached diagnostic for - * @return The detached diagnostic associated with the Smithy file - */ - public static Diagnostic detachedDiagnostic(SmithyFile smithyFile) { + private static Diagnostic detachedDiagnostic(SmithyFile smithyFile) { Range range; if (smithyFile.document() == null) { range = LspAdapter.origin(); @@ -75,4 +113,71 @@ public static Diagnostic detachedDiagnostic(SmithyFile smithyFile) { private static Diagnostic createDiagnostic(Range range, String title, String code) { return new Diagnostic(range, title, DiagnosticSeverity.Warning, "smithy-language-server", code); } + + private static EventToDiagnostic eventToDiagnostic(SmithyFile smithyFile) { + if (!(smithyFile instanceof IdlFile idlFile)) { + return new Simple(); + } + + var idlParse = idlFile.getParse(); + var view = StatementView.createAtStart(idlParse).orElse(null); + if (view == null) { + return new Simple(); + } else { + var documentParser = DocumentParser.forStatements(smithyFile.document(), view.parseResult().statements()); + return new Idl(view, documentParser); + } + } + + private sealed interface EventToDiagnostic { + default Range getDiagnosticRange(ValidationEvent event) { + var start = LspAdapter.toPosition(event.getSourceLocation()); + var end = LspAdapter.toPosition(event.getSourceLocation()); + end.setCharacter(end.getCharacter() + 1); // Range is exclusive + + return new Range(start, end); + } + + default Diagnostic toDiagnostic(ValidationEvent event) { + var diagnosticSeverity = switch (event.getSeverity()) { + case ERROR, DANGER -> DiagnosticSeverity.Error; + case WARNING -> DiagnosticSeverity.Warning; + case NOTE -> DiagnosticSeverity.Information; + default -> DiagnosticSeverity.Hint; + }; + var diagnosticRange = getDiagnosticRange(event); + var message = event.getId() + ": " + event.getMessage(); + return new Diagnostic(diagnosticRange, message, diagnosticSeverity, "Smithy"); + } + } + + private record Simple() implements EventToDiagnostic {} + + private record Idl(StatementView view, DocumentParser parser) implements EventToDiagnostic { + @Override + public Range getDiagnosticRange(ValidationEvent event) { + Position eventStart = LspAdapter.toPosition(event.getSourceLocation()); + final Range defaultRange = EventToDiagnostic.super.getDiagnosticRange(event); + + if (event.getShapeId().isPresent()) { + int eventStartIndex = parser.getDocument().indexOfPosition(eventStart); + Syntax.Statement statement = view.getStatementAt(eventStartIndex).orElse(null); + + if (statement instanceof Syntax.Statement.MemberDef def + && event.containsId("Target") + && def.target() != null) { + Range targetRange = LspAdapter.identRange(def.target(), parser.getDocument()); + return Objects.requireNonNullElse(targetRange, defaultRange); + } else if (statement instanceof Syntax.Statement.TraitApplication app) { + Range traitIdRange = LspAdapter.identRange(app.id(), parser.getDocument()); + if (traitIdRange != null) { + traitIdRange.getStart().setCharacter(traitIdRange.getStart().getCharacter() - 1); // include @ + } + return Objects.requireNonNullElse(traitIdRange, defaultRange); + } + } + + return Objects.requireNonNullElse(parser.findContiguousRange(event.getSourceLocation()), defaultRange); + } + } } diff --git a/src/main/java/software/amazon/smithy/lsp/document/Document.java b/src/main/java/software/amazon/smithy/lsp/document/Document.java index 365af4c9..aba934f4 100644 --- a/src/main/java/software/amazon/smithy/lsp/document/Document.java +++ b/src/main/java/software/amazon/smithy/lsp/document/Document.java @@ -24,7 +24,7 @@ public final class Document { private final StringBuilder buffer; private int[] lineIndices; - private Document(StringBuilder buffer, int[] lineIndices, int changeVersion) { + private Document(StringBuilder buffer, int[] lineIndices) { this.buffer = buffer; this.lineIndices = lineIndices; } @@ -36,14 +36,14 @@ private Document(StringBuilder buffer, int[] lineIndices, int changeVersion) { public static Document of(String string) { StringBuilder buffer = new StringBuilder(string); int[] lineIndicies = computeLineIndicies(buffer); - return new Document(buffer, lineIndicies, 0); + return new Document(buffer, lineIndicies); } /** * @return A copy of this document */ public Document copy() { - return new Document(new StringBuilder(copyText()), lineIndices.clone(), 0); + return new Document(new StringBuilder(copyText()), lineIndices.clone()); } /** @@ -259,23 +259,6 @@ public int lastIndexOf(String s, int before) { return buffer.lastIndexOf(s, before); } - /** - * @param c The character to find the last index of - * @param before The index to stop the search at - * @param line The line to search within - * @return The index of the last occurrence of {@code c} before {@code before} - * on the line {@code line} or {@code -1} if one doesn't exist - */ - int lastIndexOfOnLine(char c, int before, int line) { - int lineIdx = indexOfLine(line); - for (int i = before; i >= lineIdx; i--) { - if (buffer.charAt(i) == c) { - return i; - } - } - return -1; - } - /** * @return A reference to the text in this document */ @@ -351,19 +334,6 @@ public CharBuffer borrowToken(Position position) { return CharBuffer.wrap(buffer, startIdx + 1, endIdx); } - /** - * @param position The position within the id to borrow - * @return A reference to the id that the given {@code position} is - * within, or {@code null} if the position is not within an id - */ - public CharBuffer borrowId(Position position) { - DocumentId id = copyDocumentId(position); - if (id == null) { - return null; - } - return id.idSlice(); - } - /** * @param line The line to borrow * @return A reference to the text in the given line, or {@code null} if @@ -422,32 +392,6 @@ public String copyRange(Range range) { return borrowed.toString(); } - /** - * @param position The position within the token to copy - * @return A copy of the token that the given {@code position} is within, - * or {@code null} if the position is not within a token - */ - public String copyToken(Position position) { - CharSequence token = borrowToken(position); - if (token == null) { - return null; - } - return token.toString(); - } - - /** - * @param position The position within the id to copy - * @return A copy of the id that the given {@code position} is - * within, or {@code null} if the position is not within an id - */ - public String copyId(Position position) { - CharBuffer id = borrowId(position); - if (id == null) { - return null; - } - return id.toString(); - } - /** * @param position The position within the id to get * @return A new id that the given {@code position} is @@ -546,19 +490,6 @@ private static boolean isIdChar(char c) { return Character.isLetterOrDigit(c) || c == '_' || c == '$' || c == '#' || c == '.'; } - /** - * @param line The line to copy - * @return A copy of the text in the given line, or {@code null} if the line - * doesn't exist - */ - public String copyLine(int line) { - CharBuffer borrowed = borrowLine(line); - if (borrowed == null) { - return null; - } - return borrowed.toString(); - } - /** * @param start The index of the start of the span to copy * @param end The index of the end of the span to copy @@ -580,18 +511,6 @@ public int length() { return buffer.length(); } - /** - * @param index The index to get the character at - * @return The character at the given index, or {@code \u0000} if one - * doesn't exist - */ - char charAt(int index) { - if (index < 0 || index >= length()) { - return '\u0000'; - } - return buffer.charAt(index); - } - // Adapted from String::split private static int[] computeLineIndicies(StringBuilder buffer) { int matchCount = 0; diff --git a/src/main/java/software/amazon/smithy/lsp/document/DocumentImports.java b/src/main/java/software/amazon/smithy/lsp/document/DocumentImports.java index 0c5d9c60..47eefd7e 100644 --- a/src/main/java/software/amazon/smithy/lsp/document/DocumentImports.java +++ b/src/main/java/software/amazon/smithy/lsp/document/DocumentImports.java @@ -7,6 +7,7 @@ import java.util.Set; import org.eclipse.lsp4j.Range; +import software.amazon.smithy.lsp.protocol.LspAdapter; /** * The imports of a document, including the range they occupy. @@ -14,4 +15,6 @@ * @param importsRange The range of the imports * @param imports The set of imported shape ids. They are not guaranteed to be valid shape ids */ -public record DocumentImports(Range importsRange, Set imports) {} +public record DocumentImports(Range importsRange, Set imports) { + static final DocumentImports EMPTY = new DocumentImports(LspAdapter.origin(), Set.of()); +} diff --git a/src/main/java/software/amazon/smithy/lsp/document/DocumentNamespace.java b/src/main/java/software/amazon/smithy/lsp/document/DocumentNamespace.java index 94c8b79b..d6e6ce39 100644 --- a/src/main/java/software/amazon/smithy/lsp/document/DocumentNamespace.java +++ b/src/main/java/software/amazon/smithy/lsp/document/DocumentNamespace.java @@ -6,6 +6,7 @@ package software.amazon.smithy.lsp.document; import org.eclipse.lsp4j.Range; +import software.amazon.smithy.lsp.protocol.LspAdapter; /** * The namespace of the document, including the range it occupies. @@ -13,4 +14,6 @@ * @param statementRange The range of the statement, including {@code namespace} * @param namespace The namespace of the document. Not guaranteed to be a valid namespace */ -public record DocumentNamespace(Range statementRange, CharSequence namespace) {} +public record DocumentNamespace(Range statementRange, String namespace) { + static final DocumentNamespace NONE = new DocumentNamespace(LspAdapter.origin(), ""); +} diff --git a/src/main/java/software/amazon/smithy/lsp/document/DocumentParser.java b/src/main/java/software/amazon/smithy/lsp/document/DocumentParser.java index 2299e2bf..6b322ee6 100644 --- a/src/main/java/software/amazon/smithy/lsp/document/DocumentParser.java +++ b/src/main/java/software/amazon/smithy/lsp/document/DocumentParser.java @@ -5,16 +5,13 @@ package software.amazon.smithy.lsp.document; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; import software.amazon.smithy.lsp.protocol.LspAdapter; import software.amazon.smithy.lsp.syntax.Syntax; -import software.amazon.smithy.lsp.syntax.SyntaxSearch; import software.amazon.smithy.model.SourceLocation; import software.amazon.smithy.utils.SimpleParser; @@ -34,16 +31,9 @@ private DocumentParser(Document document, List statements) { } static DocumentParser of(String text) { - return DocumentParser.forDocument(Document.of(text)); - } - - /** - * @param document Document to create a parser for - * @return A parser for the given document - */ - public static DocumentParser forDocument(Document document) { - Syntax.IdlParse parse = Syntax.parseIdl(document); - return new DocumentParser(document, parse.statements()); + Document document = Document.of(text); + Syntax.IdlParseResult parse = Syntax.parseIdl(document); + return DocumentParser.forStatements(document, parse.statements()); } /** @@ -56,23 +46,23 @@ public static DocumentParser forStatements(Document document, List imports; @@ -80,116 +70,47 @@ public DocumentImports documentImports() { Syntax.Statement statement = statements.get(i); if (statement instanceof Syntax.Statement.Use firstUse) { imports = new HashSet<>(); - imports.add(firstUse.use().copyValueFrom(document)); - Range useRange = firstUse.rangeIn(document); + imports.add(firstUse.use().stringValue()); + Range useRange = document.rangeBetween(firstUse.start(), firstUse.end()); Position start = useRange.getStart(); Position end = useRange.getEnd(); i++; while (i < statements.size()) { statement = statements.get(i); if (statement instanceof Syntax.Statement.Use use) { - imports.add(use.use().copyValueFrom(document)); - end = use.rangeIn(document).getEnd(); + imports.add(use.use().stringValue()); + end = document.rangeBetween(use.start(), use.end()).getEnd(); i++; } else { break; } } return new DocumentImports(new Range(start, end), imports); + } else if (statement instanceof Syntax.Statement.ShapeDef) { + break; } } - return null; - } - - /** - * @return A map of start position to {@link DocumentShape} for each shape - * and/or shape reference in the document. - */ - public Map documentShapes() { - Map documentShapes = new HashMap<>(); - for (Syntax.Statement statement : statements) { - switch (statement) { - case Syntax.Statement.ShapeDef shapeDef -> { - String shapeName = shapeDef.shapeName().copyValueFrom(document); - Range range = shapeDef.shapeName().rangeIn(document); - var shape = new DocumentShape(range, shapeName, DocumentShape.Kind.DefinedShape, null); - documentShapes.put(range.getStart(), shape); - } - case Syntax.Statement.MemberDef memberDef -> { - String shapeName = memberDef.name().copyValueFrom(document); - Range range = memberDef.name().rangeIn(document); - DocumentShape target = null; - if (memberDef.target() != null && !memberDef.target().isEmpty()) { - String targetName = memberDef.target().copyValueFrom(document); - Range targetRange = memberDef.target().rangeIn(document); - target = new DocumentShape(targetRange, targetName, DocumentShape.Kind.Targeted, null); - documentShapes.put(targetRange.getStart(), target); - } - var shape = new DocumentShape(range, shapeName, DocumentShape.Kind.DefinedMember, target); - documentShapes.put(range.getStart(), shape); - } - case Syntax.Statement.ElidedMemberDef elidedMemberDef -> { - String shapeName = elidedMemberDef.name().copyValueFrom(document); - Range range = elidedMemberDef.rangeIn(document); - var shape = new DocumentShape(range, shapeName, DocumentShape.Kind.Elided, null); - documentShapes.put(range.getStart(), shape); - } - case Syntax.Statement.EnumMemberDef enumMemberDef -> { - String shapeName = enumMemberDef.name().copyValueFrom(document); - Range range = enumMemberDef.rangeIn(document); - var shape = new DocumentShape(range, shapeName, DocumentShape.Kind.DefinedMember, null); - documentShapes.put(range.getStart(), shape); - } - default -> { - } - } - } - return documentShapes; + return DocumentImports.EMPTY; } /** - * @return The {@link DocumentVersion} for the underlying document, or - * {@code null} if it couldn't be found + * @return The {@link DocumentVersion} for the underlying document. */ public DocumentVersion documentVersion() { for (Syntax.Statement statement : statements) { if (statement instanceof Syntax.Statement.Control control && control.value() instanceof Syntax.Node.Str str) { - String key = control.key().copyValueFrom(document); + String key = control.key().stringValue(); if (key.equals("version")) { - String version = str.copyValueFrom(document); - Range range = control.rangeIn(document); + String version = str.stringValue(); + Range range = document.rangeBetween(control.start(), control.end()); return new DocumentVersion(range, version); } } else if (statement instanceof Syntax.Statement.Namespace) { break; } } - return null; - } - - /** - * @param sourceLocation The source location of the start of the trait - * application. The filename must be the same as - * the underlying document's (this is not checked), - * and the position must be on the {@code @} - * @return The range of the trait id from the {@code @} up to the trait's - * body or end, or null if the {@code sourceLocation} isn't on an {@code @} - * or there's no id next to the {@code @} - */ - public Range traitIdRange(SourceLocation sourceLocation) { - int position = document.indexOfPosition(LspAdapter.toPosition(sourceLocation)); - int statementIndex = SyntaxSearch.statementIndex(statements, position); - if (statementIndex < 0) { - return null; - } - - if (statements.get(statementIndex) instanceof Syntax.Statement.TraitApplication traitApplication) { - Range range = traitApplication.id().rangeIn(document); - range.getStart().setCharacter(range.getStart().getCharacter() - 1); // include @ - return range; - } - return null; + return DocumentVersion.EMPTY; } /** diff --git a/src/main/java/software/amazon/smithy/lsp/document/DocumentShape.java b/src/main/java/software/amazon/smithy/lsp/document/DocumentShape.java deleted file mode 100644 index 1fe748e1..00000000 --- a/src/main/java/software/amazon/smithy/lsp/document/DocumentShape.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.lsp.document; - -import org.eclipse.lsp4j.Range; - -/** - * A Shape definition OR reference within a document, including the range it occupies. - * - *

Shapes can be defined/referenced in various ways within a Smithy file, each - * corresponding to a specific {@link Kind}. For each kind, the range spans the - * shape name/id only. - */ -public record DocumentShape( - Range range, - CharSequence shapeName, - Kind kind, - DocumentShape targetReference -) { - public boolean isKind(Kind other) { - return this.kind.equals(other); - } - - public boolean hasMemberTarget() { - return isKind(Kind.DefinedMember) && targetReference() != null; - } - - /** - * The different kinds of {@link DocumentShape}s that can exist, corresponding to places - * that a shape definition or reference may appear. This is non-exhaustive (for now). - */ - public enum Kind { - DefinedShape, - DefinedMember, - Elided, - Targeted, - Inline - } -} diff --git a/src/main/java/software/amazon/smithy/lsp/document/DocumentVersion.java b/src/main/java/software/amazon/smithy/lsp/document/DocumentVersion.java index da710cc3..a64512bb 100644 --- a/src/main/java/software/amazon/smithy/lsp/document/DocumentVersion.java +++ b/src/main/java/software/amazon/smithy/lsp/document/DocumentVersion.java @@ -6,6 +6,7 @@ package software.amazon.smithy.lsp.document; import org.eclipse.lsp4j.Range; +import software.amazon.smithy.lsp.protocol.LspAdapter; /** * The Smithy version of the document, including the range it occupies. @@ -13,4 +14,6 @@ * @param range The range of the version statement * @param version The literal text of the version value */ -public record DocumentVersion(Range range, String version) {} +public record DocumentVersion(Range range, String version) { + static final DocumentVersion EMPTY = new DocumentVersion(LspAdapter.origin(), ""); +} diff --git a/src/main/java/software/amazon/smithy/lsp/language/CompleterContext.java b/src/main/java/software/amazon/smithy/lsp/language/CompleterContext.java new file mode 100644 index 00000000..125356ea --- /dev/null +++ b/src/main/java/software/amazon/smithy/lsp/language/CompleterContext.java @@ -0,0 +1,92 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.lsp.language; + +import java.util.Set; +import org.eclipse.lsp4j.CompletionItemKind; +import org.eclipse.lsp4j.Range; +import software.amazon.smithy.lsp.document.DocumentId; +import software.amazon.smithy.lsp.project.Project; + +/** + * Simple POJO capturing common properties that completers need. + */ +final class CompleterContext { + private final String matchToken; + private final Range insertRange; + private final Project project; + private Set exclude = Set.of(); + private CompletionItemKind literalKind = CompletionItemKind.Field; + + private CompleterContext(String matchToken, Range insertRange, Project project) { + this.matchToken = matchToken; + this.insertRange = insertRange; + this.project = project; + } + + /** + * @param id The id at the cursor position. + * @param insertRange The range to insert completion text in. + * @param project The project the completion was triggered in. + * @return A new completer context. + */ + static CompleterContext create(DocumentId id, Range insertRange, Project project) { + String matchToken = getMatchToken(id); + return new CompleterContext(matchToken, insertRange, project); + } + + private static String getMatchToken(DocumentId id) { + return id != null + ? id.copyIdValue().toLowerCase() + : ""; + } + + /** + * @return The token to match candidates against. + */ + String matchToken() { + return matchToken; + } + + /** + * @return The range to insert completion text. + */ + Range insertRange() { + return insertRange; + } + + /** + * @return The project the completion was triggered in. + */ + Project project() { + return project; + } + + /** + * @return The set of tokens to exclude. + */ + Set exclude() { + return exclude; + } + + CompleterContext withExclude(Set exclude) { + this.exclude = exclude; + return this; + } + + /** + * @return The kind of completion to use for {@link CompletionCandidates.Literals}, + * which will be displayed in the client. + */ + CompletionItemKind literalKind() { + return literalKind; + } + + CompleterContext withLiteralKind(CompletionItemKind literalKind) { + this.literalKind = literalKind; + return this; + } +} diff --git a/src/main/java/software/amazon/smithy/lsp/language/CompletionCandidates.java b/src/main/java/software/amazon/smithy/lsp/language/CompletionCandidates.java index c18e3280..44b2fa8b 100644 --- a/src/main/java/software/amazon/smithy/lsp/language/CompletionCandidates.java +++ b/src/main/java/software/amazon/smithy/lsp/language/CompletionCandidates.java @@ -41,8 +41,6 @@ sealed interface CompletionCandidates { "list", "map", "structure", "union", "service", "resource", "operation", "apply")); - // TODO: Maybe BUILTIN_CONTROLS and BUILTIN_METADATA should be regular - // Labeled/Members, with custom mappers. Literals BUILTIN_CONTROLS = new Literals(Builtins.CONTROL.members().stream() .map(member -> "$" + member.getMemberName() + ": " + defaultCandidates(member).value()) .toList()); diff --git a/src/main/java/software/amazon/smithy/lsp/language/CompletionHandler.java b/src/main/java/software/amazon/smithy/lsp/language/CompletionHandler.java index f43d1bc6..48fc881e 100644 --- a/src/main/java/software/amazon/smithy/lsp/language/CompletionHandler.java +++ b/src/main/java/software/amazon/smithy/lsp/language/CompletionHandler.java @@ -18,12 +18,11 @@ import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.jsonrpc.CancelChecker; import software.amazon.smithy.lsp.document.DocumentId; +import software.amazon.smithy.lsp.project.IdlFile; import software.amazon.smithy.lsp.project.Project; -import software.amazon.smithy.lsp.project.SmithyFile; import software.amazon.smithy.lsp.protocol.LspAdapter; -import software.amazon.smithy.lsp.syntax.NodeCursor; +import software.amazon.smithy.lsp.syntax.StatementView; import software.amazon.smithy.lsp.syntax.Syntax; -import software.amazon.smithy.lsp.syntax.SyntaxSearch; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.StructureShape; @@ -32,9 +31,9 @@ */ public final class CompletionHandler { private final Project project; - private final SmithyFile smithyFile; + private final IdlFile smithyFile; - public CompletionHandler(Project project, SmithyFile smithyFile) { + public CompletionHandler(Project project, IdlFile smithyFile) { this.project = project; this.smithyFile = smithyFile; } @@ -60,40 +59,40 @@ public List handle(CompletionParams params, CancelChecker cc) { return Collections.emptyList(); } - IdlPosition idlPosition = IdlPosition.at(smithyFile, position).orElse(null); + Syntax.IdlParseResult parseResult = smithyFile.getParse(); + int documentIndex = smithyFile.document().indexOfPosition(position); + IdlPosition idlPosition = StatementView.createAt(parseResult, documentIndex) + .map(IdlPosition::of) + .orElse(null); if (cc.isCanceled() || idlPosition == null) { return Collections.emptyList(); } - SimpleCompletions.Builder builder = SimpleCompletions.builder(id, insertRange).project(project); + CompleterContext context = CompleterContext.create(id, insertRange, project); return switch (idlPosition) { - case IdlPosition.ControlKey ignored -> builder - .literalKind(CompletionItemKind.Constant) - .buildSimpleCompletions() - .getCompletionItems(CompletionCandidates.BUILTIN_CONTROLS); + case IdlPosition.ControlKey ignored -> + new SimpleCompleter(context.withLiteralKind(CompletionItemKind.Constant)) + .getCompletionItems(CompletionCandidates.BUILTIN_CONTROLS); - case IdlPosition.MetadataKey ignored -> builder - .literalKind(CompletionItemKind.Field) - .buildSimpleCompletions() - .getCompletionItems(CompletionCandidates.BUILTIN_METADATA); + case IdlPosition.MetadataKey ignored -> + new SimpleCompleter(context.withLiteralKind(CompletionItemKind.Field)) + .getCompletionItems(CompletionCandidates.BUILTIN_METADATA); - case IdlPosition.StatementKeyword ignored -> builder - .literalKind(CompletionItemKind.Keyword) - .buildSimpleCompletions() - .getCompletionItems(CompletionCandidates.KEYWORD); + case IdlPosition.StatementKeyword ignored -> + new SimpleCompleter(context.withLiteralKind(CompletionItemKind.Keyword)) + .getCompletionItems(CompletionCandidates.KEYWORD); - case IdlPosition.Namespace ignored -> builder - .literalKind(CompletionItemKind.Module) - .buildSimpleCompletions() - .getCompletionItems(CompletionCandidates.Custom.PROJECT_NAMESPACES); + case IdlPosition.Namespace ignored -> + new SimpleCompleter(context.withLiteralKind(CompletionItemKind.Module)) + .getCompletionItems(CompletionCandidates.Custom.PROJECT_NAMESPACES); - case IdlPosition.MetadataValue metadataValue -> metadataValueCompletions(metadataValue, builder); + case IdlPosition.MetadataValue metadataValue -> metadataValueCompletions(metadataValue, context); - case IdlPosition.MemberName memberName -> memberNameCompletions(memberName, builder); + case IdlPosition.MemberName memberName -> memberNameCompletions(memberName, context); - default -> modelBasedCompletions(idlPosition, builder); + default -> modelBasedCompletions(idlPosition, context); }; } @@ -110,7 +109,6 @@ private static Position getTokenPosition(CompletionParams params) { private static Range getInsertRange(DocumentId id, Position position) { if (id == null || id.idSlice().isEmpty()) { - // TODO: This is confusing // When we receive the completion request, we're always on the // character either after what has just been typed, or we're in // empty space and have manually triggered a completion. To account @@ -125,45 +123,15 @@ private static Range getInsertRange(DocumentId id, Position position) { private List metadataValueCompletions( IdlPosition.MetadataValue metadataValue, - SimpleCompletions.Builder builder + CompleterContext context ) { var result = ShapeSearch.searchMetadataValue(metadataValue); - Set excludeKeys = getOtherPresentKeys(result); + Set excludeKeys = result.getOtherPresentKeys(); CompletionCandidates candidates = CompletionCandidates.fromSearchResult(result); - return builder.exclude(excludeKeys).buildSimpleCompletions().getCompletionItems(candidates); + return new SimpleCompleter(context.withExclude(excludeKeys)).getCompletionItems(candidates); } - private Set getOtherPresentKeys(NodeSearch.Result result) { - Syntax.Node.Kvps terminalContainer; - NodeCursor.Key terminalKey; - switch (result) { - case NodeSearch.Result.ObjectShape obj -> { - terminalContainer = obj.node(); - terminalKey = null; - } - case NodeSearch.Result.ObjectKey key -> { - terminalContainer = key.key().parent(); - terminalKey = key.key(); - } - default -> { - return null; - } - } - - Set ignoreKeys = new HashSet<>(); - terminalContainer.kvps().forEach(kvp -> { - String key = kvp.key().copyValueFrom(smithyFile.document()); - ignoreKeys.add(key); - }); - - if (terminalKey != null) { - ignoreKeys.remove(terminalKey.name()); - } - - return ignoreKeys; - } - - private List modelBasedCompletions(IdlPosition idlPosition, SimpleCompletions.Builder builder) { + private List modelBasedCompletions(IdlPosition idlPosition, CompleterContext context) { if (project.modelResult().getResult().isEmpty()) { return List.of(); } @@ -171,16 +139,16 @@ private List modelBasedCompletions(IdlPosition idlPosition, Simp Model model = project.modelResult().getResult().get(); if (idlPosition instanceof IdlPosition.ElidedMember elidedMember) { - return elidedMemberCompletions(elidedMember, model, builder); + return elidedMemberCompletions(elidedMember, context, model); } else if (idlPosition instanceof IdlPosition.TraitValue traitValue) { - return traitValueCompletions(traitValue, model, builder); + return traitValueCompletions(traitValue, context, model); } CompletionCandidates candidates = CompletionCandidates.shapeCandidates(idlPosition); if (candidates instanceof CompletionCandidates.Shapes shapes) { - return builder.buildShapeCompletions(idlPosition, model).getCompletionItems(shapes); + return new ShapeCompleter(idlPosition, model, context).getCompletionItems(shapes); } else if (candidates != CompletionCandidates.NONE) { - return builder.buildSimpleCompletions().getCompletionItems(candidates); + return new SimpleCompleter(context).getCompletionItems(candidates); } return List.of(); @@ -188,45 +156,37 @@ private List modelBasedCompletions(IdlPosition idlPosition, Simp private List elidedMemberCompletions( IdlPosition.ElidedMember elidedMember, - Model model, - SimpleCompletions.Builder builder + CompleterContext context, + Model model ) { - CompletionCandidates candidates = getElidableMemberCandidates(elidedMember.statementIndex(), model); + CompletionCandidates candidates = getElidableMemberCandidates(elidedMember, model); if (candidates == null) { return List.of(); } - Set otherMembers = SyntaxSearch.otherMemberNames( - elidedMember.smithyFile().document(), - elidedMember.smithyFile().statements(), - elidedMember.statementIndex()); - return builder.exclude(otherMembers).buildSimpleCompletions().getCompletionItems(candidates); + Set otherMembers = elidedMember.view().otherMemberNames(); + return new SimpleCompleter(context.withExclude(otherMembers)).getCompletionItems(candidates); } private List traitValueCompletions( IdlPosition.TraitValue traitValue, - Model model, - SimpleCompletions.Builder builder + CompleterContext context, + Model model ) { var result = ShapeSearch.searchTraitValue(traitValue, model); - Set excludeKeys = getOtherPresentKeys(result); + Set excludeKeys = result.getOtherPresentKeys(); CompletionCandidates candidates = CompletionCandidates.fromSearchResult(result); - return builder.exclude(excludeKeys).buildSimpleCompletions().getCompletionItems(candidates); + return new SimpleCompleter(context.withExclude(excludeKeys)).getCompletionItems(candidates); } - private List memberNameCompletions( - IdlPosition.MemberName memberName, - SimpleCompletions.Builder builder - ) { - Syntax.Statement.ShapeDef shapeDef = SyntaxSearch.closestShapeDefBeforeMember( - smithyFile.statements(), - memberName.statementIndex()); + private List memberNameCompletions(IdlPosition.MemberName memberName, CompleterContext context) { + Syntax.Statement.ShapeDef shapeDef = memberName.view().nearestShapeDefBefore(); if (shapeDef == null) { return List.of(); } - String shapeType = shapeDef.shapeType().copyValueFrom(smithyFile.document()); + String shapeType = shapeDef.shapeType().stringValue(); StructureShape shapeMembersDef = Builtins.getMembersForShapeType(shapeType); CompletionCandidates candidates = null; @@ -236,7 +196,7 @@ private List memberNameCompletions( if (project.modelResult().getResult().isPresent()) { CompletionCandidates elidedCandidates = getElidableMemberCandidates( - memberName.statementIndex(), + memberName, project.modelResult().getResult().get()); if (elidedCandidates != null) { @@ -250,27 +210,20 @@ private List memberNameCompletions( return List.of(); } - Set otherMembers = SyntaxSearch.otherMemberNames( - smithyFile.document(), - smithyFile.statements(), - memberName.statementIndex()); - return builder.exclude(otherMembers).buildSimpleCompletions().getCompletionItems(candidates); + Set otherMembers = memberName.view().otherMemberNames(); + return new SimpleCompleter(context.withExclude(otherMembers)).getCompletionItems(candidates); } - private CompletionCandidates getElidableMemberCandidates(int statementIndex, Model model) { - var resourceAndMixins = ShapeSearch.findForResourceAndMixins( - SyntaxSearch.closestForResourceAndMixinsBeforeMember(smithyFile.statements(), statementIndex), - smithyFile, - model); - + private CompletionCandidates getElidableMemberCandidates(IdlPosition idlPosition, Model model) { Set memberNames = new HashSet<>(); - if (resourceAndMixins.resource() != null) { - memberNames.addAll(resourceAndMixins.resource().getIdentifiers().keySet()); - memberNames.addAll(resourceAndMixins.resource().getProperties().keySet()); - } - - resourceAndMixins.mixins() + var forResourceAndMixins = idlPosition.view().nearestForResourceAndMixinsBefore(); + ShapeSearch.findResource(forResourceAndMixins.forResource(), idlPosition.view(), model) + .ifPresent(resourceShape -> { + memberNames.addAll(resourceShape.getIdentifiers().keySet()); + memberNames.addAll(resourceShape.getProperties().keySet()); + }); + ShapeSearch.findMixins(forResourceAndMixins.mixins(), idlPosition.view(), model) .forEach(mixinShape -> memberNames.addAll(mixinShape.getMemberNames())); if (memberNames.isEmpty()) { diff --git a/src/main/java/software/amazon/smithy/lsp/language/DefinitionHandler.java b/src/main/java/software/amazon/smithy/lsp/language/DefinitionHandler.java index 9986f1f4..30e066fd 100644 --- a/src/main/java/software/amazon/smithy/lsp/language/DefinitionHandler.java +++ b/src/main/java/software/amazon/smithy/lsp/language/DefinitionHandler.java @@ -12,9 +12,11 @@ import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.Position; import software.amazon.smithy.lsp.document.DocumentId; +import software.amazon.smithy.lsp.project.IdlFile; import software.amazon.smithy.lsp.project.Project; -import software.amazon.smithy.lsp.project.SmithyFile; import software.amazon.smithy.lsp.protocol.LspAdapter; +import software.amazon.smithy.lsp.syntax.StatementView; +import software.amazon.smithy.lsp.syntax.Syntax; import software.amazon.smithy.model.Model; /** @@ -22,9 +24,9 @@ */ public final class DefinitionHandler { final Project project; - final SmithyFile smithyFile; + final IdlFile smithyFile; - public DefinitionHandler(Project project, SmithyFile smithyFile) { + public DefinitionHandler(Project project, IdlFile smithyFile) { this.project = project; this.smithyFile = smithyFile; } @@ -46,8 +48,11 @@ public List handle(DefinitionParams params) { } Model model = modelResult.get(); - return IdlPosition.at(smithyFile, position) - .flatMap(idlPosition -> ShapeSearch.findShapeDefinition(idlPosition, model, id)) + Syntax.IdlParseResult parseResult = smithyFile.getParse(); + int documentIndex = smithyFile.document().indexOfPosition(position); + return StatementView.createAt(parseResult, documentIndex) + .map(IdlPosition::of) + .flatMap(idlPosition -> ShapeSearch.findShapeDefinition(idlPosition, id, model)) .map(LspAdapter::toLocation) .map(List::of) .orElse(List.of()); diff --git a/src/main/java/software/amazon/smithy/lsp/language/DocumentSymbolHandler.java b/src/main/java/software/amazon/smithy/lsp/language/DocumentSymbolHandler.java new file mode 100644 index 00000000..7aa47fa0 --- /dev/null +++ b/src/main/java/software/amazon/smithy/lsp/language/DocumentSymbolHandler.java @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.lsp.language; + +import java.util.List; +import java.util.function.Consumer; +import org.eclipse.lsp4j.DocumentSymbol; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.SymbolKind; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import software.amazon.smithy.lsp.document.Document; +import software.amazon.smithy.lsp.protocol.LspAdapter; +import software.amazon.smithy.lsp.syntax.Syntax; + +public record DocumentSymbolHandler(Document document, List statements) { + /** + * @return A list of DocumentSymbol + */ + public List> handle() { + return statements.stream() + .mapMulti(this::addSymbols) + .toList(); + } + + private void addSymbols(Syntax.Statement statement, Consumer> consumer) { + switch (statement) { + case Syntax.Statement.TraitApplication app -> addSymbol(consumer, app.id(), SymbolKind.Class); + + case Syntax.Statement.ShapeDef def -> addSymbol(consumer, def.shapeName(), SymbolKind.Class); + + case Syntax.Statement.EnumMemberDef def -> addSymbol(consumer, def.name(), SymbolKind.Enum); + + case Syntax.Statement.ElidedMemberDef def -> addSymbol(consumer, def.name(), SymbolKind.Property); + + case Syntax.Statement.MemberDef def -> { + addSymbol(consumer, def.name(), SymbolKind.Property); + if (def.target() != null) { + addSymbol(consumer, def.target(), SymbolKind.Class); + } + } + default -> { + } + } + } + + private void addSymbol( + Consumer> consumer, + Syntax.Ident ident, + SymbolKind symbolKind + ) { + Range range = LspAdapter.identRange(ident, document); + if (range == null) { + return; + } + + DocumentSymbol symbol = new DocumentSymbol(ident.stringValue(), symbolKind, range, range); + consumer.accept(Either.forRight(symbol)); + } +} diff --git a/src/main/java/software/amazon/smithy/lsp/language/DynamicMemberTarget.java b/src/main/java/software/amazon/smithy/lsp/language/DynamicMemberTarget.java index e133c21b..70082804 100644 --- a/src/main/java/software/amazon/smithy/lsp/language/DynamicMemberTarget.java +++ b/src/main/java/software/amazon/smithy/lsp/language/DynamicMemberTarget.java @@ -6,8 +6,6 @@ package software.amazon.smithy.lsp.language; import java.util.Map; -import software.amazon.smithy.lsp.document.Document; -import software.amazon.smithy.lsp.project.SmithyFile; import software.amazon.smithy.lsp.syntax.NodeCursor; import software.amazon.smithy.lsp.syntax.Syntax; import software.amazon.smithy.model.Model; @@ -38,13 +36,13 @@ sealed interface DynamicMemberTarget { Shape getTarget(NodeCursor cursor, Model model); static Map forTrait(Shape traitShape, IdlPosition.TraitValue traitValue) { - SmithyFile smithyFile = traitValue.smithyFile(); + Syntax.IdlParseResult syntaxInfo = traitValue.view().parseResult(); return switch (traitShape.getId().toString()) { case "smithy.test#smokeTests" -> Map.of( ShapeId.from("smithy.test#SmokeTestCase$params"), new OperationInput(traitValue), ShapeId.from("smithy.test#SmokeTestCase$vendorParams"), - new ShapeIdDependent("vendorParamsShape", smithyFile)); + new ShapeIdDependent("vendorParamsShape", syntaxInfo)); case "smithy.api#examples" -> Map.of( ShapeId.from("smithy.api#Example$input"), @@ -56,24 +54,23 @@ static Map forTrait(Shape traitShape, IdlPosition. ShapeId.from("smithy.test#HttpRequestTestCase$params"), new OperationInput(traitValue), ShapeId.from("smithy.test#HttpRequestTestCase$vendorParams"), - new ShapeIdDependent("vendorParamsShape", smithyFile)); + new ShapeIdDependent("vendorParamsShape", syntaxInfo)); case "smithy.test#httpResponseTests" -> Map.of( ShapeId.from("smithy.test#HttpResponseTestCase$params"), new OperationOutput(traitValue), ShapeId.from("smithy.test#HttpResponseTestCase$vendorParams"), - new ShapeIdDependent("vendorParamsShape", smithyFile)); + new ShapeIdDependent("vendorParamsShape", syntaxInfo)); default -> null; }; } - static Map forMetadata(String metadataKey, SmithyFile smithyFile) { + static Map forMetadata(String metadataKey) { return switch (metadataKey) { case "validators" -> Map.of( ShapeId.from("smithy.lang.server#Validator$configuration"), new MappedDependent( "name", - smithyFile.document(), Builtins.VALIDATOR_CONFIG_MAPPING)); default -> null; }; @@ -116,15 +113,15 @@ public Shape getTarget(NodeCursor cursor, Model model) { * using that as the id of the target shape. * * @param memberName The name of the other member to compute the value of. - * @param smithyFile The file the node is within. + * @param parseResult The parse result of the file the node is within. */ - record ShapeIdDependent(String memberName, SmithyFile smithyFile) implements DynamicMemberTarget { + record ShapeIdDependent(String memberName, Syntax.IdlParseResult parseResult) implements DynamicMemberTarget { @Override public Shape getTarget(NodeCursor cursor, Model model) { - Syntax.Node.Kvp matchingKvp = findMatchingKvp(memberName, cursor, smithyFile.document()); - if (matchingKvp.value() instanceof Syntax.Node.Str str) { - String id = str.copyValueFrom(smithyFile.document()); - return ShapeSearch.findShape(smithyFile, model, id).orElse(null); + Syntax.Node.Kvp matchingKvp = findMatchingKvp(memberName, cursor); + if (matchingKvp != null && matchingKvp.value() instanceof Syntax.Node.Str str) { + String id = str.stringValue(); + return ShapeSearch.findShape(parseResult, id, model).orElse(null); } return null; } @@ -136,17 +133,15 @@ public Shape getTarget(NodeCursor cursor, Model model) { * value. * * @param memberName The name of the member to compute the value of. - * @param document The document the node is within. * @param mapping A mapping of {@code memberName} values to corresponding * member target ids. */ - record MappedDependent(String memberName, Document document, Map mapping) - implements DynamicMemberTarget { + record MappedDependent(String memberName, Map mapping) implements DynamicMemberTarget { @Override public Shape getTarget(NodeCursor cursor, Model model) { - Syntax.Node.Kvp matchingKvp = findMatchingKvp(memberName, cursor, document); - if (matchingKvp.value() instanceof Syntax.Node.Str str) { - String value = str.copyValueFrom(document); + Syntax.Node.Kvp matchingKvp = findMatchingKvp(memberName, cursor); + if (matchingKvp != null && matchingKvp.value() instanceof Syntax.Node.Str str) { + String value = str.stringValue(); ShapeId targetId = mapping.get(value); if (targetId != null) { return model.getShape(targetId).orElse(null); @@ -160,7 +155,7 @@ public Shape getTarget(NodeCursor cursor, Model model) { // comparison to parsing or NodeCursor construction, which are optimized for // speed and memory usage (instead of key lookup), and the number of keys // is assumed to be low in most cases. - private static Syntax.Node.Kvp findMatchingKvp(String keyName, NodeCursor cursor, Document document) { + private static Syntax.Node.Kvp findMatchingKvp(String keyName, NodeCursor cursor) { // This will be called after skipping a ValueForKey, so that will be previous if (!cursor.hasPrevious()) { // TODO: Log @@ -169,7 +164,7 @@ private static Syntax.Node.Kvp findMatchingKvp(String keyName, NodeCursor cursor NodeCursor.Edge edge = cursor.previous(); if (edge instanceof NodeCursor.ValueForKey(var ignored, Syntax.Node.Kvps parent)) { for (Syntax.Node.Kvp kvp : parent.kvps()) { - String key = kvp.key().copyValueFrom(document); + String key = kvp.key().stringValue(); if (!keyName.equals(key)) { continue; } diff --git a/src/main/java/software/amazon/smithy/lsp/language/HoverHandler.java b/src/main/java/software/amazon/smithy/lsp/language/HoverHandler.java index cabdae6b..b7d72b1f 100644 --- a/src/main/java/software/amazon/smithy/lsp/language/HoverHandler.java +++ b/src/main/java/software/amazon/smithy/lsp/language/HoverHandler.java @@ -16,9 +16,11 @@ import org.eclipse.lsp4j.MarkupContent; import org.eclipse.lsp4j.Position; import software.amazon.smithy.lsp.document.DocumentId; +import software.amazon.smithy.lsp.project.IdlFile; import software.amazon.smithy.lsp.project.Project; -import software.amazon.smithy.lsp.project.SmithyFile; import software.amazon.smithy.lsp.syntax.NodeCursor; +import software.amazon.smithy.lsp.syntax.StatementView; +import software.amazon.smithy.lsp.syntax.Syntax; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.shapes.MemberShape; @@ -41,7 +43,7 @@ public final class HoverHandler { public static final Hover EMPTY = new Hover(new MarkupContent("markdown", "")); private final Project project; - private final SmithyFile smithyFile; + private final IdlFile smithyFile; private final Severity minimumSeverity; /** @@ -49,7 +51,7 @@ public final class HoverHandler { * @param smithyFile Smithy file the hover is in * @param minimumSeverity Minimum severity of validation events to show */ - public HoverHandler(Project project, SmithyFile smithyFile, Severity minimumSeverity) { + public HoverHandler(Project project, IdlFile smithyFile, Severity minimumSeverity) { this.project = project; this.smithyFile = smithyFile; this.minimumSeverity = minimumSeverity; @@ -66,7 +68,11 @@ public Hover handle(HoverParams params) { return EMPTY; } - IdlPosition idlPosition = IdlPosition.at(smithyFile, position).orElse(null); + Syntax.IdlParseResult parseResult = smithyFile.getParse(); + int documentIndex = smithyFile.document().indexOfPosition(position); + IdlPosition idlPosition = StatementView.createAt(parseResult, documentIndex) + .map(IdlPosition::of) + .orElse(null); return switch (idlPosition) { case IdlPosition.ControlKey ignored -> Builtins.CONTROL.getMember(id.copyIdValueForElidedMember()) @@ -110,10 +116,10 @@ private Hover modelSensitiveHover(DocumentId id, IdlPosition idlPosition) { Optional matchingShape = switch (idlPosition) { // TODO: Handle resource ids and properties. This only works for mixins right now. case IdlPosition.ElidedMember elidedMember -> - ShapeSearch.findElidedMemberParent(elidedMember, model, id) + ShapeSearch.findElidedMemberParent(elidedMember, id, model) .flatMap(shape -> shape.getMember(id.copyIdValueForElidedMember())); - default -> ShapeSearch.findShapeDefinition(idlPosition, model, id); + default -> ShapeSearch.findShapeDefinition(idlPosition, id, model); }; if (matchingShape.isEmpty()) { diff --git a/src/main/java/software/amazon/smithy/lsp/language/IdlPosition.java b/src/main/java/software/amazon/smithy/lsp/language/IdlPosition.java index 84b9d6eb..40f36008 100644 --- a/src/main/java/software/amazon/smithy/lsp/language/IdlPosition.java +++ b/src/main/java/software/amazon/smithy/lsp/language/IdlPosition.java @@ -5,12 +5,12 @@ package software.amazon.smithy.lsp.language; -import java.util.Optional; -import org.eclipse.lsp4j.Position; -import software.amazon.smithy.lsp.project.SmithyFile; +import software.amazon.smithy.lsp.syntax.StatementView; import software.amazon.smithy.lsp.syntax.Syntax; -import software.amazon.smithy.lsp.syntax.SyntaxSearch; +/** + * Represents different kinds of positions within an IDL file. + */ sealed interface IdlPosition { default boolean isEasyShapeReference() { return switch (this) { @@ -25,128 +25,97 @@ default boolean isEasyShapeReference() { }; } - SmithyFile smithyFile(); - - record TraitId(SmithyFile smithyFile) implements IdlPosition {} + StatementView view(); - record MemberTarget(SmithyFile smithyFile) implements IdlPosition {} + record TraitId(StatementView view) implements IdlPosition {} - record ShapeDef(SmithyFile smithyFile) implements IdlPosition {} + record MemberTarget(StatementView view) implements IdlPosition {} - record Mixin(SmithyFile smithyFile) implements IdlPosition {} + record ShapeDef(StatementView view) implements IdlPosition {} - record ApplyTarget(SmithyFile smithyFile) implements IdlPosition {} + record Mixin(StatementView view) implements IdlPosition {} - record UseTarget(SmithyFile smithyFile) implements IdlPosition {} + record ApplyTarget(StatementView view) implements IdlPosition {} - record Namespace(SmithyFile smithyFile) implements IdlPosition {} + record UseTarget(StatementView view) implements IdlPosition {} - record TraitValue( - int documentIndex, - int statementIndex, - Syntax.Statement.TraitApplication traitApplication, - SmithyFile smithyFile - ) implements IdlPosition {} + record Namespace(StatementView view) implements IdlPosition {} - record NodeMemberTarget( - int documentIndex, - int statementIndex, - Syntax.Statement.NodeMemberDef nodeMemberDef, - SmithyFile smithyFile - ) implements IdlPosition {} + record TraitValue(StatementView view, Syntax.Statement.TraitApplication application) implements IdlPosition {} - record ControlKey(SmithyFile smithyFile) implements IdlPosition {} + record NodeMemberTarget(StatementView view, Syntax.Statement.NodeMemberDef nodeMember) implements IdlPosition {} - record MetadataKey(SmithyFile smithyFile) implements IdlPosition {} + record ControlKey(StatementView view) implements IdlPosition {} - record MetadataValue( - int documentIndex, - Syntax.Statement.Metadata metadata, - SmithyFile smithyFile - ) implements IdlPosition {} + record MetadataKey(StatementView view) implements IdlPosition {} - record StatementKeyword(SmithyFile smithyFile) implements IdlPosition {} + record MetadataValue(StatementView view, Syntax.Statement.Metadata metadata) implements IdlPosition {} - record MemberName(int documentIndex, int statementIndex, SmithyFile smithyFile) implements IdlPosition {} + record StatementKeyword(StatementView view) implements IdlPosition {} - record ElidedMember(int documentIndex, int statementIndex, SmithyFile smithyFile) implements IdlPosition {} + record MemberName(StatementView view) implements IdlPosition {} - record ForResource(SmithyFile smithyFile) implements IdlPosition {} + record ElidedMember(StatementView view) implements IdlPosition {} - static Optional at(SmithyFile smithyFile, Position position) { - int documentIndex = smithyFile.document().indexOfPosition(position); - if (documentIndex < 0) { - return Optional.empty(); - } + record ForResource(StatementView view) implements IdlPosition {} - int statementIndex = SyntaxSearch.statementIndex(smithyFile.statements(), documentIndex); - if (statementIndex < 0) { - return Optional.empty(); - } + record Unknown(StatementView view) implements IdlPosition {} - Syntax.Statement statement = smithyFile.statements().get(statementIndex); - IdlPosition idlPosition = switch (statement) { + static IdlPosition of(StatementView view) { + int documentIndex = view.documentIndex(); + return switch (view.getStatement()) { case Syntax.Statement.Incomplete incomplete - when incomplete.ident().isIn(documentIndex) -> new IdlPosition.StatementKeyword(smithyFile); + when incomplete.ident().isIn(documentIndex) -> new IdlPosition.StatementKeyword(view); case Syntax.Statement.ShapeDef shapeDef - when shapeDef.shapeType().isIn(documentIndex) -> new IdlPosition.StatementKeyword(smithyFile); + when shapeDef.shapeType().isIn(documentIndex) -> new IdlPosition.StatementKeyword(view); case Syntax.Statement.Apply apply - when apply.id().isIn(documentIndex) -> new IdlPosition.ApplyTarget(smithyFile); + when apply.id().isIn(documentIndex) -> new IdlPosition.ApplyTarget(view); case Syntax.Statement.Metadata m - when m.key().isIn(documentIndex) -> new IdlPosition.MetadataKey(smithyFile); + when m.key().isIn(documentIndex) -> new IdlPosition.MetadataKey(view); case Syntax.Statement.Metadata m - when m.value() != null && m.value().isIn(documentIndex) -> new IdlPosition.MetadataValue( - documentIndex, m, smithyFile); + when m.value() != null && m.value().isIn(documentIndex) -> new IdlPosition.MetadataValue(view, m); case Syntax.Statement.Control c - when c.key().isIn(documentIndex) -> new IdlPosition.ControlKey(smithyFile); + when c.key().isIn(documentIndex) -> new IdlPosition.ControlKey(view); case Syntax.Statement.TraitApplication t - when t.id().isEmpty() || t.id().isIn(documentIndex) -> new IdlPosition.TraitId(smithyFile); + when t.id().isEmpty() || t.id().isIn(documentIndex) -> new IdlPosition.TraitId(view); case Syntax.Statement.Use u - when u.use().isIn(documentIndex) -> new IdlPosition.UseTarget(smithyFile); + when u.use().isIn(documentIndex) -> new IdlPosition.UseTarget(view); case Syntax.Statement.MemberDef m - when m.inTarget(documentIndex) -> new IdlPosition.MemberTarget(smithyFile); + when m.inTarget(documentIndex) -> new IdlPosition.MemberTarget(view); case Syntax.Statement.MemberDef m - when m.name().isIn(documentIndex) -> new IdlPosition.MemberName( - documentIndex, statementIndex, smithyFile); + when m.name().isIn(documentIndex) -> new IdlPosition.MemberName(view); case Syntax.Statement.NodeMemberDef m - when m.inValue(documentIndex) -> new IdlPosition.NodeMemberTarget( - documentIndex, statementIndex, m, smithyFile); + when m.inValue(documentIndex) -> new IdlPosition.NodeMemberTarget(view, m); case Syntax.Statement.Namespace n - when n.namespace().isIn(documentIndex) -> new IdlPosition.Namespace(smithyFile); + when n.namespace().isIn(documentIndex) -> new IdlPosition.Namespace(view); case Syntax.Statement.TraitApplication t - when t.value() != null && t.value().isIn(documentIndex) -> new IdlPosition.TraitValue( - documentIndex, statementIndex, t, smithyFile); + when t.value() != null && t.value().isIn(documentIndex) -> new IdlPosition.TraitValue(view, t); - case Syntax.Statement.ElidedMemberDef ignored -> new IdlPosition.ElidedMember( - documentIndex, statementIndex, smithyFile); + case Syntax.Statement.ElidedMemberDef ignored -> new IdlPosition.ElidedMember(view); - case Syntax.Statement.Mixins ignored -> new IdlPosition.Mixin(smithyFile); + case Syntax.Statement.Mixins ignored -> new IdlPosition.Mixin(view); - case Syntax.Statement.ShapeDef ignored -> new IdlPosition.ShapeDef(smithyFile); + case Syntax.Statement.ShapeDef ignored -> new IdlPosition.ShapeDef(view); - case Syntax.Statement.NodeMemberDef ignored -> new IdlPosition.MemberName( - documentIndex, statementIndex, smithyFile); + case Syntax.Statement.NodeMemberDef ignored -> new IdlPosition.MemberName(view); - case Syntax.Statement.Block ignored -> new IdlPosition.MemberName( - documentIndex, statementIndex, smithyFile); + case Syntax.Statement.Block ignored -> new IdlPosition.MemberName(view); - case Syntax.Statement.ForResource ignored -> new IdlPosition.ForResource(smithyFile); + case Syntax.Statement.ForResource ignored -> new IdlPosition.ForResource(view); - default -> null; + default -> new IdlPosition.Unknown(view); }; - - return Optional.ofNullable(idlPosition); } } diff --git a/src/main/java/software/amazon/smithy/lsp/language/NodeSearch.java b/src/main/java/software/amazon/smithy/lsp/language/NodeSearch.java index 493c80c2..a81de9f4 100644 --- a/src/main/java/software/amazon/smithy/lsp/language/NodeSearch.java +++ b/src/main/java/software/amazon/smithy/lsp/language/NodeSearch.java @@ -5,7 +5,9 @@ package software.amazon.smithy.lsp.language; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import software.amazon.smithy.lsp.syntax.NodeCursor; import software.amazon.smithy.lsp.syntax.Syntax; import software.amazon.smithy.model.Model; @@ -68,6 +70,39 @@ static Result search( sealed interface Result { None NONE = new None(); + /** + * @return The string values of other keys in {@link ObjectKey} and {@link ObjectShape}, + * or an empty set. + */ + default Set getOtherPresentKeys() { + Syntax.Node.Kvps terminalContainer; + NodeCursor.Key terminalKey; + switch (this) { + case NodeSearch.Result.ObjectShape obj -> { + terminalContainer = obj.node(); + terminalKey = null; + } + case NodeSearch.Result.ObjectKey key -> { + terminalContainer = key.key().parent(); + terminalKey = key.key(); + } + default -> { + return Set.of(); + } + } + + Set otherPresentKeys = new HashSet<>(); + for (var kvp : terminalContainer.kvps()) { + otherPresentKeys.add(kvp.key().stringValue()); + } + + if (terminalKey != null) { + otherPresentKeys.remove(terminalKey.name()); + } + + return otherPresentKeys; + } + /** * No result - the path is invalid in the model. */ diff --git a/src/main/java/software/amazon/smithy/lsp/language/ShapeCompletions.java b/src/main/java/software/amazon/smithy/lsp/language/ShapeCompleter.java similarity index 69% rename from src/main/java/software/amazon/smithy/lsp/language/ShapeCompletions.java rename to src/main/java/software/amazon/smithy/lsp/language/ShapeCompleter.java index 0c4d8c74..7c5cc339 100644 --- a/src/main/java/software/amazon/smithy/lsp/language/ShapeCompletions.java +++ b/src/main/java/software/amazon/smithy/lsp/language/ShapeCompleter.java @@ -15,44 +15,60 @@ import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.jsonrpc.messages.Either; -import software.amazon.smithy.lsp.document.DocumentImports; import software.amazon.smithy.lsp.document.DocumentNamespace; -import software.amazon.smithy.lsp.project.SmithyFile; import software.amazon.smithy.lsp.protocol.LspAdapter; +import software.amazon.smithy.lsp.syntax.Syntax; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.loader.Prelude; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeVisitor; import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.traits.ErrorTrait; import software.amazon.smithy.model.traits.MixinTrait; +import software.amazon.smithy.model.traits.PrivateTrait; import software.amazon.smithy.model.traits.RequiredTrait; import software.amazon.smithy.model.traits.TraitDefinition; /** * Maps {@link CompletionCandidates.Shapes} to {@link CompletionItem}s. + * + * @param idlPosition The position of the cursor in the IDL file. + * @param model The model to get shape completions from. + * @param context The context for creating completions. */ -final class ShapeCompletions { - private final Model model; - private final SmithyFile smithyFile; - private final Matcher matcher; - private final Mapper mapper; +record ShapeCompleter(IdlPosition idlPosition, Model model, CompleterContext context) { + List getCompletionItems(CompletionCandidates.Shapes candidates) { + AddItems addItems; + if (idlPosition instanceof IdlPosition.TraitId) { + addItems = new AddDeepTraitBodyItem(model); + } else { + addItems = AddItems.NOOP; + } - private ShapeCompletions(Model model, SmithyFile smithyFile, Matcher matcher, Mapper mapper) { - this.model = model; - this.smithyFile = smithyFile; - this.matcher = matcher; - this.mapper = mapper; - } + ToLabel toLabel; + ModifyItems modifyItems; + boolean shouldMatchFullId = idlPosition instanceof IdlPosition.UseTarget + || context.matchToken().contains("#") + || context.matchToken().contains("."); + if (shouldMatchFullId) { + toLabel = (shape) -> shape.getId().toString(); + modifyItems = ModifyItems.NOOP; + } else { + toLabel = (shape) -> shape.getId().getName(); + modifyItems = new AddImportTextEdits(idlPosition.view().parseResult()); + } - List getCompletionItems(CompletionCandidates.Shapes candidates) { - return streamShapes(candidates) + Matcher matcher = new Matcher(context.matchToken(), toLabel, idlPosition.view().parseResult().namespace()); + Mapper mapper = new Mapper(context.insertRange(), toLabel, addItems, modifyItems); + return streamCandidates(candidates) .filter(matcher::test) .mapMulti(mapper::accept) .toList(); } - private Stream streamShapes(CompletionCandidates.Shapes candidates) { + private Stream streamCandidates(CompletionCandidates.Shapes candidates) { return switch (candidates) { case ANY_SHAPE -> model.shapes(); case STRING_SHAPES -> model.getStringShapes().stream(); @@ -65,43 +81,15 @@ private Stream streamShapes(CompletionCandidates.Shapes candida .filter(shape -> !shape.isMemberShape() && !shape.hasTrait(TraitDefinition.ID) && !shape.hasTrait(MixinTrait.ID)); - case USE_TARGET -> model.shapes() - .filter(shape -> !shape.isMemberShape() - && !shape.getId().getNamespace().contentEquals(smithyFile.namespace()) - && !smithyFile.hasImport(shape.getId().toString())); + case USE_TARGET -> model.shapes().filter(this::shouldImport); }; } - static ShapeCompletions create( - IdlPosition idlPosition, - Model model, - String matchToken, - Range insertRange - ) { - AddItems addItems = AddItems.DEFAULT; - ModifyItems modifyItems = ModifyItems.DEFAULT; - - if (idlPosition instanceof IdlPosition.TraitId) { - addItems = new AddDeepTraitBodyItem(model); - } - - ToLabel toLabel; - if (shouldMatchFullId(idlPosition, matchToken)) { - toLabel = (shape) -> shape.getId().toString(); - } else { - toLabel = (shape) -> shape.getId().getName(); - modifyItems = new AddImportTextEdits(idlPosition.smithyFile()); - } - - Matcher matcher = new Matcher(matchToken, toLabel, idlPosition.smithyFile()); - Mapper mapper = new Mapper(insertRange, toLabel, addItems, modifyItems); - return new ShapeCompletions(model, idlPosition.smithyFile(), matcher, mapper); - } - - private static boolean shouldMatchFullId(IdlPosition idlPosition, String matchToken) { - return idlPosition instanceof IdlPosition.UseTarget - || matchToken.contains("#") - || matchToken.contains("."); + private boolean shouldImport(Shape shape) { + return !shape.isMemberShape() + && !shape.getId().getNamespace().equals(idlPosition.view().parseResult().namespace().namespace()) + && !idlPosition.view().parseResult().imports().imports().contains(shape.getId().toString()) + && !shape.hasTrait(PrivateTrait.ID); } /** @@ -111,11 +99,12 @@ private static boolean shouldMatchFullId(IdlPosition idlPosition, String matchTo * @param matchToken The token to match shapes against, i.e. the token * being typed. * @param toLabel The way to get the label to match against from a shape. - * @param smithyFile The current Smithy file. + * @param namespace The namespace of the current Smithy file. */ - private record Matcher(String matchToken, ToLabel toLabel, SmithyFile smithyFile) { + private record Matcher(String matchToken, ToLabel toLabel, DocumentNamespace namespace) { boolean test(Shape shape) { - return smithyFile.isAccessible(shape) && toLabel.toLabel(shape).toLowerCase().startsWith(matchToken); + return toLabel.toLabel(shape).toLowerCase().startsWith(matchToken) + && (shape.getId().getNamespace().equals(namespace.namespace()) || !shape.hasTrait(PrivateTrait.ID)); } } @@ -165,7 +154,7 @@ private interface ToLabel { * shape. */ private interface AddItems { - AddItems DEFAULT = new AddItems() { + AddItems NOOP = new AddItems() { }; default void add(Mapper mapper, String shapeLabel, Shape shape, Consumer consumer) { @@ -229,7 +218,7 @@ public String memberShape(MemberShape shape) { * context, additional text edits, etc. */ private interface ModifyItems { - ModifyItems DEFAULT = new ModifyItems() { + ModifyItems NOOP = new ModifyItems() { }; default void modify(Mapper mapper, String shapeLabel, Shape shape, CompletionItem completionItem) { @@ -238,29 +227,35 @@ default void modify(Mapper mapper, String shapeLabel, Shape shape, CompletionIte /** * Adds text edits for use statements for shapes that need to be imported. + * + * @param syntaxInfo Syntax info of the current Smithy file. */ - private static final class AddImportTextEdits implements ModifyItems { - private final SmithyFile smithyFile; - - AddImportTextEdits(SmithyFile smithyFile) { - this.smithyFile = smithyFile; - } - + private record AddImportTextEdits(Syntax.IdlParseResult syntaxInfo) implements ModifyItems { @Override public void modify(Mapper mapper, String shapeLabel, Shape shape, CompletionItem completionItem) { - if (smithyFile.inScope(shape.getId())) { + if (inScope(shape.getId())) { return; } // We can only know where to put the import if there's already use statements, or a namespace - smithyFile.documentImports().map(DocumentImports::importsRange) - .or(() -> smithyFile.documentNamespace().map(DocumentNamespace::statementRange)) - .ifPresent(range -> { - Range editRange = LspAdapter.point(range.getEnd()); - String insertText = System.lineSeparator() + "use " + shape.getId().toString(); - TextEdit importEdit = new TextEdit(editRange, insertText); - completionItem.setAdditionalTextEdits(List.of(importEdit)); - }); + if (!syntaxInfo.imports().imports().isEmpty()) { + addEdit(completionItem, syntaxInfo.imports().importsRange(), shape); + } else if (!syntaxInfo.namespace().namespace().isEmpty()) { + addEdit(completionItem, syntaxInfo.namespace().statementRange(), shape); + } + } + + private boolean inScope(ShapeId shapeId) { + return Prelude.isPublicPreludeShape(shapeId) + || shapeId.getNamespace().equals(syntaxInfo.namespace().namespace()) + || syntaxInfo.imports().imports().contains(shapeId.toString()); + } + + private void addEdit(CompletionItem completionItem, Range range, Shape shape) { + Range editRange = LspAdapter.point(range.getEnd()); + String insertText = System.lineSeparator() + "use " + shape.getId().toString(); + TextEdit importEdit = new TextEdit(editRange, insertText); + completionItem.setAdditionalTextEdits(List.of(importEdit)); } } } diff --git a/src/main/java/software/amazon/smithy/lsp/language/ShapeSearch.java b/src/main/java/software/amazon/smithy/lsp/language/ShapeSearch.java index 0ebb3967..5eb1c770 100644 --- a/src/main/java/software/amazon/smithy/lsp/language/ShapeSearch.java +++ b/src/main/java/software/amazon/smithy/lsp/language/ShapeSearch.java @@ -11,8 +11,8 @@ import software.amazon.smithy.lsp.document.DocumentId; import software.amazon.smithy.lsp.project.SmithyFile; import software.amazon.smithy.lsp.syntax.NodeCursor; +import software.amazon.smithy.lsp.syntax.StatementView; import software.amazon.smithy.lsp.syntax.Syntax; -import software.amazon.smithy.lsp.syntax.SyntaxSearch; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.loader.Prelude; import software.amazon.smithy.model.shapes.ResourceShape; @@ -33,27 +33,27 @@ private ShapeSearch() { * Attempts to find a shape using a token, {@code nameOrId}. * *

When {@code nameOrId} does not contain a '#', this searches for shapes - * either in {@code smithyFile}'s namespace, in {@code smithyFile}'s + * either in {@code idlParse}'s namespace, in {@code idlParse}'s * imports, or the prelude, in that order. When {@code nameOrId} does contain * a '#', it is assumed to be a full shape id and is searched for directly. * - * @param smithyFile The file {@code nameOrId} is within. - * @param model The model to search. - * @param nameOrId The name or shape id of the shape to find. + * @param parseResult The parse result of the file {@code nameOrId} is within. + * @param nameOrId The name or shape id of the shape to find. + * @param model The model to search. * @return The shape, if found. */ - static Optional findShape(SmithyFile smithyFile, Model model, String nameOrId) { + static Optional findShape(Syntax.IdlParseResult parseResult, String nameOrId, Model model) { return switch (nameOrId) { case String s when s.isEmpty() -> Optional.empty(); case String s when s.contains("#") -> tryFrom(s).flatMap(model::getShape); case String s -> { - Optional fromCurrent = tryFromParts(smithyFile.namespace().toString(), s) + Optional fromCurrent = tryFromParts(parseResult.namespace().namespace(), s) .flatMap(model::getShape); if (fromCurrent.isPresent()) { yield fromCurrent; } - for (String fileImport : smithyFile.imports()) { + for (String fileImport : parseResult.imports().imports()) { Optional imported = tryFrom(fileImport) .filter(importId -> importId.getName().equals(s)) .flatMap(model::getShape); @@ -88,16 +88,16 @@ private static Optional tryFromParts(String namespace, String name) { * Attempts to find the shape referenced by {@code id} at {@code idlPosition} in {@code model}. * * @param idlPosition The position of the potential shape reference. - * @param model The model to search for shapes in. - * @param id The identifier at {@code idlPosition}. + * @param id The identifier at {@code idlPosition}. + * @param model The model to search for shapes in. * @return The shape, if found. */ - static Optional findShapeDefinition(IdlPosition idlPosition, Model model, DocumentId id) { + static Optional findShapeDefinition(IdlPosition idlPosition, DocumentId id, Model model) { return switch (idlPosition) { case IdlPosition.TraitValue traitValue -> { var result = searchTraitValue(traitValue, model); if (result instanceof NodeSearch.Result.TerminalShape(var s, var m) && s.hasTrait(IdRefTrait.class)) { - yield findShape(idlPosition.smithyFile(), m, id.copyIdValue()); + yield findShape(idlPosition.view().parseResult(), id.copyIdValue(), m); } else if (result instanceof NodeSearch.Result.ObjectKey(var key, var container, var m) && !container.isMapShape()) { yield container.getMember(key.name()); @@ -109,80 +109,84 @@ static Optional findShapeDefinition(IdlPosition idlPosition, Mo var result = searchNodeMemberTarget(nodeMemberTarget); if (result instanceof NodeSearch.Result.TerminalShape(Shape shape, var ignored) && shape.hasTrait(IdRefTrait.class)) { - yield findShape(nodeMemberTarget.smithyFile(), model, id.copyIdValue()); + yield findShape(nodeMemberTarget.view().parseResult(), id.copyIdValue(), model); } yield Optional.empty(); } // Note: This could be made more specific, at least for mixins case IdlPosition.ElidedMember elidedMember -> - findElidedMemberParent(elidedMember, model, id); + findElidedMemberParent(elidedMember, id, model); case IdlPosition pos when pos.isEasyShapeReference() -> - findShape(pos.smithyFile(), model, id.copyIdValue()); + findShape(pos.view().parseResult(), id.copyIdValue(), model); default -> Optional.empty(); }; } - record ForResourceAndMixins(ResourceShape resource, List mixins) {} - - static ForResourceAndMixins findForResourceAndMixins( - SyntaxSearch.ForResourceAndMixins forResourceAndMixins, - SmithyFile smithyFile, + /** + * @param forResource The nullable for-resource statement. + * @param view A statement view containing the for-resource statement. + * @param model The model to search in. + * @return A resource shape matching the given for-resource statement, if found. + */ + static Optional findResource( + Syntax.Statement.ForResource forResource, + StatementView view, Model model ) { - ResourceShape resourceShape = null; - if (forResourceAndMixins.forResource() != null) { - String resourceNameOrId = forResourceAndMixins.forResource() - .resource() - .copyValueFrom(smithyFile.document()); - - resourceShape = findShape(smithyFile, model, resourceNameOrId) - .flatMap(Shape::asResourceShape) - .orElse(null); + if (forResource != null) { + String resourceNameOrId = forResource.resource().stringValue(); + return findShape(view.parseResult(), resourceNameOrId, model) + .flatMap(Shape::asResourceShape); } - List mixins = List.of(); - if (forResourceAndMixins.mixins() != null) { - mixins = new ArrayList<>(forResourceAndMixins.mixins().mixins().size()); - for (Syntax.Ident ident : forResourceAndMixins.mixins().mixins()) { - String mixinNameOrId = ident.copyValueFrom(smithyFile.document()); - findShape(smithyFile, model, mixinNameOrId).ifPresent(mixins::add); + return Optional.empty(); + } + + /** + * @param mixins The nullable mixins statement. + * @param view The statement view containing the mixins statement. + * @param model The model to search in. + * @return A list of the mixin shapes matching those in the mixin statement. + */ + static List findMixins(Syntax.Statement.Mixins mixins, StatementView view, Model model) { + if (mixins != null) { + List mixinShapes = new ArrayList<>(mixins.mixins().size()); + for (Syntax.Ident ident : mixins.mixins()) { + String mixinNameOrId = ident.stringValue(); + findShape(view.parseResult(), mixinNameOrId, model).ifPresent(mixinShapes::add); } + return mixinShapes; } - - return new ForResourceAndMixins(resourceShape, mixins); + return List.of(); } /** * @param elidedMember The elided member position - * @param model The model to search in - * @param id The identifier of the elided member + * @param id The identifier of the elided member + * @param model The model to search in * @return The shape the elided member comes from, if found. */ static Optional findElidedMemberParent( IdlPosition.ElidedMember elidedMember, - Model model, - DocumentId id + DocumentId id, + Model model ) { - var forResourceAndMixins = findForResourceAndMixins( - SyntaxSearch.closestForResourceAndMixinsBeforeMember( - elidedMember.smithyFile().statements(), - elidedMember.statementIndex()), - elidedMember.smithyFile(), - model); + var view = elidedMember.view(); + var forResourceAndMixins = view.nearestForResourceAndMixinsBefore(); String searchToken = id.copyIdValueForElidedMember(); // TODO: Handle ambiguity - Optional foundResource = Optional.ofNullable(forResourceAndMixins.resource()) + Optional foundResource = findResource(forResourceAndMixins.forResource(), view, model) .filter(shape -> shape.getIdentifiers().containsKey(searchToken) || shape.getProperties().containsKey(searchToken)); if (foundResource.isPresent()) { return foundResource; } - return forResourceAndMixins.mixins() + return findMixins(forResourceAndMixins.mixins(), view, model) .stream() .filter(shape -> shape.getAllMembers().containsKey(searchToken)) .findFirst(); @@ -194,16 +198,14 @@ static Optional findElidedMemberParent( * @return The shape that {@code traitValue} is being applied to, if found. */ static Optional findTraitTarget(IdlPosition.TraitValue traitValue, Model model) { - Syntax.Statement.ShapeDef shapeDef = SyntaxSearch.closestShapeDefAfterTrait( - traitValue.smithyFile().statements(), - traitValue.statementIndex()); + Syntax.Statement.ShapeDef shapeDef = traitValue.view().nearestShapeDefAfter(); if (shapeDef == null) { return Optional.empty(); } - String shapeName = shapeDef.shapeName().copyValueFrom(traitValue.smithyFile().document()); - return findShape(traitValue.smithyFile(), model, shapeName); + String shapeName = shapeDef.shapeName().stringValue(); + return findShape(traitValue.view().parseResult(), shapeName, model); } /** @@ -224,17 +226,16 @@ static boolean isObjectShape(Shape shape) { * {@link Builtins} model. */ static NodeSearch.Result searchMetadataValue(IdlPosition.MetadataValue metadataValue) { - String metadataKey = metadataValue.metadata().key().copyValueFrom(metadataValue.smithyFile().document()); + String metadataKey = metadataValue.metadata().key().stringValue(); Shape metadataValueShapeDef = Builtins.getMetadataValue(metadataKey); if (metadataValueShapeDef == null) { return NodeSearch.Result.NONE; } NodeCursor cursor = NodeCursor.create( - metadataValue.smithyFile().document(), metadataValue.metadata().value(), - metadataValue.documentIndex()); - var dynamicTargets = DynamicMemberTarget.forMetadata(metadataKey, metadataValue.smithyFile()); + metadataValue.view().documentIndex()); + var dynamicTargets = DynamicMemberTarget.forMetadata(metadataKey); return NodeSearch.search(cursor, Builtins.MODEL, metadataValueShapeDef, dynamicTargets); } @@ -244,18 +245,14 @@ static NodeSearch.Result searchMetadataValue(IdlPosition.MetadataValue metadataV * within the {@link Builtins} model. */ static NodeSearch.Result searchNodeMemberTarget(IdlPosition.NodeMemberTarget nodeMemberTarget) { - Syntax.Statement.ShapeDef shapeDef = SyntaxSearch.closestShapeDefBeforeMember( - nodeMemberTarget.smithyFile().statements(), - nodeMemberTarget.statementIndex()); + Syntax.Statement.ShapeDef shapeDef = nodeMemberTarget.view().nearestShapeDefBefore(); if (shapeDef == null) { return NodeSearch.Result.NONE; } - String shapeType = shapeDef.shapeType().copyValueFrom(nodeMemberTarget.smithyFile().document()); - String memberName = nodeMemberTarget.nodeMemberDef() - .name() - .copyValueFrom(nodeMemberTarget.smithyFile().document()); + String shapeType = shapeDef.shapeType().stringValue(); + String memberName = nodeMemberTarget.nodeMember().name().stringValue(); Shape memberShapeDef = Builtins.getMemberTargetForShapeType(shapeType, memberName); if (memberShapeDef == null) { @@ -268,14 +265,13 @@ static NodeSearch.Result searchNodeMemberTarget(IdlPosition.NodeMemberTarget nod // // TODO: Note that searchTraitValue has to do a similar thing, but parsing // trait values always yields at least an empty Kvps, so it is kind of the same. - if (nodeMemberTarget.nodeMemberDef().value() == null) { + if (nodeMemberTarget.nodeMember().value() == null) { return new NodeSearch.Result.TerminalShape(memberShapeDef, Builtins.MODEL); } NodeCursor cursor = NodeCursor.create( - nodeMemberTarget.smithyFile().document(), - nodeMemberTarget.nodeMemberDef().value(), - nodeMemberTarget.documentIndex()); + nodeMemberTarget.nodeMember().value(), + nodeMemberTarget.view().documentIndex()); return NodeSearch.search(cursor, Builtins.MODEL, memberShapeDef); } @@ -285,17 +281,16 @@ static NodeSearch.Result searchNodeMemberTarget(IdlPosition.NodeMemberTarget nod * @return The result of searching from {@code traitValue} within {@code model}. */ static NodeSearch.Result searchTraitValue(IdlPosition.TraitValue traitValue, Model model) { - String traitName = traitValue.traitApplication().id().copyValueFrom(traitValue.smithyFile().document()); - Optional maybeTraitShape = findShape(traitValue.smithyFile(), model, traitName); + String traitName = traitValue.application().id().stringValue(); + Optional maybeTraitShape = findShape(traitValue.view().parseResult(), traitName, model); if (maybeTraitShape.isEmpty()) { return NodeSearch.Result.NONE; } Shape traitShape = maybeTraitShape.get(); NodeCursor cursor = NodeCursor.create( - traitValue.smithyFile().document(), - traitValue.traitApplication().value(), - traitValue.documentIndex()); + traitValue.application().value(), + traitValue.view().documentIndex()); if (cursor.isTerminal() && isObjectShape(traitShape)) { // In this case, we've just started to type '@myTrait(foo)', which to the parser looks like 'foo' is just // an identifier. But this would mean you don't get member completions when typing the first trait value diff --git a/src/main/java/software/amazon/smithy/lsp/language/SimpleCompletions.java b/src/main/java/software/amazon/smithy/lsp/language/SimpleCompleter.java similarity index 67% rename from src/main/java/software/amazon/smithy/lsp/language/SimpleCompletions.java rename to src/main/java/software/amazon/smithy/lsp/language/SimpleCompleter.java index a8ed386b..04150084 100644 --- a/src/main/java/software/amazon/smithy/lsp/language/SimpleCompletions.java +++ b/src/main/java/software/amazon/smithy/lsp/language/SimpleCompleter.java @@ -9,30 +9,37 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Predicate; import java.util.stream.Stream; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.CompletionItemKind; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.jsonrpc.messages.Either; -import software.amazon.smithy.lsp.document.DocumentId; -import software.amazon.smithy.lsp.project.Project; +import software.amazon.smithy.lsp.project.IdlFile; import software.amazon.smithy.lsp.util.StreamUtils; -import software.amazon.smithy.model.Model; -final class SimpleCompletions { - private final Project project; - private final Matcher matcher; - private final Mapper mapper; +/** + * Maps simple {@link CompletionCandidates} to {@link CompletionItem}s. + * + * @param context The context for creating completions. + * + * @see ShapeCompleter for what maps {@link CompletionCandidates.Shapes}. + */ +record SimpleCompleter(CompleterContext context) { + List getCompletionItems(CompletionCandidates candidates) { + Matcher matcher; + if (context.exclude().isEmpty()) { + matcher = new DefaultMatcher(context.matchToken()); + } else { + matcher = new ExcludingMatcher(context.matchToken(), context.exclude()); + } - private SimpleCompletions(Project project, Matcher matcher, Mapper mapper) { - this.project = project; - this.matcher = matcher; - this.mapper = mapper; + Mapper mapper = new Mapper(context().insertRange(), context().literalKind()); + + return getCompletionItems(candidates, matcher, mapper); } - List getCompletionItems(CompletionCandidates candidates) { + private List getCompletionItems(CompletionCandidates candidates, Matcher matcher, Mapper mapper) { return switch (candidates) { case CompletionCandidates.Constant(var value) when !value.isEmpty() && matcher.testConstant(value) -> List.of(mapper.constant(value)); @@ -57,9 +64,7 @@ List getCompletionItems(CompletionCandidates candidates) { .map(mapper::elided) .toList(); - case CompletionCandidates.Custom custom - // TODO: Need to get rid of this stupid null check - when project != null -> getCompletionItems(customCandidates(custom)); + case CompletionCandidates.Custom custom -> getCompletionItems(customCandidates(custom), matcher, mapper); case CompletionCandidates.And(var one, var two) -> { List oneItems = getCompletionItems(one); @@ -69,6 +74,7 @@ List getCompletionItems(CompletionCandidates candidates) { completionItems.addAll(twoItems); yield completionItems; } + default -> List.of(); }; } @@ -85,69 +91,26 @@ private CompletionCandidates customCandidates(CompletionCandidates.Custom custom } private Stream streamNamespaces() { - return project.smithyFiles().values().stream() - .map(smithyFile -> smithyFile.namespace().toString()) + return context().project().smithyFiles().values().stream() + .map(smithyFile -> switch (smithyFile) { + case IdlFile idlFile -> idlFile.getParse().namespace().namespace(); + default -> ""; + }) .filter(namespace -> !namespace.isEmpty()); } - static Builder builder(DocumentId id, Range insertRange) { - return new Builder(id, insertRange); - } - - static final class Builder { - private final DocumentId id; - private final Range insertRange; - private Project project = null; - private Set exclude = null; - private CompletionItemKind literalKind = CompletionItemKind.Field; - - private Builder(DocumentId id, Range insertRange) { - this.id = id; - this.insertRange = insertRange; - } - - Builder project(Project project) { - this.project = project; - return this; - } - - Builder exclude(Set exclude) { - this.exclude = exclude; - return this; - } - - Builder literalKind(CompletionItemKind literalKind) { - this.literalKind = literalKind; - return this; - } - - SimpleCompletions buildSimpleCompletions() { - Matcher matcher = getMatcher(id, exclude); - Mapper mapper = new Mapper(insertRange, literalKind); - return new SimpleCompletions(project, matcher, mapper); - } - - ShapeCompletions buildShapeCompletions(IdlPosition idlPosition, Model model) { - return ShapeCompletions.create(idlPosition, model, getMatchToken(id), insertRange); - } - } - - private static Matcher getMatcher(DocumentId id, Set exclude) { - String matchToken = getMatchToken(id); - if (exclude == null || exclude.isEmpty()) { - return new DefaultMatcher(matchToken); - } else { - return new ExcludingMatcher(matchToken, exclude); - } - } - - private static String getMatchToken(DocumentId id) { - return id != null - ? id.copyIdValue().toLowerCase() - : ""; - } - - private sealed interface Matcher extends Predicate { + /** + * Matches different kinds of completion candidates against the text of + * whatever triggered the completion, used to filter out candidates. + * + * @apiNote LSP has support for client-side matching/filtering, but only when + * the completion items don't have text edits. We use text edits to have more + * control over the range the completion text will occupy, so we need to do + * matching/filtering server-side. + * + * @see LSP Completion Docs + */ + private sealed interface Matcher { String matchToken(); default boolean testConstant(String constant) { @@ -170,7 +133,6 @@ default boolean testElided(String memberName) { return test(memberName) || test("$" + memberName); } - @Override default boolean test(String s) { return s.toLowerCase().startsWith(matchToken()); } @@ -193,13 +155,20 @@ public boolean test(String s) { } } + /** + * Maps different kinds of completion candidates to {@link CompletionItem}s. + * + * @param insertRange The range the completion text will occupy. + * @param literalKind The completion item kind that will be shown in the + * client for {@link CompletionCandidates.Literals}. + */ private record Mapper(Range insertRange, CompletionItemKind literalKind) { CompletionItem constant(String value) { return textEditCompletion(value, CompletionItemKind.Constant); } CompletionItem literal(String value) { - return textEditCompletion(value, CompletionItemKind.Field); + return textEditCompletion(value, literalKind); } CompletionItem labeled(Map.Entry entry) { diff --git a/src/main/java/software/amazon/smithy/lsp/project/IdlFile.java b/src/main/java/software/amazon/smithy/lsp/project/IdlFile.java new file mode 100644 index 00000000..f171fbc6 --- /dev/null +++ b/src/main/java/software/amazon/smithy/lsp/project/IdlFile.java @@ -0,0 +1,46 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.lsp.project; + +import java.util.concurrent.locks.ReentrantLock; +import software.amazon.smithy.lsp.document.Document; +import software.amazon.smithy.lsp.syntax.Syntax; + +public final class IdlFile extends SmithyFile { + private final ReentrantLock idlParseLock = new ReentrantLock(); + private Syntax.IdlParseResult parseResult; + + IdlFile(String path, Document document, Syntax.IdlParseResult parseResult) { + super(path, document); + this.parseResult = parseResult; + } + + @Override + public void reparse() { + Syntax.IdlParseResult parse = Syntax.parseIdl(document()); + + idlParseLock.lock(); + try { + this.parseResult = parse; + } finally { + idlParseLock.unlock(); + } + } + + /** + * @return The latest computed {@link Syntax.IdlParseResult} of this Smithy file + * @apiNote Don't call this method over and over. {@link Syntax.IdlParseResult} is + * immutable so just call this once and use the returned value. + */ + public Syntax.IdlParseResult getParse() { + idlParseLock.lock(); + try { + return parseResult; + } finally { + idlParseLock.unlock(); + } + } +} diff --git a/src/main/java/software/amazon/smithy/lsp/project/Project.java b/src/main/java/software/amazon/smithy/lsp/project/Project.java index 1a793200..baae3773 100644 --- a/src/main/java/software/amazon/smithy/lsp/project/Project.java +++ b/src/main/java/software/amazon/smithy/lsp/project/Project.java @@ -6,13 +6,11 @@ package software.amazon.smithy.lsp.project; import java.nio.file.Path; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.function.Supplier; import java.util.logging.Logger; @@ -25,6 +23,7 @@ import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.shapes.AbstractShapeBuilder; import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ToShapeId; import software.amazon.smithy.model.traits.Trait; import software.amazon.smithy.model.validation.ValidatedResult; import software.amazon.smithy.utils.IoUtils; @@ -40,20 +39,30 @@ public final class Project { private final List dependencies; private final Map smithyFiles; private final Supplier assemblerFactory; + private final Map> definedShapesByFile; private ValidatedResult modelResult; // TODO: Move this into SmithyFileDependenciesIndex private Map> perFileMetadata; private SmithyFileDependenciesIndex smithyFileDependenciesIndex; - private Project(Builder builder) { - this.root = Objects.requireNonNull(builder.root); - this.config = builder.config; - this.dependencies = builder.dependencies; - this.smithyFiles = builder.smithyFiles; - this.modelResult = builder.modelResult; - this.assemblerFactory = builder.assemblerFactory; - this.perFileMetadata = builder.perFileMetadata; - this.smithyFileDependenciesIndex = builder.smithyFileDependenciesIndex; + Project(Path root, + ProjectConfig config, + List dependencies, + Map smithyFiles, + Supplier assemblerFactory, + Map> definedShapesByFile, + ValidatedResult modelResult, + Map> perFileMetadata, + SmithyFileDependenciesIndex smithyFileDependenciesIndex) { + this.root = root; + this.config = config; + this.dependencies = dependencies; + this.smithyFiles = smithyFiles; + this.assemblerFactory = assemblerFactory; + this.definedShapesByFile = definedShapesByFile; + this.modelResult = modelResult; + this.perFileMetadata = perFileMetadata; + this.smithyFileDependenciesIndex = smithyFileDependenciesIndex; } /** @@ -63,10 +72,15 @@ private Project(Builder builder) { * @return The empty project */ public static Project empty(Path root) { - return builder() - .root(root) - .modelResult(ValidatedResult.empty()) - .build(); + return new Project(root, + ProjectConfig.empty(), + List.of(), + new HashMap<>(), + Model::assembler, + new HashMap<>(), + ValidatedResult.empty(), + new HashMap<>(), + new SmithyFileDependenciesIndex()); } /** @@ -119,6 +133,13 @@ public Map smithyFiles() { return this.smithyFiles; } + /** + * @return A map of paths to the set of shape ids defined in the file at that path. + */ + public Map> definedShapesByFile() { + return this.definedShapesByFile; + } + /** * @return The latest result of loading this project */ @@ -224,7 +245,6 @@ public void updateFiles(Set addUris, Set removeUris, Set // So we don't have to recompute the paths later Set removedPaths = new HashSet<>(removeUris.size()); - Set changedPaths = new HashSet<>(changeUris.size()); Set visited = new HashSet<>(); @@ -245,7 +265,6 @@ public void updateFiles(Set addUris, Set removeUris, Set for (String uri : changeUris) { String path = LspAdapter.toPath(uri); - changedPaths.add(path); removeFileForReload(assembler, builder, path, visited); removeDependentsForReload(assembler, builder, path, visited); @@ -281,25 +300,19 @@ public void updateFiles(Set addUris, Set removeUris, Set for (String visitedPath : visited) { if (!removedPaths.contains(visitedPath)) { - SmithyFile current = smithyFiles.get(visitedPath); - Set updatedShapes = getFileShapes(visitedPath, smithyFiles.get(visitedPath).shapes()); - // Only recompute the rest of the smithy file if it changed - if (changedPaths.contains(visitedPath)) { - // TODO: Could cache validation events - this.smithyFiles.put(visitedPath, - ProjectLoader.buildSmithyFile(visitedPath, current.document(), updatedShapes).build()); - } else { - current.setShapes(updatedShapes); - } + Set currentShapes = definedShapesByFile.getOrDefault(visitedPath, Set.of()); + this.definedShapesByFile.put(visitedPath, getFileShapes(visitedPath, currentShapes)); + } else { + this.definedShapesByFile.remove(visitedPath); } } for (String uri : addUris) { String path = LspAdapter.toPath(uri); - Set fileShapes = getFileShapes(path, Collections.emptySet()); Document document = Document.of(IoUtils.readUtf8File(path)); - SmithyFile smithyFile = ProjectLoader.buildSmithyFile(path, document, fileShapes).build(); - smithyFiles.put(path, smithyFile); + SmithyFile smithyFile = SmithyFile.create(path, document); + this.smithyFiles.put(path, smithyFile); + this.definedShapesByFile.put(path, getFileShapes(path, Set.of())); } } @@ -324,21 +337,21 @@ private void removeFileForReload( visited.add(path); - for (Shape shape : smithyFiles.get(path).shapes()) { - builder.removeShape(shape.getId()); + for (ToShapeId toShapeId : definedShapesByFile.getOrDefault(path, Set.of())) { + builder.removeShape(toShapeId.toShapeId()); // This shape may have traits applied to it in other files, // so simply removing the shape loses the information about // those traits. // This shape's dependencies files will be removed and re-loaded - smithyFileDependenciesIndex.getDependenciesFiles(shape).forEach((depPath) -> + smithyFileDependenciesIndex.getDependenciesFiles(toShapeId).forEach((depPath) -> removeFileForReload(assembler, builder, depPath, visited)); // Traits applied in other files are re-added to the assembler so if/when the shape // is reloaded, it will have those traits - smithyFileDependenciesIndex.getTraitsAppliedInOtherFiles(shape).forEach((trait) -> - assembler.addTrait(shape.getId(), trait)); + smithyFileDependenciesIndex.getTraitsAppliedInOtherFiles(toShapeId).forEach((trait) -> + assembler.addTrait(toShapeId.toShapeId(), trait)); } } @@ -350,8 +363,8 @@ private void removeDependentsForReload( ) { // This file may apply traits to shapes in other files. Normally, letting the assembler simply reparse // the file would be fine because it would ignore the duplicated trait application coming from the same - // source location. But if the apply statement is changed/removed, the old application isn't removed, so we - // could get a duplicate trait, or a merged array trait. + // source location. But if the apply statement is changed/removed, the old trait isn't removed, so we + // could get a duplicate application, or a merged array application. smithyFileDependenciesIndex.getDependentFiles(path).forEach((depPath) -> removeFileForReload(assembler, builder, depPath, visited)); smithyFileDependenciesIndex.getAppliedTraitsInFile(path).forEach((shapeId, traits) -> { @@ -375,80 +388,11 @@ private void addRemainingMetadataForReload(Model.Builder builder, Set fi } } - private Set getFileShapes(String path, Set orDefault) { + private Set getFileShapes(String path, Set orDefault) { return this.modelResult.getResult() .map(model -> model.shapes() .filter(shape -> shape.getSourceLocation().getFilename().equals(path)) - .collect(Collectors.toSet())) + .collect(Collectors.toSet())) .orElse(orDefault); } - - static Builder builder() { - return new Builder(); - } - - static final class Builder { - private Path root; - private ProjectConfig config = ProjectConfig.empty(); - private final List dependencies = new ArrayList<>(); - private final Map smithyFiles = new HashMap<>(); - private ValidatedResult modelResult; - private Supplier assemblerFactory = Model::assembler; - private Map> perFileMetadata = new HashMap<>(); - private SmithyFileDependenciesIndex smithyFileDependenciesIndex = new SmithyFileDependenciesIndex(); - - private Builder() { - } - - public Builder root(Path root) { - this.root = root; - return this; - } - - public Builder config(ProjectConfig config) { - this.config = config; - return this; - } - - public Builder dependencies(List paths) { - this.dependencies.clear(); - this.dependencies.addAll(paths); - return this; - } - - public Builder addDependency(Path path) { - this.dependencies.add(path); - return this; - } - - public Builder smithyFiles(Map smithyFiles) { - this.smithyFiles.clear(); - this.smithyFiles.putAll(smithyFiles); - return this; - } - - public Builder modelResult(ValidatedResult modelResult) { - this.modelResult = modelResult; - return this; - } - - public Builder assemblerFactory(Supplier assemblerFactory) { - this.assemblerFactory = assemblerFactory; - return this; - } - - public Builder perFileMetadata(Map> perFileMetadata) { - this.perFileMetadata = perFileMetadata; - return this; - } - - public Builder smithyFileDependenciesIndex(SmithyFileDependenciesIndex smithyFileDependenciesIndex) { - this.smithyFileDependenciesIndex = smithyFileDependenciesIndex; - return this; - } - - public Project build() { - return new Project(this); - } - } } diff --git a/src/main/java/software/amazon/smithy/lsp/project/ProjectAndFile.java b/src/main/java/software/amazon/smithy/lsp/project/ProjectAndFile.java index 0d8b7494..0a79da85 100644 --- a/src/main/java/software/amazon/smithy/lsp/project/ProjectAndFile.java +++ b/src/main/java/software/amazon/smithy/lsp/project/ProjectAndFile.java @@ -9,8 +9,10 @@ * Simple wrapper for a project and a file in that project, which many * server functions act upon. * + * @param uri The uri of the file * @param project The project, non-nullable * @param file The file within {@code project}, non-nullable + * @param isDetached Whether the project and file represent a detached project */ -public record ProjectAndFile(Project project, ProjectFile file) { +public record ProjectAndFile(String uri, Project project, ProjectFile file, boolean isDetached) { } diff --git a/src/main/java/software/amazon/smithy/lsp/project/ProjectLoader.java b/src/main/java/software/amazon/smithy/lsp/project/ProjectLoader.java index 6d18ff95..538b5cca 100644 --- a/src/main/java/software/amazon/smithy/lsp/project/ProjectLoader.java +++ b/src/main/java/software/amazon/smithy/lsp/project/ProjectLoader.java @@ -24,23 +24,16 @@ import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.eclipse.lsp4j.Position; import software.amazon.smithy.lsp.ServerState; import software.amazon.smithy.lsp.document.Document; -import software.amazon.smithy.lsp.document.DocumentImports; -import software.amazon.smithy.lsp.document.DocumentNamespace; -import software.amazon.smithy.lsp.document.DocumentParser; -import software.amazon.smithy.lsp.document.DocumentShape; -import software.amazon.smithy.lsp.document.DocumentVersion; import software.amazon.smithy.lsp.protocol.LspAdapter; -import software.amazon.smithy.lsp.syntax.Syntax; import software.amazon.smithy.lsp.util.Result; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.loader.ModelAssembler; import software.amazon.smithy.model.loader.ModelDiscovery; import software.amazon.smithy.model.node.ArrayNode; import software.amazon.smithy.model.node.Node; -import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ToShapeId; import software.amazon.smithy.model.validation.ValidatedResult; import software.amazon.smithy.utils.IoUtils; @@ -69,28 +62,29 @@ private ProjectLoader() { public static Project loadDetached(String uri, String text) { LOGGER.info("Loading detachedProjects project at " + uri); String asPath = LspAdapter.toPath(uri); - ValidatedResult modelResult = Model.assembler() + Supplier assemblerFactory; + try { + assemblerFactory = createModelAssemblerFactory(List.of()); + } catch (MalformedURLException e) { + // Note: This can't happen because we have no dependencies to turn into URLs + throw new RuntimeException(e); + } + + ValidatedResult modelResult = assemblerFactory.get() .addUnparsedModel(asPath, text) .assemble(); Path path = Paths.get(asPath); List sources = Collections.singletonList(path); - Project.Builder builder = Project.builder() - .root(path.getParent()) - .config(ProjectConfig.builder() - .sources(Collections.singletonList(asPath)) - .build()) - .modelResult(modelResult); - - Map smithyFiles = computeSmithyFiles(sources, modelResult, (filePath) -> { + var definedShapesByFile = computeDefinedShapesByFile(sources, modelResult); + var smithyFiles = createSmithyFiles(definedShapesByFile, (filePath) -> { // NOTE: isSmithyJarFile and isJarFile typically take in a URI (filePath is a path), but // the model stores jar paths as URIs if (LspAdapter.isSmithyJarFile(filePath) || LspAdapter.isJarFile(filePath)) { return Document.of(IoUtils.readUtf8Url(LspAdapter.jarUrl(filePath))); } else if (filePath.equals(asPath)) { - Document document = Document.of(text); - return document; + return Document.of(text); } else { // TODO: Make generic 'please file a bug report' exception throw new IllegalStateException( @@ -100,9 +94,15 @@ public static Project loadDetached(String uri, String text) { } }); - return builder.smithyFiles(smithyFiles) - .perFileMetadata(computePerFileMetadata(modelResult)) - .build(); + return new Project(path.getParent(), + ProjectConfig.builder().sources(List.of(asPath)).build(), + List.of(), + smithyFiles, + assemblerFactory, + definedShapesByFile, + modelResult, + computePerFileMetadata(modelResult), + new SmithyFileDependenciesIndex()); } /** @@ -137,30 +137,20 @@ public static Result> load(Path root, ServerState state // The model assembler factory is used to get assemblers that already have the correct // dependencies resolved for future loads - Result, Exception> assemblerFactoryResult = createModelAssemblerFactory(dependencies); - if (assemblerFactoryResult.isErr()) { - return Result.err(Collections.singletonList(assemblerFactoryResult.unwrapErr())); + Supplier assemblerFactory; + try { + assemblerFactory = createModelAssemblerFactory(dependencies); + } catch (MalformedURLException e) { + return Result.err(List.of(e)); } - Supplier assemblerFactory = assemblerFactoryResult.unwrap(); ModelAssembler assembler = assemblerFactory.get(); // Note: The model assembler can handle loading all smithy files in a directory, so there's some potential // here for inconsistent behavior. List allSmithyFilePaths = collectAllSmithyPaths(root, config.sources(), config.imports()); - Result, Exception> loadModelResult = Result.ofFallible(() -> { - for (Path path : allSmithyFilePaths) { - Document managed = state.getManagedDocument(path); - if (managed != null) { - assembler.addUnparsedModel(path.toString(), managed.copyText()); - } else { - assembler.addImport(path); - } - } - - return assembler.assemble(); - }); + Result, Exception> loadModelResult = loadModel(state, allSmithyFilePaths, assembler); // TODO: Assembler can fail if a file is not found. We can be more intelligent about // handling this case to allow partially loading the project, but we will need to // collect and report the errors somehow. For now, using collectAllSmithyPaths skips @@ -171,15 +161,8 @@ public static Result> load(Path root, ServerState state } ValidatedResult modelResult = loadModelResult.unwrap(); - - Project.Builder projectBuilder = Project.builder() - .root(root) - .config(config) - .dependencies(dependencies) - .modelResult(modelResult) - .assemblerFactory(assemblerFactory); - - Map smithyFiles = computeSmithyFiles(allSmithyFilePaths, modelResult, (filePath) -> { + var definedShapesByFile = computeDefinedShapesByFile(allSmithyFilePaths, modelResult); + var smithyFiles = createSmithyFiles(definedShapesByFile, (filePath) -> { // NOTE: isSmithyJarFile and isJarFile typically take in a URI (filePath is a path), but // the model stores jar paths as URIs if (LspAdapter.isSmithyJarFile(filePath) || LspAdapter.isJarFile(filePath)) { @@ -197,78 +180,75 @@ public static Result> load(Path root, ServerState state return Document.of(IoUtils.readUtf8File(filePath)); }); - return Result.ok(projectBuilder.smithyFiles(smithyFiles) - .perFileMetadata(computePerFileMetadata(modelResult)) - .smithyFileDependenciesIndex(SmithyFileDependenciesIndex.compute(modelResult)) - .build()); + return Result.ok(new Project(root, + config, + dependencies, + smithyFiles, + assemblerFactory, + definedShapesByFile, + modelResult, + computePerFileMetadata(modelResult), + SmithyFileDependenciesIndex.compute(modelResult))); + } + + private static Result, Exception> loadModel( + ServerState state, + List models, + ModelAssembler assembler + ) { + try { + for (Path path : models) { + Document managed = state.getManagedDocument(path); + if (managed != null) { + assembler.addUnparsedModel(path.toString(), managed.copyText()); + } else { + assembler.addImport(path); + } + } + + return Result.ok(assembler.assemble()); + } catch (Exception e) { + return Result.err(e); + } } static Result> load(Path root) { return load(root, new ServerState()); } - private static Map computeSmithyFiles( + private static Map> computeDefinedShapesByFile( List allSmithyFilePaths, - ValidatedResult modelResult, - Function documentProvider + ValidatedResult modelResult ) { - Map> shapesByFile; - if (modelResult.getResult().isPresent()) { - Model model = modelResult.getResult().get(); - shapesByFile = model.shapes().collect(Collectors.groupingByConcurrent( - shape -> shape.getSourceLocation().getFilename(), Collectors.toSet())); - } else { - shapesByFile = new HashMap<>(allSmithyFilePaths.size()); - } + Map> definedShapesByFile = modelResult.getResult().map(Model::shapes) + .orElseGet(Stream::empty) + .collect(Collectors.groupingByConcurrent( + shape -> shape.getSourceLocation().getFilename(), Collectors.toSet())); - // There may be smithy files part of the project that aren't part of the model + // There may be smithy files part of the project that aren't part of the model, e.g. empty files for (Path smithyFilePath : allSmithyFilePaths) { String pathString = smithyFilePath.toString(); - if (!shapesByFile.containsKey(pathString)) { - shapesByFile.put(pathString, Collections.emptySet()); - } + definedShapesByFile.putIfAbsent(pathString, Set.of()); } - Map smithyFiles = new HashMap<>(allSmithyFilePaths.size()); - for (Map.Entry> shapesByFileEntry : shapesByFile.entrySet()) { - String path = shapesByFileEntry.getKey(); + return definedShapesByFile; + } + + private static Map createSmithyFiles( + Map> definedShapesByFile, + Function documentProvider + ) { + Map smithyFiles = new HashMap<>(definedShapesByFile.size()); + + for (String path : definedShapesByFile.keySet()) { Document document = documentProvider.apply(path); - Set fileShapes = shapesByFileEntry.getValue(); - SmithyFile smithyFile = buildSmithyFile(path, document, fileShapes).build(); + SmithyFile smithyFile = SmithyFile.create(path, document); smithyFiles.put(path, smithyFile); } return smithyFiles; } - /** - * Computes extra information about what is in the Smithy file and where, - * such as the namespace, imports, version number, and shapes. - * - * @param path Path of the Smithy file - * @param document The document backing the Smithy file - * @param shapes The shapes defined in the Smithy file - * @return A builder for the Smithy file - */ - public static SmithyFile.Builder buildSmithyFile(String path, Document document, Set shapes) { - DocumentParser documentParser = DocumentParser.forDocument(document); - DocumentNamespace namespace = documentParser.documentNamespace(); - DocumentImports imports = documentParser.documentImports(); - Map documentShapes = documentParser.documentShapes(); - DocumentVersion documentVersion = documentParser.documentVersion(); - Syntax.IdlParse parse = Syntax.parseIdl(document); - List statements = parse.statements(); - return SmithyFile.builder() - .path(path) - .document(document) - .shapes(shapes) - .namespace(namespace) - .imports(imports) - .documentShapes(documentShapes) - .documentVersion(documentVersion) - .statements(statements); - } - // This is gross, but necessary to deal with the way that array metadata gets merged. // When we try to reload a single file, we need to make sure we remove the metadata for // that file. But if there's array metadata, a single key contains merged elements from @@ -300,40 +280,30 @@ static Map> computePerFileMetadata(ValidatedResult, Exception> createModelAssemblerFactory(List dependencies) { + private static Supplier createModelAssemblerFactory(List dependencies) + throws MalformedURLException { // We don't want the model to be broken when there are unknown traits, // because that will essentially disable language server features, so // we need to allow unknown traits for each factory. - // TODO: There's almost certainly a better way to to this if (dependencies.isEmpty()) { - return Result.ok(() -> Model.assembler().putProperty(ModelAssembler.ALLOW_UNKNOWN_TRAITS, true)); + return () -> Model.assembler().putProperty(ModelAssembler.ALLOW_UNKNOWN_TRAITS, true); } - Result result = createDependenciesClassLoader(dependencies); - if (result.isErr()) { - return Result.err(result.unwrapErr()); - } - return Result.ok(() -> { - URLClassLoader classLoader = result.unwrap(); - return Model.assembler(classLoader) - .discoverModels(classLoader) - .putProperty(ModelAssembler.ALLOW_UNKNOWN_TRAITS, true); - }); + URLClassLoader classLoader = createDependenciesClassLoader(dependencies); + return () -> Model.assembler(classLoader) + .discoverModels(classLoader) + .putProperty(ModelAssembler.ALLOW_UNKNOWN_TRAITS, true); } - private static Result createDependenciesClassLoader(List dependencies) { + private static URLClassLoader createDependenciesClassLoader(List dependencies) throws MalformedURLException { // Taken (roughly) from smithy-ci IsolatedRunnable - try { - URL[] urls = new URL[dependencies.size()]; - int i = 0; - for (Path dependency : dependencies) { - urls[i++] = dependency.toUri().toURL(); - } - return Result.ok(new URLClassLoader(urls)); - } catch (MalformedURLException e) { - return Result.err(e); + URL[] urls = new URL[dependencies.size()]; + int i = 0; + for (Path dependency : dependencies) { + urls[i++] = dependency.toUri().toURL(); } + return new URLClassLoader(urls); } // sources and imports can contain directories or files, relative or absolute diff --git a/src/main/java/software/amazon/smithy/lsp/project/SmithyFile.java b/src/main/java/software/amazon/smithy/lsp/project/SmithyFile.java index 5cc23442..a3251e11 100644 --- a/src/main/java/software/amazon/smithy/lsp/project/SmithyFile.java +++ b/src/main/java/software/amazon/smithy/lsp/project/SmithyFile.java @@ -5,241 +5,45 @@ package software.amazon.smithy.lsp.project; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import org.eclipse.lsp4j.Position; import software.amazon.smithy.lsp.document.Document; -import software.amazon.smithy.lsp.document.DocumentImports; -import software.amazon.smithy.lsp.document.DocumentNamespace; -import software.amazon.smithy.lsp.document.DocumentShape; -import software.amazon.smithy.lsp.document.DocumentVersion; import software.amazon.smithy.lsp.syntax.Syntax; -import software.amazon.smithy.model.loader.Prelude; -import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.model.traits.PrivateTrait; /** * The language server's representation of a Smithy file. - * - *

Note: This currently is only ever a .smithy file, but could represent - * a .json file in the future. */ -public final class SmithyFile implements ProjectFile { +public sealed class SmithyFile implements ProjectFile permits IdlFile { private final String path; private final Document document; - // TODO: If we have more complex use-cases for partially updating SmithyFile, we - // could use a toBuilder() - private Set shapes; - private final DocumentNamespace namespace; - private final DocumentImports imports; - private final Map documentShapes; - private final DocumentVersion documentVersion; - private List statements; - private SmithyFile(Builder builder) { - this.path = builder.path; - this.document = builder.document; - this.shapes = builder.shapes; - this.namespace = builder.namespace; - this.imports = builder.imports; - this.documentShapes = builder.documentShapes; - this.documentVersion = builder.documentVersion; - this.statements = builder.statements; + SmithyFile(String path, Document document) { + this.path = path; + this.document = document; } - /** - * @return The path of this Smithy file - */ + static SmithyFile create(String path, Document document) { + // TODO: Make a better abstraction for loading an arbitrary project file + if (path.endsWith(".smithy")) { + Syntax.IdlParseResult parse = Syntax.parseIdl(document); + return new IdlFile(path, document, parse); + } else { + return new SmithyFile(path, document); + } + } + + @Override public String path() { return path; } - /** - * @return The {@link Document} backing this Smithy file - */ + @Override public Document document() { return document; } /** - * @return The Shapes defined in this Smithy file - */ - public Set shapes() { - return shapes; - } - - void setShapes(Set shapes) { - this.shapes = shapes; - } - - /** - * @return This Smithy file's imports, if they exist - */ - public Optional documentImports() { - return Optional.ofNullable(this.imports); - } - - /** - * @return The ids of shapes imported into this Smithy file - */ - public Set imports() { - return documentImports() - .map(DocumentImports::imports) - .orElse(Collections.emptySet()); - } - - /** - * @return This Smithy file's namespace, if one exists - */ - public Optional documentNamespace() { - return Optional.ofNullable(namespace); - } - - /** - * @return The shapes in this Smithy file, including referenced shapes - */ - public Collection documentShapes() { - if (documentShapes == null) { - return Collections.emptyList(); - } - return documentShapes.values(); - } - - /** - * @return A map of {@link Position} to the {@link DocumentShape} they are - * the starting position of - */ - public Map documentShapesByStartPosition() { - if (documentShapes == null) { - return Collections.emptyMap(); - } - return documentShapes; - } - - /** - * @return The string literal namespace of this Smithy file, or an empty string - */ - public CharSequence namespace() { - return documentNamespace() - .map(DocumentNamespace::namespace) - .orElse(""); - } - - /** - * @return This Smithy file's version, if it exists - */ - public Optional documentVersion() { - return Optional.ofNullable(documentVersion); - } - - /** - * @param shapeId The shape id to check - * @return Whether {@code shapeId} is in this SmithyFile's imports - */ - public boolean hasImport(String shapeId) { - if (imports == null || imports.imports().isEmpty()) { - return false; - } - return imports.imports().contains(shapeId); - } - - public boolean isAccessible(Shape shape) { - return shape.getId().getNamespace().contentEquals(namespace()) - || !shape.hasTrait(PrivateTrait.ID); - } - - /** - * @return The parsed statements in this file - */ - public List statements() { - return statements; - } - - /** - * Re-parses the underlying {@link #document()}, updating {@link #statements()}. + * Reparse the underlying {@link #document()}. */ public void reparse() { - Syntax.IdlParse parse = Syntax.parseIdl(document); - this.statements = parse.statements(); - } - - /** - * @param shapeId The shape id to check - * @return Whether the given shape id is in scope for this file - */ - public boolean inScope(ShapeId shapeId) { - return Prelude.isPublicPreludeShape(shapeId) - || shapeId.getNamespace().contentEquals(namespace()) - || hasImport(shapeId.toString()); - } - - /** - * @return A {@link SmithyFile} builder - */ - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String path; - private Document document; - private Set shapes; - private DocumentNamespace namespace; - private DocumentImports imports; - private Map documentShapes; - private DocumentVersion documentVersion; - private List statements; - - private Builder() { - } - - public Builder path(String path) { - this.path = path; - return this; - } - - public Builder document(Document document) { - this.document = document; - return this; - } - - public Builder shapes(Set shapes) { - this.shapes = shapes; - return this; - } - - public Builder namespace(DocumentNamespace namespace) { - this.namespace = namespace; - return this; - } - - public Builder imports(DocumentImports imports) { - this.imports = imports; - return this; - } - - public Builder documentShapes(Map documentShapes) { - this.documentShapes = documentShapes; - return this; - } - - public Builder documentVersion(DocumentVersion documentVersion) { - this.documentVersion = documentVersion; - return this; - } - - public Builder statements(List statements) { - this.statements = statements; - return this; - } - - public SmithyFile build() { - return new SmithyFile(this); - } + // Don't parse JSON files, at least for now } } diff --git a/src/main/java/software/amazon/smithy/lsp/protocol/LspAdapter.java b/src/main/java/software/amazon/smithy/lsp/protocol/LspAdapter.java index 106fa18d..1bd9e540 100644 --- a/src/main/java/software/amazon/smithy/lsp/protocol/LspAdapter.java +++ b/src/main/java/software/amazon/smithy/lsp/protocol/LspAdapter.java @@ -15,6 +15,8 @@ import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; +import software.amazon.smithy.lsp.document.Document; +import software.amazon.smithy.lsp.syntax.Syntax; import software.amazon.smithy.model.FromSourceLocation; import software.amazon.smithy.model.SourceLocation; @@ -112,6 +114,35 @@ public static Range of(int startLine, int startCharacter, int endLine, int endCh .build(); } + /** + * @param ident Identifier to get the range of + * @param document Document the identifier is in + * @return The range of the identifier in the given document + */ + public static Range identRange(Syntax.Ident ident, Document document) { + int line = document.lineOfIndex(ident.start()); + if (line < 0) { + return null; + } + + int lineStart = document.indexOfLine(line); + if (lineStart < 0) { + return null; + } + + int startCharacter = ident.start() - lineStart; + int endCharacter = ident.end() - lineStart; + return LspAdapter.lineSpan(line, startCharacter, endCharacter); + } + + /** + * @param range The range to check + * @return Whether the range's start is equal to it's end + */ + public static boolean isEmpty(Range range) { + return range.getStart().equals(range.getEnd()); + } + /** * Get a {@link Position} from a {@link SourceLocation}, making the line/columns * 0-indexed. diff --git a/src/main/java/software/amazon/smithy/lsp/syntax/NodeCursor.java b/src/main/java/software/amazon/smithy/lsp/syntax/NodeCursor.java index a8e4edb9..ac1cc3a5 100644 --- a/src/main/java/software/amazon/smithy/lsp/syntax/NodeCursor.java +++ b/src/main/java/software/amazon/smithy/lsp/syntax/NodeCursor.java @@ -7,7 +7,6 @@ import java.util.ArrayList; import java.util.List; -import software.amazon.smithy.lsp.document.Document; /** * A moveable index into a path from the root of a {@link Syntax.Node} to a @@ -25,13 +24,12 @@ public final class NodeCursor { } /** - * @param document The document the node value is within * @param value The node value to create the cursor for * @param documentIndex The index within the document to create the cursor for * @return A node cursor from the start of {@code value} to {@code documentIndex} * within {@code document}. */ - public static NodeCursor create(Document document, Syntax.Node value, int documentIndex) { + public static NodeCursor create(Syntax.Node value, int documentIndex) { List edges = new ArrayList<>(); NodeCursor cursor = new NodeCursor(edges); @@ -47,7 +45,7 @@ public static NodeCursor create(Document document, Syntax.Node value, int docume Syntax.Node.Kvp lastKvp = null; for (Syntax.Node.Kvp kvp : kvps.kvps()) { if (kvp.key.isIn(documentIndex)) { - String key = kvp.key.copyValueFrom(document); + String key = kvp.key.stringValue(); edges.add(new NodeCursor.Key(key, kvps)); edges.add(new NodeCursor.Terminal(kvp)); return cursor; @@ -56,7 +54,7 @@ public static NodeCursor create(Document document, Syntax.Node value, int docume lastKvp = kvp; break; } - String key = kvp.key.copyValueFrom(document); + String key = kvp.key.stringValue(); edges.add(new NodeCursor.ValueForKey(key, kvps)); next = kvp.value; break iteration; @@ -65,7 +63,7 @@ public static NodeCursor create(Document document, Syntax.Node value, int docume } } if (lastKvp != null && lastKvp.value == null) { - edges.add(new NodeCursor.ValueForKey(lastKvp.key.copyValueFrom(document), kvps)); + edges.add(new NodeCursor.ValueForKey(lastKvp.key.stringValue(), kvps)); edges.add(new NodeCursor.Terminal(lastKvp)); return cursor; } @@ -139,7 +137,7 @@ public Edge previous() { * @return Whether the path consists of a single, terminal, node. */ public boolean isTerminal() { - return edges.size() == 1 && edges.get(0) instanceof Terminal; + return edges.size() == 1 && edges.getFirst() instanceof Terminal; } /** diff --git a/src/main/java/software/amazon/smithy/lsp/syntax/Parser.java b/src/main/java/software/amazon/smithy/lsp/syntax/Parser.java index 087050f1..9f2684be 100644 --- a/src/main/java/software/amazon/smithy/lsp/syntax/Parser.java +++ b/src/main/java/software/amazon/smithy/lsp/syntax/Parser.java @@ -68,10 +68,34 @@ void parseIdl() { Syntax.Statement.Err err = new Syntax.Statement.Err(e.message); err.start = position(); err.end = position(); - errors.add(err); + addError(err); } } + void parseIdlBetween(int start, int end) { + try { + rewindTo(start); + ws(); + while (!eof() && position() < end) { + statement(); + ws(); + } + } catch (Parser.Eof e) { + Syntax.Statement.Err err = new Syntax.Statement.Err(e.message); + err.start = position(); + err.end = position(); + addError(err); + } + } + + private void addStatement(Syntax.Statement statement) { + statements.add(statement); + } + + private void addError(Syntax.Err err) { + errors.add(err); + } + private void setStart(Syntax.Item item) { if (eof()) { item.start = position() - 1; @@ -166,7 +190,7 @@ private Syntax.Node traitValueKvps(int from) { Syntax.Node.Err kvpErr = kvp(kvps, ')'); if (kvpErr != null) { - errors.add(kvpErr); + addError(kvpErr); } ws(); @@ -181,7 +205,8 @@ private Syntax.Node nodeIdent() { do { skip(); } while (!isWs() && !isStructuralBreakpoint() && !eof()); - return new Syntax.Ident(start, position()); + int end = position(); + return new Syntax.Ident(start, end, document.copySpan(start, end)); } private Syntax.Node.Obj obj() { @@ -198,7 +223,7 @@ private Syntax.Node.Obj obj() { Syntax.Err kvpErr = kvp(obj.kvps, '}'); if (kvpErr != null) { - errors.add(kvpErr); + addError(kvpErr); } ws(); @@ -207,7 +232,7 @@ private Syntax.Node.Obj obj() { Syntax.Node.Err err = new Syntax.Node.Err("missing }"); setStart(err); setEnd(err); - errors.add(err); + addError(err); setEnd(obj); return obj; @@ -249,7 +274,7 @@ private Syntax.Node.Err kvp(Syntax.Node.Kvps kvps, char close) { return nodeErr("unexpected eof"); } else { if (err != null) { - errors.add(err); + addError(err); } err = nodeErr("expected :"); @@ -257,7 +282,7 @@ private Syntax.Node.Err kvp(Syntax.Node.Kvps kvps, char close) { if (is(close)) { if (err != null) { - errors.add(err); + addError(err); } return nodeErr("expected value"); @@ -269,7 +294,7 @@ private Syntax.Node.Err kvp(Syntax.Node.Kvps kvps, char close) { setEnd(kvp); } if (err != null) { - errors.add(err); + addError(err); } return nodeErr("expected value"); @@ -278,7 +303,7 @@ private Syntax.Node.Err kvp(Syntax.Node.Kvps kvps, char close) { Syntax.Node value = parseNode(); if (value instanceof Syntax.Node.Err e) { if (err != null) { - errors.add(err); + addError(err); } err = e; } else if (err == null) { @@ -306,7 +331,7 @@ private Syntax.Node.Arr arr() { Syntax.Node elem = parseNode(); if (elem instanceof Syntax.Node.Err e) { - errors.add(e); + addError(e); } else { arr.elements.add(elem); } @@ -314,7 +339,7 @@ private Syntax.Node.Arr arr() { } Syntax.Node.Err err = nodeErr("missing ]"); - errors.add(err); + addError(err); setEnd(arr); return arr; @@ -340,17 +365,14 @@ private Syntax.Node str() { } rewindTo(end + 3); - Syntax.Node.Str str = new Syntax.Node.Str(); - str.start = start; - setEnd(str); - return str; + int strEnd = position(); + return new Syntax.Node.Str(start, strEnd, document.copySpan(start + 3, strEnd - 3)); } + // Empty string skip(); - Syntax.Node.Str str = new Syntax.Node.Str(); - str.start = start; - setEnd(str); - return str; + int strEnd = position(); + return new Syntax.Node.Str(start, strEnd, ""); } int last = '"'; @@ -359,10 +381,8 @@ private Syntax.Node str() { while (!isNl() && !eof()) { if (is('"') && last != '\\') { skip(); // '"' - Syntax.Node.Str str = new Syntax.Node.Str(); - str.start = start; - setEnd(str); - return str; + int strEnd = position(); + return new Syntax.Node.Str(start, strEnd, document.copySpan(start + 1, strEnd - 1)); } last = peek(); skip(); @@ -418,6 +438,12 @@ private Syntax.Node.Err nodeErr(String message) { return err; } + private void skipUntilStatementStart() { + while (!is('@') && !is('$') && !isIdentStart() && !eof()) { + skip(); + } + } + private void statement() { if (is('@')) { traitApplication(null); @@ -429,7 +455,8 @@ private void statement() { Syntax.Ident ident = ident(); if (ident.isEmpty()) { if (!isWs()) { - skip(); + // TODO: Capture all this in an error + skipUntilStatementStart(); } return; } @@ -440,7 +467,7 @@ private void statement() { Syntax.Statement.Incomplete incomplete = new Syntax.Statement.Incomplete(ident); incomplete.start = start; incomplete.end = position(); - statements.add(incomplete); + addStatement(incomplete); if (!isWs()) { skip(); @@ -448,7 +475,7 @@ private void statement() { return; } - String identCopy = ident.copyValueFrom(document); + String identCopy = ident.stringValue(); switch (identCopy) { case "apply" -> { @@ -474,7 +501,7 @@ private void statement() { Syntax.Statement.ShapeDef shapeDef = new Syntax.Statement.ShapeDef(ident, name); shapeDef.start = start; setEnd(shapeDef); - statements.add(shapeDef); + addStatement(shapeDef); sp(); optionalForResourceAndMixins(); @@ -530,7 +557,7 @@ private void statement() { private Syntax.Statement.Block startBlock(Syntax.Statement.Block parent) { Syntax.Statement.Block block = new Syntax.Statement.Block(parent, statements.size()); setStart(block); - statements.add(block); + addStatement(block); if (is('{')) { skip(); } else { @@ -564,7 +591,7 @@ private void operationMembers(Syntax.Statement.Block parent) { var memberDef = new Syntax.Statement.MemberDef(parent, memberName); memberDef.start = opMemberStart; setEnd(memberDef); - statements.add(memberDef); + addStatement(memberDef); ws(); continue; } @@ -585,14 +612,14 @@ private void operationMembers(Syntax.Statement.Block parent) { opMemberDef.colonPos = colonPos; opMemberDef.target = ident(); setEnd(opMemberDef); - statements.add(opMemberDef); + addStatement(opMemberDef); } else { var nodeMemberDef = new Syntax.Statement.NodeMemberDef(parent, memberName); nodeMemberDef.start = opMemberStart; nodeMemberDef.colonPos = colonPos; nodeMemberDef.value = parseNode(); setEnd(nodeMemberDef); - statements.add(nodeMemberDef); + addStatement(nodeMemberDef); } ws(); @@ -605,7 +632,7 @@ private void control() { Syntax.Ident ident = ident(); Syntax.Statement.Control control = new Syntax.Statement.Control(ident); control.start = start; - statements.add(control); + addStatement(control); sp(); if (!is(':')) { @@ -626,7 +653,7 @@ private void apply(int start, Syntax.Ident name) { Syntax.Statement.Apply apply = new Syntax.Statement.Apply(name); apply.start = start; setEnd(apply); - statements.add(apply); + addStatement(apply); sp(); if (is('@')) { @@ -653,7 +680,7 @@ private void apply(int start, Syntax.Ident name) { private void metadata(int start, Syntax.Ident name) { Syntax.Statement.Metadata metadata = new Syntax.Statement.Metadata(name); metadata.start = start; - statements.add(metadata); + addStatement(metadata); sp(); if (!is('=')) { @@ -673,33 +700,33 @@ private void use(int start, Syntax.Ident name) { Syntax.Statement.Use use = new Syntax.Statement.Use(name); use.start = start; setEnd(use); - statements.add(use); + addStatement(use); } private void namespace(int start, Syntax.Ident name) { Syntax.Statement.Namespace namespace = new Syntax.Statement.Namespace(name); namespace.start = start; setEnd(namespace); - statements.add(namespace); + addStatement(namespace); } private void optionalForResourceAndMixins() { int maybeStart = position(); Syntax.Ident maybe = optIdent(); - if (maybe.copyValueFrom(document).equals("for")) { + if (maybe.stringValue().equals("for")) { sp(); Syntax.Ident resource = ident(); Syntax.Statement.ForResource forResource = new Syntax.Statement.ForResource(resource); forResource.start = maybeStart; - statements.add(forResource); + addStatement(forResource); ws(); setEnd(forResource); maybeStart = position(); maybe = optIdent(); } - if (maybe.copyValueFrom(document).equals("with")) { + if (maybe.stringValue().equals("with")) { sp(); Syntax.Statement.Mixins mixins = new Syntax.Statement.Mixins(); mixins.start = maybeStart; @@ -710,7 +737,7 @@ private void optionalForResourceAndMixins() { // If we're on an identifier, just assume the [ was meant to be there if (!isIdentStart()) { setEnd(mixins); - statements.add(mixins); + addStatement(mixins); return; } } else { @@ -731,7 +758,7 @@ private void optionalForResourceAndMixins() { } setEnd(mixins); - statements.add(mixins); + addStatement(mixins); } } @@ -745,7 +772,7 @@ private void member(Syntax.Statement.Block parent) { Syntax.Ident name = ident(); Syntax.Statement.MemberDef memberDef = new Syntax.Statement.MemberDef(parent, name); memberDef.start = start; - statements.add(memberDef); + addStatement(memberDef); sp(); if (is(':')) { @@ -755,7 +782,7 @@ private void member(Syntax.Statement.Block parent) { addErr(position(), position(), "expected :"); if (isWs() || is('}')) { setEnd(memberDef); - statements.add(memberDef); + addStatement(memberDef); return; } } @@ -786,7 +813,7 @@ private void enumMember(Syntax.Statement.Block parent) { Syntax.Ident name = ident(); var enumMemberDef = new Syntax.Statement.EnumMemberDef(parent, name); enumMemberDef.start = start; - statements.add(enumMemberDef); + addStatement(enumMemberDef); ws(); if (is('=')) { @@ -809,14 +836,14 @@ private void elidedMember(Syntax.Statement.Block parent) { var elidedMemberDef = new Syntax.Statement.ElidedMemberDef(parent, name); elidedMemberDef.start = start; setEnd(elidedMemberDef); - statements.add(elidedMemberDef); + addStatement(elidedMemberDef); } private void inlineMember(Syntax.Statement.Block parent, int start, Syntax.Ident name) { var inlineMemberDef = new Syntax.Statement.InlineMemberDef(parent, name); inlineMemberDef.start = start; setEnd(inlineMemberDef); - statements.add(inlineMemberDef); + addStatement(inlineMemberDef); ws(); while (is('@')) { @@ -851,7 +878,7 @@ private void nodeMember(Syntax.Statement.Block parent) { addErr(position(), position(), "expected :"); if (isWs() || is('}')) { setEnd(nodeMemberDef); - statements.add(nodeMemberDef); + addStatement(nodeMemberDef); return; } } @@ -863,7 +890,7 @@ private void nodeMember(Syntax.Statement.Block parent) { nodeMemberDef.value = parseNode(); } setEnd(nodeMemberDef); - statements.add(nodeMemberDef); + addStatement(nodeMemberDef); } private void traitApplication(Syntax.Statement.Block parent) { @@ -872,7 +899,7 @@ private void traitApplication(Syntax.Statement.Block parent) { Syntax.Ident id = ident(); var application = new Syntax.Statement.TraitApplication(parent, id); application.start = startPos; - statements.add(application); + addStatement(application); if (is('(')) { int start = position(); @@ -911,14 +938,14 @@ private Syntax.Ident ident() { addErr(start, end, "expected identifier"); return Syntax.Ident.EMPTY; } - return new Syntax.Ident(start, end); + return new Syntax.Ident(start, end, document.copySpan(start, end)); } private void addErr(int start, int end, String message) { Syntax.Statement.Err err = new Syntax.Statement.Err(message); err.start = start; err.end = end; - errors.add(err); + addError(err); } private void recoverToMemberStart() { diff --git a/src/main/java/software/amazon/smithy/lsp/syntax/StatementView.java b/src/main/java/software/amazon/smithy/lsp/syntax/StatementView.java new file mode 100644 index 00000000..b9884e38 --- /dev/null +++ b/src/main/java/software/amazon/smithy/lsp/syntax/StatementView.java @@ -0,0 +1,228 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.lsp.syntax; + +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * An IDL parse result at a specific position within the underlying document. + * + * @param parseResult The IDL parse result + * @param statementIndex The index of the statement {@code documentIndex} is within + * @param documentIndex The index within the underlying document + */ +public record StatementView(Syntax.IdlParseResult parseResult, int statementIndex, int documentIndex) { + + /** + * @param parseResult The parse result to create a view of + * @return An optional view of the first statement in the given parse result, + * or empty if the parse result has no statements + */ + public static Optional createAtStart(Syntax.IdlParseResult parseResult) { + if (parseResult.statements().isEmpty()) { + return Optional.empty(); + } + + return createAt(parseResult, parseResult.statements().getFirst().start()); + } + + /** + * @param parseResult The parse result to create a view of + * @param documentIndex The index within the underlying document + * @return An optional view of the statement the given documentIndex is within + * in the given parse result, or empty if the index is not within a statement + */ + public static Optional createAt(Syntax.IdlParseResult parseResult, int documentIndex) { + if (documentIndex < 0) { + return Optional.empty(); + } + + int statementIndex = statementIndex(parseResult.statements(), documentIndex); + if (statementIndex < 0) { + return Optional.empty(); + } + + return Optional.of(new StatementView(parseResult, statementIndex, documentIndex)); + } + + private static int statementIndex(List statements, int position) { + int low = 0; + int up = statements.size() - 1; + + while (low <= up) { + int mid = (low + up) / 2; + Syntax.Statement statement = statements.get(mid); + if (statement.isIn(position)) { + if (statement instanceof Syntax.Statement.Block) { + return statementIndexBetween(statements, mid, up, position); + } else { + return mid; + } + } else if (statement.start() > position) { + up = mid - 1; + } else if (statement.end() < position) { + low = mid + 1; + } else { + return -1; + } + } + + Syntax.Statement last = statements.get(up); + if (last instanceof Syntax.Statement.MemberStatement memberStatement) { + // Note: parent() can be null for TraitApplication. + if (memberStatement.parent() != null && memberStatement.parent().isIn(position)) { + return memberStatement.parent().statementIndex(); + } + } + + return -1; + } + + private static int statementIndexBetween(List statements, int lower, int upper, int position) { + int ogLower = lower; + lower += 1; + while (lower <= upper) { + int mid = (lower + upper) / 2; + Syntax.Statement statement = statements.get(mid); + if (statement.isIn(position)) { + // Could have nested blocks, like in an inline structure definition + if (statement instanceof Syntax.Statement.Block) { + return statementIndexBetween(statements, mid, upper, position); + } + return mid; + } else if (statement.start() > position) { + upper = mid - 1; + } else if (statement.end() < position) { + lower = mid + 1; + } else { + return ogLower; + } + } + + return ogLower; + } + + /** + * @return The non-nullable statement that {@link #documentIndex()} is within + */ + public Syntax.Statement getStatement() { + return parseResult.statements().get(statementIndex); + } + + /** + * @param documentIndex The index within the underlying document + * @return The optional statement the given index is within + */ + public Optional getStatementAt(int documentIndex) { + int statementIndex = statementIndex(parseResult.statements(), documentIndex); + if (statementIndex < 0) { + return Optional.empty(); + } + return Optional.of(parseResult.statements().get(statementIndex)); + } + + /** + * @return The nearest shape def before this view + */ + public Syntax.Statement.ShapeDef nearestShapeDefBefore() { + int searchStatementIndex = statementIndex - 1; + while (searchStatementIndex >= 0) { + Syntax.Statement statement = parseResult.statements().get(searchStatementIndex); + if (statement instanceof Syntax.Statement.ShapeDef shapeDef) { + return shapeDef; + } + searchStatementIndex--; + } + return null; + } + + /** + * @return The nearest for resource and mixins before this view + */ + public Syntax.ForResourceAndMixins nearestForResourceAndMixinsBefore() { + int searchStatementIndex = statementIndex; + while (searchStatementIndex >= 0) { + Syntax.Statement searchStatement = parseResult.statements().get(searchStatementIndex); + if (searchStatement instanceof Syntax.Statement.Block) { + Syntax.Statement.ForResource forResource = null; + Syntax.Statement.Mixins mixins = null; + + int lastSearchIndex = searchStatementIndex - 2; + searchStatementIndex--; + while (searchStatementIndex >= 0 && searchStatementIndex >= lastSearchIndex) { + Syntax.Statement candidateStatement = parseResult.statements().get(searchStatementIndex); + if (candidateStatement instanceof Syntax.Statement.Mixins m) { + mixins = m; + } else if (candidateStatement instanceof Syntax.Statement.ForResource f) { + forResource = f; + } + searchStatementIndex--; + } + + return new Syntax.ForResourceAndMixins(forResource, mixins); + } + searchStatementIndex--; + } + + return new Syntax.ForResourceAndMixins(null, null); + } + + /** + * @return The names of all the other members around this view + */ + public Set otherMemberNames() { + Set found = new HashSet<>(); + int searchIndex = statementIndex; + int lastMemberStatementIndex = statementIndex; + while (searchIndex >= 0) { + Syntax.Statement statement = parseResult.statements().get(searchIndex); + if (statement instanceof Syntax.Statement.Block block) { + lastMemberStatementIndex = block.lastStatementIndex(); + break; + } else if (searchIndex != statementIndex) { + addMemberName(found, statement); + } + searchIndex--; + } + searchIndex = statementIndex + 1; + while (searchIndex <= lastMemberStatementIndex) { + Syntax.Statement statement = parseResult.statements().get(searchIndex); + addMemberName(found, statement); + searchIndex++; + } + return found; + } + + private static void addMemberName(Set memberNames, Syntax.Statement statement) { + switch (statement) { + case Syntax.Statement.MemberDef def -> memberNames.add(def.name().stringValue()); + case Syntax.Statement.NodeMemberDef def -> memberNames.add(def.name().stringValue()); + case Syntax.Statement.InlineMemberDef def -> memberNames.add(def.name().stringValue()); + case Syntax.Statement.ElidedMemberDef def -> memberNames.add(def.name().stringValue()); + default -> { + } + } + } + + /** + * @return The nearest shape def after this view + */ + public Syntax.Statement.ShapeDef nearestShapeDefAfter() { + for (int i = statementIndex + 1; i < parseResult.statements().size(); i++) { + Syntax.Statement statement = parseResult.statements().get(i); + if (statement instanceof Syntax.Statement.ShapeDef shapeDef) { + return shapeDef; + } else if (!(statement instanceof Syntax.Statement.TraitApplication)) { + return null; + } + } + + return null; + } +} diff --git a/src/main/java/software/amazon/smithy/lsp/syntax/Syntax.java b/src/main/java/software/amazon/smithy/lsp/syntax/Syntax.java index d6740fbf..e6b27667 100644 --- a/src/main/java/software/amazon/smithy/lsp/syntax/Syntax.java +++ b/src/main/java/software/amazon/smithy/lsp/syntax/Syntax.java @@ -9,15 +9,18 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; -import org.eclipse.lsp4j.Range; import software.amazon.smithy.lsp.document.Document; +import software.amazon.smithy.lsp.document.DocumentImports; +import software.amazon.smithy.lsp.document.DocumentNamespace; +import software.amazon.smithy.lsp.document.DocumentParser; +import software.amazon.smithy.lsp.document.DocumentVersion; /** * Provides classes that represent the syntactic structure of a Smithy file, and * a means to parse Smithy files into those classes. *

*

IDL Syntax

- * The result of a parse, {@link Syntax.IdlParse}, is a list of {@link Statement}, + * The result of a parse, {@link IdlParseResult}, is a list of {@link Statement}, * rather than a syntax tree. For example, the following: * * \@someTrait @@ -58,10 +61,6 @@ * - Any `final` field is definitely assigned, whereas any non `final` field * may be null (other than {@link Statement#start} and {@link Statement#end}, * which are definitely assigned). - * - Concrete text is not stored in {@link Statement}s. Instead, - * {@link Statement#start} and {@link Statement#end} can be used to copy a - * value from the underlying document as needed. This is done to reduce the - * memory footprint of parsing. *

*

Node Syntax

* This class also provides classes for the JSON-like Smithy Node, which can @@ -74,28 +73,66 @@ public final class Syntax { private Syntax() { } - public record IdlParse(List statements, List errors) {} + /** + * Wrapper for {@link Statement.ForResource} and {@link Statement.Mixins}, + * which often are used together. + * + * @param forResource The nullable for-resource statement. + * @param mixins The nullable mixins statement. + */ + public record ForResourceAndMixins(Statement.ForResource forResource, Statement.Mixins mixins) {} - public record NodeParse(Node value, List errors) {} + /** + * The result of parsing an IDL document, containing some extra computed + * info that is used often. + * + * @param statements The parsed statements. + * @param errors The errors that occurred during parsing. + * @param version The IDL version that was parsed. + * @param namespace The namespace that was parsed + * @param imports The imports that were parsed. + */ + public record IdlParseResult( + List statements, + List errors, + DocumentVersion version, + DocumentNamespace namespace, + DocumentImports imports + ) {} /** - * @param document The document to parse - * @return The IDL parse result + * @param document The document to parse. + * @return The IDL parse result. */ - public static IdlParse parseIdl(Document document) { + public static IdlParseResult parseIdl(Document document) { Parser parser = new Parser(document); parser.parseIdl(); - return new IdlParse(parser.statements, parser.errors); + List statements = parser.statements; + DocumentParser documentParser = DocumentParser.forStatements(document, statements); + return new IdlParseResult( + statements, + parser.errors, + documentParser.documentVersion(), + documentParser.documentNamespace(), + documentParser.documentImports()); } /** - * @param document The document to parse - * @return The Node parse result + * The result of parsing a Node document. + * + * @param value The parsed node. + * @param errors The errors that occurred during parsing. */ - public static NodeParse parseNode(Document document) { + public record NodeParseResult(Node value, List errors) {} + + /** + * @param document The document to parse. + * @return The Node parse result. + */ + public static NodeParseResult parseNode(Document document) { Parser parser = new Parser(document); Node node = parser.parseNode(); - return new NodeParse(node, parser.errors); + return new NodeParseResult(node, parser.errors); } /** @@ -121,14 +158,6 @@ public final int end() { public final boolean isIn(int pos) { return start <= pos && end > pos; } - - /** - * @param document The document to get the range in - * @return The range of this item in the given {@code document} - */ - public final Range rangeIn(Document document) { - return document.rangeBetween(start, end); - } } /** @@ -264,12 +293,16 @@ public List elements() { * identifiers, so this class a single subclass {@link Ident}. */ public static sealed class Str extends Node { - /** - * @param document Document to copy the string value from - * @return The literal string value, excluding enclosing "" - */ - public String copyValueFrom(Document document) { - return document.copySpan(start + 1, end - 1); // Don't include the '"'s + final String value; + + Str(int start, int end, String value) { + this.start = start; + this.end = end; + this.value = value; + } + + public String stringValue() { + return value; } } @@ -515,8 +548,6 @@ public List mixins() { * from a statement to the {@link Block} it resides within when * searching for the statement corresponding to a given character offset * in a document.

- * - * @see SyntaxSearch#statementIndex(List, int) */ abstract static sealed class MemberStatement extends Statement { final Block parent; @@ -723,24 +754,15 @@ public String message() { * (i.e. `.`, `#`, `$`, `_` digits, alphas). */ public static final class Ident extends Node.Str { - static final Ident EMPTY = new Ident(-1, -1); + static final Ident EMPTY = new Ident(-1, -1, ""); - Ident(int start, int end) { - this.start = start; - this.end = end; + Ident(int start, int end, String value) { + super(start, end, value); } public boolean isEmpty() { return (start - end) == 0; } - - @Override - public String copyValueFrom(Document document) { - if (start < 0 && end < 0) { - return ""; - } - return document.copySpan(start, end); // There's no '"'s here - } } /** diff --git a/src/main/java/software/amazon/smithy/lsp/syntax/SyntaxSearch.java b/src/main/java/software/amazon/smithy/lsp/syntax/SyntaxSearch.java deleted file mode 100644 index ae3720b4..00000000 --- a/src/main/java/software/amazon/smithy/lsp/syntax/SyntaxSearch.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.lsp.syntax; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import software.amazon.smithy.lsp.document.Document; - -/** - * Low-level API to query specific information about {@link Syntax.Statement}s - * and {@link Syntax.Node}s. - */ -public final class SyntaxSearch { - private SyntaxSearch() { - } - - /** - * @param statements The statements to search - * @param position The character offset in the document - * @return The index of the statement in the list of statements that the - * given position is within, or -1 if it was not found. - */ - public static int statementIndex(List statements, int position) { - int low = 0; - int up = statements.size() - 1; - - while (low <= up) { - int mid = (low + up) / 2; - Syntax.Statement statement = statements.get(mid); - if (statement.isIn(position)) { - if (statement instanceof Syntax.Statement.Block) { - return statementIndexBetween(statements, mid, up, position); - } else { - return mid; - } - } else if (statement.start() > position) { - up = mid - 1; - } else if (statement.end() < position) { - low = mid + 1; - } else { - return -1; - } - } - - Syntax.Statement last = statements.get(up); - if (last instanceof Syntax.Statement.MemberStatement memberStatement) { - // Note: parent() can be null for TraitApplication. - if (memberStatement.parent() != null && memberStatement.parent().isIn(position)) { - return memberStatement.parent().statementIndex(); - } - } - - return -1; - } - - private static int statementIndexBetween(List statements, int lower, int upper, int position) { - int ogLower = lower; - lower += 1; - while (lower <= upper) { - int mid = (lower + upper) / 2; - Syntax.Statement statement = statements.get(mid); - if (statement.isIn(position)) { - // Could have nested blocks, like in an inline structure definition - if (statement instanceof Syntax.Statement.Block) { - return statementIndexBetween(statements, mid, upper, position); - } - return mid; - } else if (statement.start() > position) { - upper = mid - 1; - } else if (statement.end() < position) { - lower = mid + 1; - } else { - return ogLower; - } - } - - return ogLower; - } - - /** - * @param statements The statements to search - * @param memberStatementIndex The index of the statement to search from - * @return The closest shape def statement appearing before the given index - * or {@code null} if none was found. - */ - public static Syntax.Statement.ShapeDef closestShapeDefBeforeMember( - List statements, - int memberStatementIndex - ) { - int searchStatementIdx = memberStatementIndex - 1; - while (searchStatementIdx >= 0) { - Syntax.Statement searchStatement = statements.get(searchStatementIdx); - if (searchStatement instanceof Syntax.Statement.ShapeDef shapeDef) { - return shapeDef; - } - searchStatementIdx--; - } - return null; - } - - /** - * @param forResource The nullable for-resource statement - * @param mixins The nullable mixins statement - */ - public record ForResourceAndMixins(Syntax.Statement.ForResource forResource, Syntax.Statement.Mixins mixins) {} - - /** - * @param statements The statements to search - * @param memberStatementIndex The index of the statement to search from - * @return The closest adjacent {@link Syntax.Statement.ForResource} and - * {@link Syntax.Statement.Mixins} to the statement at the given index. - */ - public static ForResourceAndMixins closestForResourceAndMixinsBeforeMember( - List statements, - int memberStatementIndex - ) { - int searchStatementIndex = memberStatementIndex; - while (searchStatementIndex >= 0) { - Syntax.Statement searchStatement = statements.get(searchStatementIndex); - if (searchStatement instanceof Syntax.Statement.Block) { - Syntax.Statement.ForResource forResource = null; - Syntax.Statement.Mixins mixins = null; - - int lastSearchIndex = searchStatementIndex - 2; - searchStatementIndex--; - while (searchStatementIndex >= 0 && searchStatementIndex >= lastSearchIndex) { - Syntax.Statement candidateStatement = statements.get(searchStatementIndex); - if (candidateStatement instanceof Syntax.Statement.Mixins m) { - mixins = m; - } else if (candidateStatement instanceof Syntax.Statement.ForResource f) { - forResource = f; - } - searchStatementIndex--; - } - - return new ForResourceAndMixins(forResource, mixins); - } - searchStatementIndex--; - } - - return new ForResourceAndMixins(null, null); - } - - /** - * @param document The document to search within - * @param statements The statements to search - * @param memberStatementIndex The index of the member statement to search around - * @return The names of other members around (but not including) the member at - * {@code memberStatementIndex}. - */ - public static Set otherMemberNames( - Document document, - List statements, - int memberStatementIndex - ) { - Set found = new HashSet<>(); - int searchIndex = memberStatementIndex; - int lastMemberStatementIndex = memberStatementIndex; - while (searchIndex >= 0) { - Syntax.Statement statement = statements.get(searchIndex); - if (statement instanceof Syntax.Statement.Block block) { - lastMemberStatementIndex = block.lastStatementIndex(); - break; - } else if (searchIndex != memberStatementIndex) { - addMemberName(document, found, statement); - } - searchIndex--; - } - searchIndex = memberStatementIndex + 1; - while (searchIndex <= lastMemberStatementIndex) { - Syntax.Statement statement = statements.get(searchIndex); - addMemberName(document, found, statement); - searchIndex++; - } - return found; - } - - private static void addMemberName(Document document, Set memberNames, Syntax.Statement statement) { - switch (statement) { - case Syntax.Statement.MemberDef def -> memberNames.add(def.name().copyValueFrom(document)); - case Syntax.Statement.NodeMemberDef def -> memberNames.add(def.name().copyValueFrom(document)); - case Syntax.Statement.InlineMemberDef def -> memberNames.add(def.name().copyValueFrom(document)); - case Syntax.Statement.ElidedMemberDef def -> memberNames.add(def.name().copyValueFrom(document)); - default -> { - } - } - } - - /** - * @param statements The statements to search - * @param traitStatementIndex The index of the trait statement to search from - * @return The closest shape def statement after {@code traitStatementIndex}, - * or null if none was found. - */ - public static Syntax.Statement.ShapeDef closestShapeDefAfterTrait( - List statements, - int traitStatementIndex - ) { - for (int i = traitStatementIndex + 1; i < statements.size(); i++) { - Syntax.Statement statement = statements.get(i); - if (statement instanceof Syntax.Statement.ShapeDef shapeDef) { - return shapeDef; - } else if (!(statement instanceof Syntax.Statement.TraitApplication)) { - return null; - } - } - - return null; - } -} diff --git a/src/test/java/software/amazon/smithy/lsp/SmithyLanguageServerTest.java b/src/test/java/software/amazon/smithy/lsp/SmithyLanguageServerTest.java index dde93b90..26df2bfa 100644 --- a/src/test/java/software/amazon/smithy/lsp/SmithyLanguageServerTest.java +++ b/src/test/java/software/amazon/smithy/lsp/SmithyLanguageServerTest.java @@ -17,7 +17,6 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.startsWith; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static software.amazon.smithy.lsp.LspMatchers.diagnosticWithMessage; import static software.amazon.smithy.lsp.LspMatchers.hasLabel; import static software.amazon.smithy.lsp.LspMatchers.hasText; @@ -27,7 +26,6 @@ import static software.amazon.smithy.lsp.SmithyMatchers.hasValue; import static software.amazon.smithy.lsp.UtilMatchers.anOptionalOf; import static software.amazon.smithy.lsp.document.DocumentTest.safeString; -import static software.amazon.smithy.lsp.project.ProjectTest.toPath; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; @@ -43,30 +41,26 @@ import java.util.stream.Collectors; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.CompletionParams; -import org.eclipse.lsp4j.DefinitionParams; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.DidChangeTextDocumentParams; 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.FileChangeType; import org.eclipse.lsp4j.FormattingOptions; import org.eclipse.lsp4j.Hover; -import org.eclipse.lsp4j.HoverParams; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.Range; -import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextEdit; -import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.services.LanguageClient; import org.junit.jupiter.api.Test; import software.amazon.smithy.build.model.MavenConfig; import software.amazon.smithy.build.model.SmithyBuildConfig; +import software.amazon.smithy.lsp.diagnostics.SmithyDiagnostics; import software.amazon.smithy.lsp.document.Document; import software.amazon.smithy.lsp.ext.SelectorParams; import software.amazon.smithy.lsp.project.Project; @@ -97,296 +91,6 @@ public void runsSelector() throws Exception { assertThat(locations, not(empty())); } - @Test - public void completion() throws Exception { - String model = safeString(""" - $version: "2" - namespace com.foo - - structure Foo { - bar: String - } - - @default(0) - integer Bar - """); - TestWorkspace workspace = TestWorkspace.singleModel(model); - SmithyLanguageServer server = initFromWorkspace(workspace); - - String uri = workspace.getUri("main.smithy"); - - // String - CompletionParams memberTargetParams = new RequestBuilders.PositionRequest() - .uri(uri) - .line(4) - .character(10) - .buildCompletion(); - // @default - CompletionParams traitParams = new RequestBuilders.PositionRequest() - .uri(uri) - .line(7) - .character(2) - .buildCompletion(); - CompletionParams wsParams = new RequestBuilders.PositionRequest() - .uri(uri) - .line(2) - .character(1) - .buildCompletion(); - - List memberTargetCompletions = server.completion(memberTargetParams).get().getLeft(); - List traitCompletions = server.completion(traitParams).get().getLeft(); - List wsCompletions = server.completion(wsParams).get().getLeft(); - - assertThat(memberTargetCompletions, containsInAnyOrder(hasLabel("String"))); - assertThat(traitCompletions, containsInAnyOrder(hasLabel("default"))); - assertThat(wsCompletions, empty()); - } - - @Test - public void completionImports() throws Exception { - String model1 = safeString(""" - $version: "2" - namespace com.foo - - structure Foo { - } - """); - String model2 = safeString(""" - $version: "2" - namespace com.bar - - string Bar - """); - TestWorkspace workspace = TestWorkspace.multipleModels(model1, model2); - SmithyLanguageServer server = initFromWorkspace(workspace); - - String uri = workspace.getUri("model-0.smithy"); - - DidOpenTextDocumentParams openParams = new RequestBuilders.DidOpen() - .uri(uri) - .text(model1) - .build(); - server.didOpen(openParams); - - DidChangeTextDocumentParams changeParams = new RequestBuilders.DidChange() - .uri(uri) - .version(2) - .range(new RangeBuilder() - .startLine(3) - .startCharacter(15) - .endLine(3) - .endCharacter(15) - .build()) - .text(safeString("\n bar: Ba")) - .build(); - server.didChange(changeParams); - - // bar: Ba - CompletionParams completionParams = new RequestBuilders.PositionRequest() - .uri(uri) - .line(4) - .character(11) - .buildCompletion(); - List completions = server.completion(completionParams).get().getLeft(); - - assertThat(completions, containsInAnyOrder(hasLabel("Bar"))); - - Document document = server.getFirstProject().getDocument(uri); - // TODO: The server puts the 'use' on the wrong line - assertThat(completions.get(0).getAdditionalTextEdits(), containsInAnyOrder(makesEditedDocument(document, safeString(""" - $version: "2" - namespace com.foo - use com.bar#Bar - - structure Foo { - bar: Ba - } - """)))); - } - - @Test - public void definition() throws Exception { - String model = safeString(""" - $version: "2" - namespace com.foo - - @trait - string myTrait - - structure Foo { - bar: Baz - } - - @myTrait("") - string Baz - """); - TestWorkspace workspace = TestWorkspace.singleModel(model); - SmithyLanguageServer server = initFromWorkspace(workspace); - - String uri = workspace.getUri("main.smithy"); - - // bar: Baz - DefinitionParams memberTargetParams = new RequestBuilders.PositionRequest() - .uri(uri) - .line(7) - .character(9) - .buildDefinition(); - // @myTrait - DefinitionParams traitParams = new RequestBuilders.PositionRequest() - .uri(uri) - .line(10) - .character(1) - .buildDefinition(); - DefinitionParams wsParams = new RequestBuilders.PositionRequest() - .uri(uri) - .line(2) - .character(0) - .buildDefinition(); - - List memberTargetLocations = server.definition(memberTargetParams).get().getLeft(); - List traitLocations = server.definition(traitParams).get().getLeft(); - List wsLocations = server.definition(wsParams).get().getLeft(); - - Document document = server.getFirstProject().getDocument(uri); - assertNotNull(document); - - assertThat(memberTargetLocations, hasSize(1)); - Location memberTargetLocation = memberTargetLocations.get(0); - assertThat(memberTargetLocation.getUri(), equalTo(uri)); - assertThat(memberTargetLocation.getRange().getStart(), equalTo(new Position(11, 0))); - // TODO - // assertThat(document.borrowRange(memberTargetLocation.getRange()), equalTo("")); - - assertThat(traitLocations, hasSize(1)); - Location traitLocation = traitLocations.get(0); - assertThat(traitLocation.getUri(), equalTo(uri)); - assertThat(traitLocation.getRange().getStart(), equalTo(new Position(4, 0))); - - assertThat(wsLocations, empty()); - } - - @Test - public void hover() throws Exception { - String model = safeString(""" - $version: "2" - namespace com.foo - - @trait - string myTrait - - structure Foo { - bar: Bar - } - - @myTrait("") - structure Bar { - baz: String - } - """); - TestWorkspace workspace = TestWorkspace.singleModel(model); - SmithyLanguageServer server = initFromWorkspace(workspace); - String uri = workspace.getUri("main.smithy"); - - // bar: Bar - HoverParams memberParams = new RequestBuilders.PositionRequest() - .uri(uri) - .line(7) - .character(9) - .buildHover(); - // @myTrait("") - HoverParams traitParams = new RequestBuilders.PositionRequest() - .uri(uri) - .line(10) - .character(1) - .buildHover(); - HoverParams wsParams = new RequestBuilders.PositionRequest() - .uri(uri) - .line(2) - .character(0) - .buildHover(); - - Hover memberHover = server.hover(memberParams).get(); - Hover traitHover = server.hover(traitParams).get(); - Hover wsHover = server.hover(wsParams).get(); - - assertThat(memberHover.getContents().getRight().getValue(), containsString("structure Bar")); - assertThat(traitHover.getContents().getRight().getValue(), containsString("string myTrait")); - assertThat(wsHover.getContents().getRight().getValue(), equalTo("")); - } - - @Test - public void hoverWithBrokenModel() throws Exception { - String model = safeString(""" - $version: "2" - namespace com.foo - - structure Foo { - bar: Bar - baz: String - } - """); - TestWorkspace workspace = TestWorkspace.singleModel(model); - SmithyLanguageServer server = initFromWorkspace(workspace); - - String uri = workspace.getUri("main.smithy"); - - // baz: String - HoverParams params = new RequestBuilders.PositionRequest() - .uri(uri) - .line(5) - .character(9) - .buildHover(); - Hover hover = server.hover(params).get(); - - assertThat(hover.getContents().getRight().getValue(), containsString("string String")); - } - - @Test - public void documentSymbol() throws Exception { - String model = safeString(""" - $version: "2" - namespace com.foo - - @trait - string myTrait - - structure Foo { - @required - bar: Bar - } - - structure Bar { - @myTrait("foo") - baz: Baz - } - - @myTrait("abc") - integer Baz - """); - TestWorkspace workspace = TestWorkspace.singleModel(model); - SmithyLanguageServer server = initFromWorkspace(workspace); - - String uri = workspace.getUri("main.smithy"); - - server.didOpen(RequestBuilders.didOpen() - .uri(uri) - .build()); - - server.getState().lifecycleManager().waitForAllTasks(); - - DocumentSymbolParams params = new DocumentSymbolParams(new TextDocumentIdentifier(uri)); - List> response = server.documentSymbol(params).get(); - List documentSymbols = response.stream().map(Either::getRight).toList(); - List names = documentSymbols.stream().map(DocumentSymbol::getName).collect(Collectors.toList()); - - assertThat(names, hasItem("myTrait")); - assertThat(names, hasItem("Foo")); - assertThat(names, hasItem("bar")); - assertThat(names, hasItem("Bar")); - assertThat(names, hasItem("baz")); - assertThat(names, hasItem("Baz")); - } - @Test public void formatting() throws Exception { String model = safeString(""" @@ -533,251 +237,6 @@ public void didChangeReloadsModel() throws Exception { containsInAnyOrder(eventWithMessage(containsString("Error creating trait")))); } - @Test - public void didChangeThenDefinition() throws Exception { - String model = safeString(""" - $version: "2" - namespace com.foo - - structure Foo { - bar: Bar - } - - string Bar - """); - TestWorkspace workspace = TestWorkspace.singleModel(model); - SmithyLanguageServer server = initFromWorkspace(workspace); - String uri = workspace.getUri("main.smithy"); - - server.didOpen(new RequestBuilders.DidOpen() - .uri(uri) - .text(model) - .build()); - DefinitionParams definitionParams = new RequestBuilders.PositionRequest() - .uri(uri) - .line(4) - .character(9) - .buildDefinition(); - Location initialLocation = server.definition(definitionParams).get().getLeft().get(0); - assertThat(initialLocation.getUri(), equalTo(uri)); - assertThat(initialLocation.getRange().getStart(), equalTo(new Position(7, 0))); - - RangeBuilder range = new RangeBuilder() - .startLine(5) - .startCharacter(1) - .endLine(5) - .endCharacter(1); - RequestBuilders.DidChange change = new RequestBuilders.DidChange().uri(uri); - server.didChange(change.range(range.build()).text(safeString("\n\n")).build()); - server.didChange(change.range(range.shiftNewLine().shiftNewLine().build()).text("s").build()); - server.didChange(change.range(range.shiftRight().build()).text("t").build()); - server.didChange(change.range(range.shiftRight().build()).text("r").build()); - server.didChange(change.range(range.shiftRight().build()).text("i").build()); - server.didChange(change.range(range.shiftRight().build()).text("n").build()); - server.didChange(change.range(range.shiftRight().build()).text("g").build()); - server.didChange(change.range(range.shiftRight().build()).text(" ").build()); - server.didChange(change.range(range.shiftRight().build()).text("B").build()); - server.didChange(change.range(range.shiftRight().build()).text("a").build()); - server.didChange(change.range(range.shiftRight().build()).text("z").build()); - - server.getState().lifecycleManager().getTask(uri).get(); - - assertThat(server.getFirstProject().getDocument(uri).copyText(), equalTo(safeString(""" - $version: "2" - namespace com.foo - - structure Foo { - bar: Bar - } - - string Baz - - string Bar - """))); - - Location afterChanges = server.definition(definitionParams).get().getLeft().get(0); - assertThat(afterChanges.getUri(), equalTo(uri)); - assertThat(afterChanges.getRange().getStart(), equalTo(new Position(9, 0))); - } - - @Test - public void definitionWithApply() throws Exception { - Path root = toPath(getClass().getResource("project/apply")); - SmithyLanguageServer server = initFromRoot(root); - String foo = root.resolve("model/foo.smithy").toUri().toString(); - String bar = root.resolve("model/bar.smithy").toUri().toString(); - - server.didOpen(new RequestBuilders.DidOpen() - .uri(foo) - .build()); - - // on 'apply >MyOpInput' - RequestBuilders.PositionRequest myOpInputRequest = new RequestBuilders.PositionRequest() - .uri(foo) - .line(5) - .character(6); - - Location myOpInputLocation = server.definition(myOpInputRequest.buildDefinition()).get().getLeft().get(0); - assertThat(myOpInputLocation.getUri(), equalTo(foo)); - assertThat(myOpInputLocation.getRange().getStart(), equalTo(new Position(9, 0))); - - Hover myOpInputHover = server.hover(myOpInputRequest.buildHover()).get(); - String myOpInputHoverContent = myOpInputHover.getContents().getRight().getValue(); - assertThat(myOpInputHoverContent, containsString("@tags")); - assertThat(myOpInputHoverContent, containsString("structure MyOpInput with [HasMyBool]")); - assertThat(myOpInputHoverContent, containsString("/// even more docs")); - assertThat(myOpInputHoverContent, containsString("apply MyOpInput$myBool")); - - // on 'with [>HasMyBool]' - RequestBuilders.PositionRequest hasMyBoolRequest = new RequestBuilders.PositionRequest() - .uri(foo) - .line(9) - .character(26); - - Location hasMyBoolLocation = server.definition(hasMyBoolRequest.buildDefinition()).get().getLeft().get(0); - assertThat(hasMyBoolLocation.getUri(), equalTo(bar)); - assertThat(hasMyBoolLocation.getRange().getStart(), equalTo(new Position(6, 0))); - - Hover hasMyBoolHover = server.hover(hasMyBoolRequest.buildHover()).get(); - String hasMyBoolHoverContent = hasMyBoolHover.getContents().getRight().getValue(); - assertThat(hasMyBoolHoverContent, containsString("@mixin")); - assertThat(hasMyBoolHoverContent, containsString("@tags")); - assertThat(hasMyBoolHoverContent, containsString("structure HasMyBool")); - assertThat(hasMyBoolHoverContent, not(containsString("///"))); - assertThat(hasMyBoolHoverContent, not(containsString("@documentation"))); - } - - @Test - public void newShapeMixinCompletion() throws Exception { - String model = safeString(""" - $version: "2" - namespace com.foo - - @mixin - structure Foo {} - - """); - TestWorkspace workspace = TestWorkspace.singleModel(model); - SmithyLanguageServer server = initFromWorkspace(workspace); - String uri = workspace.getUri("main.smithy"); - - server.didOpen(new RequestBuilders.DidOpen() - .uri(uri) - .text(model) - .build()); - - RangeBuilder range = new RangeBuilder() - .startLine(6) - .startCharacter(0) - .endLine(6) - .endCharacter(0); - RequestBuilders.DidChange change = new RequestBuilders.DidChange().uri(uri); - server.didChange(change.range(range.build()).text("s").build()); - server.didChange(change.range(range.shiftRight().build()).text("t").build()); - server.didChange(change.range(range.shiftRight().build()).text("r").build()); - server.didChange(change.range(range.shiftRight().build()).text("u").build()); - server.didChange(change.range(range.shiftRight().build()).text("c").build()); - server.didChange(change.range(range.shiftRight().build()).text("t").build()); - server.didChange(change.range(range.shiftRight().build()).text("u").build()); - server.didChange(change.range(range.shiftRight().build()).text("r").build()); - server.didChange(change.range(range.shiftRight().build()).text("e").build()); - server.didChange(change.range(range.shiftRight().build()).text(" ").build()); - server.didChange(change.range(range.shiftRight().build()).text("B").build()); - server.didChange(change.range(range.shiftRight().build()).text("a").build()); - server.didChange(change.range(range.shiftRight().build()).text("r").build()); - server.didChange(change.range(range.shiftRight().build()).text(" ").build()); - server.didChange(change.range(range.shiftRight().build()).text("w").build()); - server.didChange(change.range(range.shiftRight().build()).text("i").build()); - server.didChange(change.range(range.shiftRight().build()).text("t").build()); - server.didChange(change.range(range.shiftRight().build()).text("h").build()); - server.didChange(change.range(range.shiftRight().build()).text(" ").build()); - server.didChange(change.range(range.shiftRight().build()).text("[]").build()); - server.didChange(change.range(range.shiftRight().build()).text("F").build()); - - server.getState().lifecycleManager().getTask(uri).get(); - - assertThat(server.getFirstProject().getDocument(uri).copyText(), equalTo(safeString(""" - $version: "2" - namespace com.foo - - @mixin - structure Foo {} - - structure Bar with [F]"""))); - - Position currentPosition = range.build().getStart(); - CompletionParams completionParams = new RequestBuilders.PositionRequest() - .uri(uri) - .position(range.shiftRight().build().getStart()) - .buildCompletion(); - - assertThat(server.getFirstProject().getDocument(uri).copyToken(currentPosition), equalTo("F")); - - List completions = server.completion(completionParams).get().getLeft(); - - assertThat(completions, containsInAnyOrder(hasLabel("Foo"))); - } - - @Test - public void existingShapeMixinCompletion() throws Exception { - String model = safeString(""" - $version: "2" - namespace com.foo - - @mixin - structure Foo {} - - structure Bar {} - """); - TestWorkspace workspace = TestWorkspace.singleModel(model); - SmithyLanguageServer server = initFromWorkspace(workspace); - String uri = workspace.getUri("main.smithy"); - - server.didOpen(new RequestBuilders.DidOpen() - .uri(uri) - .text(model) - .build()); - - RangeBuilder range = new RangeBuilder() - .startLine(6) - .startCharacter(13) - .endLine(6) - .endCharacter(13); - RequestBuilders.DidChange change = new RequestBuilders.DidChange().uri(uri); - server.didChange(change.range(range.build()).text(" ").build()); - server.didChange(change.range(range.shiftRight().build()).text("w").build()); - server.didChange(change.range(range.shiftRight().build()).text("i").build()); - server.didChange(change.range(range.shiftRight().build()).text("t").build()); - server.didChange(change.range(range.shiftRight().build()).text("h").build()); - server.didChange(change.range(range.shiftRight().build()).text(" ").build()); - server.didChange(change.range(range.shiftRight().build()).text("[]").build()); - server.didChange(change.range(range.shiftRight().build()).text("F").build()); - - server.getState().lifecycleManager().getTask(uri).get(); - - assertThat(server.getFirstProject().getDocument(uri).copyText(), equalTo(safeString(""" - $version: "2" - namespace com.foo - - @mixin - structure Foo {} - - structure Bar with [F] {} - """))); - - Position currentPosition = range.build().getStart(); - CompletionParams completionParams = new RequestBuilders.PositionRequest() - .uri(uri) - .position(range.shiftRight().build().getStart()) - .buildCompletion(); - - assertThat(server.getFirstProject().getDocument(uri).copyToken(currentPosition), equalTo("F")); - - List completions = server.completion(completionParams).get().getLeft(); - - assertThat(completions, containsInAnyOrder(hasLabel("Foo"))); - } - @Test public void diagnosticsOnMemberTarget() { String model = safeString(""" @@ -792,7 +251,8 @@ public void diagnosticsOnMemberTarget() { SmithyLanguageServer server = initFromWorkspace(workspace); String uri = workspace.getUri("main.smithy"); - List diagnostics = server.getFileDiagnostics(uri); + List diagnostics = SmithyDiagnostics.getFileDiagnostics( + server.getState().findProjectAndFile(uri), server.getMinimumSeverity()); assertThat(diagnostics, hasSize(1)); Diagnostic diagnostic = diagnostics.get(0); @@ -816,7 +276,8 @@ public void diagnosticsOnInvalidStructureMember() { SmithyLanguageServer server = initFromWorkspace(workspace); String uri = workspace.getUri("main.smithy"); - List diagnostics = server.getFileDiagnostics(uri); + List diagnostics = SmithyDiagnostics.getFileDiagnostics( + server.getState().findProjectAndFile(uri), server.getMinimumSeverity()); assertThat(diagnostics, hasSize(1)); Diagnostic diagnostic = diagnostics.getFirst(); @@ -843,7 +304,8 @@ public void diagnosticsOnUse() { SmithyLanguageServer server = initFromWorkspace(workspace); String uri = workspace.getUri("main.smithy"); - List diagnostics = server.getFileDiagnostics(uri); + List diagnostics = SmithyDiagnostics.getFileDiagnostics( + server.getState().findProjectAndFile(uri), server.getMinimumSeverity()); Diagnostic diagnostic = diagnostics.getFirst(); Document document = server.getFirstProject().getDocument(uri); @@ -872,7 +334,8 @@ public void diagnosticOnTrait() { .text(model) .build()); - List diagnostics = server.getFileDiagnostics(uri); + List diagnostics = SmithyDiagnostics.getFileDiagnostics( + server.getState().findProjectAndFile(uri), server.getMinimumSeverity()); assertThat(diagnostics, hasSize(1)); Diagnostic diagnostic = diagnostics.get(0); @@ -916,7 +379,8 @@ public void diagnosticsOnShape() throws Exception { .uri(uri) .build()); - List diagnostics = server.getFileDiagnostics(uri); + List diagnostics = SmithyDiagnostics.getFileDiagnostics( + server.getState().findProjectAndFile(uri), server.getMinimumSeverity()); assertThat(diagnostics, hasSize(1)); Diagnostic diagnostic = diagnostics.get(0); @@ -1724,118 +1188,6 @@ public void brokenBuildFileEventuallyConsistent() throws Exception { assertThat(projectAndFile.project().modelResult(), hasValue(hasShapeWithId("com.foo#Foo"))); } - @Test - public void completionHoverDefinitionWithAbsoluteIds() throws Exception { - String modelText1 = safeString(""" - $version: "2" - namespace com.foo - use com.bar#Bar - @com.bar#baz - structure Foo { - bar: com.bar#Bar - } - """); - String modelText2 = safeString(""" - $version: "2" - namespace com.bar - string Bar - string Bar2 - @trait - structure baz {} - """); - TestWorkspace workspace = TestWorkspace.multipleModels(modelText1, modelText2); - SmithyLanguageServer server = initFromWorkspace(workspace); - - String uri = workspace.getUri("model-0.smithy"); - - // use com.b - RequestBuilders.PositionRequest useTarget = RequestBuilders.positionRequest() - .uri(uri) - .line(2) - .character(8); - // @com.b - RequestBuilders.PositionRequest trait = RequestBuilders.positionRequest() - .uri(uri) - .line(3) - .character(2); - // bar: com.ba - RequestBuilders.PositionRequest memberTarget = RequestBuilders.positionRequest() - .uri(uri) - .line(5) - .character(14); - - List useTargetCompletions = server.completion(useTarget.buildCompletion()).get().getLeft(); - List traitCompletions = server.completion(trait.buildCompletion()).get().getLeft(); - List memberTargetCompletions = server.completion(memberTarget.buildCompletion()).get().getLeft(); - - assertThat(useTargetCompletions, containsInAnyOrder(hasLabel("com.bar#Bar2"))); // won't match 'Bar' because its already imported - assertThat(traitCompletions, containsInAnyOrder(hasLabel("com.bar#baz"))); - assertThat(memberTargetCompletions, containsInAnyOrder(hasLabel("com.bar#Bar"), hasLabel("com.bar#Bar2"))); - - List useTargetLocations = server.definition(useTarget.buildDefinition()).get().getLeft(); - List traitLocations = server.definition(trait.buildDefinition()).get().getLeft(); - List memberTargetLocations = server.definition(memberTarget.buildDefinition()).get().getLeft(); - - String uri1 = workspace.getUri("model-1.smithy"); - - assertThat(useTargetLocations, hasSize(1)); - assertThat(useTargetLocations.get(0).getUri(), equalTo(uri1)); - assertThat(useTargetLocations.get(0).getRange().getStart(), equalTo(new Position(2, 0))); - - assertThat(traitLocations, hasSize(1)); - assertThat(traitLocations.get(0).getUri(), equalTo(uri1)); - assertThat(traitLocations.get(0).getRange().getStart(), equalTo(new Position(5, 0))); - - assertThat(memberTargetLocations, hasSize(1)); - assertThat(memberTargetLocations.get(0).getUri(), equalTo(uri1)); - assertThat(memberTargetLocations.get(0).getRange().getStart(), equalTo(new Position(2, 0))); - - Hover useTargetHover = server.hover(useTarget.buildHover()).get(); - Hover traitHover = server.hover(trait.buildHover()).get(); - Hover memberTargetHover = server.hover(memberTarget.buildHover()).get(); - - assertThat(useTargetHover.getContents().getRight().getValue(), containsString("string Bar")); - assertThat(traitHover.getContents().getRight().getValue(), containsString("structure baz {}")); - assertThat(memberTargetHover.getContents().getRight().getValue(), containsString("string Bar")); - } - - @Test - public void useCompletionDoesntAutoImport() throws Exception { - String modelText1 = safeString(""" - $version: "2" - namespace com.foo - """); - String modelText2 = safeString(""" - $version: "2" - namespace com.bar - string Bar - """); - TestWorkspace workspace = TestWorkspace.multipleModels(modelText1, modelText2); - SmithyLanguageServer server = initFromWorkspace(workspace); - - String uri = workspace.getUri("model-0.smithy"); - server.didOpen(RequestBuilders.didOpen() - .uri(uri) - .text(modelText1) - .build()); - server.didChange(RequestBuilders.didChange() - .uri(uri) - .range(LspAdapter.point(2, 0)) - .text("use co") - .build()); - - List completions = server.completion(RequestBuilders.positionRequest() - .uri(uri) - .line(2) - .character(6) - .buildCompletion()) - .get() - .getLeft(); - - assertThat(completions, containsInAnyOrder(hasLabel("com.bar#Bar"))); - assertThat(completions.get(0).getAdditionalTextEdits(), nullValue()); - } - @Test public void loadsMultipleRoots() { TestWorkspace workspaceFoo = TestWorkspace.builder() diff --git a/src/test/java/software/amazon/smithy/lsp/SmithyVersionRefactoringTest.java b/src/test/java/software/amazon/smithy/lsp/SmithyVersionRefactoringTest.java index f7337b54..a806b492 100644 --- a/src/test/java/software/amazon/smithy/lsp/SmithyVersionRefactoringTest.java +++ b/src/test/java/software/amazon/smithy/lsp/SmithyVersionRefactoringTest.java @@ -60,7 +60,8 @@ public void noVersionDiagnostic() throws Exception { server.didOpen(new RequestBuilders.DidOpen().uri(uri).text(model).build()); - List diagnostics = server.getFileDiagnostics(uri); + List diagnostics = SmithyDiagnostics.getFileDiagnostics( + server.getState().findProjectAndFile(uri), server.getMinimumSeverity()); List codes = diagnostics.stream() .filter(d -> d.getCode().isLeft()) .map(d -> d.getCode().getLeft()) @@ -110,7 +111,8 @@ public void oldVersionDiagnostic() throws Exception { server.didOpen(new RequestBuilders.DidOpen().uri(uri).text(model).build()); - List diagnostics = server.getFileDiagnostics(uri); + List diagnostics = SmithyDiagnostics.getFileDiagnostics( + server.getState().findProjectAndFile(uri), server.getMinimumSeverity()); List codes = diagnostics.stream() .filter(d -> d.getCode().isLeft()) .map(d -> d.getCode().getLeft()) @@ -161,7 +163,8 @@ public void mostRecentVersion() { server.didOpen(new RequestBuilders.DidOpen().uri(uri).text(model).build()); - List diagnostics = server.getFileDiagnostics(uri); + List diagnostics = SmithyDiagnostics.getFileDiagnostics( + server.getState().findProjectAndFile(uri), server.getMinimumSeverity()); List codes = diagnostics.stream() .filter(d -> d.getCode().isLeft()) .map(d -> d.getCode().getLeft()) @@ -180,7 +183,8 @@ public void noShapes() { server.didOpen(new RequestBuilders.DidOpen().uri(uri).text(model).build()); - List diagnostics = server.getFileDiagnostics(uri); + List diagnostics = SmithyDiagnostics.getFileDiagnostics( + server.getState().findProjectAndFile(uri), server.getMinimumSeverity()); List codes = diagnostics.stream() .filter(d -> d.getCode().isLeft()) .map(d -> d.getCode().getLeft()) diff --git a/src/test/java/software/amazon/smithy/lsp/document/DocumentParserTest.java b/src/test/java/software/amazon/smithy/lsp/document/DocumentParserTest.java index 0891145b..814882e5 100644 --- a/src/test/java/software/amazon/smithy/lsp/document/DocumentParserTest.java +++ b/src/test/java/software/amazon/smithy/lsp/document/DocumentParserTest.java @@ -6,15 +6,12 @@ package software.amazon.smithy.lsp.document; import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static software.amazon.smithy.lsp.document.DocumentTest.safeString; -import static software.amazon.smithy.lsp.document.DocumentTest.string; -import java.util.Map; import java.util.stream.Stream; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; @@ -40,19 +37,19 @@ public void getsDocumentNamespace() { DocumentParser notNamespace = DocumentParser.of(safeString("namespace !foo")); DocumentParser trailingComment = DocumentParser.of(safeString("namespace com.foo//foo\n")); - assertThat(noNamespace.documentNamespace(), nullValue()); - assertThat(incompleteNamespace.documentNamespace(), nullValue()); - assertThat(incompleteNamespaceValue.documentNamespace(), nullValue()); - assertThat(likeNamespace.documentNamespace(), nullValue()); - assertThat(otherLikeNamespace.documentNamespace(), nullValue()); - assertThat(namespaceAtEnd.documentNamespace().namespace().toString(), equalTo("com.foo")); + assertThat(noNamespace.documentNamespace().statementRange(), equalTo(LspAdapter.origin())); + assertThat(incompleteNamespace.documentNamespace().statementRange(), equalTo(LspAdapter.origin())); + assertThat(incompleteNamespaceValue.documentNamespace().statementRange(), equalTo(LspAdapter.origin())); + assertThat(likeNamespace.documentNamespace().statementRange(), equalTo(LspAdapter.origin())); + assertThat(otherLikeNamespace.documentNamespace().statementRange(), equalTo(LspAdapter.origin())); + assertThat(namespaceAtEnd.documentNamespace().namespace(), equalTo("com.foo")); assertThat(namespaceAtEnd.documentNamespace().statementRange(), equalTo(LspAdapter.of(2, 0, 2, 17))); - assertThat(brokenNamespace.documentNamespace(), nullValue()); - assertThat(commentedNamespace.documentNamespace(), nullValue()); - assertThat(wsPrefixedNamespace.documentNamespace().namespace().toString(), equalTo("com.foo")); + assertThat(brokenNamespace.documentNamespace().statementRange(), equalTo(LspAdapter.origin())); + assertThat(commentedNamespace.documentNamespace().statementRange(), equalTo(LspAdapter.origin())); + assertThat(wsPrefixedNamespace.documentNamespace().namespace(), equalTo("com.foo")); assertThat(wsPrefixedNamespace.documentNamespace().statementRange(), equalTo(LspAdapter.of(1, 4, 1, 21))); - assertThat(notNamespace.documentNamespace(), nullValue()); - assertThat(trailingComment.documentNamespace().namespace().toString(), equalTo("com.foo")); + assertThat(notNamespace.documentNamespace().statementRange(), equalTo(LspAdapter.origin())); + assertThat(trailingComment.documentNamespace().namespace(), equalTo("com.foo")); assertThat(trailingComment.documentNamespace().statementRange(), equalTo(LspAdapter.of(0, 0, 0, 17))); } @@ -68,15 +65,15 @@ public void getsDocumentImports() { DocumentParser multiImports = DocumentParser.of(safeString("use com.foo#bar\nuse com.foo#baz")); DocumentParser notImport = DocumentParser.of(safeString("usea com.foo#bar")); - assertThat(noImports.documentImports(), nullValue()); - assertThat(incompleteImport.documentImports(), nullValue()); - assertThat(incompleteImportValue.documentImports(), nullValue()); + assertThat(noImports.documentImports().importsRange(), equalTo(LspAdapter.origin())); + assertThat(incompleteImport.documentImports().importsRange(), equalTo(LspAdapter.origin())); + assertThat(incompleteImportValue.documentImports().importsRange(), equalTo(LspAdapter.origin())); assertThat(oneImport.documentImports().imports(), containsInAnyOrder("com.foo#bar")); assertThat(leadingWsImport.documentImports().imports(), containsInAnyOrder("com.foo#bar")); assertThat(trailingCommentImport.documentImports().imports(), containsInAnyOrder("com.foo#bar")); - assertThat(commentedImport.documentImports(), nullValue()); + assertThat(commentedImport.documentImports().importsRange(), equalTo(LspAdapter.origin())); assertThat(multiImports.documentImports().imports(), containsInAnyOrder("com.foo#bar", "com.foo#baz")); - assertThat(notImport.documentImports(), nullValue()); + assertThat(notImport.documentImports().importsRange(), equalTo(LspAdapter.origin())); // Some of these aren't shape ids, but its ok DocumentParser brokenImport = DocumentParser.of(safeString("use com.foo")); @@ -108,20 +105,20 @@ public void getsDocumentVersion() { DocumentParser notSecond = DocumentParser.of(safeString("$foo: \"bar\"\n$bar: 1\n// abc\n$baz: 2\n $version: \"2\"")); DocumentParser notFirstNoVersion = DocumentParser.of(safeString("$foo: \"bar\"\nfoo\n")); - assertThat(noVersion.documentVersion(), nullValue()); - assertThat(notVersion.documentVersion(), nullValue()); - assertThat(noDollar.documentVersion(), nullValue()); + assertThat(noVersion.documentVersion().range(), equalTo(LspAdapter.origin())); + assertThat(notVersion.documentVersion().range(), equalTo(LspAdapter.origin())); + assertThat(noDollar.documentVersion().range(), equalTo(LspAdapter.origin())); assertThat(noColon.documentVersion().version(), equalTo("2")); - assertThat(commented.documentVersion(), nullValue()); + assertThat(commented.documentVersion().range(), equalTo(LspAdapter.origin())); assertThat(leadingWs.documentVersion().version(), equalTo("2")); assertThat(leadingLines.documentVersion().version(), equalTo("2")); - assertThat(notStringNode.documentVersion(), nullValue()); + assertThat(notStringNode.documentVersion().range(), equalTo(LspAdapter.origin())); assertThat(trailingComment.documentVersion().version(), equalTo("2")); assertThat(trailingLine.documentVersion().version(), equalTo("2")); - assertThat(invalidNode.documentVersion(), nullValue()); + assertThat(invalidNode.documentVersion().range(), equalTo(LspAdapter.origin())); assertThat(notFirst.documentVersion().version(), equalTo("2")); assertThat(notSecond.documentVersion().version(), equalTo("2")); - assertThat(notFirstNoVersion.documentVersion(), nullValue()); + assertThat(notFirstNoVersion.documentVersion().range(), equalTo(LspAdapter.origin())); Range leadingWsRange = leadingWs.documentVersion().range(); Range trailingCommentRange = trailingComment.documentVersion().range(); @@ -135,92 +132,6 @@ public void getsDocumentVersion() { assertThat(notSecond.getDocument().copyRange(notSecondRange), equalTo("$version: \"2\"")); } - @Test - public void getsDocumentShapes() { - String text = """ - $version: "2" - namespace com.foo - string Foo - structure Bar { - bar: Foo - } - enum Baz { - ONE - TWO - } - intEnum Biz { - ONE = 1 - } - @mixin - structure Boz { - elided: String - } - structure Mixed with [Boz] { - $elided - } - operation Get { - input := { - a: Integer - } - } - """; - DocumentParser parser = DocumentParser.of(safeString(text)); - Map documentShapes = parser.documentShapes(); - - DocumentShape fooDef = documentShapes.get(new Position(2, 7)); - DocumentShape barDef = documentShapes.get(new Position(3, 10)); - DocumentShape barMemberDef = documentShapes.get(new Position(4, 4)); - DocumentShape targetFoo = documentShapes.get(new Position(4, 9)); - DocumentShape bazDef = documentShapes.get(new Position(6, 5)); - DocumentShape bazOneDef = documentShapes.get(new Position(7, 4)); - DocumentShape bazTwoDef = documentShapes.get(new Position(8, 4)); - DocumentShape bizDef = documentShapes.get(new Position(10, 8)); - DocumentShape bizOneDef = documentShapes.get(new Position(11, 4)); - DocumentShape bozDef = documentShapes.get(new Position(14, 10)); - DocumentShape elidedDef = documentShapes.get(new Position(15, 4)); - DocumentShape targetString = documentShapes.get(new Position(15, 12)); - DocumentShape mixedDef = documentShapes.get(new Position(17, 10)); - DocumentShape elided = documentShapes.get(new Position(18, 4)); - DocumentShape get = documentShapes.get(new Position(20, 10)); - DocumentShape getInputA = documentShapes.get(new Position(22, 8)); - - assertThat(fooDef.kind(), equalTo(DocumentShape.Kind.DefinedShape)); - assertThat(fooDef.shapeName(), string("Foo")); - assertThat(barDef.kind(), equalTo(DocumentShape.Kind.DefinedShape)); - assertThat(barDef.shapeName(), string("Bar")); - assertThat(barMemberDef.kind(), equalTo(DocumentShape.Kind.DefinedMember)); - assertThat(barMemberDef.shapeName(), string("bar")); - assertThat(barMemberDef.targetReference(), equalTo(targetFoo)); - assertThat(targetFoo.kind(), equalTo(DocumentShape.Kind.Targeted)); - assertThat(targetFoo.shapeName(), string("Foo")); - assertThat(bazDef.kind(), equalTo(DocumentShape.Kind.DefinedShape)); - assertThat(bazDef.shapeName(), string("Baz")); - assertThat(bazOneDef.kind(), equalTo(DocumentShape.Kind.DefinedMember)); - assertThat(bazOneDef.shapeName(), string("ONE")); - assertThat(bazTwoDef.kind(), equalTo(DocumentShape.Kind.DefinedMember)); - assertThat(bazTwoDef.shapeName(), string("TWO")); - assertThat(bizDef.kind(), equalTo(DocumentShape.Kind.DefinedShape)); - assertThat(bizDef.shapeName(), string("Biz")); - assertThat(bizOneDef.kind(), equalTo(DocumentShape.Kind.DefinedMember)); - assertThat(bizOneDef.shapeName(), string("ONE")); - assertThat(bozDef.kind(), equalTo(DocumentShape.Kind.DefinedShape)); - assertThat(bozDef.shapeName(), string("Boz")); - assertThat(elidedDef.kind(), equalTo(DocumentShape.Kind.DefinedMember)); - assertThat(elidedDef.shapeName(), string("elided")); - assertThat(elidedDef.targetReference(), equalTo(targetString)); - assertThat(targetString.kind(), equalTo(DocumentShape.Kind.Targeted)); - assertThat(targetString.shapeName(), string("String")); - assertThat(mixedDef.kind(), equalTo(DocumentShape.Kind.DefinedShape)); - assertThat(mixedDef.shapeName(), string("Mixed")); - assertThat(elided.kind(), equalTo(DocumentShape.Kind.Elided)); - assertThat(elided.shapeName(), string("elided")); - assertThat(parser.getDocument().borrowRange(elided.range()), string("$elided")); - assertThat(get.kind(), equalTo(DocumentShape.Kind.DefinedShape)); - assertThat(get.shapeName(), string("Get")); - assertThat(getInputA.kind(), equalTo(DocumentShape.Kind.DefinedMember)); - assertThat(getInputA.shapeName(), string("a")); - } - @ParameterizedTest @MethodSource("contiguousRangeTestCases") public void findsContiguousRange(SourceLocation input, Range expected) { diff --git a/src/test/java/software/amazon/smithy/lsp/language/CompletionHandlerTest.java b/src/test/java/software/amazon/smithy/lsp/language/CompletionHandlerTest.java index 81007c00..07f16727 100644 --- a/src/test/java/software/amazon/smithy/lsp/language/CompletionHandlerTest.java +++ b/src/test/java/software/amazon/smithy/lsp/language/CompletionHandlerTest.java @@ -9,9 +9,12 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.startsWith; import java.util.ArrayList; @@ -26,9 +29,9 @@ import software.amazon.smithy.lsp.TestWorkspace; import software.amazon.smithy.lsp.TextWithPositions; import software.amazon.smithy.lsp.document.Document; +import software.amazon.smithy.lsp.project.IdlFile; import software.amazon.smithy.lsp.project.Project; import software.amazon.smithy.lsp.project.ProjectLoader; -import software.amazon.smithy.lsp.project.SmithyFile; public class CompletionHandlerTest { @Test @@ -1033,6 +1036,35 @@ public void completesNodeMemberTargetStart() { assertThat(comps, containsInAnyOrder("\"\"", "[]", "{}", "[]")); } + @Test + public void completesAbsoluteShapeIds() { + TextWithPositions text = TextWithPositions.from(""" + $version: "2" + namespace com.foo + structure Foo { + bar: smithy.% + } + """); + List comps = getCompLabels(text); + + assertThat(comps, hasItem("smithy.api#String")); + } + + @Test + public void completesUseTarget() { + TextWithPositions text = TextWithPositions.from(""" + $version: "2" + namespace com.foo + use smithy.api#Strin% + """); + List comps = getCompItems(text.text(), text.positions()); + + assertThat(comps, hasSize(1)); + CompletionItem item = comps.get(0); + assertThat(item.getTextEdit().getLeft().getNewText(), equalTo("smithy.api#String")); + assertThat(item.getAdditionalTextEdits(), nullValue()); + } + private static List getCompLabels(TextWithPositions textWithPositions) { return getCompLabels(textWithPositions.text(), textWithPositions.positions()); } @@ -1045,7 +1077,7 @@ private static List getCompItems(String text, Position... positi TestWorkspace workspace = TestWorkspace.singleModel(text); Project project = ProjectLoader.load(workspace.getRoot(), new ServerState()).unwrap(); String uri = workspace.getUri("main.smithy"); - SmithyFile smithyFile = project.getSmithyFile(uri); + IdlFile smithyFile = (IdlFile) project.getSmithyFile(uri); List completionItems = new ArrayList<>(); CompletionHandler handler = new CompletionHandler(project, smithyFile); diff --git a/src/test/java/software/amazon/smithy/lsp/language/DefinitionHandlerTest.java b/src/test/java/software/amazon/smithy/lsp/language/DefinitionHandlerTest.java index 5383d527..0dbed9c4 100644 --- a/src/test/java/software/amazon/smithy/lsp/language/DefinitionHandlerTest.java +++ b/src/test/java/software/amazon/smithy/lsp/language/DefinitionHandlerTest.java @@ -25,11 +25,12 @@ import software.amazon.smithy.lsp.ServerState; import software.amazon.smithy.lsp.TestWorkspace; import software.amazon.smithy.lsp.TextWithPositions; +import software.amazon.smithy.lsp.project.IdlFile; import software.amazon.smithy.lsp.project.Project; import software.amazon.smithy.lsp.project.ProjectLoader; import software.amazon.smithy.lsp.project.SmithyFile; +import software.amazon.smithy.lsp.syntax.StatementView; import software.amazon.smithy.lsp.syntax.Syntax; -import software.amazon.smithy.lsp.syntax.SyntaxSearch; public class DefinitionHandlerTest { @Test @@ -312,6 +313,21 @@ public void idRefTraitValue() { assertIsShapeDef(result, result.locations.getFirst(), "string Bar"); } + @Test + public void absoluteShapeId() { + TextWithPositions text = TextWithPositions.from(""" + $version: "2" + namespace com.foo + structure Foo { + bar: %smithy.api#String + } + """); + GetLocationsResult result = getLocations(text); + assertThat(result.locations, hasSize(1)); + assertThat(result.locations.getFirst().getUri(), endsWith("prelude.smithy")); + assertIsShapeDef(result, result.locations.getFirst(), "string String"); + } + private static void assertIsShapeDef( GetLocationsResult result, Location location, @@ -323,17 +339,18 @@ private static void assertIsShapeDef( int documentIndex = smithyFile.document().indexOfPosition(location.getRange().getStart()); assertThat(documentIndex, greaterThanOrEqualTo(0)); - int statementIndex = SyntaxSearch.statementIndex(smithyFile.statements(), documentIndex); - assertThat(statementIndex, greaterThanOrEqualTo(0)); + StatementView view = StatementView.createAt(((IdlFile) smithyFile).getParse(), documentIndex).orElse(null); + assertThat(view, notNullValue()); + assertThat(view.statementIndex(), greaterThanOrEqualTo(0)); - var statement = smithyFile.statements().get(statementIndex); + var statement = view.getStatement(); if (statement instanceof Syntax.Statement.ShapeDef shapeDef) { - String shapeType = shapeDef.shapeType().copyValueFrom(smithyFile.document()); - String shapeName = shapeDef.shapeName().copyValueFrom(smithyFile.document()); + String shapeType = shapeDef.shapeType().stringValue(); + String shapeName = shapeDef.shapeName().stringValue(); assertThat(shapeType + " " + shapeName, equalTo(expected)); } else if (statement instanceof Syntax.Statement.MemberDef memberDef) { - String memberName = memberDef.name().copyValueFrom(smithyFile.document()); - String memberTarget = memberDef.target().copyValueFrom(smithyFile.document()); + String memberName = memberDef.name().stringValue(); + String memberTarget = memberDef.target().stringValue(); assertThat(memberName + ": " + memberTarget, equalTo(expected)); } else { fail("Expected shape or member def, but was " + statement.getClass().getName()); @@ -353,7 +370,7 @@ private static GetLocationsResult getLocations(String text, Position... position SmithyFile smithyFile = project.getSmithyFile(uri); List locations = new ArrayList<>(); - DefinitionHandler handler = new DefinitionHandler(project, smithyFile); + DefinitionHandler handler = new DefinitionHandler(project, (IdlFile) smithyFile); for (Position position : positions) { DefinitionParams params = RequestBuilders.positionRequest() .uri(uri) diff --git a/src/test/java/software/amazon/smithy/lsp/language/DocumentSymbolTest.java b/src/test/java/software/amazon/smithy/lsp/language/DocumentSymbolTest.java new file mode 100644 index 00000000..ab2e521e --- /dev/null +++ b/src/test/java/software/amazon/smithy/lsp/language/DocumentSymbolTest.java @@ -0,0 +1,62 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.lsp.language; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItems; +import static software.amazon.smithy.lsp.document.DocumentTest.safeString; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.lsp.ServerState; +import software.amazon.smithy.lsp.TestWorkspace; +import software.amazon.smithy.lsp.project.IdlFile; +import software.amazon.smithy.lsp.project.Project; +import software.amazon.smithy.lsp.project.ProjectLoader; + +public class DocumentSymbolTest { + @Test + public void documentSymbols() { + String model = safeString(""" + $version: "2" + namespace com.foo + + @trait + string myTrait + + structure Foo { + @required + bar: Bar + } + + structure Bar { + @myTrait("foo") + baz: Baz + } + + @myTrait("abc") + integer Baz + """); + List names = getDocumentSymbolNames(model); + + assertThat(names, hasItems("myTrait", "Foo", "bar", "Bar", "baz", "Baz")); + } + + private static List getDocumentSymbolNames(String text) { + TestWorkspace workspace = TestWorkspace.singleModel(text); + Project project = ProjectLoader.load(workspace.getRoot(), new ServerState()).unwrap(); + String uri = workspace.getUri("main.smithy"); + IdlFile idlFile = (IdlFile) project.getSmithyFile(uri); + + List names = new ArrayList<>(); + var handler = new DocumentSymbolHandler(idlFile.document(), idlFile.getParse().statements()); + for (var sym : handler.handle()) { + names.add(sym.getRight().getName()); + } + return names; + } +} diff --git a/src/test/java/software/amazon/smithy/lsp/language/HoverHandlerTest.java b/src/test/java/software/amazon/smithy/lsp/language/HoverHandlerTest.java index 026f3f93..1bdfdc4c 100644 --- a/src/test/java/software/amazon/smithy/lsp/language/HoverHandlerTest.java +++ b/src/test/java/software/amazon/smithy/lsp/language/HoverHandlerTest.java @@ -18,6 +18,8 @@ import software.amazon.smithy.lsp.RequestBuilders; import software.amazon.smithy.lsp.ServerState; import software.amazon.smithy.lsp.TestWorkspace; +import software.amazon.smithy.lsp.TextWithPositions; +import software.amazon.smithy.lsp.project.IdlFile; import software.amazon.smithy.lsp.project.Project; import software.amazon.smithy.lsp.project.ProjectLoader; import software.amazon.smithy.lsp.project.SmithyFile; @@ -106,6 +108,24 @@ public void nodeMemberTarget() { assertThat(hovers, contains(containsString("operation Bar"))); } + @Test + public void absoluteShapeId() { + TextWithPositions text = TextWithPositions.from(""" + $version: "2" + namespace com.foo + structure Foo { + bar: %smithy.api#String + } + """); + List hovers = getHovers(text); + + assertThat(hovers, contains(containsString("string String"))); + } + + private static List getHovers(TextWithPositions text) { + return getHovers(text.text(), text.positions()); + } + private static List getHovers(String text, Position... positions) { TestWorkspace workspace = TestWorkspace.singleModel(text); Project project = ProjectLoader.load(workspace.getRoot(), new ServerState()).unwrap(); @@ -113,7 +133,7 @@ private static List getHovers(String text, Position... positions) { SmithyFile smithyFile = project.getSmithyFile(uri); List hover = new ArrayList<>(); - HoverHandler handler = new HoverHandler(project, smithyFile, Severity.WARNING); + HoverHandler handler = new HoverHandler(project, (IdlFile) smithyFile, Severity.WARNING); for (Position position : positions) { HoverParams params = RequestBuilders.positionRequest() .uri(uri) diff --git a/src/test/java/software/amazon/smithy/lsp/project/ProjectTest.java b/src/test/java/software/amazon/smithy/lsp/project/ProjectTest.java index bffea311..a7c983f3 100644 --- a/src/test/java/software/amazon/smithy/lsp/project/ProjectTest.java +++ b/src/test/java/software/amazon/smithy/lsp/project/ProjectTest.java @@ -19,13 +19,13 @@ import static software.amazon.smithy.lsp.SmithyMatchers.eventWithMessage; import static software.amazon.smithy.lsp.SmithyMatchers.hasShapeWithId; import static software.amazon.smithy.lsp.UtilMatchers.anOptionalOf; -import static software.amazon.smithy.lsp.document.DocumentTest.string; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; +import java.util.Set; import java.util.logging.Logger; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; @@ -36,6 +36,7 @@ import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.ToShapeId; import software.amazon.smithy.model.traits.LengthTrait; import software.amazon.smithy.model.traits.PatternTrait; import software.amazon.smithy.model.traits.TagsTrait; @@ -120,24 +121,19 @@ public void loadsWhenModelHasInvalidSyntax() { assertThat(eventIds, hasItem("Model")); assertThat(project.smithyFiles().keySet(), hasItem(containsString("main.smithy"))); - SmithyFile main = project.getSmithyFile(LspAdapter.toUri(root.resolve("main.smithy").toString())); + IdlFile main = (IdlFile) project.getSmithyFile(LspAdapter.toUri(root.resolve("main.smithy").toString())); assertThat(main, not(nullValue())); assertThat(main.document(), not(nullValue())); - assertThat(main.namespace(), string("com.foo")); - assertThat(main.imports(), empty()); + assertThat(main.getParse().namespace().namespace(), equalTo("com.foo")); + assertThat(main.getParse().imports().imports(), empty()); - assertThat(main.shapes(), hasSize(2)); - List shapeIds = main.shapes().stream() - .map(Shape::toShapeId) + assertThat(project.definedShapesByFile().keySet(), hasItem(main.path())); + Set mainShapes = project.definedShapesByFile().get(main.path()); + List shapeIds = mainShapes.stream() + .map(ToShapeId::toShapeId) .map(ShapeId::toString) .collect(Collectors.toList()); assertThat(shapeIds, hasItems("com.foo#Foo", "com.foo#Foo$bar")); - - assertThat(main.documentShapes(), hasSize(4)); - List documentShapeNames = main.documentShapes().stream() - .map(documentShape -> documentShape.shapeName().toString()) - .collect(Collectors.toList()); - assertThat(documentShapeNames, hasItems("Foo", "bar", "String", "A")); } @Test @@ -149,31 +145,29 @@ public void loadsProjectWithMultipleNamespaces() { assertThat(project.modelResult().getValidationEvents(), empty()); assertThat(project.smithyFiles().keySet(), hasItems(containsString("a.smithy"), containsString("b.smithy"))); - SmithyFile a = project.getSmithyFile(LspAdapter.toUri(root.resolve("model/a.smithy").toString())); + IdlFile a = (IdlFile) project.getSmithyFile(LspAdapter.toUri(root.resolve("model/a.smithy").toString())); assertThat(a.document(), not(nullValue())); - assertThat(a.namespace(), string("a")); - List aShapeIds = a.shapes().stream() - .map(Shape::toShapeId) + assertThat(a.getParse().namespace().namespace(), equalTo("a")); + + assertThat(project.definedShapesByFile().keySet(), hasItem(a.path())); + Set aShapes = project.definedShapesByFile().get(a.path()); + List aShapeIds = aShapes.stream() + .map(ToShapeId::toShapeId) .map(ShapeId::toString) .collect(Collectors.toList()); assertThat(aShapeIds, hasItems("a#Hello", "a#HelloInput", "a#HelloOutput")); - List aDocumentShapeNames = a.documentShapes().stream() - .map(documentShape -> documentShape.shapeName().toString()) - .collect(Collectors.toList()); - assertThat(aDocumentShapeNames, hasItems("Hello", "name", "String")); - SmithyFile b = project.getSmithyFile(LspAdapter.toUri(root.resolve("model/b.smithy").toString())); + IdlFile b = (IdlFile) project.getSmithyFile(LspAdapter.toUri(root.resolve("model/b.smithy").toString())); assertThat(b.document(), not(nullValue())); - assertThat(b.namespace(), string("b")); - List bShapeIds = b.shapes().stream() - .map(Shape::toShapeId) + assertThat(b.getParse().namespace().namespace(), equalTo("b")); + + assertThat(project.definedShapesByFile().keySet(), hasItem(b.path())); + Set bShapes = project.definedShapesByFile().get(b.path()); + List bShapeIds = bShapes.stream() + .map(ToShapeId::toShapeId) .map(ShapeId::toString) .collect(Collectors.toList()); assertThat(bShapeIds, hasItems("b#Hello", "b#HelloInput", "b#HelloOutput")); - List bDocumentShapeNames = b.documentShapes().stream() - .map(documentShape -> documentShape.shapeName().toString()) - .collect(Collectors.toList()); - assertThat(bDocumentShapeNames, hasItems("Hello", "name", "String")); } @Test diff --git a/src/test/java/software/amazon/smithy/lsp/syntax/IdlParserTest.java b/src/test/java/software/amazon/smithy/lsp/syntax/IdlParserTest.java index a41a836e..004cceb0 100644 --- a/src/test/java/software/amazon/smithy/lsp/syntax/IdlParserTest.java +++ b/src/test/java/software/amazon/smithy/lsp/syntax/IdlParserTest.java @@ -290,7 +290,7 @@ public void stringKeysInTraits() { "bar": "baz" ) """; - Syntax.IdlParse parse = Syntax.parseIdl(Document.of(text)); + Syntax.IdlParseResult parse = Syntax.parseIdl(Document.of(text)); assertThat(parse.statements(), hasSize(1)); assertThat(parse.statements().get(0), instanceOf(Syntax.Statement.TraitApplication.class)); @@ -310,7 +310,7 @@ public void broken(String desc, String text, List expectedErrorMessages, if (desc.equals("trait missing member value")) { System.out.println(); } - Syntax.IdlParse parse = Syntax.parseIdl(Document.of(text)); + Syntax.IdlParseResult parse = Syntax.parseIdl(Document.of(text)); List errorMessages = parse.errors().stream().map(Syntax.Err::message).toList(); List types = parse.statements().stream() .map(Syntax.Statement::type) @@ -459,7 +459,7 @@ private static Stream brokenProvider() { } private static void assertTypesEqual(String text, Syntax.Statement.Type... types) { - Syntax.IdlParse parse = Syntax.parseIdl(Document.of(text)); + Syntax.IdlParseResult parse = Syntax.parseIdl(Document.of(text)); List actualTypes = parse.statements().stream() .map(Syntax.Statement::type) .filter(type -> type != Syntax.Statement.Type.Block) diff --git a/src/test/java/software/amazon/smithy/lsp/syntax/NodeParserTest.java b/src/test/java/software/amazon/smithy/lsp/syntax/NodeParserTest.java index e6b7dabe..6f45d5f7 100644 --- a/src/test/java/software/amazon/smithy/lsp/syntax/NodeParserTest.java +++ b/src/test/java/software/amazon/smithy/lsp/syntax/NodeParserTest.java @@ -7,6 +7,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; import static org.junit.jupiter.api.Assertions.fail; import java.math.BigDecimal; @@ -140,7 +142,7 @@ public void goodStrings(String text, String expectedValue) { Document document = Document.of(text); Syntax.Node value = Syntax.parseNode(document).value(); if (value instanceof Syntax.Node.Str s) { - String actualValue = s.copyValueFrom(document); + String actualValue = s.stringValue(); if (!expectedValue.equals(actualValue)) { fail(String.format("expected text of %s to be parsed as a string with value %s, but was %s", text, expectedValue, actualValue)); @@ -164,7 +166,7 @@ public void goodIdents(String text, String expectedValue) { Document document = Document.of(text); Syntax.Node value = Syntax.parseNode(document).value(); if (value instanceof Syntax.Ident ident) { - String actualValue = ident.copyValueFrom(document); + String actualValue = ident.stringValue(); if (!expectedValue.equals(actualValue)) { fail(String.format("expected text of %s to be parsed as an ident with value %s, but was %s", text, expectedValue, actualValue)); @@ -211,7 +213,7 @@ private static Stream goodNumbersProvider() { @ParameterizedTest @MethodSource("brokenProvider") public void broken(String desc, String text, List expectedErrorMessages, List expectedTypes) { - Syntax.NodeParse parse = Syntax.parseNode(Document.of(text)); + Syntax.NodeParseResult parse = Syntax.parseNode(Document.of(text)); List errorMessages = parse.errors().stream().map(Syntax.Err::message).toList(); List types = getNodeTypes(parse.value()); @@ -363,6 +365,35 @@ public void parsesTextBlocks() { Syntax.Node.Type.Str); } + @Test + public void stringValues() { + Syntax.Node node = Syntax.parseNode(Document.of(""" + [ + "abc", + "", + \""" + foo + \""" + ] + """)).value(); + + assertThat(node, instanceOf(Syntax.Node.Arr.class)); + Syntax.Node.Arr arr = (Syntax.Node.Arr) node; + assertThat(arr.elements(), hasSize(3)); + + Syntax.Node first = arr.elements().get(0); + assertThat(first, instanceOf(Syntax.Node.Str.class)); + assertThat(((Syntax.Node.Str) first).stringValue(), equalTo("abc")); + + Syntax.Node second = arr.elements().get(1); + assertThat(second, instanceOf(Syntax.Node.Str.class)); + assertThat(((Syntax.Node.Str) second).stringValue(), equalTo("")); + + Syntax.Node third = arr.elements().get(2); + assertThat(third, instanceOf(Syntax.Node.Str.class)); + assertThat(((Syntax.Node.Str) third).stringValue().trim(), equalTo("foo")); + } + private static void assertTypesEqual(String text, Syntax.Node.Type... types) { assertThat(getNodeTypes(Syntax.parseNode(Document.of(text)).value()), contains(types)); } diff --git a/src/test/java/software/amazon/smithy/lsp/syntax/SyntaxSearchTest.java b/src/test/java/software/amazon/smithy/lsp/syntax/SyntaxSearchTest.java index cc8d9c16..167a3994 100644 --- a/src/test/java/software/amazon/smithy/lsp/syntax/SyntaxSearchTest.java +++ b/src/test/java/software/amazon/smithy/lsp/syntax/SyntaxSearchTest.java @@ -21,7 +21,7 @@ public void findsNodeCursor() { }"""); Document document = Document.of(text); Syntax.Node value = Syntax.parseNode(document).value(); - NodeCursor cursor = NodeCursor.create(document, value, document.indexOfPosition(1, 4)); + NodeCursor cursor = NodeCursor.create(value, document.indexOfPosition(1, 4)); assertCursorMatches(cursor, new NodeCursor(List.of( new NodeCursor.Obj(null), @@ -38,7 +38,7 @@ public void findsNodeCursorWhenBroken() { }"""); Document document = Document.of(text); Syntax.Node value = Syntax.parseNode(document).value(); - NodeCursor cursor = NodeCursor.create(document, value, document.indexOfPosition(1, 4)); + NodeCursor cursor = NodeCursor.create(value, document.indexOfPosition(1, 4)); assertCursorMatches(cursor, new NodeCursor(List.of( new NodeCursor.Obj(null),