diff --git a/src/main/java/software/amazon/smithy/lsp/language/Candidates.java b/src/main/java/software/amazon/smithy/lsp/language/CompletionCandidates.java similarity index 72% rename from src/main/java/software/amazon/smithy/lsp/language/Candidates.java rename to src/main/java/software/amazon/smithy/lsp/language/CompletionCandidates.java index f59fa78d..c18e3280 100644 --- a/src/main/java/software/amazon/smithy/lsp/language/Candidates.java +++ b/src/main/java/software/amazon/smithy/lsp/language/CompletionCandidates.java @@ -22,13 +22,13 @@ import software.amazon.smithy.model.traits.IdRefTrait; /** - * Candidates for code-completions. + * Candidates for code completions. * *

There are different kinds of completion candidates, each of which may * need to be represented differently, filtered, and/or mapped to IDE-specific * data structures in their own way.

*/ -sealed interface Candidates { +sealed interface CompletionCandidates { Constant NONE = new Constant(""); Constant EMPTY_STRING = new Constant("\"\""); Constant EMPTY_OBJ = new Constant("{}"); @@ -43,14 +43,12 @@ sealed interface Candidates { "apply")); // TODO: Maybe BUILTIN_CONTROLS and BUILTIN_METADATA should be regular // Labeled/Members, with custom mappers. - Literals BUILTIN_CONTROLS = new Candidates.Literals( - Builtins.CONTROL.members().stream() - .map(member -> "$" + member.getMemberName() + ": " + Candidates.defaultCandidates(member).value()) - .toList()); - Literals BUILTIN_METADATA = new Candidates.Literals( - Builtins.METADATA.members().stream() - .map(member -> member.getMemberName() + " = []") - .toList()); + Literals BUILTIN_CONTROLS = new Literals(Builtins.CONTROL.members().stream() + .map(member -> "$" + member.getMemberName() + ": " + defaultCandidates(member).value()) + .toList()); + Literals BUILTIN_METADATA = new Literals(Builtins.METADATA.members().stream() + .map(member -> member.getMemberName() + " = []") + .toList()); Labeled SMITHY_IDL_VERSION = new Labeled(Stream.of("1.0", "2.0") .collect(StreamUtils.toWrappedMap())); Labeled VALIDATOR_NAMES = new Labeled(Builtins.VALIDATOR_CONFIG_MAPPING.keySet().stream() @@ -64,7 +62,7 @@ sealed interface Candidates { * @return A constant value corresponding to the 'default' or 'empty' value * of a shape. */ - static Candidates.Constant defaultCandidates(Shape shape) { + static Constant defaultCandidates(Shape shape) { if (shape.hasTrait(DefaultTrait.class)) { DefaultTrait defaultTrait = shape.expectTrait(DefaultTrait.class); return new Constant(Node.printJson(defaultTrait.toNode())); @@ -85,7 +83,7 @@ static Candidates.Constant defaultCandidates(Shape shape) { * @param result The search result to get candidates from. * @return The completion candidates for {@code result}. */ - static Candidates fromSearchResult(NodeSearch.Result result) { + static CompletionCandidates fromSearchResult(NodeSearch.Result result) { return switch (result) { case NodeSearch.Result.TerminalShape(Shape shape, var ignored) -> terminalCandidates(shape); @@ -98,39 +96,53 @@ static Candidates fromSearchResult(NodeSearch.Result result) { case NodeSearch.Result.ArrayShape(var ignored, ListShape shape, Model model) -> model.getShape(shape.getMember().getTarget()) - .map(Candidates::terminalCandidates) + .map(CompletionCandidates::terminalCandidates) .orElse(NONE); default -> NONE; }; } + /** + * @param idlPosition The position in the idl to get candidates for. + * @return The candidates for shape completions. + */ + static CompletionCandidates shapeCandidates(IdlPosition idlPosition) { + return switch (idlPosition) { + case IdlPosition.UseTarget ignored -> Shapes.USE_TARGET; + case IdlPosition.TraitId ignored -> Shapes.TRAITS; + case IdlPosition.Mixin ignored -> Shapes.MIXINS; + case IdlPosition.ForResource ignored -> Shapes.RESOURCE_SHAPES; + case IdlPosition.MemberTarget ignored -> Shapes.MEMBER_TARGETABLE; + case IdlPosition.ApplyTarget ignored -> Shapes.ANY_SHAPE; + case IdlPosition.NodeMemberTarget nodeMemberTarget -> fromSearchResult( + ShapeSearch.searchNodeMemberTarget(nodeMemberTarget)); + default -> CompletionCandidates.NONE; + }; + } + /** * @param model The model that {@code shape} is a part of. * @param shape The shape to get member candidates for. * @return If a struct or union shape, returns {@link Members} candidates. * Otherwise, {@link #NONE}. */ - static Candidates membersCandidates(Model model, Shape shape) { + static CompletionCandidates membersCandidates(Model model, Shape shape) { if (shape.isStructureShape() || shape.isUnionShape()) { return new Members(shape.getAllMembers().entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - entry -> model.getShape(entry.getValue().getTarget()) - .map(Candidates::defaultCandidates) - .orElse(NONE)))); + .collect(StreamUtils.mappingValue(member -> model.getShape(member.getTarget()) + .map(CompletionCandidates::defaultCandidates) + .orElse(NONE)))); } else if (shape instanceof MapShape mapShape) { - EnumShape enumKey = model.getShape(mapShape.getKey().getTarget()) + return model.getShape(mapShape.getKey().getTarget()) .flatMap(Shape::asEnumShape) - .orElse(null); - if (enumKey != null) { - return terminalCandidates(enumKey); - } + .map(CompletionCandidates::terminalCandidates) + .orElse(NONE); } return NONE; } - private static Candidates terminalCandidates(Shape shape) { + private static CompletionCandidates terminalCandidates(Shape shape) { Builtins.BuiltinShape builtinShape = Builtins.BUILTIN_SHAPES.get(shape.getId()); if (builtinShape != null) { return forBuiltin(builtinShape); @@ -155,7 +167,7 @@ private static Candidates terminalCandidates(Shape shape) { }; } - private static Candidates forBuiltin(Builtins.BuiltinShape builtinShape) { + private static CompletionCandidates forBuiltin(Builtins.BuiltinShape builtinShape) { return switch (builtinShape) { case SmithyIdlVersion -> SMITHY_IDL_VERSION; case AnyNamespace -> Custom.NAMESPACE_FILTER; @@ -176,40 +188,46 @@ private static Candidates forBuiltin(Builtins.BuiltinShape builtinShape) { * * @param value The completion value. */ - record Constant(String value) implements Candidates {} + record Constant(String value) implements CompletionCandidates {} /** * Multiple values to be completed as literals, like keywords. * * @param literals The completion values. */ - record Literals(List literals) implements Candidates {} + record Literals(List literals) implements CompletionCandidates {} /** * Multiple label -> value pairs, where the label is displayed to the user, * and may be used for matching, and the value is the literal text to complete. * + *

For example, completing enum value in a trait may display and match on the + * name, like FOO, but complete the actual value, like "foo". + * * @param labeled The labeled completion values. */ - record Labeled(Map labeled) implements Candidates {} + record Labeled(Map labeled) implements CompletionCandidates {} /** * Multiple name -> constant pairs, where the name corresponds to a member * name, and the constant is a default/empty value for that member. * + *

For example, shape members can be completed as {@code name: constant}. + * * @param members The members completion values. */ - record Members(Map members) implements Candidates {} + record Members(Map members) implements CompletionCandidates {} /** * Multiple member names to complete as elided members. + * * @apiNote These are distinct from {@link Literals} because they may have * custom filtering/mapping, and may appear _with_ {@link Literals} in an * {@link And}. * * @param memberNames The member names completion values. */ - record ElidedMembers(Collection memberNames) implements Candidates {} + record ElidedMembers(Collection memberNames) implements CompletionCandidates {} /** * A combination of two sets of completion candidates, of possibly different @@ -218,13 +236,13 @@ record ElidedMembers(Collection memberNames) implements Candidates {} * @param one The first set of completion candidates. * @param two The second set of completion candidates. */ - record And(Candidates one, Candidates two) implements Candidates {} + record And(CompletionCandidates one, CompletionCandidates two) implements CompletionCandidates {} /** * Shape completion candidates, each corresponding to a different set of * shapes that will be selected from the model. */ - enum Shapes implements Candidates { + enum Shapes implements CompletionCandidates { ANY_SHAPE, USE_TARGET, TRAITS, @@ -239,7 +257,7 @@ enum Shapes implements Candidates { /** * Candidates that require a custom computation to generate, lazily. */ - enum Custom implements Candidates { + enum Custom implements CompletionCandidates { NAMESPACE_FILTER, VALIDATOR_NAME, PROJECT_NAMESPACES, diff --git a/src/main/java/software/amazon/smithy/lsp/language/CompletionHandler.java b/src/main/java/software/amazon/smithy/lsp/language/CompletionHandler.java index 182e1f10..f43d1bc6 100644 --- a/src/main/java/software/amazon/smithy/lsp/language/CompletionHandler.java +++ b/src/main/java/software/amazon/smithy/lsp/language/CompletionHandler.java @@ -72,22 +72,22 @@ public List handle(CompletionParams params, CancelChecker cc) { case IdlPosition.ControlKey ignored -> builder .literalKind(CompletionItemKind.Constant) .buildSimpleCompletions() - .getCompletionItems(Candidates.BUILTIN_CONTROLS); + .getCompletionItems(CompletionCandidates.BUILTIN_CONTROLS); case IdlPosition.MetadataKey ignored -> builder .literalKind(CompletionItemKind.Field) .buildSimpleCompletions() - .getCompletionItems(Candidates.BUILTIN_METADATA); + .getCompletionItems(CompletionCandidates.BUILTIN_METADATA); case IdlPosition.StatementKeyword ignored -> builder .literalKind(CompletionItemKind.Keyword) .buildSimpleCompletions() - .getCompletionItems(Candidates.KEYWORD); + .getCompletionItems(CompletionCandidates.KEYWORD); case IdlPosition.Namespace ignored -> builder .literalKind(CompletionItemKind.Module) .buildSimpleCompletions() - .getCompletionItems(Candidates.Custom.PROJECT_NAMESPACES); + .getCompletionItems(CompletionCandidates.Custom.PROJECT_NAMESPACES); case IdlPosition.MetadataValue metadataValue -> metadataValueCompletions(metadataValue, builder); @@ -129,7 +129,7 @@ private List metadataValueCompletions( ) { var result = ShapeSearch.searchMetadataValue(metadataValue); Set excludeKeys = getOtherPresentKeys(result); - Candidates candidates = Candidates.fromSearchResult(result); + CompletionCandidates candidates = CompletionCandidates.fromSearchResult(result); return builder.exclude(excludeKeys).buildSimpleCompletions().getCompletionItems(candidates); } @@ -176,10 +176,10 @@ private List modelBasedCompletions(IdlPosition idlPosition, Simp return traitValueCompletions(traitValue, model, builder); } - Candidates candidates = shapeCandidates(idlPosition); - if (candidates instanceof Candidates.Shapes shapes) { + CompletionCandidates candidates = CompletionCandidates.shapeCandidates(idlPosition); + if (candidates instanceof CompletionCandidates.Shapes shapes) { return builder.buildShapeCompletions(idlPosition, model).getCompletionItems(shapes); - } else if (candidates != Candidates.NONE) { + } else if (candidates != CompletionCandidates.NONE) { return builder.buildSimpleCompletions().getCompletionItems(candidates); } @@ -191,7 +191,7 @@ private List elidedMemberCompletions( Model model, SimpleCompletions.Builder builder ) { - Candidates candidates = getElidableMemberCandidates(elidedMember.statementIndex(), model); + CompletionCandidates candidates = getElidableMemberCandidates(elidedMember.statementIndex(), model); if (candidates == null) { return List.of(); } @@ -210,24 +210,10 @@ private List traitValueCompletions( ) { var result = ShapeSearch.searchTraitValue(traitValue, model); Set excludeKeys = getOtherPresentKeys(result); - Candidates candidates = Candidates.fromSearchResult(result); + CompletionCandidates candidates = CompletionCandidates.fromSearchResult(result); return builder.exclude(excludeKeys).buildSimpleCompletions().getCompletionItems(candidates); } - private Candidates shapeCandidates(IdlPosition idlPosition) { - return switch (idlPosition) { - case IdlPosition.UseTarget ignored -> Candidates.Shapes.USE_TARGET; - case IdlPosition.TraitId ignored -> Candidates.Shapes.TRAITS; - case IdlPosition.Mixin ignored -> Candidates.Shapes.MIXINS; - case IdlPosition.ForResource ignored -> Candidates.Shapes.RESOURCE_SHAPES; - case IdlPosition.MemberTarget ignored -> Candidates.Shapes.MEMBER_TARGETABLE; - case IdlPosition.ApplyTarget ignored -> Candidates.Shapes.ANY_SHAPE; - case IdlPosition.NodeMemberTarget nodeMemberTarget -> Candidates.fromSearchResult( - ShapeSearch.searchNodeMemberTarget(nodeMemberTarget)); - default -> Candidates.NONE; - }; - } - private List memberNameCompletions( IdlPosition.MemberName memberName, SimpleCompletions.Builder builder @@ -243,20 +229,20 @@ private List memberNameCompletions( String shapeType = shapeDef.shapeType().copyValueFrom(smithyFile.document()); StructureShape shapeMembersDef = Builtins.getMembersForShapeType(shapeType); - Candidates candidates = null; + CompletionCandidates candidates = null; if (shapeMembersDef != null) { - candidates = Candidates.membersCandidates(Builtins.MODEL, shapeMembersDef); + candidates = CompletionCandidates.membersCandidates(Builtins.MODEL, shapeMembersDef); } if (project.modelResult().getResult().isPresent()) { - Candidates elidedCandidates = getElidableMemberCandidates( + CompletionCandidates elidedCandidates = getElidableMemberCandidates( memberName.statementIndex(), project.modelResult().getResult().get()); if (elidedCandidates != null) { candidates = candidates == null ? elidedCandidates - : new Candidates.And(candidates, elidedCandidates); + : new CompletionCandidates.And(candidates, elidedCandidates); } } @@ -271,7 +257,7 @@ private List memberNameCompletions( return builder.exclude(otherMembers).buildSimpleCompletions().getCompletionItems(candidates); } - private Candidates getElidableMemberCandidates(int statementIndex, Model model) { + private CompletionCandidates getElidableMemberCandidates(int statementIndex, Model model) { var resourceAndMixins = ShapeSearch.findForResourceAndMixins( SyntaxSearch.closestForResourceAndMixinsBeforeMember(smithyFile.statements(), statementIndex), smithyFile, @@ -291,6 +277,6 @@ private Candidates getElidableMemberCandidates(int statementIndex, Model model) return null; } - return new Candidates.ElidedMembers(memberNames); + return new CompletionCandidates.ElidedMembers(memberNames); } } diff --git a/src/main/java/software/amazon/smithy/lsp/language/ShapeCompletions.java b/src/main/java/software/amazon/smithy/lsp/language/ShapeCompletions.java index 42571928..0c4d8c74 100644 --- a/src/main/java/software/amazon/smithy/lsp/language/ShapeCompletions.java +++ b/src/main/java/software/amazon/smithy/lsp/language/ShapeCompletions.java @@ -30,7 +30,7 @@ import software.amazon.smithy.model.traits.TraitDefinition; /** - * Maps {@link Candidates.Shapes} to {@link CompletionItem}s. + * Maps {@link CompletionCandidates.Shapes} to {@link CompletionItem}s. */ final class ShapeCompletions { private final Model model; @@ -45,14 +45,14 @@ private ShapeCompletions(Model model, SmithyFile smithyFile, Matcher matcher, Ma this.mapper = mapper; } - List getCompletionItems(Candidates.Shapes candidates) { + List getCompletionItems(CompletionCandidates.Shapes candidates) { return streamShapes(candidates) .filter(matcher::test) .mapMulti(mapper::accept) .toList(); } - private Stream streamShapes(Candidates.Shapes candidates) { + private Stream streamShapes(CompletionCandidates.Shapes candidates) { return switch (candidates) { case ANY_SHAPE -> model.shapes(); case STRING_SHAPES -> model.getStringShapes().stream(); @@ -202,7 +202,7 @@ public void add(Mapper mapper, String shapeLabel, Shape shape, Consumer getCompletionItems(Candidates candidates) { + List getCompletionItems(CompletionCandidates candidates) { return switch (candidates) { - case Candidates.Constant(var value) + case CompletionCandidates.Constant(var value) when !value.isEmpty() && matcher.testConstant(value) -> List.of(mapper.constant(value)); - case Candidates.Literals(var literals) -> literals.stream() + case CompletionCandidates.Literals(var literals) -> literals.stream() .filter(matcher::testLiteral) .map(mapper::literal) .toList(); - case Candidates.Labeled(var labeled) -> labeled.entrySet().stream() + case CompletionCandidates.Labeled(var labeled) -> labeled.entrySet().stream() .filter(matcher::testLabeled) .map(mapper::labeled) .toList(); - case Candidates.Members(var members) -> members.entrySet().stream() + case CompletionCandidates.Members(var members) -> members.entrySet().stream() .filter(matcher::testMember) .map(mapper::member) .toList(); - case Candidates.ElidedMembers(var memberNames) -> memberNames.stream() + case CompletionCandidates.ElidedMembers(var memberNames) -> memberNames.stream() .filter(matcher::testElided) .map(mapper::elided) .toList(); - case Candidates.Custom custom + case CompletionCandidates.Custom custom // TODO: Need to get rid of this stupid null check when project != null -> getCompletionItems(customCandidates(custom)); - case Candidates.And(var one, var two) -> { + case CompletionCandidates.And(var one, var two) -> { List oneItems = getCompletionItems(one); List twoItems = getCompletionItems(two); List completionItems = new ArrayList<>(oneItems.size() + twoItems.size()); @@ -73,14 +73,14 @@ List getCompletionItems(Candidates candidates) { }; } - private Candidates customCandidates(Candidates.Custom custom) { + private CompletionCandidates customCandidates(CompletionCandidates.Custom custom) { return switch (custom) { - case NAMESPACE_FILTER -> new Candidates.Labeled(Stream.concat(Stream.of("*"), streamNamespaces()) + case NAMESPACE_FILTER -> new CompletionCandidates.Labeled(Stream.concat(Stream.of("*"), streamNamespaces()) .collect(StreamUtils.toWrappedMap())); - case VALIDATOR_NAME -> Candidates.VALIDATOR_NAMES; + case VALIDATOR_NAME -> CompletionCandidates.VALIDATOR_NAMES; - case PROJECT_NAMESPACES -> new Candidates.Literals(streamNamespaces().toList()); + case PROJECT_NAMESPACES -> new CompletionCandidates.Literals(streamNamespaces().toList()); }; } @@ -162,7 +162,7 @@ default boolean testLabeled(Map.Entry labeled) { return test(labeled.getKey()) || test(labeled.getValue()); } - default boolean testMember(Map.Entry member) { + default boolean testMember(Map.Entry member) { return test(member.getKey()); } @@ -206,7 +206,7 @@ CompletionItem labeled(Map.Entry entry) { return textEditCompletion(entry.getKey(), CompletionItemKind.EnumMember, entry.getValue()); } - CompletionItem member(Map.Entry entry) { + CompletionItem member(Map.Entry entry) { String value = entry.getKey() + ": " + entry.getValue().value(); return textEditCompletion(entry.getKey(), CompletionItemKind.Field, value); } diff --git a/src/main/java/software/amazon/smithy/lsp/project/ProjectLoader.java b/src/main/java/software/amazon/smithy/lsp/project/ProjectLoader.java index dafbb7ba..6d18ff95 100644 --- a/src/main/java/software/amazon/smithy/lsp/project/ProjectLoader.java +++ b/src/main/java/software/amazon/smithy/lsp/project/ProjectLoader.java @@ -267,7 +267,6 @@ public static SmithyFile.Builder buildSmithyFile(String path, Document document, .documentShapes(documentShapes) .documentVersion(documentVersion) .statements(statements); - // .changeVersion(document.changeVersion()); } // This is gross, but necessary to deal with the way that array metadata gets merged. diff --git a/src/main/java/software/amazon/smithy/lsp/project/SmithyFile.java b/src/main/java/software/amazon/smithy/lsp/project/SmithyFile.java index ba6d3680..5cc23442 100644 --- a/src/main/java/software/amazon/smithy/lsp/project/SmithyFile.java +++ b/src/main/java/software/amazon/smithy/lsp/project/SmithyFile.java @@ -40,7 +40,6 @@ public final class SmithyFile implements ProjectFile { private final Map documentShapes; private final DocumentVersion documentVersion; private List statements; - private int changeVersion; private SmithyFile(Builder builder) { this.path = builder.path; @@ -51,7 +50,6 @@ private SmithyFile(Builder builder) { this.documentShapes = builder.documentShapes; this.documentVersion = builder.documentVersion; this.statements = builder.statements; - this.changeVersion = builder.changeVersion; } /** @@ -155,14 +153,6 @@ public boolean isAccessible(Shape shape) { || !shape.hasTrait(PrivateTrait.ID); } - public int changeVersion() { - return changeVersion; - } - - public void setChangeVersion(int changeVersion) { - this.changeVersion = changeVersion; - } - /** * @return The parsed statements in this file */ @@ -204,7 +194,6 @@ public static final class Builder { private Map documentShapes; private DocumentVersion documentVersion; private List statements; - private int changeVersion; private Builder() { } @@ -249,11 +238,6 @@ public Builder statements(List statements) { return this; } - public Builder changeVersion(int changeVersion) { - this.changeVersion = changeVersion; - return this; - } - public SmithyFile build() { return new SmithyFile(this); } diff --git a/src/main/java/software/amazon/smithy/lsp/util/StreamUtils.java b/src/main/java/software/amazon/smithy/lsp/util/StreamUtils.java index 86b0d669..55187018 100644 --- a/src/main/java/software/amazon/smithy/lsp/util/StreamUtils.java +++ b/src/main/java/software/amazon/smithy/lsp/util/StreamUtils.java @@ -6,6 +6,7 @@ package software.amazon.smithy.lsp.util; import java.util.Map; +import java.util.function.Function; import java.util.stream.Collector; import java.util.stream.Collectors; @@ -13,11 +14,11 @@ public final class StreamUtils { private StreamUtils() { } - public static Collector, ?, Map> toMap() { - return Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue); - } - public static Collector> toWrappedMap() { return Collectors.toMap(s -> s, s -> "\"" + s + "\""); } + + public static Collector, ?, Map> mappingValue(Function valueMapper) { + return Collectors.toMap(Map.Entry::getKey, entry -> valueMapper.apply(entry.getValue())); + } } diff --git a/src/test/java/software/amazon/smithy/lsp/language/CompletionHandlerTest.java b/src/test/java/software/amazon/smithy/lsp/language/CompletionHandlerTest.java index a633888c..81007c00 100644 --- a/src/test/java/software/amazon/smithy/lsp/language/CompletionHandlerTest.java +++ b/src/test/java/software/amazon/smithy/lsp/language/CompletionHandlerTest.java @@ -274,7 +274,7 @@ public void completesStatementKeywords() { app%"""); List comps = getCompLabels(text); - String[] keywords = Candidates.KEYWORD.literals().toArray(new String[0]); + String[] keywords = CompletionCandidates.KEYWORD.literals().toArray(new String[0]); assertThat(comps, containsInAnyOrder(keywords)); }