Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are constraints used by the LS? Could we possibly add more constraints to save energy doing validations like urls etc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They really aren't right now. Originally I intended to use them for validation, but I scrapped that idea to use the validation errors from smithy-build. However, with trait values, required members get autopopulated in completions, so maybe a future update could do that for node values too.

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