diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/lsp.md | 43 |
1 files changed, 31 insertions, 12 deletions
diff --git a/docs/lsp.md b/docs/lsp.md index 258e409e..097386e6 100644 --- a/docs/lsp.md +++ b/docs/lsp.md @@ -23,9 +23,9 @@ core shell worker | | snapshot path + content | | |--------------------------->| | | | lsp.query(...) - | | Event .lsp_resp{id,rows}| + | | Event .lsp_resp{id,rows} | |<--------------------------|<---------------------------| - | lspResponse -> jump, or open a results buffer | + | lspResponse -> atomic edit, jump, or results buffer | ``` The shell already ran this exact pattern for pty readers, so the async part is @@ -40,8 +40,10 @@ Three rules make it safe: query is in flight; a borrowed slice would be a use-after-free the length of one keystroke. - **One query in flight, identified by a monotonic id.** A second press bumps - the id, which makes the older answer stale — `lspResponse` drops any id it is - not waiting for. This is also what makes a closed pane safe. + the id, which makes the older answer stale. The pending request also records + the pane serial, so a closed-and-reused slot cannot accept its response. +- **Mutating answers are revision-checked.** Rename records the file revision + sent to the worker and applies nothing if the user edited before it answered. - **No rows is a legal answer.** A backend that cannot answer appends nothing, which is indistinguishable from a language server still starting up, and the core does nothing. There is no error path to render. @@ -67,10 +69,19 @@ already produces and `n`/`N` already step — so: - **several rows** → an output buffer, which `n`/`N` walk which means helix's multi-result picker required **no picker code at all**. The -`+Search` buffer *is* the picker. Kinds whose answer is prose rather than -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. +`+Search` buffer *is* the picker. Non-location answers (`hover`, `code_action`, +`format`) open `+Hover`/`+Lsp` instead and do not arm the stepper — `n` over a +documentation blurb would step to nowhere. + +Rename is deliberately the one exception to rows as presentation. The backend +emits `@edit START END` records through `lsp.edit()`, using half-open byte +offsets into the exact `Req.source` snapshot it resolved. The core validates +that every range is ordered, non-overlapping and in bounds, checks that the +pane serial and file revision still match, then substitutes the requested name +across all ranges with one allocation and one undo transaction. A malformed, +stale, or empty response changes nothing. The current ZLS backend resolves and +renames references in the **current file only**; it does not claim a workspace +rename. **A path UNDER `Req.root` is written relative to it; everything else keeps its full absolute path** (`lsp.rel`). `Req.root` is the directory of the file the @@ -186,7 +197,7 @@ Verified against `helix-term/src/keymap/default.rs`, not from memory. | `gi` | implementation | | | `gr` | references | | | `SPC l k` | hover | opens `+Hover` | -| `SPC l r` | rename | tag input, like Find/Grep | +| `SPC l r` | rename | tag input; applies current-file references in one undo step | | `SPC l a` | code action | | | `SPC l h` | select references | | | `SPC l s` / `SPC l S` | document / workspace symbols | `S` takes a query | @@ -266,9 +277,11 @@ read is in `req` (`path`, `source` (NUL-terminated), `offset`, `arg`, `root`). `Io.Writer.Allocating`), so a backend never allocates the result, never frees it, and cannot get the allocator wrong. `arena` is freed wholesale on return; `gpa` is for a backend's own scratch. Use `lsp.row()` to emit a location, -`lsp.rel()` to spell its path against `req.root` and `lsp.lineCol()` to convert -an offset, so every backend's rows are byte-identical in shape. `rel` allocates -nothing — it returns a slice of what you hand it. +`lsp.rel()` to spell its path against `req.root`, `lsp.lineCol()` to convert an +offset, and `lsp.edit()` for each half-open range of a rename response. Location +rows are byte-identical across backends; rename ranges are consumed by the core +and never rendered. `rel` allocates nothing — it returns a slice of what you +hand it. ## How the implementations are judged @@ -281,6 +294,7 @@ probes, every backend. and a kind that answers without claiming is `unclaimed-works`. Correctness is a substring the rows must contain, so returning a confident wrong location scores worse than returning nothing. + - **Latency.** `cold` (first query, index construction included) and `warm` (median of 20). They differ by orders of magnitude for an indexing backend and both matter: cold is what the first keypress costs, warm is what every @@ -292,4 +306,9 @@ probes, every backend. but is charged in build time and dependency surface rather than in lines we maintain. +The user-visible rename contract is also pinned through the actual TTY, +leader prompt, worker and ZLS backend by `test/snapshots/lsp-rename.snap`: both +resolved occurrences change, a shadowed local does not, and undo/redo treats +the response as one transaction. + Run `zig build lspbench -- --json` for machine-readable output. |
