Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hover provider #63

Merged
merged 10 commits into from
Aug 29, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
capabilities.setDefinitionProvider(true);
capabilities.setDeclarationProvider(true);
capabilities.setCompletionProvider(new CompletionOptions(true, null));
capabilities.setHoverProvider(false);
capabilities.setHoverProvider(true);

return Utils.completableFuture(new InitializeResult(capabilities));
}
Expand Down Expand Up @@ -180,5 +180,4 @@ public CompletableFuture<List<? extends Location>> selectorCommand(SelectorParam
}
return CompletableFuture.completedFuture(Collections.emptyList());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
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;
Expand All @@ -43,8 +44,11 @@
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
import org.eclipse.lsp4j.DidSaveTextDocumentParams;
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;
Expand All @@ -69,6 +73,7 @@
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;
Expand Down Expand Up @@ -374,6 +379,7 @@ private String getLine(List<String> lines, Position position) {
@Override
public CompletableFuture<Either<List<? extends Location>, 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,
Expand All @@ -389,21 +395,12 @@ public CompletableFuture<Either<List<? extends Location>, List<? extends Locatio
if (initialShapeId.isPresent()) {
Model model = project.getModel().unwrap();
Shape initialShape = model.getShape(initialShapeId.get()).get();
// Find the first non-member neighbor shape or trait applied to a member whose name matches the token.
Walker shapeWalker = new Walker(NeighborProviderIndex.of(model).getProvider());
Optional<ShapeId> target = 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(found))
.findFirst();
// Use location on target, or else default to initial shape.
locations = Collections.singletonList(project.getLocations().get(target.orElse(initialShapeId.get())));
Optional<Shape> 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
Expand All @@ -423,6 +420,73 @@ public CompletableFuture<Either<List<? extends Location>, List<? extends Locatio
}
}

@Override
public CompletableFuture<Hover> hover(HoverParams params) {
Hover hover = new Hover();
MarkupContent content = new MarkupContent();
content.setKind("markdown");
Optional<ShapeId> initialShapeId = project.getShapeIdFromLocation(params.getTextDocument().getUri(),
params.getPosition());
Shape shapeToSerialize = null;
Model model = project.getModel().unwrap();
// TODO More granular error handling
try {
String token = findToken(params.getTextDocument().getUri(), params.getPosition());
LspLog.println("Found token: " + token);
if (initialShapeId.isPresent()) {
Shape initialShape = model.getShape(initialShapeId.get()).get();
Optional<Shape> 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);
}
} catch (Exception e) {
LspLog.println("Failed to determine hover content: " + e);
}

// If a shape to serialize has been found, serialize the shape and set the markup content to render
if (shapeToSerialize != null) {
content.setValue("```smithy\n" + getSerializedShape(shapeToSerialize, model) + "\n```");
}

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<Shape> 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 getSerializedShape(Shape shape, Model model) {
SmithyIdlModelSerializer serializer = SmithyIdlModelSerializer.builder()
.metadataFilter(key -> false)
.shapeFilter(s -> s.getId().equals(shape.getId()))
.serializePrelude().build();
Map<Path, String> serialized = serializer.serialize(model);
Path path = Paths.get(shape.getId().getNamespace() + ".smithy");
return serialized.get(path).trim();
}

@Override
public void didChange(DidChangeTextDocumentParams params) {
File original = fileUri(params.getTextDocument());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ public void initializeServer() throws Exception {
assertTrue(capabilities.getDefinitionProvider().getLeft());
assertTrue(capabilities.getDeclarationProvider().getLeft());
assertEquals(new CompletionOptions(true, null), capabilities.getCompletionProvider());
assertFalse(capabilities.getHoverProvider().getLeft());
assertTrue(capabilities.getHoverProvider().getLeft());
}

@Test
public void initializeWithTemporaryWorkspace() throws Exception {
public void initializeWithTemporaryWorkspace() {
InitializeParams initParams = new InitializeParams();
SmithyLanguageServer languageServer = new SmithyLanguageServer();
languageServer.initialize(initParams);
Expand Down
Loading