diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/lsp.md | 89 |
1 files changed, 88 insertions, 1 deletions
diff --git a/docs/lsp.md b/docs/lsp.md index c89c42f7..0c6a2f97 100644 --- a/docs/lsp.md +++ b/docs/lsp.md @@ -71,6 +71,76 @@ locations (`hover`, `code_action`, `format`, `rename`) open `+Hover`/`+Lsp` instead and do not arm the stepper — `n` over a documentation blurb would step to nowhere. +`completion` is the kind this shape changes the most. Every other editor answers +a dot with a popup of NAMES to insert; a seam that returns locations cannot +insert anything, so this one answers with the candidates' **declarations** — +one `path:LINE:COL-ENDCOL` row each, in the same `+Search` buffer, steppable +with `n`. That is a different and arguably better answer to "what goes here": +you read the definitions rather than a list of words. It is the one location +kind that does NOT jump on a single row, because with one candidate you still +want to see the list rather than be teleported into it. + +A results buffer is REFILLED rather than reopened when the same kind is asked +again — the rule `runSearch` always had, and which the language path was +missing. It survived being missing while every query was a deliberate press +(`gr` twice left two identical lists and you closed one); Tab after a dot is an +ordinary typing keystroke, and measured, twenty of them stacked **fifteen** +byte-identical `+Search` panes, crushed the file to one visible line, and then +ran `freeSlot` out so the key was silently eaten for the rest of the session. +Unlike a search the ARGUMENT is not part of the identity: a language query is +asked about a different symbol every time with the same (usually empty) arg, so +the kind is the unit. + +Making it work needed one trick. A completion is asked for exactly when the +line is half-typed, and a half-typed line does not parse: `switch (e) { . }` +loses the whole switch to the parser's error recovery, taking with it every +ancestor an expected-type resolution needs. ZLS answers this with a private +token scanner welded to its `*Server`. `lsp_zls.completionSource` instead makes +the tree PARSE — it splices a placeholder in after the dot, in the six +spellings a half-typed line can need (an identifier, and the same again closing +a prong, a statement, a paren or a brace), and keeps the one that both makes +the dot reachable in the tree and leaves the fewest parse errors. Everything +after that is ZLS's ordinary public resolution over an ordinary tree. + +**A Tab the backend cannot answer still indents.** The keystroke has already +diverted by the time "no rows" comes back, so `lspResponse` performs the indent +the Tab prong skipped — on the condition that the cursor has not moved since, +so nobody who kept typing gets four spaces landing behind their hands. Without +that, a dot in a comment, in a string, or on a line nothing can be made of ate +the keystroke outright. With several cursors Tab never diverts at all: a +language query is a per-keystroke action inside a per-selection replay, so +asking would stop the replay dead and collapse the multicursor. + +### What it costs, and what it cannot do + +Per press, measured by `zig build lspbench` on this repo: + +| | ReleaseFast | Debug (what `zig build` installs) | +|---|---|---| +| a switch arm in `src/pardes.zig` (12.8k lines) | 8.8 ms | 87 ms | +| `std.` — 91 candidates, each alias-resolved into the stdlib | 26 ms | 204 ms | + +It is a worker thread, so the editor does not block; but the second press of +Tab joins the first query on the UI thread (`old.cancel(io)` in the shell) and +that wait is real. Pre-existing and shared by every LSP kind — not this +feature's to fix, but it is what a fast double-Tab feels like. + +Known limitations, in the order you will meet them: + +- **`@This()` anywhere in a container makes the whole container unresolvable**, + so `var list: std.ArrayList(u8) = .` — the most common decl literal in this + codebase — answers nothing. This is not the completion filter: `hover` and a + plain field access on the same struct return nothing either. It is the case a + user hits first, and it is upstream of everything here. +- **Only the break AT THE CURSOR is repaired.** Zig's error recovery runs + forward, so an unrepaired break earlier in the file swallows the declaration + the cursor is in and the answer is empty. While typing you normally have one + broken spot, which is the case this works for. +- **A dependency module** (`@import("vaxis")`) cannot be typed at all, for the + same reason `gd` on `vaxis.init` finds nothing. +- **`error.`** is not handled — the position context is `.error_access`, which + no branch claims. + ## The keymap is helix's, exactly Verified against `helix-term/src/keymap/default.rs`, not from memory. @@ -92,6 +162,18 @@ Verified against `helix-term/src/keymap/default.rs`, not from memory. | `]D` / `[D` | last / first diagnostic | | | `=` | format | | | `Ctrl`+left-click | definition | the mouse spelling of `gd` | +| `Tab` in INSERT mode, right after a `.` | completion | what could go here, and where each of those is defined | + +Tab is the one key here that is not helix's and not a goto. helix's `Tab` +completes; pardes's shows you the CANDIDATES' DECLARATIONS in a `+Search` +buffer and inserts nothing, because that is what a seam returning locations can +honestly do — see below. It only diverts where an answer is possible: on a +terminal, in an output buffer, or in a file the backend does not speak +(`lsp.speaks`, which the core asks and the backend answers), Tab indents +exactly as it always did. A Tab that silently does nothing would be worse than +not having the feature. Nothing about the mode changes either — the pane is +still in insert, so typing goes on and walking the answer with `n` means +pressing `Esc` first, the same as for every other results buffer. Ctrl-click rides the ordinary left-click drag rather than firing on the press: a click does not place the modal cursor until RELEASE, so a query asked at @@ -136,10 +218,15 @@ nothing else: ```zig pub fn query(gpa, arena, req: Req, out: *std.Io.Writer) void +pub fn speaks(path: []const u8) bool pub const supports: std.EnumSet(Kind) pub const backend_name = "..." ``` +`supports` says what a backend can do; `speaks` says what it can do it TO, and +exists for the one key that must not be eaten when the answer is no — see the +Tab note above. + `query` runs on a worker thread with no access to the core — everything it may read is in `req` (`path`, `source` (NUL-terminated), `offset`, `arg`, `root`). `out` is a plain `std.Io.Writer`: the shell owns the buffer behind it (an @@ -151,7 +238,7 @@ byte-identical in shape. ## How the implementations are judged -`zig build lspbench` — same harness, same corpus (pardes's own `src/`), same 17 +`zig build lspbench` — same harness, same corpus (pardes's own `src/`), same 22 probes, every backend. - **Feature completeness.** Which kinds return rows, and whether the rows |
