Skip to content

Commit

Permalink
Add completions for for build files (#193)
Browse files Browse the repository at this point in the history
The server now can provide completions for smithy-build.json and
.smithy-project.json. The implementation works roughly the same as other
node-like completions, using a new builtins model that specifies the
structure of the build files. I also had to override the completion item
mapper to make sure it wrapped object keys in strings, which is
necessary in json.
  • Loading branch information
milesziemer authored Feb 13, 2025
1 parent fcaa7d5 commit 6c4eef4
Show file tree
Hide file tree
Showing 8 changed files with 532 additions and 24 deletions.
21 changes: 14 additions & 7 deletions src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
package software.amazon.smithy.lsp;

import static java.util.concurrent.CompletableFuture.completedFuture;
import static java.util.concurrent.CompletableFuture.supplyAsync;
import static org.eclipse.lsp4j.jsonrpc.CompletableFutures.computeAsync;

import java.io.IOException;
import java.util.ArrayList;
Expand Down Expand Up @@ -79,7 +81,6 @@
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;
Expand All @@ -93,6 +94,7 @@
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.language.BuildCompletionHandler;
import software.amazon.smithy.lsp.language.CompletionHandler;
import software.amazon.smithy.lsp.language.DefinitionHandler;
import software.amazon.smithy.lsp.language.DocumentSymbolHandler;
Expand Down Expand Up @@ -537,13 +539,18 @@ public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completio
return completedFuture(Either.forLeft(Collections.emptyList()));
}

if (!(projectAndFile.file() instanceof IdlFile smithyFile)) {
return completedFuture(Either.forLeft(List.of()));
}

Project project = projectAndFile.project();
var handler = new CompletionHandler(project, smithyFile);
return CompletableFutures.computeAsync((cc) -> Either.forLeft(handler.handle(params, cc)));
return switch (projectAndFile.file()) {
case IdlFile idlFile -> {
var handler = new CompletionHandler(project, idlFile);
yield computeAsync((cc) -> Either.forLeft(handler.handle(params, cc)));
}
case BuildFile buildFile -> {
var handler = new BuildCompletionHandler(project, buildFile);
yield supplyAsync(() -> Either.forLeft(handler.handle(params)));
}
default -> completedFuture(Either.forLeft(List.of()));
};
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.lsp.language;

import java.util.List;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionParams;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import software.amazon.smithy.lsp.document.DocumentId;
import software.amazon.smithy.lsp.project.BuildFile;
import software.amazon.smithy.lsp.project.Project;
import software.amazon.smithy.lsp.syntax.NodeCursor;
import software.amazon.smithy.model.shapes.Shape;

