diff --git a/rust/contracts/rust-core-api.md b/rust/contracts/rust-core-api.md index fe66a7a7..b8247433 100644 --- a/rust/contracts/rust-core-api.md +++ b/rust/contracts/rust-core-api.md @@ -1806,6 +1806,20 @@ isReadOnly }` where `classification` is `workspace`, `externalReadOnly` (a source file of the language that produced the jump, opened read-only) or `rejected`. See `rust/fixtures/lsp/navigation-target-v1.json`. +`lsp.declarationOrUsages` accepts `{ uri, position, definitions }`: the +document and zero-based `{ line, utf16Column }` of a go-to-declaration gesture +(caret or clicked character) and the definition answer received for it, each +`{ uri, range }` (at most 100). It returns `{ action }`: `usages` when every +definition location is the gesture's own place, otherwise `definitions`, +including an empty answer, which hosts report as no definition. A location is +the gesture's own place when its `uri` equals `uri` and its range contains the +position, end included, because an editor resolves the identifier left of the +caret. This is IntelliJ IDEA's Go to Declaration or Usages: on `usages` the host +asks for `references` with `includeDeclaration: false`, opens a single usage +directly, lists several and reports none. Every gesture that goes to a +declaration (Cmd/Ctrl+B, Cmd/Ctrl+click, F12, the editor menu) uses this +decision. See `rust/fixtures/lsp/declaration-or-usages-v1.json`. + `editor.parseNavigationInput` accepts `{ text }` and returns `{ target }`. `target` is null for invalid input, otherwise `{ line, column, selectsWholeLine }` contains zero-based line and UTF-16 column. One-based positive decimal line and @@ -4839,6 +4853,12 @@ The result is `{ symbols: WorkspaceSymbolItem[] }`, where each item is server's `workspaceSymbol/resolve` supplies it, which Core does not call. Items without `name` or `location.uri` are dropped. +Operation `references` (`textDocument/references`) accepts optional +`includeDeclaration` (boolean), sent as LSP `params.context.includeDeclaration`. +Missing means `true`, the previous behaviour; the usages shown from a +declaration (`lsp.declarationOrUsages`) pass `false` so the language server +leaves the declaration out. Other operations ignore the field. + Completion requests accept optional `completionContext` with LSP `triggerKind` (`1` invoked, `2` trigger character, `3` incomplete-list refresh) and `triggerCharacter` (required and one character for kind `2`). Missing context diff --git a/rust/fixtures/lsp/declaration-or-usages-v1.json b/rust/fixtures/lsp/declaration-or-usages-v1.json new file mode 100644 index 00000000..6ed85c51 --- /dev/null +++ b/rust/fixtures/lsp/declaration-or-usages-v1.json @@ -0,0 +1,1634 @@ +{ + "version": 1, + "command": "lsp.declarationOrUsages", + "cases": [ + { + "name": "a use opens its declaration", + "request": { + "uri": "file:///work/app/src/main/java/a/Main.java", + "position": { + "line": 5, + "utf16Column": 20 + }, + "definitions": [ + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 3, + "utf16Column": 18 + }, + "end": { + "line": 3, + "utf16Column": 22 + } + } + } + ] + }, + "expected": { + "action": "definitions" + } + }, + { + "name": "the declaration itself shows its usages", + "request": { + "uri": "file:///work/app/src/main/java/a/Main.java", + "position": { + "line": 3, + "utf16Column": 20 + }, + "definitions": [ + { + "uri": "file:///work/app/src/main/java/a/Main.java", + "range": { + "start": { + "line": 3, + "utf16Column": 18 + }, + "end": { + "line": 3, + "utf16Column": 22 + } + } + } + ] + }, + "expected": { + "action": "usages" + } + }, + { + "name": "a caret right after the declaration's name is still on it", + "request": { + "uri": "file:///work/app/src/main/java/a/Main.java", + "position": { + "line": 3, + "utf16Column": 22 + }, + "definitions": [ + { + "uri": "file:///work/app/src/main/java/a/Main.java", + "range": { + "start": { + "line": 3, + "utf16Column": 18 + }, + "end": { + "line": 3, + "utf16Column": 22 + } + } + } + ] + }, + "expected": { + "action": "usages" + } + }, + { + "name": "a caret right before the name is on it", + "request": { + "uri": "file:///work/app/src/main/java/a/Main.java", + "position": { + "line": 3, + "utf16Column": 18 + }, + "definitions": [ + { + "uri": "file:///work/app/src/main/java/a/Main.java", + "range": { + "start": { + "line": 3, + "utf16Column": 18 + }, + "end": { + "line": 3, + "utf16Column": 22 + } + } + } + ] + }, + "expected": { + "action": "usages" + } + }, + { + "name": "another place on the same line opens it", + "request": { + "uri": "file:///work/app/src/main/java/a/Main.java", + "position": { + "line": 3, + "utf16Column": 23 + }, + "definitions": [ + { + "uri": "file:///work/app/src/main/java/a/Main.java", + "range": { + "start": { + "line": 3, + "utf16Column": 18 + }, + "end": { + "line": 3, + "utf16Column": 22 + } + } + } + ] + }, + "expected": { + "action": "definitions" + } + }, + { + "name": "the same range in another document opens it", + "request": { + "uri": "file:///work/app/src/main/java/a/Main.java", + "position": { + "line": 3, + "utf16Column": 20 + }, + "definitions": [ + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 3, + "utf16Column": 18 + }, + "end": { + "line": 3, + "utf16Column": 22 + } + } + } + ] + }, + "expected": { + "action": "definitions" + } + }, + { + "name": "a multi-line range contains the position", + "request": { + "uri": "file:///work/app/src/main/java/a/Main.java", + "position": { + "line": 4, + "utf16Column": 0 + }, + "definitions": [ + { + "uri": "file:///work/app/src/main/java/a/Main.java", + "range": { + "start": { + "line": 3, + "utf16Column": 18 + }, + "end": { + "line": 5, + "utf16Column": 2 + } + } + } + ] + }, + "expected": { + "action": "usages" + } + }, + { + "name": "one other place among several opens the list", + "request": { + "uri": "file:///work/app/src/main/java/a/Main.java", + "position": { + "line": 3, + "utf16Column": 20 + }, + "definitions": [ + { + "uri": "file:///work/app/src/main/java/a/Main.java", + "range": { + "start": { + "line": 3, + "utf16Column": 18 + }, + "end": { + "line": 3, + "utf16Column": 22 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 7, + "utf16Column": 4 + }, + "end": { + "line": 7, + "utf16Column": 8 + } + } + } + ] + }, + "expected": { + "action": "definitions" + } + }, + { + "name": "an empty answer is reported as no definition", + "request": { + "uri": "file:///work/app/src/main/java/a/Main.java", + "position": { + "line": 3, + "utf16Column": 20 + }, + "definitions": [] + }, + "expected": { + "action": "definitions" + } + }, + { + "name": "an inverted range is refused", + "request": { + "uri": "file:///work/app/src/main/java/a/Main.java", + "position": { + "line": 3, + "utf16Column": 20 + }, + "definitions": [ + { + "uri": "file:///work/app/src/main/java/a/Main.java", + "range": { + "start": { + "line": 3, + "utf16Column": 22 + }, + "end": { + "line": 3, + "utf16Column": 18 + } + } + } + ] + }, + "expectedError": "invalid_request" + }, + { + "name": "an empty document URI is refused", + "request": { + "uri": "", + "position": { + "line": 3, + "utf16Column": 20 + }, + "definitions": [] + }, + "expectedError": "invalid_request" + }, + { + "name": "an unknown field is refused", + "request": { + "uri": "file:///work/app/src/main/java/a/Main.java", + "position": { + "line": 3, + "utf16Column": 20 + }, + "definitions": [], + "extra": 1 + }, + "expectedError": "invalid_request" + }, + { + "name": "more than 100 definitions are refused", + "request": { + "uri": "file:///work/app/src/main/java/a/Main.java", + "position": { + "line": 3, + "utf16Column": 20 + }, + "definitions": [ + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 0, + "utf16Column": 0 + }, + "end": { + "line": 0, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 1, + "utf16Column": 0 + }, + "end": { + "line": 1, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 2, + "utf16Column": 0 + }, + "end": { + "line": 2, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 3, + "utf16Column": 0 + }, + "end": { + "line": 3, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 4, + "utf16Column": 0 + }, + "end": { + "line": 4, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 5, + "utf16Column": 0 + }, + "end": { + "line": 5, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 6, + "utf16Column": 0 + }, + "end": { + "line": 6, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 7, + "utf16Column": 0 + }, + "end": { + "line": 7, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 8, + "utf16Column": 0 + }, + "end": { + "line": 8, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 9, + "utf16Column": 0 + }, + "end": { + "line": 9, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 10, + "utf16Column": 0 + }, + "end": { + "line": 10, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 11, + "utf16Column": 0 + }, + "end": { + "line": 11, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 12, + "utf16Column": 0 + }, + "end": { + "line": 12, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 13, + "utf16Column": 0 + }, + "end": { + "line": 13, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 14, + "utf16Column": 0 + }, + "end": { + "line": 14, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 15, + "utf16Column": 0 + }, + "end": { + "line": 15, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 16, + "utf16Column": 0 + }, + "end": { + "line": 16, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 17, + "utf16Column": 0 + }, + "end": { + "line": 17, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 18, + "utf16Column": 0 + }, + "end": { + "line": 18, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 19, + "utf16Column": 0 + }, + "end": { + "line": 19, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 20, + "utf16Column": 0 + }, + "end": { + "line": 20, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 21, + "utf16Column": 0 + }, + "end": { + "line": 21, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 22, + "utf16Column": 0 + }, + "end": { + "line": 22, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 23, + "utf16Column": 0 + }, + "end": { + "line": 23, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 24, + "utf16Column": 0 + }, + "end": { + "line": 24, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 25, + "utf16Column": 0 + }, + "end": { + "line": 25, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 26, + "utf16Column": 0 + }, + "end": { + "line": 26, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 27, + "utf16Column": 0 + }, + "end": { + "line": 27, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 28, + "utf16Column": 0 + }, + "end": { + "line": 28, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 29, + "utf16Column": 0 + }, + "end": { + "line": 29, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 30, + "utf16Column": 0 + }, + "end": { + "line": 30, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 31, + "utf16Column": 0 + }, + "end": { + "line": 31, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 32, + "utf16Column": 0 + }, + "end": { + "line": 32, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 33, + "utf16Column": 0 + }, + "end": { + "line": 33, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 34, + "utf16Column": 0 + }, + "end": { + "line": 34, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 35, + "utf16Column": 0 + }, + "end": { + "line": 35, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 36, + "utf16Column": 0 + }, + "end": { + "line": 36, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 37, + "utf16Column": 0 + }, + "end": { + "line": 37, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 38, + "utf16Column": 0 + }, + "end": { + "line": 38, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 39, + "utf16Column": 0 + }, + "end": { + "line": 39, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 40, + "utf16Column": 0 + }, + "end": { + "line": 40, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 41, + "utf16Column": 0 + }, + "end": { + "line": 41, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 42, + "utf16Column": 0 + }, + "end": { + "line": 42, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 43, + "utf16Column": 0 + }, + "end": { + "line": 43, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 44, + "utf16Column": 0 + }, + "end": { + "line": 44, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 45, + "utf16Column": 0 + }, + "end": { + "line": 45, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 46, + "utf16Column": 0 + }, + "end": { + "line": 46, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 47, + "utf16Column": 0 + }, + "end": { + "line": 47, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 48, + "utf16Column": 0 + }, + "end": { + "line": 48, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 49, + "utf16Column": 0 + }, + "end": { + "line": 49, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 50, + "utf16Column": 0 + }, + "end": { + "line": 50, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 51, + "utf16Column": 0 + }, + "end": { + "line": 51, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 52, + "utf16Column": 0 + }, + "end": { + "line": 52, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 53, + "utf16Column": 0 + }, + "end": { + "line": 53, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 54, + "utf16Column": 0 + }, + "end": { + "line": 54, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 55, + "utf16Column": 0 + }, + "end": { + "line": 55, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 56, + "utf16Column": 0 + }, + "end": { + "line": 56, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 57, + "utf16Column": 0 + }, + "end": { + "line": 57, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 58, + "utf16Column": 0 + }, + "end": { + "line": 58, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 59, + "utf16Column": 0 + }, + "end": { + "line": 59, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 60, + "utf16Column": 0 + }, + "end": { + "line": 60, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 61, + "utf16Column": 0 + }, + "end": { + "line": 61, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 62, + "utf16Column": 0 + }, + "end": { + "line": 62, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 63, + "utf16Column": 0 + }, + "end": { + "line": 63, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 64, + "utf16Column": 0 + }, + "end": { + "line": 64, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 65, + "utf16Column": 0 + }, + "end": { + "line": 65, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 66, + "utf16Column": 0 + }, + "end": { + "line": 66, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 67, + "utf16Column": 0 + }, + "end": { + "line": 67, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 68, + "utf16Column": 0 + }, + "end": { + "line": 68, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 69, + "utf16Column": 0 + }, + "end": { + "line": 69, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 70, + "utf16Column": 0 + }, + "end": { + "line": 70, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 71, + "utf16Column": 0 + }, + "end": { + "line": 71, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 72, + "utf16Column": 0 + }, + "end": { + "line": 72, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 73, + "utf16Column": 0 + }, + "end": { + "line": 73, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 74, + "utf16Column": 0 + }, + "end": { + "line": 74, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 75, + "utf16Column": 0 + }, + "end": { + "line": 75, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 76, + "utf16Column": 0 + }, + "end": { + "line": 76, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 77, + "utf16Column": 0 + }, + "end": { + "line": 77, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 78, + "utf16Column": 0 + }, + "end": { + "line": 78, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 79, + "utf16Column": 0 + }, + "end": { + "line": 79, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 80, + "utf16Column": 0 + }, + "end": { + "line": 80, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 81, + "utf16Column": 0 + }, + "end": { + "line": 81, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 82, + "utf16Column": 0 + }, + "end": { + "line": 82, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 83, + "utf16Column": 0 + }, + "end": { + "line": 83, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 84, + "utf16Column": 0 + }, + "end": { + "line": 84, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 85, + "utf16Column": 0 + }, + "end": { + "line": 85, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 86, + "utf16Column": 0 + }, + "end": { + "line": 86, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 87, + "utf16Column": 0 + }, + "end": { + "line": 87, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 88, + "utf16Column": 0 + }, + "end": { + "line": 88, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 89, + "utf16Column": 0 + }, + "end": { + "line": 89, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 90, + "utf16Column": 0 + }, + "end": { + "line": 90, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 91, + "utf16Column": 0 + }, + "end": { + "line": 91, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 92, + "utf16Column": 0 + }, + "end": { + "line": 92, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 93, + "utf16Column": 0 + }, + "end": { + "line": 93, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 94, + "utf16Column": 0 + }, + "end": { + "line": 94, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 95, + "utf16Column": 0 + }, + "end": { + "line": 95, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 96, + "utf16Column": 0 + }, + "end": { + "line": 96, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 97, + "utf16Column": 0 + }, + "end": { + "line": 97, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 98, + "utf16Column": 0 + }, + "end": { + "line": 98, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 99, + "utf16Column": 0 + }, + "end": { + "line": 99, + "utf16Column": 1 + } + } + }, + { + "uri": "file:///work/app/src/main/java/a/Circle.java", + "range": { + "start": { + "line": 100, + "utf16Column": 0 + }, + "end": { + "line": 100, + "utf16Column": 1 + } + } + } + ] + }, + "expectedError": "invalid_request" + } + ] +} diff --git a/rust/lithe-core/src/lsp/declaration_or_usages.rs b/rust/lithe-core/src/lsp/declaration_or_usages.rs new file mode 100644 index 00000000..44e3c61e --- /dev/null +++ b/rust/lithe-core/src/lsp/declaration_or_usages.rs @@ -0,0 +1,115 @@ +//! Whether a go-to-declaration gesture shows the declaration or its usages +//! (`lsp.declarationOrUsages`). +//! +//! Editors follow IntelliJ IDEA's Go to Declaration or Usages: from a use the +//! gesture opens the declaration, and from the declaration itself, where the +//! definition answer is the place the caret already sits on, it shows the +//! usages instead. Hosts send the definition answer they received and ask the +//! language server for references without the declaration when Core says +//! `usages`; Core owns the decision so every host and gesture agree. + +use crate::protocol::{CoreError, ErrorCode}; +use serde::Deserialize; +use serde_json::{json, Value}; + +/// The most definition locations one decision accepts; a definition answer +/// is a handful of places, so more is a malformed request. +const MAX_DEFINITIONS: usize = 100; + +/// Zero-based line and UTF-16 column, as the host's semantic requests use. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct Position { + line: u32, + utf16_column: u32, +} + +/// A half-open range; `start` never follows `end`. +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct Range { + start: Position, + end: Position, +} + +/// One definition location as the host received it. +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct Place { + uri: String, + range: Range, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct Request { + /// The document the gesture was made in, as the semantic request named it. + uri: String, + /// Where the gesture was made: the caret or the clicked character. + position: Position, + /// The definition answer for that position, in the server's order. + definitions: Vec, +} + +/// Returns `{ action }`: `usages` when every definition location is the +/// gesture's own place, otherwise `definitions` (including an empty answer, +/// which the host reports as no definition). +/// +/// A location is the gesture's own place when it is in the same document and +/// its range contains the position, end included: an editor resolves the +/// identifier left of the caret, so a caret right after a declaration's name +/// is still on that declaration. +pub(crate) fn decide_command(payload: Value) -> Result { + let request: Request = serde_json::from_value(payload).map_err(|error| { + invalid("Invalid declaration-or-usages request").with_details(error.to_string()) + })?; + if request.uri.is_empty() + || request.definitions.len() > MAX_DEFINITIONS + || request + .definitions + .iter() + .any(|place| place.uri.is_empty() || place.range.end < place.range.start) + { + return Err(invalid("Invalid declaration-or-usages request")); + } + let own_place = |place: &Place| { + place.uri == request.uri + && place.range.start <= request.position + && request.position <= place.range.end + }; + let usages = !request.definitions.is_empty() && request.definitions.iter().all(own_place); + Ok(json!({ "action": if usages { "usages" } else { "definitions" } })) +} + +fn invalid(message: &str) -> CoreError { + CoreError::new(ErrorCode::InvalidRequest, message) +} + +#[cfg(test)] +mod tests { + use serde_json::{json, Value}; + + /// Every shared-fixture case yields its expected data or error code + /// through the public JSON command. + #[test] + fn matches_shared_fixture() { + let fixture: Value = serde_json::from_str(include_str!( + "../../../fixtures/lsp/declaration-or-usages-v1.json" + )) + .unwrap(); + for case in fixture["cases"].as_array().unwrap() { + let response: Value = serde_json::from_str(&crate::execute_json( + &json!({"command": "lsp.declarationOrUsages", "payload": case["request"]}) + .to_string(), + )) + .unwrap(); + if let Some(code) = case.get("expectedError") { + assert_eq!(response["ok"], false, "{}: {response}", case["name"]); + assert_eq!(&response["error"]["code"], code, "{}", case["name"]); + } else { + assert_eq!(response["ok"], true, "{}: {response}", case["name"]); + assert_eq!(response["data"], case["expected"], "{}", case["name"]); + } + } + } +} diff --git a/rust/lithe-core/src/lsp/interface/client.rs b/rust/lithe-core/src/lsp/interface/client.rs index 137ff754..2b2195de 100644 --- a/rust/lithe-core/src/lsp/interface/client.rs +++ b/rust/lithe-core/src/lsp/interface/client.rs @@ -1132,7 +1132,7 @@ fn feature_request_params(request: &ClientFeatureRequest) -> Result Ok(json!({ "textDocument": text_document, "position": lsp_position_json(required_position(request)?), - "context": { "includeDeclaration": true } + "context": { "includeDeclaration": request.include_declaration.unwrap_or(true) } })), "textDocument/rename" => Ok(json!({ "textDocument": text_document, @@ -2465,6 +2465,7 @@ mod formatting_and_completion_tests { code_action: None, command: None, formatting_options: options, + include_declaration: None, } } diff --git a/rust/lithe-core/src/lsp/interface/engine.rs b/rust/lithe-core/src/lsp/interface/engine.rs index e1789d3d..132a2737 100644 --- a/rust/lithe-core/src/lsp/interface/engine.rs +++ b/rust/lithe-core/src/lsp/interface/engine.rs @@ -317,6 +317,12 @@ pub struct SemanticRequest { /// `formatting` and `rangeFormatting`; unset fields keep Core defaults. #[serde(default)] pub formatting_options: Option, + /// For `references`: whether the answer lists the declaration itself + /// (LSP `ReferenceContext.includeDeclaration`). Unset means `true`; the + /// usages shown from a declaration (`lsp.declarationOrUsages`) pass + /// `false` so the language server leaves the declaration out. + #[serde(default)] + pub include_declaration: Option, } #[derive(Debug, Clone, Deserialize)] @@ -2609,6 +2615,7 @@ impl RuntimeSession { code_action: request.code_action, command: request.command, formatting_options: request.formatting_options, + include_declaration: request.include_declaration, }; client_feature_request_canonical(feature_request)? } @@ -6200,6 +6207,7 @@ mod tests { code_action: None, command: None, formatting_options: None, + include_declaration: None, }, operation_id.clone(), ) @@ -8001,6 +8009,7 @@ mod tests { code_action: None, command: None, formatting_options: None, + include_declaration: None, }, "op-formatting".to_string(), ) @@ -8069,6 +8078,7 @@ mod tests { code_action: None, command: None, formatting_options: None, + include_declaration: None, }, "op-formatting-2".to_string(), ) @@ -8204,6 +8214,7 @@ mod tests { "arguments": [] })), formatting_options: None, + include_declaration: None, }, operation_id, ) diff --git a/rust/lithe-core/src/lsp/interface/types.rs b/rust/lithe-core/src/lsp/interface/types.rs index 895a347c..5c6681ef 100644 --- a/rust/lithe-core/src/lsp/interface/types.rs +++ b/rust/lithe-core/src/lsp/interface/types.rs @@ -383,6 +383,9 @@ pub struct ClientFeatureRequest { /// Caller `FormattingOptions` for `formatting` and `rangeFormatting`. #[serde(default)] pub formatting_options: Option, + /// `textDocument/references` `context.includeDeclaration`; unset means `true`. + #[serde(default)] + pub include_declaration: Option, } #[cfg(test)] diff --git a/rust/lithe-core/src/lsp/mod.rs b/rust/lithe-core/src/lsp/mod.rs index 37f8fb2b..ea2e495a 100644 --- a/rust/lithe-core/src/lsp/mod.rs +++ b/rust/lithe-core/src/lsp/mod.rs @@ -1,5 +1,6 @@ //! Language tooling is split by responsibility while this facade keeps the Core command API stable. +pub(crate) mod declaration_or_usages; pub(crate) mod document_identity; pub(crate) mod host_policy; pub(crate) mod interface; diff --git a/rust/lithe-core/src/lsp/tests.rs b/rust/lithe-core/src/lsp/tests.rs index adb27ed8..b4b3329b 100644 --- a/rust/lithe-core/src/lsp/tests.rs +++ b/rust/lithe-core/src/lsp/tests.rs @@ -776,6 +776,7 @@ fn client_core_tracks_documents_and_feature_requests() { code_action: None, command: None, formatting_options: None, + include_declaration: None, }) .unwrap(); assert_eq!( @@ -787,6 +788,47 @@ fn client_core_tracks_documents_and_feature_requests() { assert_eq!(request_message["params"]["position"]["character"], 12); } +/// The usages shown from a declaration ask the server to leave the +/// declaration out; every other references request keeps it, as before. +#[test] +fn client_references_request_passes_include_declaration() { + let opened = client_open_document(ClientOpenDocumentRequest { + state: LspClientState::default(), + uri: "file:///tmp/project/Main.java".to_string(), + language_id: "java".to_string(), + text: "class Main {}\n".to_string(), + }) + .unwrap(); + for (include_declaration, sent) in [(None, true), (Some(true), true), (Some(false), false)] { + let requested = client_feature_request(ClientFeatureRequest { + query: None, + state: opened.state.clone(), + uri: "file:///tmp/project/Main.java".to_string(), + method: "textDocument/references".to_string(), + position: Some(LspPosition { + line: 0, + utf16_column: 7, + }), + new_name: None, + range: None, + diagnostics: Vec::new(), + completion_context: None, + completion_item: None, + code_action: None, + command: None, + formatting_options: None, + include_declaration, + }) + .unwrap(); + let message: Value = serde_json::from_str(&requested.messages[0]).unwrap(); + assert_eq!(message["method"], "textDocument/references"); + assert_eq!( + message["params"]["context"]["includeDeclaration"], sent, + "{include_declaration:?}" + ); + } +} + #[test] fn client_change_document_emits_incremental_ranges_when_the_server_supports_them() { let opened = client_open_document(ClientOpenDocumentRequest { @@ -1199,6 +1241,7 @@ fn client_core_preserves_class_completion_namespace_and_import_edits() { code_action: None, command: None, formatting_options: None, + include_declaration: None, }) .unwrap(); let completed = client_apply_server_message(ClientApplyServerMessageRequest { @@ -1248,6 +1291,7 @@ fn client_core_preserves_class_completion_namespace_and_import_edits() { code_action: None, command: None, formatting_options: None, + include_declaration: None, }) .unwrap(); let resolved = client_apply_server_message(ClientApplyServerMessageRequest { @@ -1627,6 +1671,7 @@ fn client_core_shapes_feature_responses_for_swift_models() { code_action: None, command: None, formatting_options: None, + include_declaration: None, }) .unwrap(); let completed = client_apply_server_message(ClientApplyServerMessageRequest { @@ -1680,6 +1725,7 @@ fn client_core_shapes_feature_responses_for_swift_models() { code_action: None, command: None, formatting_options: None, + include_declaration: None, }) .unwrap(); let renamed = client_apply_server_message(ClientApplyServerMessageRequest { @@ -1722,6 +1768,7 @@ fn client_core_shapes_feature_responses_for_swift_models() { code_action: None, command: None, formatting_options: None, + include_declaration: None, }) .unwrap(); let formatted = client_apply_server_message(ClientApplyServerMessageRequest { @@ -1801,6 +1848,7 @@ fn client_core_shapes_feature_responses_for_swift_models() { code_action: None, command: None, formatting_options: None, + include_declaration: None, }) .unwrap(); let code_action_request: Value = serde_json::from_str(&code_actions.messages[0]).unwrap(); @@ -1884,6 +1932,7 @@ fn client_core_shapes_feature_responses_for_swift_models() { })), command: None, formatting_options: None, + include_declaration: None, }) .unwrap(); let code_action_resolve_request: Value = @@ -1951,6 +2000,7 @@ fn client_core_shapes_feature_responses_for_swift_models() { code_action: None, command: None, formatting_options: None, + include_declaration: None, }) .unwrap(); let completion_resolve_request: Value = @@ -2004,6 +2054,7 @@ fn client_core_shapes_feature_responses_for_swift_models() { "arguments": [{ "label": "rename" }] })), formatting_options: None, + include_declaration: None, }) .unwrap(); let execute_request: Value = serde_json::from_str(&execute_command.messages[0]).unwrap(); @@ -2050,6 +2101,7 @@ fn client_requests_and_normalizes_document_links() { code_action: None, command: None, formatting_options: None, + include_declaration: None, }) .unwrap(); let request_json: Value = serde_json::from_str(&request.messages[0]).unwrap(); @@ -2112,6 +2164,7 @@ fn client_requests_and_normalizes_workspace_symbols_without_a_document() { code_action: None, command: None, formatting_options: None, + include_declaration: None, }) .unwrap(); let request_json: Value = serde_json::from_str(&request.messages[0]).unwrap(); @@ -2197,6 +2250,7 @@ fn client_core_requests_and_shapes_inlay_hints_folding_ranges_and_code_lenses() code_action: None, command: None, formatting_options: None, + include_declaration: None, }) .unwrap(); let request_json: Value = serde_json::from_str(&inlay_request.messages[0]).unwrap(); @@ -2251,6 +2305,7 @@ fn client_core_requests_and_shapes_inlay_hints_folding_ranges_and_code_lenses() code_action: None, command: None, formatting_options: None, + include_declaration: None, }) .unwrap(); let request_json: Value = serde_json::from_str(&folding_request.messages[0]).unwrap(); @@ -2295,6 +2350,7 @@ fn client_core_requests_and_shapes_inlay_hints_folding_ranges_and_code_lenses() code_action: None, command: None, formatting_options: None, + include_declaration: None, }) .unwrap(); let code_lens_response = client_apply_server_message(ClientApplyServerMessageRequest { @@ -2352,6 +2408,7 @@ fn navigation_locations_preserve_uris_without_fabricating_virtual_file_paths() { code_action: None, command: None, formatting_options: None, + include_declaration: None, }) .unwrap(); let response = client_apply_server_message(ClientApplyServerMessageRequest { diff --git a/rust/lithe-core/src/protocol/command.rs b/rust/lithe-core/src/protocol/command.rs index 6ae3747d..86adef87 100644 --- a/rust/lithe-core/src/protocol/command.rs +++ b/rust/lithe-core/src/protocol/command.rs @@ -463,6 +463,9 @@ pub enum CoreCommand { /// Decides whether a language-server jump target opens, opens read-only /// or is refused (`lsp.classifyNavigationTarget`). LspClassifyNavigationTarget, + /// Decides whether a go-to-declaration gesture opens the declaration or + /// shows its usages (`lsp.declarationOrUsages`). + LspDeclarationOrUsages, /// Applies a deterministic line-level editing transform /// (`editor.lineEdit`). EditorLineEdit, @@ -1019,6 +1022,7 @@ impl CoreCommand { "lsp.planWorkspaceEdit" => Some(Self::LspPlanWorkspaceEdit), "lsp.documentIdentity" => Some(Self::LspDocumentIdentity), "lsp.classifyNavigationTarget" => Some(Self::LspClassifyNavigationTarget), + "lsp.declarationOrUsages" => Some(Self::LspDeclarationOrUsages), "editor.lineEdit" => Some(Self::EditorLineEdit), "editor.planTextEdits" => Some(Self::EditorPlanTextEdits), "editor.parseNavigationInput" => Some(Self::EditorParseNavigationInput), diff --git a/rust/lithe-core/src/runtime/dispatcher.rs b/rust/lithe-core/src/runtime/dispatcher.rs index 4179161f..e1789fc5 100644 --- a/rust/lithe-core/src/runtime/dispatcher.rs +++ b/rust/lithe-core/src/runtime/dispatcher.rs @@ -1934,6 +1934,11 @@ fn execute_parsed(mut parsed: CoreRequest) -> CoreResponse { parsed.payload, crate::lsp::navigation_target::classify_command, ), + CoreCommand::LspDeclarationOrUsages => plugin_response( + id, + parsed.payload, + crate::lsp::declaration_or_usages::decide_command, + ), CoreCommand::EditorPlanTextEdits => plugin_response( id, parsed.payload,