software.amazon.smithy.lsp.SmithyTextDocumentService Maven / Gradle / Ivy
Show all versions of smithy-language-server Show documentation
/*
* 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 com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionParams;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.CompletionParams;
import org.eclipse.lsp4j.DefinitionParams;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DidChangeTextDocumentParams;
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
import org.eclipse.lsp4j.DidSaveTextDocumentParams;
import org.eclipse.lsp4j.DocumentFormattingParams;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.HoverParams;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.LocationLink;
import org.eclipse.lsp4j.MarkupContent;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.MessageType;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.TextDocumentItem;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.TextDocumentService;
import smithyfmt.Formatter;
import smithyfmt.Result;
import software.amazon.smithy.lsp.codeactions.SmithyCodeActions;
import software.amazon.smithy.lsp.diagnostics.VersionDiagnostics;
import software.amazon.smithy.lsp.editor.SmartInput;
import software.amazon.smithy.lsp.ext.Completions;
import software.amazon.smithy.lsp.ext.Constants;
import software.amazon.smithy.lsp.ext.Document;
import software.amazon.smithy.lsp.ext.DocumentPreamble;
import software.amazon.smithy.lsp.ext.LspLog;
import software.amazon.smithy.lsp.ext.SmithyBuildLoader;
import software.amazon.smithy.lsp.ext.SmithyProject;
import software.amazon.smithy.lsp.ext.model.SmithyBuildExtensions;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.SourceLocation;
import software.amazon.smithy.model.knowledge.NeighborProviderIndex;
import software.amazon.smithy.model.loader.ParserUtils;
import software.amazon.smithy.model.neighbor.Walker;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.SmithyIdlModelSerializer;
import software.amazon.smithy.model.validation.ValidatedResult;
import software.amazon.smithy.model.validation.ValidationEvent;
import software.amazon.smithy.utils.SimpleParser;
public class SmithyTextDocumentService implements TextDocumentService {
private final List baseCompletions = new ArrayList<>();
private Optional client;
private final List noLocations = Collections.emptyList();
private SmithyProject project;
private final File temporaryFolder;
// when files are edited, their contents will be persisted in memory and removed
// on didSave or didClose
private final Map temporaryContents = new ConcurrentHashMap<>();
// We use this function to hash filepaths to the same location in temporary
// folder
private final HashFunction hash = Hashing.murmur3_128();
/**
* @param client Language Client to be used by text document service.
* @param tempFile Temporary File to be used by text document service.
*/
public SmithyTextDocumentService(Optional client, File tempFile) {
this.client = client;
this.temporaryFolder = tempFile;
}
public void setClient(LanguageClient client) {
this.client = Optional.of(client);
}
public Optional getRoot() {
return Optional.ofNullable(project).map(SmithyProject::getRoot);
}
/**
* Processes extensions.
*
* 1. Downloads external dependencies as jars 2. Creates a model from just
* external jars 3. Updates locations index with symbols found in external jars
*
* @param ext extensions
* @param root workspace root
*/
public void createProject(SmithyBuildExtensions ext, File root) {
Either loaded = SmithyProject.load(ext, root);
if (loaded.isRight()) {
this.project = loaded.getRight();
clearAllDiagnostics();
sendInfo("Project loaded with " + this.project.getExternalJars().size() + " external jars and "
+ this.project.getSmithyFiles().size() + " discovered smithy files");
} else {
sendError("Failed to create Smithy project: " + loaded.getLeft().toString());
}
}
/**
* Discovers Smithy build files and loads the smithy project defined by them.
*
* @param root workspace root
*/
public void createProject(File root) {
LspLog.println("Recreating project from " + root);
SmithyBuildExtensions.Builder result = SmithyBuildExtensions.builder();
List brokenFiles = new ArrayList<>();
for (String file : Constants.BUILD_FILES) {
File smithyBuild = Paths.get(root.getAbsolutePath(), file).toFile();
if (smithyBuild.isFile()) {
try {
SmithyBuildExtensions local = SmithyBuildLoader.load(smithyBuild.toPath());
result.merge(local);
LspLog.println("Loaded build extensions " + local + " from " + smithyBuild.getAbsolutePath());
} catch (Exception e) {
LspLog.println("Failed to load config from" + smithyBuild + ": " + e);
brokenFiles.add(smithyBuild.toString());
}
}
}
if (brokenFiles.isEmpty()) {
createProject(result.build(), root);
} else {
sendError("Failed to load the build, following files have problems: \n" + String.join("\n", brokenFiles));
}
}
private MessageParams msg(final MessageType sev, final String cont) {
return new MessageParams(sev, cont);
}
@Override
public CompletableFuture, CompletionList>> completion(CompletionParams params) {
LspLog.println("Asking to complete " + params + " in class " + params.getTextDocument().getClass());
try {
String documentUri = params.getTextDocument().getUri();
String token = findToken(documentUri, params.getPosition());
DocumentPreamble preamble = Document.detectPreamble(textBufferContents(documentUri));
boolean isTraitShapeId = isTraitShapeId(documentUri, params.getPosition());
Optional target = Optional.empty();
if (isTraitShapeId) {
target = getTraitTarget(documentUri, params.getPosition(), preamble.getCurrentNamespace());
}
List items = Completions.resolveImports(project.getCompletions(token, isTraitShapeId,
target),
preamble);
LspLog.println("Completion items: " + items);
return Utils.completableFuture(Either.forLeft(items));
} catch (Exception e) {
LspLog.println(
"Failed to identify token for completion in " + params.getTextDocument().getUri() + ": " + e);
}
return Utils.completableFuture(Either.forLeft(baseCompletions));
}
// Determine the target of a trait, if present.
private Optional getTraitTarget(String documentUri, Position position, Optional namespace)
throws IOException {
List contents = textBufferContents(documentUri);
String currentLine = contents.get(position.getLine()).trim();
if (currentLine.startsWith("apply")) {
return getApplyStatementTarget(currentLine, namespace);
}
// Iterate through the rest of the model file, skipping docs and other traits to get trait's target.
for (int i = position.getLine() + 1; i < contents.size(); i++) {
String line = contents.get(i).trim();
// If an empty line is encountered, assume the trait's target has not yet been written.
if (line.equals("")) {
return Optional.empty();
// Skip comments lines
} else if (line.startsWith("//")) {
// Skip other traits.
} else if (line.startsWith("@")) {
// Jump to end of trait.
i = getEndOfTrait(i, contents);
} else {
// Offset the target shape position by accounting for leading whitespace.
String originalLine = contents.get(i);
int offset = 1;
while (originalLine.charAt(offset) == ' ') {
offset++;
}
return project.getShapeIdFromLocation(documentUri, new Position(i, offset));
}
}
return Optional.empty();
}
// Determine target shape from an apply statement.
private Optional getApplyStatementTarget(String applyStatement, Optional namespace) {
SimpleParser parser = new SimpleParser(applyStatement);
parser.expect('a');
parser.expect('p');
parser.expect('p');
parser.expect('l');
parser.expect('y');
parser.ws();
String name = ParserUtils.parseShapeId(parser);
if (namespace.isPresent()) {
return Optional.of(ShapeId.fromParts(namespace.get(), name));
}
return Optional.empty();
}
// Find the line where the trait ends.
private int getEndOfTrait(int lineNumber, List contents) {
String line = contents.get(lineNumber);
if (line.contains("(")) {
if (hasClosingParen(line)) {
return lineNumber;
}
for (int i = lineNumber + 1; i < contents.size(); i++) {
String nextLine = contents.get(i).trim();
if (hasClosingParen(nextLine)) {
return i;
}
}
}
return lineNumber;
}
// Determine if the line has an unquoted closing parenthesis.
private boolean hasClosingParen(String line) {
boolean quote = false;
for (int i = 0; i < line.length(); i++) {
char c = line.charAt(i);
if (c == '"' && !quote) {
quote = true;
} else if (c == '"' && quote) {
quote = false;
}
if (c == ')' && !quote) {
return true;
}
}
return false;
}
// Work backwards from current position to determine if position is part of a trait shapeId.
private boolean isTraitShapeId(String documentUri, Position position) throws IOException {
String line = getLine(textBufferContents(documentUri), position);
for (int i = position.getCharacter() - 1; i >= 0; i--) {
char c = line.charAt(i);
if (c == '@') {
return true;
}
if (c == ' ') {
return false;
}
}
return false;
}
@Override
public CompletableFuture resolveCompletionItem(CompletionItem unresolved) {
return Utils.completableFuture(unresolved);
}
private List readAll(File f) throws IOException {
return Files.readAllLines(f.toPath());
}
private File designatedTemporaryFile(File source) {
String hashed = hash.hashString(source.getAbsolutePath(), StandardCharsets.UTF_8).toString();
return new File(this.temporaryFolder, hashed + Constants.SMITHY_EXTENSION);
}
private List textBufferContents(String path) throws IOException {
List contents;
if (Utils.isSmithyJarFile(path)) {
contents = Utils.jarFileContents(path);
} else {
String tempContents = temporaryContents.get(fileFromUri(path));
if (tempContents != null) {
LspLog.println("Path " + path + " was found in temporary buffer");
contents = Arrays.stream(tempContents.split("\n")).collect(Collectors.toList());
} else {
try {
contents = readAll(new File(URI.create(path)));
} catch (IllegalArgumentException e) {
contents = readAll(new File(path));
}
}
}
return contents;
}
private String findToken(String path, Position p) throws IOException {
List contents = textBufferContents(path);
String line = contents.get(p.getLine());
int col = p.getCharacter();
LspLog.println("Trying to find a token in line '" + line + "' at position " + p);
String before = line.substring(0, col);
String after = line.substring(col, line.length());
StringBuilder beforeAcc = new StringBuilder();
StringBuilder afterAcc = new StringBuilder();
int idx = 0;
while (idx < after.length()) {
if (Character.isLetterOrDigit(after.charAt(idx))) {
afterAcc.append(after.charAt(idx));
idx = idx + 1;
} else {
idx = after.length();
}
}
idx = before.length() - 1;
while (idx > 0) {
char c = before.charAt(idx);
if (Character.isLetterOrDigit(c)) {
beforeAcc.append(c);
idx = idx - 1;
} else {
idx = 0;
}
}
return beforeAcc.reverse().append(afterAcc).toString();
}
private String getLine(List lines, Position position) {
return lines.get(position.getLine());
}
@Override
public CompletableFuture, List extends LocationLink>>> definition(
DefinitionParams params) {
// TODO More granular error handling
try {
// This attempts to return the definition location that corresponds to a position within a text document.
// First, the position is used to find any shapes in the model that are defined at that location. Next,
// a token is extracted from the raw text document. The model is walked from the starting shapeId and any
// the locations of neighboring shapes that match the token are returned. For example, if the position
// is the input of an operation, the token will be the name of the input structure, and the operation will
// be walked to return the location of where the input structure is defined. This allows go-to-definition
// to jump from the input of the operation, to where the input structure is actually defined.
List locations;
Optional initialShapeId = project.getShapeIdFromLocation(params.getTextDocument().getUri(),
params.getPosition());
String found = findToken(params.getTextDocument().getUri(), params.getPosition());
if (initialShapeId.isPresent()) {
Model model = project.getModel().unwrap();
Shape initialShape = model.getShape(initialShapeId.get()).get();
Optional target = getTargetShape(initialShape, found, model);
// Use location of target shape or default to the location of the initial shape.
ShapeId shapeId = target.map(Shape::getId).orElse(initialShapeId.get());
Location shapeLocation = project.getLocations().get(shapeId);
locations = Collections.singletonList(shapeLocation);
} else {
// If the definition params do not have a matching shape at that location, return locations of all
// shapes that match token by shape name. This makes it possible link the shape name in a line
// comment to its definition.
locations = project.getLocations().entrySet().stream()
.filter(entry -> entry.getKey().getName().equals(found))
.map(Map.Entry::getValue)
.collect(Collectors.toList());
}
return Utils.completableFuture(Either.forLeft(locations));
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
return Utils.completableFuture(Either.forLeft(noLocations));
}
}
@Override
public CompletableFuture hover(HoverParams params) {
Hover hover = new Hover();
MarkupContent content = new MarkupContent();
content.setKind("markdown");
Optional initialShapeId = project.getShapeIdFromLocation(params.getTextDocument().getUri(),
params.getPosition());
// TODO More granular error handling
try {
Shape shapeToSerialize;
Model model = project.getModel().unwrap();
String token = findToken(params.getTextDocument().getUri(), params.getPosition());
LspLog.println("Found token: " + token);
if (initialShapeId.isPresent()) {
Shape initialShape = model.getShape(initialShapeId.get()).get();
Optional target = initialShape.asMemberShape()
.map(memberShape -> model.getShape(memberShape.getTarget()))
.orElse(getTargetShape(initialShape, token, model));
shapeToSerialize = target.orElse(initialShape);
} else {
shapeToSerialize = model.shapes()
.filter(shape -> !shape.isMemberShape())
.filter(shape -> shape.getId().getName().equals(token))
.findAny()
.orElse(null);
}
if (shapeToSerialize != null) {
content.setValue(getHoverContentsForShape(shapeToSerialize, model));
}
} catch (Exception e) {
LspLog.println("Failed to determine hover content: " + e);
}
hover.setContents(content);
return Utils.completableFuture(hover);
}
// Finds the first non-member neighbor shape or trait applied to a member whose name matches the token.
private Optional getTargetShape(Shape initialShape, String token, Model model) {
LspLog.println("Finding target of: " + initialShape);
Walker shapeWalker = new Walker(NeighborProviderIndex.of(model).getProvider());
return shapeWalker.walkShapes(initialShape).stream()
.flatMap(shape -> {
if (shape.isMemberShape()) {
return shape.getAllTraits().values().stream()
.map(trait -> trait.toShapeId());
} else {
return Stream.of(shape.getId());
}
})
.filter(shapeId -> shapeId.getName().equals(token))
.map(shapeId -> model.getShape(shapeId).get())
.findFirst();
}
private String getHoverContentsForShape(Shape shape, Model model) {
try {
String serializedShape = serializeShape(shape, model);
return "```smithy\n" + serializedShape + "\n```";
} catch (Exception e) {
List validationEvents = getValidationEventsForShape(shape);
StringBuilder contents = new StringBuilder();
contents.append("Can't display shape ").append("`").append(shape.getId().toString()).append("`:");
for (ValidationEvent event : validationEvents) {
contents.append(System.lineSeparator()).append(event.getMessage());
}
if (validationEvents.isEmpty()) {
contents.append(System.lineSeparator()).append(e);
}
return contents.toString();
}
}
private String serializeShape(Shape shape, Model model) {
SmithyIdlModelSerializer serializer = SmithyIdlModelSerializer.builder()
.metadataFilter(key -> false)
.shapeFilter(s -> s.getId().equals(shape.getId()))
.serializePrelude().build();
Map serialized = serializer.serialize(model);
Path path = Paths.get(shape.getId().getNamespace() + ".smithy");
return serialized.get(path).trim();
}
private List getValidationEventsForShape(Shape shape) {
return project.getModel().getValidationEvents().stream()
.filter(validationEvent -> shape.getId().equals(validationEvent.getShapeId().orElse(null)))
.collect(Collectors.toList());
}
@Override
public CompletableFuture>> codeAction(CodeActionParams params) {
List> versionCodeActions =
SmithyCodeActions.versionCodeActions(params).stream()
.map(Either::forRight)
.collect(Collectors.toList());
return Utils.completableFuture(versionCodeActions);
}
@Override
public void didChange(DidChangeTextDocumentParams params) {
File original = fileUri(params.getTextDocument());
File tempFile = null;
try {
if (params.getContentChanges().size() > 0) {
tempFile = designatedTemporaryFile(original);
String contents = params.getContentChanges().get(0).getText();
unstableContents(original, contents);
Files.write(tempFile.toPath(), contents.getBytes());
}
} catch (Exception e) {
LspLog.println("Failed to write temporary contents for file " + original + " into temporary file "
+ tempFile + " : " + e);
}
report(recompile(original, Optional.ofNullable(tempFile)));
}
private void stableContents(File file) {
this.temporaryContents.remove(file);
}
private void unstableContents(File file, String contents) {
LspLog.println("Hashed filename " + file + " into " + designatedTemporaryFile(file));
this.temporaryContents.put(file, contents);
}
@Override
public void didOpen(DidOpenTextDocumentParams params) {
String rawUri = params.getTextDocument().getUri();
if (Utils.isFile(rawUri)) {
report(recompile(fileUri(params.getTextDocument()), Optional.empty()));
}
}
@Override
public void didClose(DidCloseTextDocumentParams params) {
File file = fileUri(params.getTextDocument());
stableContents(file);
report(recompile(file, Optional.empty()));
}
@Override
public void didSave(DidSaveTextDocumentParams params) {
File file = fileUri(params.getTextDocument());
stableContents(file);
report(recompile(file, Optional.empty()));
}
@Override
public CompletableFuture> formatting(DocumentFormattingParams params) {
File file = fileUri(params.getTextDocument());
final CompletableFuture> emptyResult =
Utils.completableFuture(Collections.emptyList());
final Optional content = Utils.optOr(
Optional.ofNullable(temporaryContents.get(file)).map(SmartInput::fromInput),
() -> SmartInput.fromPathSafe(file.toPath())
);
if (content.isPresent()) {
SmartInput input = content.get();
final Result result = Formatter.format(input.getInput());
final Range fullRange = input.getRange();
if (result.isSuccess() && !result.getValue().equals(input.getInput())) {
return Utils.completableFuture(Collections.singletonList(new TextEdit(
fullRange,
result.getValue()
)));
} else if (!result.isSuccess()) {
LspLog.println("Failed to format: " + result.getError());
return emptyResult;
} else {
return emptyResult;
}
} else {
LspLog.println("Content is unavailable, not formatting.");
return emptyResult;
}
}
private File fileUri(TextDocumentIdentifier tdi) {
return fileFromUri(tdi.getUri());
}
private File fileUri(TextDocumentItem tdi) {
return fileFromUri(tdi.getUri());
}
private File fileFromUri(String uri) {
try {
return new File(URI.create(uri));
} catch (IllegalArgumentException e) {
return new File(uri);
}
}
/**
* @param result Either a fatal error message, or a list of diagnostics to
* publish
*/
public void report(Either> result) {
client.ifPresent(cl -> {
if (result.isLeft()) {
cl.showMessage(msg(MessageType.Error, result.getLeft()));
} else {
result.getRight().forEach(cl::publishDiagnostics);
}
});
}
/**
* Breaks down a list of validation events into a per-file list of diagnostics,
* explicitly publishing an empty list of diagnostics for files not present in
* validation events.
*
* @param events output of the Smithy model builder
* @param allFiles all the files registered for the project
* @return a list of LSP diagnostics to publish
*/
public List createPerFileDiagnostics(List events, List allFiles) {
// URI is used because conversion toString deals with platform specific path separator
Map> byUri = new HashMap<>();
for (ValidationEvent ev : events) {
URI finalUri;
try {
// can be a uri in the form of jar:file:/some-path
// if we have a jar we go to smithyjar
// else we make sure `file:` scheme is used
String fileName = ev.getSourceLocation().getFilename();
String uri = Utils.isJarFile(fileName)
? Utils.toSmithyJarFile(fileName)
: !Utils.isFile(fileName) ? "file:" + fileName
: fileName;
finalUri = new URI(uri);
} catch (URISyntaxException ex) {
// can also be something like C:\Some\path in which case creating a URI will fail
// so after a file conversion, we call .toURI to produce a standard `file:/C:/Some/path`
finalUri = new File(ev.getSourceLocation().getFilename()).toURI();
}
if (byUri.containsKey(finalUri)) {
byUri.get(finalUri).add(ProtocolAdapter.toDiagnostic(ev));
} else {
List l = new ArrayList<>();
l.add(ProtocolAdapter.toDiagnostic(ev));
byUri.put(finalUri, l);
}
}
allFiles.forEach(f -> {
List versionDiagnostics = VersionDiagnostics.createVersionDiagnostics(f, temporaryContents);
if (!byUri.containsKey(f.toURI())) {
byUri.put(f.toURI(), versionDiagnostics);
} else {
byUri.get(f.toURI()).addAll(versionDiagnostics);
}
});
List diagnostics = new ArrayList<>();
byUri.forEach((key, value) -> diagnostics.add(new PublishDiagnosticsParams(key.toString(), value)));
return diagnostics;
}
public void clearAllDiagnostics() {
report(Either.forRight(createPerFileDiagnostics(this.project.getModel().getValidationEvents(),
this.project.getSmithyFiles())));
}
/**
* Main recompilation method, responsible for reloading the model, persisting it
* if necessary, and massaging validation events into publishable diagnostics.
*
* @param path file that triggered recompilation
* @param temporary optional location of a temporary file with most recent
* contents
* @return either a fatal error message, or a list of diagnostics
*/
public Either> recompile(File path, Optional temporary) {
// File latestContents = temporary.orElse(path);
Either loadedModel;
if (!temporary.isPresent()) {
// if there's no temporary file present (didOpen/didClose/didSave)
// we want to rebuild the model with the original path
// optionally removing a temporary file
// This protects against a conflict during the didChange -> didSave sequence
loadedModel = this.project.recompile(path, designatedTemporaryFile(path));
} else {
// If there's a temporary file present (didChange), we want to
// replace the original path with a temporary one (to avoid conflicting
// definitions)
loadedModel = this.project.recompile(temporary.get(), path);
}
if (loadedModel.isLeft()) {
return Either.forLeft(path + " is not okay!" + loadedModel.getLeft().toString());
} else {
ValidatedResult result = loadedModel.getRight().getModel();
// If we're working with a temporary file, we don't want to persist the result
// of the project
if (!temporary.isPresent()) {
this.project = loadedModel.getRight();
}
List events = new ArrayList<>();
List allFiles;
if (temporary.isPresent()) {
allFiles = project.getSmithyFiles().stream().filter(f -> !f.equals(temporary.get()))
.collect(Collectors.toList());
// We need to remap some validation events
// from temporary files to the one on which didChange was invoked
for (ValidationEvent ev : result.getValidationEvents()) {
if (ev.getSourceLocation().getFilename().equals(temporary.get().getAbsolutePath())) {
SourceLocation sl = new SourceLocation(path.getAbsolutePath(), ev.getSourceLocation().getLine(),
ev.getSourceLocation().getColumn());
ValidationEvent newEvent = ev.toBuilder().sourceLocation(sl).build();
events.add(newEvent);
} else {
events.add(ev);
}
}
} else {
events.addAll(result.getValidationEvents());
allFiles = project.getSmithyFiles();
}
LspLog.println(
"Recompiling " + path + " (with temporary content " + temporary + ") raised " + events.size()
+ " diagnostics");
return Either.forRight(createPerFileDiagnostics(events, allFiles));
}
}
/**
* Run a selector expression against the loaded model in the workspace.
* @param expression the selector expression
* @return list of locations of shapes that match expression
*/
public Either> runSelector(String expression) {
return this.project.runSelector(expression);
}
private void sendInfo(String msg) {
this.client.ifPresent(client -> client.showMessage(new MessageParams(MessageType.Info, msg)));
}
private void sendError(String msg) {
this.client.ifPresent(client -> client.showMessage(new MessageParams(MessageType.Error, msg)));
}
}