Skip to content

Commit e345264

Browse files
committed
Upgrade completions, definition, hover
This commit is a rewrite of how language features (i.e. completions, definition, hover) are implemented. It improves the accuracy and expands the functionality of each feature significantly. Improvements include: - Completions - Trait values - Builtin control keys and metadata - Namespaces, based on other namespaces in the project - Keywords - Member names (like inside resources, maps) - Member values (like inside the list of operation errors, resource property targets, etc.) - Elided members - Some trait values have special completions, like `examples` has completions for the target operation's input/output parameters - Definition - Trait values - Elided members - Shape ids referenced within trait values - Hover - Trait values - Elided members - Builtin metadata There's a lot going on here, but there's a few key pieces of this commit that all work together to make this work: At the core of these improvements is the addition of a custom parser for the IDL that provides the needed syntactic information to implement these features. See the javadoc on the Syntax class for more details on how the parser works, and why it was written that way. At a high level though, the parser produces a flat list of `Syntax.Statement`, and that list is searched through to find things, such as the statement the cursor is currently in. It is also used to search 'around' a statement, like to find the shape a trait is being applied to. Another key piece of these changes is `NodeCursor` and `NodeSearch`. There are a few places in the syntax of a smithy file where you may have a node value whose structure is (or can be) described by a Smithy model. For example, trait values. `NodeCursor` is basically two things: 1. A path from the start of a `Node` to a position within that `Node`, 2. An index into that path. `NodeSearch` is used to search a model along the path of a `NodeCursor`, from a starting shape. For example, when the cursor is within a trait value, the `NodeCursor` is that path from the root of the trait value, to the cursor position, and `NodeSearch` is used to search in the model, starting at the trait's definition, along the path of the `NodeCursor`, to find what shape corresponds to the cursor's location. That shape can then be used e.g. to provide completions. Finally, there's the `Builtins` class, and the corresponding Smithy model it uses. I originally had a completely different abstraction for describing the structure of metadata, different shape types' members, and even `smithy-build.json`. But it was basically just a 'structured graph', like a Smithy model. So I decided to just _use_ a Smithy model itself, since I already had the abstractions for traversing it (like I had to for trait values). The `Builtins` model contains shapes that define the structure of certain Smithy constructs. For example, I use it to model the shape of builtin metadata, like suppressions. I also use it to model the shape of shapes, that is, what members shapes have, and what their targets are. Some shapes in this model are considered 'builtins' (in the builtins.smithy files). Builtins are shapes that require some custom processing, or have some special meaning, like `AnyNamespace`, which is used for describing a namespace that can be used in https://smithy.io/2.0/spec/model-validation.html#suppression-metadata. The builtin model pretty 'meta', and I don't _love_ it, but it reduces a significant amount of duplicated logic. For example, if we want to give documentation for some metadata, it is as easy as adding it to the builtins model. We can also use it to add support for smithy-build.json completions, hover, and even validation, later. It would be nice if these definitions lived elsewhere, so other tooling could consume them, like the Smithy docs for example, and I have some other ideas on how we can use it, but they're out of scope here. Testing for this commit comes mostly from the completions, definitions, and hover tests, which indirectly test lower-level components like the parser (there are still some parser tests, though).
1 parent 3b1ea23 commit e345264

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+7552
-1373
lines changed

.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,4 @@ bin
2424
.settings
2525

2626
.java-version
27-
*.smithy
28-
!/src/test/resources/**/*.smithy
2927
.ammonite

build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ publishing {
141141
}
142142
}
143143

144+
checkstyle {
145+
toolVersion = "10.12.4"
146+
}
144147

145148
dependencies {
146149
implementation "org.eclipse.lsp4j:org.eclipse.lsp4j:0.23.1"
@@ -153,6 +156,8 @@ dependencies {
153156
testImplementation "org.hamcrest:hamcrest:2.2"
154157

155158
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
159+
160+
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyle.toolVersion}"
156161
}
157162

