summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-29 00:50:38 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commit4e642c1d6688baf3b98f269818b66cc7cc194c5f (patch)
tree1ccbfdcc47fd8d6100ab3bd9ed4578b41f378f34 /test
parent841038666a45b40107ee26d1f8e54ed19d0891a0 (diff)
downloadpardes-4e642c1d6688baf3b98f269818b66cc7cc194c5f.tar.gz
pardes-4e642c1d6688baf3b98f269818b66cc7cc194c5f.zip
lsp backend: ZLS as an in-process library
The seam's `query` now calls ZLS's analyser directly, on the worker thread, in this process. There is no zls binary, no subprocess, no JSON-RPC, no `initialize` handshake and no `Server` — `gd` is a function call whose answer comes back as rows. ZLS's build.zig already publishes its guts as an importable module (`b.modules.put("zls", ...)`), so this is a path dependency on the local 0.16.x checkout plus one new file, `src/lsp_zls.zig`. Construction is ZLS's own (tests/analysis_check.zig does exactly this): InternPool.init -> DiagnosticsCollection -> DocumentStore struct literal -> Analyser.init. `zig_exe_path` is null on purpose — shelling out to the compiler is the thing this backend exists to avoid — and `zig_lib_dir` is baked in from `b.graph.zig_lib_directory`, so `gd` on `std.mem.count` opens the same mem.zig the compiler used, with ZIG_LIB_DIR overriding at runtime. Offsets are `.@"utf-8"`, not ZLS's utf-16 default: `+Search` rows are byte columns and we are not on a wire. Seventeen probes, seventeen answering, no false claims, 5.9 MiB peak RSS. The features that were already Server-free are calls (hover, document symbols, code actions); the ones welded to `*Server` are reimplemented thin on top of public primitives — goto is gotoHandler minus the protocol, diagnostics is the in-process `std.zig.AstGen` branch of getAstCheckDiagnostics, references is symbolReferences' algorithm from the outside (offer every same-named identifier token back to the analyser and keep the ones that resolve to the same decl, so a shadowed local is not a false hit). What it does not do, deliberately: - Nothing is cached between queries. Each `query` builds a DocumentStore, resolves imports and throws it away, because the arena dies on return and `req.source` is a snapshot of a buffer the user is still typing into. So cold IS warm — there is no index to warm up. It is also fast enough not to need one: 124us for a local goto, 2.8ms into the stdlib, 8ms for references over a 5000-line file. A cross-query cache is a real design (a global, a mutex, an invalidation story), not a line of code, and it is the obvious next step rather than something smuggled in here. - References, rename and select-refs are THIS FILE only. Workspace-wide means loading every project file into the store and running the analyser over each; DocumentStore's own workspace iteration has the same limit (it can only see handles already loaded). Workspace symbols and workspace diagnostics DO walk the tree, because neither needs the analyser — a parse and a tree walk each. - Rename previews, format reports, code actions list. The seam hands back rows, not edits, so there is no channel through which a backend could rewrite the buffer. These answer the question the keypress asks and change nothing. - Without a zig binary, `@import("builtin")`, `@import("<pkg>")` and `@cImport` resolve to nothing — silently, which is ZLS's behaviour, not a bug introduced here. Relative imports and `std` work. - Non-.zig files answer nothing. The core does not gate the keymap by file type, so the gate is here: `gd` in a README must find nothing rather than parse prose as Zig and confidently resolve a word out of it. A whole-file report that ran and found nothing says so ("no diagnostics", "already formatted", "no code actions") rather than returning zero rows, because in this seam zero rows already means "no backend" — `lspResponse` opens nothing for an empty answer, so silence cannot also mean "checked, clean". Location queries keep the opposite rule: unresolvable is no rows. test/snapshots/lsp.snap covers the round trip end to end — gd jumping on a single result, SPC k opening +Hover, SPC s opening the +Search list that n steps, and gd on a keyword answering nothing without opening anything. The whole backend was also fuzzed at 20k queries over real, truncated and byte-smashed sources across every kind; that found two crashes (a decl's name token indexes its own file, not the requesting one, and is not necessarily an identifier at all on a half-typed line) which are fixed. emscripten does not get the backend: the web shell has no threads and no-ops the lsp effect, so it keeps the empty one the base tree shipped. DEPENDENCY: ZLS is FETCHED by the build system (build.zig.zon .url + .hash, pinned to commit 3e0d0820 on the 0.16.x branch) rather than a path dependency on the local genizah checkout, so it lands in zig-pkg/ like every other dependency and the build is reproducible from the .zon alone. Also passes -Dversion-string: ZLS's build.zig names itself by shelling out to `git describe`, and a fetched package is an extracted tarball with no .git, so every build printed a 'Failed to run git describe' warning. We pin the commit, so we already know the answer.
Diffstat (limited to 'test')
-rw-r--r--test/snapshots/lsp.golden287
-rw-r--r--test/snapshots/lsp.snap53
2 files changed, 340 insertions, 0 deletions
diff --git a/test/snapshots/lsp.golden b/test/snapshots/lsp.golden
new file mode 100644
index 00000000..c85a412b
--- /dev/null
+++ b/test/snapshots/lsp.golden
@@ -0,0 +1,287 @@
+== snap opened grid=120x40 cursor=7,2
+|Kill Newcol Tutor Debug NextColor Dump Find Grep
+| NOR /tmp/pardes-snap/lsp/cwd/a.zig Save Del NOR /tmp/pardes-snap/lsp/cwd Del
+| 1 const std = @import("std");
+| 2 a.zig
+| 3 fn helper(x: u32) u32 {
+| 4 return x + 1;
+| 5 }
+| 6
+| 7 pub fn main() void {
+| 8 _ = helper(41);
+| 9 }
+| 10
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap cursor grid=120x40 cursor=17,9
+|Kill Newcol Tutor Debug NextColor Dump Find Grep
+| NOR /tmp/pardes-snap/lsp/cwd/a.zig Save Del NOR /tmp/pardes-snap/lsp/cwd Del
+| 1 const std = @import("std");
+| 2 a.zig
+| 3 fn helper(x: u32) u32 {
+| 4 return x + 1;
+| 5 }
+| 6
+| 7 pub fn main() void {
+| 8 _ = helper(41);
+| 9 }
+| 10
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap definition grid=120x40 cursor=10,4
+|Kill Newcol Tutor Debug NextColor Dump Find Grep
+| NOR /tmp/pardes-snap/lsp/cwd/a.zig Save Del NOR /tmp/pardes-snap/lsp/cwd Del
+| 1 const std = @import("std");
+| 2 a.zig
+| 3 fn helper(x: u32) u32 {
+| 4 return x + 1;
+| 5 }
+| 6
+| 7 pub fn main() void {
+| 8 _ = helper(41);
+| 9 }
+| 10
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap hover grid=120x40 cursor=10,4
+|Kill Newcol Tutor Debug NextColor Dump Find Grep
+| NOR /tmp/pardes-snap/lsp/cwd/a.zig Save Del NOR /tmp/pardes-snap/lsp/cwd Del
+| 1 const std = @import("std");
+| 2 a.zig
+| 3 fn helper(x: u32) u32 {
+| 4 return x + 1;
+| 5 }
+| 6
+| 7 pub fn main() void {
+| 8 _ = helper(41);
+| 9 }
+| 10
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/lsp/cwd/+Hover Del
+| 1 fn helper(x: u32) u32
+| 2 (fn (u32) u32)
+== snap symbols grid=120x40 cursor=10,4
+|Kill Newcol Tutor Debug NextColor Dump Find Grep
+| NOR /tmp/pardes-snap/lsp/cwd/a.zig Save Del NOR /tmp/pardes-snap/lsp/cwd Del
+| 1 const std = @import("std");
+| 2 a.zig
+| 3 fn helper(x: u32) u32 {
+| 4 return x + 1;
+| 5 }
+| 6
+| 7 pub fn main() void {
+| 8 _ = helper(41);
+| 9 }
+| 10
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/lsp/cwd/+Search Del
+| 1 /tmp/pardes-snap/lsp/cwd/a.zig:1:7 std
+| 2 /tmp/pardes-snap/lsp/cwd/a.zig:3:4 helper fn helper(
+| 3 /tmp/pardes-snap/lsp/cwd/a.zig:7:8 main fn main() vo
+| NOR /tmp/pardes-snap/lsp/cwd/+Hover Del
+| 1 fn helper(x: u32) u32
+| 2 (fn (u32) u32)
+== snap step grid=120x40 cursor=13,2
+|Kill Newcol Tutor Debug NextColor Dump Find Grep
+| NOR /tmp/pardes-snap/lsp/cwd/a.zig Save Del NOR /tmp/pardes-snap/lsp/cwd Del
+| 1 const std = @import("std");
+| 2 a.zig
+| 3 fn helper(x: u32) u32 {
+| 4 return x + 1;
+| 5 }
+| 6
+| 7 pub fn main() void {
+| 8 _ = helper(41);
+| 9 }
+| 10
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/lsp/cwd/+Search Del
+| 1 /tmp/pardes-snap/lsp/cwd/a.zig:1:7 std
+| 2 /tmp/pardes-snap/lsp/cwd/a.zig:3:4 helper fn helper(
+| 3 /tmp/pardes-snap/lsp/cwd/a.zig:7:8 main fn main() vo
+| NOR /tmp/pardes-snap/lsp/cwd/+Hover Del
+| 1 fn helper(x: u32) u32
+| 2 (fn (u32) u32)
+== snap nothing grid=120x40 cursor=7,4
+|Kill Newcol Tutor Debug NextColor Dump Find Grep
+| NOR /tmp/pardes-snap/lsp/cwd/a.zig Save Del NOR /tmp/pardes-snap/lsp/cwd Del
+| 1 const std = @import("std");
+| 2 a.zig
+| 3 fn helper(x: u32) u32 {
+| 4 return x + 1;
+| 5 }
+| 6
+| 7 pub fn main() void {
+| 8 _ = helper(41);
+| 9 }
+| 10
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/lsp/cwd/+Search Del
+| 1 /tmp/pardes-snap/lsp/cwd/a.zig:1:7 std
+| 2 /tmp/pardes-snap/lsp/cwd/a.zig:3:4 helper fn helper(
+| 3 /tmp/pardes-snap/lsp/cwd/a.zig:7:8 main fn main() vo
+| NOR /tmp/pardes-snap/lsp/cwd/+Hover Del
+| 1 fn helper(x: u32) u32
+| 2 (fn (u32) u32)
diff --git a/test/snapshots/lsp.snap b/test/snapshots/lsp.snap
new file mode 100644
index 00000000..9ea84e1f
--- /dev/null
+++ b/test/snapshots/lsp.snap
@@ -0,0 +1,53 @@
+# Language intelligence end to end: the helix keymap, the async round trip and
+# the two dispositions of an answer.
+#
+# The backend behind the seam here is ZLS linked in as a LIBRARY
+# (src/lsp_zls.zig) — no server process, no JSON-RPC — so `gd` is a function
+# call on a worker thread whose result comes back as an ordinary event, the
+# same shape a pty read already had. Nothing in this script knows that: it
+# presses keys and reads the grid, which is the point of the seam.
+#
+# a.zig is deliberately self-contained — a definition, a call site, and not one
+# symbol from std — so these goldens do not encode this machine's stdlib
+# version or its install path.
+file a.zig const std = @import("std");\n\nfn helper(x: u32) u32 {\n return x + 1;\n}\n\npub fn main() void {\n _ = helper(41);\n}\n
+start 40 120 a.zig
+wait 8000 helper
+stable 700 20000
+snap opened
+# park the cursor inside the CALL `helper(41)` (file line 8, grid row 9)
+press left 18 10
+release left 18 10
+stable 400 5000
+snap cursor
+# gd on a call: exactly one location comes back, so it JUMPS rather than
+# opening a picker — the cursor lands on the definition's name token, line 3.
+# The jumplist is pinned first, so ctrl-o comes back.
+key g
+key d
+stable 700 15000
+snap definition
+# SPC k is hover (K is keep_selections in helix and stays that way). The answer
+# is prose, not locations, so it opens +Hover and does NOT arm the n/N stepper.
+key space
+key k
+stable 700 15000
+snap hover
+# SPC s lists the file's own symbols. Several rows, so it opens a +Search
+# buffer — which IS the picker: n steps a row and looks it, exactly as the Find
+# and `/` results already do, with no picker code of its own.
+key space
+key s
+stable 700 15000
+snap symbols
+key n
+stable 700 15000
+snap step
+# gd on something with no declaration behind it (a keyword) answers nothing,
+# and nothing is a legal answer: no buffer opens, no error is rendered.
+press left 8 5
+release left 8 5
+key g
+key d
+stable 700 10000
+snap nothing