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),