software.amazon.smithy.lsp.SmithyLanguageServer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of smithy-language-server Show documentation
Show all versions of smithy-language-server Show documentation
LSP implementation for smithy
The newest version!
/*
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package software.amazon.smithy.lsp;
import static java.util.concurrent.CompletableFuture.completedFuture;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionOptions;
import org.eclipse.lsp4j.CodeActionParams;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.CompletionOptions;
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;
import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams;
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
import org.eclipse.lsp4j.DidSaveTextDocumentParams;
import org.eclipse.lsp4j.DocumentFormattingParams;
import org.eclipse.lsp4j.DocumentSymbol;
import org.eclipse.lsp4j.DocumentSymbolParams;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.HoverParams;
import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.InitializedParams;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.LocationLink;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.MessageType;
import org.eclipse.lsp4j.ProgressParams;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.Registration;
import org.eclipse.lsp4j.RegistrationParams;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.SymbolKind;
import org.eclipse.lsp4j.TextDocumentContentChangeEvent;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.TextDocumentSyncKind;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.Unregistration;
import org.eclipse.lsp4j.UnregistrationParams;
import org.eclipse.lsp4j.WorkDoneProgressBegin;
import org.eclipse.lsp4j.WorkDoneProgressCreateParams;
import org.eclipse.lsp4j.WorkDoneProgressEnd;
import org.eclipse.lsp4j.WorkspaceFolder;
import org.eclipse.lsp4j.WorkspaceFoldersOptions;
import org.eclipse.lsp4j.WorkspaceServerCapabilities;
import org.eclipse.lsp4j.jsonrpc.CompletableFutures;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.LanguageClientAware;
import org.eclipse.lsp4j.services.LanguageServer;
import org.eclipse.lsp4j.services.TextDocumentService;
import org.eclipse.lsp4j.services.WorkspaceService;
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.handler.CompletionHandler;
import software.amazon.smithy.lsp.handler.DefinitionHandler;
import software.amazon.smithy.lsp.handler.FileWatcherRegistrationHandler;
import software.amazon.smithy.lsp.handler.HoverHandler;
import software.amazon.smithy.lsp.project.Project;
import software.amazon.smithy.lsp.project.ProjectChanges;
import software.amazon.smithy.lsp.project.ProjectLoader;
import software.amazon.smithy.lsp.project.ProjectManager;
import software.amazon.smithy.lsp.project.SmithyFile;
import software.amazon.smithy.lsp.protocol.LspAdapter;
import software.amazon.smithy.lsp.util.Result;
import software.amazon.smithy.model.SourceLocation;
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;
public class SmithyLanguageServer implements
LanguageServer, LanguageClientAware, SmithyProtocolExtensions, WorkspaceService, TextDocumentService {
private static final Logger LOGGER = Logger.getLogger(SmithyLanguageServer.class.getName());
private static final ServerCapabilities CAPABILITIES;
static {
ServerCapabilities capabilities = new ServerCapabilities();
capabilities.setTextDocumentSync(TextDocumentSyncKind.Incremental);
capabilities.setCodeActionProvider(new CodeActionOptions(SmithyCodeActions.all()));
capabilities.setDefinitionProvider(true);
capabilities.setDeclarationProvider(true);
capabilities.setCompletionProvider(new CompletionOptions(true, null));
capabilities.setHoverProvider(true);
capabilities.setDocumentFormattingProvider(true);
capabilities.setDocumentSymbolProvider(true);
WorkspaceFoldersOptions workspaceFoldersOptions = new WorkspaceFoldersOptions();
workspaceFoldersOptions.setSupported(true);
capabilities.setWorkspace(new WorkspaceServerCapabilities(workspaceFoldersOptions));
CAPABILITIES = capabilities;
}
private SmithyLanguageClient client;
private final ProjectManager projects = new ProjectManager();
private final DocumentLifecycleManager lifecycleManager = new DocumentLifecycleManager();
private Severity minimumSeverity = Severity.WARNING;
private boolean onlyReloadOnSave = false;
SmithyLanguageServer() {
}
SmithyLanguageClient getClient() {
return this.client;
}
Project getFirstProject() {
return projects.attachedProjects().values().stream().findFirst().orElse(null);
}
ProjectManager getProjects() {
return projects;
}
DocumentLifecycleManager getLifecycleManager() {
return this.lifecycleManager;
}
@Override
public void connect(LanguageClient client) {
LOGGER.finest("Connect");
this.client = new SmithyLanguageClient(client);
String message = "smithy-language-server";
try {
Properties props = new Properties();
props.load(Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream("version.properties")));
message += " version " + props.getProperty("version");
} catch (IOException e) {
this.client.error("Failed to load smithy-language-server version: " + e);
}
this.client.info(message + " started.");
}
@Override
public CompletableFuture initialize(InitializeParams params) {
LOGGER.finest("Initialize");
Optional.ofNullable(params.getProcessId())
.flatMap(ProcessHandle::of)
.ifPresent(processHandle -> processHandle.onExit().thenRun(this::exit));
// TODO: Replace with a Gson Type Adapter if more config options are added beyond `logToFile`.
Object initializationOptions = params.getInitializationOptions();
if (initializationOptions instanceof JsonObject jsonObject) {
if (jsonObject.has("diagnostics.minimumSeverity")) {
String configuredMinimumSeverity = jsonObject.get("diagnostics.minimumSeverity").getAsString();
Optional severity = Severity.fromString(configuredMinimumSeverity);
if (severity.isPresent()) {
this.minimumSeverity = severity.get();
} else {
client.error(String.format("""
Invalid value for 'diagnostics.minimumSeverity': %s.
Must be one of %s.""", configuredMinimumSeverity, Arrays.toString(Severity.values())));
}
}
if (jsonObject.has("onlyReloadOnSave")) {
this.onlyReloadOnSave = jsonObject.get("onlyReloadOnSave").getAsBoolean();
client.info("Configured only reload on save: " + this.onlyReloadOnSave);
}
}
if (params.getWorkspaceFolders() != null && !params.getWorkspaceFolders().isEmpty()) {
Either workDoneProgressToken = params.getWorkDoneToken();
if (workDoneProgressToken != null) {
WorkDoneProgressBegin notification = new WorkDoneProgressBegin();
notification.setTitle("Initializing");
client.notifyProgress(new ProgressParams(workDoneProgressToken, Either.forLeft(notification)));
}
for (WorkspaceFolder workspaceFolder : params.getWorkspaceFolders()) {
Path root = Paths.get(URI.create(workspaceFolder.getUri()));
tryInitProject(workspaceFolder.getName(), root);
}
if (workDoneProgressToken != null) {
WorkDoneProgressEnd notification = new WorkDoneProgressEnd();
client.notifyProgress(new ProgressParams(workDoneProgressToken, Either.forLeft(notification)));
}
}
LOGGER.finest("Done initialize");
return completedFuture(new InitializeResult(CAPABILITIES));
}
private void tryInitProject(String name, Path root) {
LOGGER.finest("Initializing project at " + root);
lifecycleManager.cancelAllTasks();
Result> loadResult = ProjectLoader.load(
root, projects, lifecycleManager.managedDocuments());
if (loadResult.isOk()) {
Project updatedProject = loadResult.unwrap();
resolveDetachedProjects(this.projects.getProjectByName(name), updatedProject);
this.projects.updateProjectByName(name, updatedProject);
LOGGER.finest("Initialized project at " + root);
} else {
LOGGER.severe("Init project failed");
// TODO: Maybe we just start with this anyways by default, and then add to it
// if we find a smithy-build.json, etc.
// If we overwrite an existing project with an empty one, we lose track of the state of tracked
// files. Instead, we will just keep the original project before the reload failure.
if (projects.getProjectByName(name) == null) {
projects.updateProjectByName(name, Project.empty(root));
}
String baseMessage = "Failed to load Smithy project " + name + " at " + root;
StringBuilder errorMessage = new StringBuilder(baseMessage).append(":");
for (Exception error : loadResult.unwrapErr()) {
errorMessage.append(System.lineSeparator());
errorMessage.append('\t');
errorMessage.append(error.getMessage());
}
client.error(errorMessage.toString());
String showMessage = baseMessage + ". Check server logs to find out what went wrong.";
client.showMessage(new MessageParams(MessageType.Error, showMessage));
}
}
private void resolveDetachedProjects(Project oldProject, Project updatedProject) {
// This is a project reload, so we need to resolve any added/removed files
// that need to be moved to or from detached projects.
if (oldProject != null) {
Set currentProjectSmithyPaths = oldProject.smithyFiles().keySet();
Set updatedProjectSmithyPaths = updatedProject.smithyFiles().keySet();
Set addedPaths = new HashSet<>(updatedProjectSmithyPaths);
addedPaths.removeAll(currentProjectSmithyPaths);
for (String addedPath : addedPaths) {
String addedUri = LspAdapter.toUri(addedPath);
if (projects.isDetached(addedUri)) {
projects.removeDetachedProject(addedUri);
}
}
Set removedPaths = new HashSet<>(currentProjectSmithyPaths);
removedPaths.removeAll(updatedProjectSmithyPaths);
for (String removedPath : removedPaths) {
String removedUri = LspAdapter.toUri(removedPath);
// Only move to a detached project if the file is managed
if (lifecycleManager.managedDocuments().contains(removedUri)) {
Document removedDocument = oldProject.getDocument(removedUri);
// The copy here is technically unnecessary, if we make ModelAssembler support borrowed strings
projects.createDetachedProject(removedUri, removedDocument.copyText());
}
}
}
}
private CompletableFuture registerSmithyFileWatchers() {
List registrations = FileWatcherRegistrationHandler.getSmithyFileWatcherRegistrations(
projects.attachedProjects().values());
return client.registerCapability(new RegistrationParams(registrations));
}
private CompletableFuture unregisterSmithyFileWatchers() {
List unregistrations = FileWatcherRegistrationHandler.getSmithyFileWatcherUnregistrations();
return client.unregisterCapability(new UnregistrationParams(unregistrations));
}
@Override
public void initialized(InitializedParams params) {
List registrations = FileWatcherRegistrationHandler.getBuildFileWatcherRegistrations(
projects.attachedProjects().values());
client.registerCapability(new RegistrationParams(registrations));
registerSmithyFileWatchers();
}
@Override
public WorkspaceService getWorkspaceService() {
return this;
}
@Override
public TextDocumentService getTextDocumentService() {
return this;
}
@Override
public CompletableFuture