/**
* Handles completions requests for {@link BuildFile}s.
*/
public final class BuildCompletionHandler {
private final Project project;
private final BuildFile buildFile;

public BuildCompletionHandler(Project project, BuildFile buildFile) {
this.project = project;
this.buildFile = buildFile;
}

/**
* @param params The request params
* @return A list of possible completions
*/
public List<CompletionItem> handle(CompletionParams params) {
Position position = CompletionHandler.getTokenPosition(params);
DocumentId id = buildFile.document().copyDocumentId(position);
Range insertRange = CompletionHandler.getInsertRange(id, position);

Shape buildFileShape = Builtins.getBuildFileShape(buildFile.type());

if (buildFileShape == null) {
return List.of();
}

NodeCursor cursor = NodeCursor.create(
buildFile.getParse().value(),
buildFile.document().indexOfPosition(position)
);
NodeSearch.Result searchResult = NodeSearch.search(cursor, Builtins.MODEL, buildFileShape);
var candidates = CompletionCandidates.fromSearchResult(searchResult);

var context = CompleterContext.create(id, insertRange, project)
.withExclude(searchResult.getOtherPresentKeys());
var mapper = new SimpleCompleter.BuildFileMapper(context);

return new SimpleCompleter(context, mapper).getCompletionItems(candidates);
}
}
14 changes: 14 additions & 0 deletions src/main/java/software/amazon/smithy/lsp/language/Builtins.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
import software.amazon.smithy.lsp.project.BuildFileType;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.Shape;
Expand Down Expand Up @@ -35,6 +36,7 @@ final class Builtins {
.addImport(Builtins.class.getResource("control.smithy"))
.addImport(Builtins.class.getResource("metadata.smithy"))
.addImport(Builtins.class.getResource("members.smithy"))
.addImport(Builtins.class.getResource("build.smithy"))
.assemble()
.unwrap();

Expand All @@ -51,6 +53,10 @@ final class Builtins {

static final Shape SHAPE_MEMBER_TARGETS = MODEL.expectShape(id("ShapeMemberTargets"));

static final Shape SMITHY_BUILD_JSON = MODEL.expectShape(id("SmithyBuildJson"));

static final Shape SMITHY_PROJECT_JSON = MODEL.expectShape(id("SmithyProjectJson"));

static final Map<String, ShapeId> VALIDATOR_CONFIG_MAPPING = VALIDATORS.members().stream()
.collect(Collectors.toMap(
MemberShape::getMemberName,
Expand Down Expand Up @@ -104,6 +110,14 @@ static Shape getMemberTargetForShapeType(String shapeType, String memberName) {
.orElse(null);
}

static Shape getBuildFileShape(BuildFileType type) {
return switch (type) {
case SMITHY_BUILD -> SMITHY_BUILD_JSON;
case SMITHY_PROJECT -> SMITHY_PROJECT_JSON;
default -> null;
};
}

private static ShapeId id(String name) {
return ShapeId.fromParts(NAMESPACE, name);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public List<CompletionItem> handle(CompletionParams params, CancelChecker cc) {
};
}

private static Position getTokenPosition(CompletionParams params) {
static Position getTokenPosition(CompletionParams params) {
Position position = params.getPosition();
CompletionContext context = params.getContext();
if (context != null
Expand All @@ -107,7 +107,7 @@ private static Position getTokenPosition(CompletionParams params) {
return position;
}

private static Range getInsertRange(DocumentId id, Position position) {
static Range getInsertRange(DocumentId id, Position position) {
if (id == null || id.idSlice().isEmpty()) {
// When we receive the completion request, we're always on the
// character either after what has just been typed, or we're in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@
* Maps simple {@link CompletionCandidates} to {@link CompletionItem}s.
*
* @param context The context for creating completions.
* @param mapper The mapper used to map candidates to completion items.
* Defaults to {@link Mapper}
*
* @see ShapeCompleter for what maps {@link CompletionCandidates.Shapes}.
*/
record SimpleCompleter(CompleterContext context) {
record SimpleCompleter(CompleterContext context, Mapper mapper) {
SimpleCompleter(CompleterContext context) {
this(context, new Mapper(context));
}

List<CompletionItem> getCompletionItems(CompletionCandidates candidates) {
Matcher matcher;
if (context.exclude().isEmpty()) {
Expand All @@ -34,12 +40,10 @@ List<CompletionItem> getCompletionItems(CompletionCandidates candidates) {
matcher = new ExcludingMatcher(context.matchToken(), context.exclude());
}

Mapper mapper = new Mapper(context().insertRange(), context().literalKind());

return getCompletionItems(candidates, matcher, mapper);
return getCompletionItems(candidates, matcher);
}

private List<CompletionItem> getCompletionItems(CompletionCandidates candidates, Matcher matcher, Mapper mapper) {
private List<CompletionItem> getCompletionItems(CompletionCandidates candidates, Matcher matcher) {
return switch (candidates) {
case CompletionCandidates.Constant(var value)
when !value.isEmpty() && matcher.testConstant(value) -> List.of(mapper.constant(value));
Expand All @@ -64,11 +68,11 @@ private List<CompletionItem> getCompletionItems(CompletionCandidates candidates,
.map(mapper::elided)
.toList();

case CompletionCandidates.Custom custom -> getCompletionItems(customCandidates(custom), matcher, mapper);
case CompletionCandidates.Custom custom -> getCompletionItems(customCandidates(custom), matcher);

case CompletionCandidates.And(var one, var two) -> {
List<CompletionItem> oneItems = getCompletionItems(one);
List<CompletionItem> twoItems = getCompletionItems(two);
List<CompletionItem> oneItems = getCompletionItems(one, matcher);
List<CompletionItem> twoItems = getCompletionItems(two, matcher);
List<CompletionItem> completionItems = new ArrayList<>(oneItems.size() + twoItems.size());
completionItems.addAll(oneItems);
completionItems.addAll(twoItems);
Expand Down Expand Up @@ -157,12 +161,16 @@ public boolean test(String s) {

/**
* Maps different kinds of completion candidates to {@link CompletionItem}s.
*
* @param insertRange The range the completion text will occupy.
* @param literalKind The completion item kind that will be shown in the
* client for {@link CompletionCandidates.Literals}.
*/
private record Mapper(Range insertRange, CompletionItemKind literalKind) {
static class Mapper {
private final Range insertRange;
private final CompletionItemKind literalKind;

Mapper(CompleterContext context) {
this.insertRange = context.insertRange();
this.literalKind = context.literalKind();
}

CompletionItem constant(String value) {
return textEditCompletion(value, CompletionItemKind.Constant);
}
Expand All @@ -184,16 +192,28 @@ CompletionItem elided(String memberName) {
return textEditCompletion("$" + memberName, CompletionItemKind.Field);
}

private CompletionItem textEditCompletion(String label, CompletionItemKind kind) {
protected CompletionItem textEditCompletion(String label, CompletionItemKind kind) {
return textEditCompletion(label, kind, label);
}

private CompletionItem textEditCompletion(String label, CompletionItemKind kind, String insertText) {
protected CompletionItem textEditCompletion(String label, CompletionItemKind kind, String insertText) {
CompletionItem item = new CompletionItem(label);
item.setKind(kind);
TextEdit textEdit = new TextEdit(insertRange, insertText);
item.setTextEdit(Either.forLeft(textEdit));
return item;
}
}

static final class BuildFileMapper extends Mapper {
BuildFileMapper(CompleterContext context) {
super(context);
}

@Override
CompletionItem member(Map.Entry<String, CompletionCandidates.Constant> entry) {
String value = "\"" + entry.getKey() + "\": " + entry.getValue().value();
return textEditCompletion(entry.getKey(), CompletionItemKind.Field, value);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
$version: "2.0"

namespace smithy.lang.server

structure SmithyProjectJson {
sources: Strings
imports: Strings
outputDirectory: String
dependencies: ProjectDependencies
}

list ProjectDependencies {
member: ProjectDependency
}

structure ProjectDependency {
name: String

@required
path: String
}

structure SmithyBuildJson {
@required
version: SmithyBuildVersion

outputDirectory: String
sources: Strings
imports: Strings
projections: Projections
plugins: Plugins
ignoreMissingPlugins: Boolean
maven: Maven
}

@default("1")
string SmithyBuildVersion

map Projections {
key: String
value: Projection
}

structure Projection {
abstract: Boolean
imports: Strings
transforms: Transforms
plugins: Plugins
}

map Plugins {
key: String
value: Document
}

list Transforms {
member: Transform
}

structure Transform {
@required
name: String

args: TransformArgs
}

structure TransformArgs {
}

structure Maven {
dependencies: Strings
repositories: MavenRepositories
}

list MavenRepositories {
member: MavenRepository
}

structure MavenRepository {
@required
url: String

httpCredentials: String
proxyHost: String
proxyCredentials: String
}

list Strings {
member: String
}
10 changes: 10 additions & 0 deletions src/test/java/software/amazon/smithy/lsp/LspMatchers.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ public void describeMismatchSafely(CompletionItem item, Description description)
};
}

public static Matcher<CompletionItem> hasLabelAndEditText(String label, String editText) {
return new CustomTypeSafeMatcher<>("label " + label + " editText " + editText) {
@Override
protected boolean matchesSafely(CompletionItem item) {
return label.equals(item.getLabel())
&& editText.trim().equals(item.getTextEdit().getLeft().getNewText().trim());
}
};
}

public static Matcher<TextEdit> makesEditedDocument(Document document, String expected) {
return new CustomTypeSafeMatcher<>("makes an edited document " + expected) {
@Override
Expand Down
Loading

0 comments on commit 6c4eef4

Please sign in to comment.