Skip to content

Commit

Permalink
Add support for non-project files
Browse files Browse the repository at this point in the history
This makes the language server work on files that aren't connected
(or attached) to a smithy-build.json/other build file. This works
by loading said files as they are opened in their own, single-file
projects with no dependencies, which are removed when the file is
closed. A diagnostic was also added to indicate when a file is
'detached' from a project, and appears on the first line of the file.

I could have made all detached files part of their own special project,
could be more convenient when doing something quick with multiple
files without a smithy-build.json. The smithy cli can work this way,
although you still have to specify the files to build in the command,
so we could change this in the future. The difference is I don't think
we'd have a way of opting out of the single project without some config
that would end up being more work to set up than a smithy-build.json.
  • Loading branch information
milesziemer committed Apr 8, 2024
1 parent cc605d7 commit 6fce052
Show file tree
Hide file tree
Showing 13 changed files with 694 additions and 60 deletions.
175 changes: 130 additions & 45 deletions src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest;
import org.eclipse.lsp4j.jsonrpc.services.JsonSegment;
import software.amazon.smithy.lsp.ext.serverstatus.ServerStatus;
import software.amazon.smithy.lsp.ext.serverstatus.ServerStatusParams;

/**
* Interface for protocol extensions for Smithy.
Expand All @@ -33,4 +35,13 @@ public interface SmithyProtocolExtensions {

@JsonRequest
CompletableFuture<List<? extends Location>> selectorCommand(SelectorParams selectorParams);

/**
* Get a snapshot of the server's status, useful for debugging purposes.
*
* @param params Request parameters
* @return A future containing the server's status
*/
@JsonRequest
CompletableFuture<ServerStatus> serverStatus(ServerStatusParams params);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.lsp.diagnostics;

import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DiagnosticSeverity;
import org.eclipse.lsp4j.Range;
import software.amazon.smithy.lsp.project.SmithyFile;
import software.amazon.smithy.lsp.protocol.RangeAdapter;

