diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-01 09:23:53 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-01 11:24:12 -0300 |
| commit | ae9325a5cb128d0d952afb8f9feaaca68e5e37a2 (patch) | |
| tree | 9ae44ac38f7b71edfe2882d0a882dbc61304ec80 /src/output_pane.zig | |
| parent | 848ad99fa597387a85f75e752dc4c9e10f8c24f4 (diff) | |
| download | pardes-ae9325a5cb128d0d952afb8f9feaaca68e5e37a2.tar.gz pardes-ae9325a5cb128d0d952afb8f9feaaca68e5e37a2.zip | |
lsp: a protocol client for every other language, narrated on the message row
The seam grows a second backend: src/lsp/lsp_client.zig speaks JSON-RPC to
child language servers — rust-analyzer, clangd, gopls, tsserver, pyright are
rows in a spec table — while the in-process ZLS analyser keeps .zig. One
reader thread per server owns the socket, routes responses to a mailbox
under the conn mutex (monotonic condvar), answers server-to-client requests,
feeds the diagnostics store, and narrates $/progress and state changes
through a status sink both native shells post to the transient message row:
"rust-analyzer: cargo check 88% 955/1083" lands where a save narrates, with
the same clock. Chatty progress is throttled and deduplicated; settled
states always land, which is also what makes the goldens deterministic.
Nothing wedges and nothing healthy dies: waits are deadline-bounded, a
timeout cancels and returns no rows, three consecutive timeouts restart the
server ONLY while it is idle (an indexing server is narrating its own
excuse), spawn and handshake failures back off 10s to 2min, a crash shortly
after ready counts as a failure, and only a missing binary disables a spec.
PARDES_LSP_{RS,C,GO,TS,PY} override binaries; empty disables; the snapshot
harness pins RS to test/lspmock.zig and empties the rest.
Mutating answers really mutate now: the @put record beside rename @edit
carries per-range text, so = applies the formatter (both backends) and a
same-file WorkspaceEdit rename applies atomically, one undo step, narrated
("renamed 2 range(s)"); a multi-file rename previews as rows instead of
half-applying. Malformed responses fail closed: coordinates validated not
clamped, one bad TextEdit poisons the whole edit set, poison frames kill
the connection instead of buffering forever, decoded control bytes reject a
uri, hierarchy items too deep to reserialize are skipped.
Four kinds helix does not have, on SPC l: c/C incoming/outgoing calls (rows
are call sites), t/T super/subtypes. Pull diagnostics (3.17) preferred when
advertised. Help gains a language-keys footer for the motions no builtin
row could carry; lsp.rel and look.grep now share one path-shortening rule.
zig build lspprobe drives the seam from the CLI (comma-separated kinds share
one server); measured against a 1083-crate workspace warm: gd 26ms, gr 213
rows 165ms, incoming calls 212 sites 197ms, document symbols 670 rows 347ms.
docs/lsp.md tells the whole story; lsp-evaluation.md gets an addendum.
Diffstat (limited to 'src/output_pane.zig')
| -rw-r--r-- | src/output_pane.zig | 57 |
1 files changed, 46 insertions, 11 deletions
diff --git a/src/output_pane.zig b/src/output_pane.zig index 9021e479..1850fa92 100644 --- a/src/output_pane.zig +++ b/src/output_pane.zig @@ -106,14 +106,22 @@ pub fn traits(o: Origin) Traits { .hover => .{ .name = config.hover_buffer }, // prose: an action list, a diff, a report about the backend .code_action, .format, .status, .explain => .{ .name = config.lsp_buffer }, - // Rename responses are edits consumed before an output can open; - // the exhaustive table still records the otherwise-unused trait. - .rename => .{ .name = config.lsp_buffer }, + // Rename usually resolves to edit records the core consumes before + // any buffer opens; what RENDERS is the multi-file PREVIEW — one + // location row per would-be edit — which n/N step like any list. + .rename => .{ .name = config.search_buffer, .steps = true }, .definition, .declaration, .type_definition, .implementation, .references => .{ .name = config.search_buffer, .steps = true, .jumps = true, }, + // The hierarchy kinds behave like references: a list of places, + // and a lone answer (one caller, one subtype) is a jump. + .incoming_calls, .outgoing_calls, .supertypes, .subtypes => .{ + .name = config.search_buffer, + .steps = true, + .jumps = true, + }, // completion lists WHAT COULD GO HERE, one row per candidate's // definition. It does not jump on a single row where the gotos do: // a goto answers a question whose answer is a place, so landing @@ -397,8 +405,25 @@ fn openStepped(p: *Pardes, id: usize, from: Origin, content: []u8) !void { fn helpContent(gpa: std.mem.Allocator, prefix: []const u8) ![]u8 { const full_header = "pardes builtins, and how to run each:\nSPC and its keys, a chord, a button, the\ntopbar - or the name, executed anywhere.\n\n"; const group_header = "pardes builtins under SPC"; + // The LANGUAGE KEYS are the one part of the keymap Help would otherwise + // never show: they are motions and modes, not words, so no builtin row + // carries them — yet they are the keys a reader comes looking for. Full + // listing only; a mid-chord `SPC l` view stays a pure filter. + const language_footer = + "\nlanguage keys (motions, not words):\n" ++ + "gd gD gy gi gr goto: definition,\n" ++ + " declaration, type-def,\n" ++ + " implementation, refs\n" ++ + "]d [d ]D [D diagnostics: next,\n" ++ + " prev, last, first\n" ++ + "= format (applies, one\n" ++ + " undo step)\n" ++ + "Tab after a . completion, in insert\n" ++ + "C-left-click definition, by mouse\n" ++ + "SPC l ... hover, rename, symbols,\n" ++ + " calls, types: above\n"; var len: usize = if (prefix.len == 0) - full_header.len + full_header.len + language_footer.len else group_header.len + prefix.len * 2 + 2; for (pardes.builtin_rows) |row| { @@ -431,6 +456,10 @@ fn helpContent(gpa: std.mem.Allocator, prefix: []const u8) ![]u8 { content[at] = '\n'; at += 1; } + if (prefix.len == 0) { + @memcpy(content[at..][0..language_footer.len], language_footer); + at += language_footer.len; + } std.debug.assert(at == content.len); return content; } @@ -442,22 +471,28 @@ pub fn openHelp(p: *Pardes, id: usize, prefix: []const u8) !void { return openRead(p, id, .{ .cmd = .Help }, prefix, content); } -test "full Help renders every enabled builtin row" { +test "full Help renders every builtin row, then the language keys" { const content = try helpContent(std.testing.allocator, ""); defer std.testing.allocator.free(content); - const body = content[(std.mem.lastIndexOf(u8, content, "\n\n") orelse + // FIRST blank line: the end of the header (the footer opens with one too) + const body = content[(std.mem.indexOf(u8, content, "\n\n") orelse return error.MissingHelpHeader) + 2 ..]; var lines = std.mem.splitScalar(u8, body, '\n'); for (pardes.builtin_rows) |row| try std.testing.expectEqualStrings(row.line, lines.next() orelse return error.MissingBuiltinHelpRow); - // The renderer terminates every row with a newline, so only split's empty - // trailing field may remain. Any extra non-empty field is an unregistered - // Help row and any missing row already failed in the loop above. + // ...and after the last row, the language-keys section: the one part of + // the keymap no builtin row can carry, closing the page. try std.testing.expectEqualStrings("", lines.next() orelse - return error.MissingHelpTerminator); - try std.testing.expect(lines.next() == null); + return error.MissingLanguageKeys); + try std.testing.expectEqualStrings("language keys (motions, not words):", lines.next() orelse + return error.MissingLanguageKeys); + try std.testing.expect(std.mem.indexOf(u8, body, "\ngd gD gy gi gr goto: definition,\n") != null); + // the group view stays a pure filter: no footer under a prefix + const group = try helpContent(std.testing.allocator, "l"); + defer std.testing.allocator.free(group); + try std.testing.expect(std.mem.indexOf(u8, group, "language keys") == null); } /// The complete live Config report: the generated settings and the host facts |