158163
tasks.withType(Javadoc).all {

config/checkstyle/checkstyle.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,6 @@
182182
<!-- See http://checkstyle.sf.net/config_design.html -->
183183
<module name="FinalClass"/>
184184
<module name="HideUtilityClassConstructor"/>
185-
<module name="InterfaceIsType"/>
186185
<module name="OneTopLevelClass"/>
187186

188187
<!-- Miscellaneous other checks. -->

src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,9 @@
104104
import software.amazon.smithy.lsp.ext.SelectorParams;
105105
import software.amazon.smithy.lsp.ext.ServerStatus;
106106
import software.amazon.smithy.lsp.ext.SmithyProtocolExtensions;
107-
import software.amazon.smithy.lsp.handler.CompletionHandler;
108-
import software.amazon.smithy.lsp.handler.DefinitionHandler;
109-
import software.amazon.smithy.lsp.handler.HoverHandler;
107+
import software.amazon.smithy.lsp.language.CompletionHandler;
108+
import software.amazon.smithy.lsp.language.DefinitionHandler;
109+
import software.amazon.smithy.lsp.language.HoverHandler;
110110
import software.amazon.smithy.lsp.project.BuildFile;
111111
import software.amazon.smithy.lsp.project.Project;
112112
import software.amazon.smithy.lsp.project.ProjectAndFile;
@@ -498,10 +498,11 @@ public void didChange(DidChangeTextDocumentParams params) {
498498
}
499499

500500
// Don't reload or update the project on build file changes, only on save
501-
if (projectAndFile.file() instanceof BuildFile) {
501+
if (!(projectAndFile.file() instanceof SmithyFile smithyFile)) {
502502
return;
503503
}
504504

505+
smithyFile.reparse();
505506
if (!onlyReloadOnSave) {
506507
Project project = projectAndFile.project();
507508

@@ -713,7 +714,7 @@ public CompletableFuture<Hover> hover(HoverParams params) {
713714
Project project = projectAndFile.project();
714715

715716
// TODO: Abstract away passing minimum severity
716-
Hover hover = new HoverHandler(project, smithyFile).handle(params, minimumSeverity);
717+
Hover hover = new HoverHandler(project, smithyFile, minimumSeverity).handle(params);
717718
return completedFuture(hover);
718719
}
719720

src/main/java/software/amazon/smithy/lsp/document/Document.java

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public final class Document {
2424
private final StringBuilder buffer;
2525
private int[] lineIndices;
2626

27-
private Document(StringBuilder buffer, int[] lineIndices) {
27+
private Document(StringBuilder buffer, int[] lineIndices, int changeVersion) {
2828
this.buffer = buffer;
2929
this.lineIndices = lineIndices;
3030
}
@@ -36,14 +36,14 @@ private Document(StringBuilder buffer, int[] lineIndices) {
3636
public static Document of(String string) {
3737
StringBuilder buffer = new StringBuilder(string);
3838
int[] lineIndicies = computeLineIndicies(buffer);
39-
return new Document(buffer, lineIndicies);
39+
return new Document(buffer, lineIndicies, 0);
4040
}
4141

4242
/**
4343
* @return A copy of this document
4444
*/
4545
public Document copy() {
46-
return new Document(new StringBuilder(copyText()), lineIndices.clone());
46+
return new Document(new StringBuilder(copyText()), lineIndices.clone(), 0);
4747
}
4848

4949
/**
@@ -97,20 +97,31 @@ public int indexOfLine(int line) {
9797
* doesn't exist
9898
*/
9999
public int lineOfIndex(int idx) {
100-
// TODO: Use binary search or similar
101-
if (idx >= length() || idx < 0) {
102-
return -1;
103-
}
104-
105-
for (int line = 0; line <= lastLine() - 1; line++) {
106-
int currentLineIdx = indexOfLine(line);
107-
int nextLineIdx = indexOfLine(line + 1);
108-
if (idx >= currentLineIdx && idx < nextLineIdx) {
109-
return line;
100+
int low = 0;
101+
int up = lastLine();
102+
103+
while (low <= up) {
104+
int mid = (low + up) / 2;
105+
int midLineIdx = lineIndices[mid];
106+
int midLineEndIdx = lineEndUnchecked(mid);
107+
if (idx >= midLineIdx && idx <= midLineEndIdx) {
108+
return mid;
109+
} else if (idx < midLineIdx) {
110+
up = mid - 1;
111+
} else {
112+
low = mid + 1;
110113
}
111114
}
112115

113-
return lastLine();
116+
return -1;
117+
}
118+
119+
private int lineEndUnchecked(int line) {
120+
if (line == lastLine()) {
121+
return length() - 1;
122+
} else {
123+
return lineIndices[line + 1] - 1;
124+
}
114125
}
115126

116127
/**
@@ -167,6 +178,34 @@ public Position positionAtIndex(int index) {
167178
return new Position(line, character);
168179
}
169180

181+
/**
182+
* @param start The start character offset
183+
* @param end The end character offset
184+
* @return The range between the two given offsets
185+
*/
186+
public Range rangeBetween(int start, int end) {
187+
if (end < start || start < 0) {
188+
return null;
189+
}
190+
191+
// The start is inclusive, so it should be within the bounds of the document
192+
Position startPos = positionAtIndex(start);
193+
if (startPos == null) {
194+
return null;
195+
}
196+
197+
Position endPos;
198+
if (end == length()) {
199+
int lastLine = lastLine();
200+
int lastCol = length() - lineIndices[lastLine];
201+
endPos = new Position(lastLine, lastCol);
202+
} else {
203+
endPos = positionAtIndex(end);
204+
}
205+
206+
return new Range(startPos, endPos);
207+
}
208+
170209
/**
171210
* @param line The line to find the end of
172211
* @return The index of the end of the given line, or {@code -1} if the

src/main/java/software/amazon/smithy/lsp/document/DocumentId.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,15 @@ public enum Type {
5151
public String copyIdValue() {
5252
return idSlice.toString();
5353
}
54+
55+
/**
56+
* @return The value of the id without a leading '$'
57+
*/
58+
public String copyIdValueForElidedMember() {
59+
String idValue = copyIdValue();
60+
if (idValue.startsWith("$")) {
61+
return idValue.substring(1);
62+
}
63+
return idValue;
64+
}
5465
}

0 commit comments

Comments
 (0)