software.amazon.smithy.lsp.ext.SmithyProject Maven / Gradle / Ivy
Show all versions of smithy-language-server Show documentation
/*
* Copyright 2022 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.ext;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import software.amazon.smithy.lsp.SmithyInterface;
import software.amazon.smithy.lsp.ext.model.SmithyBuildExtensions;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.selector.Selector;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.validation.ValidatedResult;
import software.amazon.smithy.model.validation.ValidatedResultException;
public final class SmithyProject {
private final List imports;
private final List smithyFiles;
private final List externalJars;
private Map locations = Collections.emptyMap();
private final ValidatedResult model;
private final File root;
SmithyProject(
List imports,
List smithyFiles,
List externalJars,
File root,
ValidatedResult model
) {
this.imports = imports;
this.root = root;
this.model = model;
this.smithyFiles = smithyFiles;
this.externalJars = externalJars;
model.getResult().ifPresent(m -> this.locations = collectLocations(m));
}
/**
* Recompile the model, adding a file to list of tracked files, potentially
* excluding some other file.
*
* This version of the method above is used when the
* file is in ephemeral storage (temporary location when file is being changed)
*
* @param changed file which may or may not be already tracked by this project.
* @param exclude file to exclude from being recompiled.
* @return either an error, or a loaded project.
*/
public Either recompile(File changed, File exclude) {
HashSet fileSet = new HashSet<>();
for (File existing : onlyExistingFiles(this.smithyFiles)) {
if (exclude != null && !existing.equals(exclude)) {
fileSet.add(existing);
}
}
if (changed.isFile()) {
fileSet.add(changed);
}
return load(this.imports, new ArrayList<>(fileSet), this.externalJars, this.root);
}
public ValidatedResult getModel() {
return this.model;
}
public List getExternalJars() {
return this.externalJars;
}
public List getSmithyFiles() {
return this.smithyFiles;
}
public List getCompletions(String token, boolean isTrait, Optional target) {
return this.model.getResult().map(model -> Completions.find(model, token, isTrait, target))
.orElse(Collections.emptyList());
}
public Map getLocations() {
return this.locations;
}
/**
* Load the project using a SmithyBuildExtensions configuration and workspace
* root.
*
* @param config configuration.
* @param root workspace root.
* @return either an error or a loaded project.
*/
public static Either load(SmithyBuildExtensions config, File root) {
List imports = config.getImports().stream().map(p -> Paths.get(root.getAbsolutePath(), p).normalize())
.collect(Collectors.toList());
if (imports.isEmpty()) {
imports.add(root.toPath());
}
LspLog.println("Imports from config: " + imports + " will be resolved against root " + root);
List smithyFiles = discoverSmithyFiles(imports, root);
LspLog.println("Discovered smithy files: " + smithyFiles);
List externalJars = downloadExternalDependencies(config);
LspLog.println("Downloaded external jars: " + externalJars);
return load(imports, smithyFiles, externalJars, root);
}
/**
* 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) {
try {
Selector selector = Selector.parse(expression);
Set shapes = selector.select(this.model.unwrap());
return Either.forRight(shapes.stream()
.map(shape -> this.locations.get(shape.getId()))
.collect(Collectors.toList()));
} catch (ValidatedResultException e) {
return Either.forLeft(e);
}
}
private static Either load(
List imports,
List smithyFiles,
List externalJars,
File root
) {
Either> model = createModel(smithyFiles, externalJars);
if (model.isLeft()) {
return Either.forLeft(model.getLeft());
} else {
model.getRight().getValidationEvents().forEach(LspLog::println);
return Either.forRight(new SmithyProject(imports, smithyFiles, externalJars, root, model.getRight()));
}
}
private static Either> createModel(
List discoveredFiles,
List externalJars
) {
return SmithyInterface.readModel(discoveredFiles, externalJars);
}
public File getRoot() {
return this.root;
}
private static Map collectLocations(Model model) {
ShapeLocationCollector collector = new FileCachingCollector();
return collector.collectDefinitionLocations(model);
}
/**
* Returns the shapeId of the shape that corresponds to the file uri and position within the model.
*
* @param uri String uri of model file.
* @param position Cursor position within model file.
* @return ShapeId of corresponding shape defined at location.
*/
public Optional getShapeIdFromLocation(String uri, Position position) {
Comparator> rangeSize = Comparator.comparing(entry ->
entry.getValue().getRange().getEnd().getLine() - entry.getValue().getRange().getStart().getLine());
return locations.entrySet().stream()
.filter(entry -> entry.getValue().getUri().endsWith(Paths.get(uri).toString()))
.filter(entry -> isPositionInRange(entry.getValue().getRange(), position))
// Since the position is in each of the overlapping shapes, return the location with the smallest range.
.sorted(rangeSize)
.map(Map.Entry::getKey)
.findFirst();
}
private boolean isPositionInRange(Range range, Position position) {
if (range.getStart().getLine() > position.getLine()) {
return false;
}
if (range.getEnd().getLine() < position.getLine()) {
return false;
}
// For single-line ranges, make sure position is between start and end chars.
if (range.getStart().getLine() == position.getLine()
&& range.getEnd().getLine() == position.getLine()) {
return (range.getStart().getCharacter() <= position.getCharacter()
&& range.getEnd().getCharacter() >= position.getCharacter());
} else if (range.getStart().getLine() == position.getLine()) {
return range.getStart().getCharacter() <= position.getCharacter();
} else if (range.getEnd().getLine() == position.getLine()) {
return range.getEnd().getCharacter() >= position.getCharacter();
}
return true;
}
private static Boolean isValidSmithyFile(Path file) {
String fName = file.getFileName().toString();
return fName.endsWith(Constants.SMITHY_EXTENSION);
}
private static List walkSmithyFolder(Path path, File root) {
try (Stream walk = Files.walk(path)) {
return walk.filter(Files::isRegularFile).filter(SmithyProject::isValidSmithyFile).map(Path::toFile)
.collect(Collectors.toList());
} catch (IOException e) {
LspLog.println("Failed to walk import '" + path + "' from root " + root + ": " + e);
return new ArrayList<>();
}
}
private static List discoverSmithyFiles(List imports, File root) {
List smithyFiles = new ArrayList<>();
imports.forEach(path -> {
if (Files.isDirectory(path)) {
smithyFiles.addAll(walkSmithyFolder(path, root));
} else if (isValidSmithyFile(path)) {
smithyFiles.add(path.resolve(root.toPath()).toFile());
}
});
return smithyFiles;
}
private static List downloadExternalDependencies(SmithyBuildExtensions ext) {
LspLog.println("Downloading external dependencies for " + ext);
try {
return DependencyDownloader.create(ext).download();
} catch (Exception e) {
LspLog.println("Failed to download external jars for " + ext + ": " + e);
return Collections.emptyList();
}
}
private static List onlyExistingFiles(Collection files) {
return files.stream().filter(File::isFile).collect(Collectors.toList());
}
}