/**
* Diagnostics for when a Smithy file is not connected to a Smithy project via
* smithy-build.json or other build file.
*/
public final class DetachedDiagnostics {
public static final String DETACHED_FILE = "detached-file";

private DetachedDiagnostics() {
}

/**
* @param smithyFile The Smithy file to get a detached diagnostic for
* @return The detached diagnostic associated with the Smithy file, or null
* if one doesn't exist (this occurs if the file doesn't have a document
* associated with it)
*/
public static Diagnostic forSmithyFile(SmithyFile smithyFile) {
if (smithyFile.getDocument() != null) {
int end = smithyFile.getDocument().lineEnd(0);
Range range = RangeAdapter.lineSpan(0, 0, end);
return new Diagnostic(
range,
"This file isn't attached to a project",
DiagnosticSeverity.Warning,
"smithy-language-server",
DETACHED_FILE
);
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.lsp.ext.serverstatus;

import java.util.List;
import org.eclipse.lsp4j.jsonrpc.validation.NonNull;

/**
* A snapshot of a project the server has open.
*/
public class OpenProject {
@NonNull
private final String root;
@NonNull
private final List<String> files;
private final boolean isDetached;

/**
* @param root The root URI of the project
* @param files The list of all file URIs tracked by the project
* @param isDetached Whether the project is detached
*/
public OpenProject(@NonNull final String root, @NonNull final List<String> files, boolean isDetached) {
this.root = root;
this.files = files;
this.isDetached = isDetached;
}

/**
* @return The root directory of the project
*/
public String getRoot() {
return root;
}

/**
* @return The list of all file URIs tracked by the project
*/
public List<String> getFiles() {
return files;
}

/**
* @return Whether the project is detached - tracking just a single open file
*/
public boolean isDetached() {
return isDetached;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.lsp.ext.serverstatus;

import java.util.List;
import org.eclipse.lsp4j.jsonrpc.validation.NonNull;

/**
* A snapshot of the server status, containing the projects it has open.
* We can add more here later as we see fit.
*/
public class ServerStatus {
@NonNull
private final List<OpenProject> openProjects;

public ServerStatus(@NonNull final List<OpenProject> openProjects) {
this.openProjects = openProjects;
}

/**
* @return The open projects tracked by the server
*/
public List<OpenProject> getOpenProjects() {
return openProjects;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.lsp.ext.serverstatus;

/**
* LSP request parameters for a ServerStatus request.
*/
public class ServerStatusParams {
public ServerStatusParams() {
}
}
72 changes: 66 additions & 6 deletions src/main/java/software/amazon/smithy/lsp/project/Project.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -21,6 +22,7 @@
import software.amazon.smithy.model.loader.ModelAssembler;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.validation.ValidatedResult;
import software.amazon.smithy.utils.IoUtils;

/**
* A Smithy project open on the client. It keeps track of its Smithy files and
Expand Down Expand Up @@ -195,17 +197,75 @@ public void updateModel(String uri, Document document, boolean validate) {

this.modelResult = assembler.assemble();

Set<Shape> updatedShapes = modelResult.getResult()
.map(model -> model.shapes()
.filter(shape -> shape.getSourceLocation().getFilename().equals(path))
.collect(Collectors.toSet()))
.orElse(previous.getShapes());

Set<Shape> updatedShapes = getFileShapes(path, previous.getShapes());
// TODO: Could cache validation events
SmithyFile updated = ProjectLoader.buildSmithyFile(path, document, updatedShapes).build();
this.smithyFiles.put(path, updated);
}

/**
* Updates this project by adding and removing files. Also runs model validation.
*
* @param addUris URIs of files to add
* @param removeUris URIs of files to remove
*/
public void updateFiles(List<String> addUris, List<String> removeUris) {
if (!modelResult.getResult().isPresent()) {
LOGGER.severe("Attempted to update files in project with no model: " + addUris + " " + removeUris);
return;
}

if (addUris.isEmpty() && removeUris.isEmpty()) {
LOGGER.info("No files provided to update");
return;
}

Model currentModel = modelResult.getResult().get();
ModelAssembler assembler = assemblerFactory.get();
if (!removeUris.isEmpty()) {
Model.Builder builder = currentModel.toBuilder();
for (String uri : removeUris) {
String path = UriAdapter.toPath(uri);
// Note: no need to remove anything from sources/imports, since they're
// based on what's in the build files.
SmithyFile smithyFile = smithyFiles.remove(path);
if (smithyFile == null) {
LOGGER.severe("Attempted to remove file not in project: " + uri);
continue;
}
for (Shape shape : smithyFile.getShapes()) {
builder.removeShape(shape.getId());
}
}
assembler.addModel(builder.build());
} else {
assembler.addModel(currentModel);
}

for (String uri : addUris) {
assembler.addImport(UriAdapter.toPath(uri));
}

this.modelResult = assembler.assemble();

for (String uri : addUris) {
String path = UriAdapter.toPath(uri);
Set<Shape> fileShapes = getFileShapes(path, Collections.emptySet());
Document document = Document.of(IoUtils.readUtf8File(path));
SmithyFile smithyFile = ProjectLoader.buildSmithyFile(path, document, fileShapes)
.build();
smithyFiles.put(path, smithyFile);
}
}

private Set<Shape> getFileShapes(String path, Set<Shape> orDefault) {
return this.modelResult.getResult()
.map(model -> model.shapes()
.filter(shape -> shape.getSourceLocation().getFilename().equals(path))
.collect(Collectors.toSet()))
.orElse(orDefault);
}

static Builder builder() {
return new Builder();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,60 @@ public final class ProjectLoader {
private ProjectLoader() {
}

/**
* Loads a detached (single-file) {@link Project} with the given file.
*
* <p>Unlike {@link #load(Path)}, this method isn't fallible since it
* doesn't do any IO.
*
* @param uri URI of the file to load into a project
* @param text Text of the file to load into a project
* @return The loaded project
*/
public static Project loadDetached(String uri, String text) {
String asPath = UriAdapter.toPath(uri);
ValidatedResult<Model> modelResult = Model.assembler()
.addUnparsedModel(asPath, text)
.assemble();

Path path = Paths.get(asPath);
List<Path> sources = Collections.singletonList(path);

Project.Builder builder = Project.builder()
.root(path) // TODO: Does this need to be a directory?
.sources(sources)
.modelResult(modelResult);

Map<String, Set<Shape>> shapes;
if (modelResult.getResult().isPresent()) {
Model model = modelResult.getResult().get();
shapes = model.shapes().collect(Collectors.groupingByConcurrent(
shape -> shape.getSourceLocation().getFilename(), Collectors.toSet()));
} else {
shapes = new HashMap<>(0);
}

Map<String, SmithyFile> smithyFiles = new HashMap<>(shapes.size());
for (Map.Entry<String, Set<Shape>> entry : shapes.entrySet()) {
String filePath = entry.getKey();
Document document;
if (UriAdapter.isSmithyJarFile(filePath) || UriAdapter.isJarFile(filePath)) {
document = Document.of(IoUtils.readUtf8Url(UriAdapter.jarUrl(filePath)));
} else if (filePath.equals(asPath)) {
document = Document.of(text);
} else {
LOGGER.severe("Found unexpected file when loading detached (single file) project: " + filePath);
continue;
}
Set<Shape> fileShapes = entry.getValue();
SmithyFile smithyFile = buildSmithyFile(filePath, document, fileShapes).build();
smithyFiles.put(filePath, smithyFile);
}
builder.smithyFiles(smithyFiles);

return builder.build();
}

/**
* Loads a {@link Project} from a given root path.
*
Expand Down
Loading

0 comments on commit 6fce052

Please sign in to comment.