summaryrefslogtreecommitdiff
path: root/docs/lsp.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/lsp.md')
-rw-r--r--docs/lsp.md43
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.