diff options
Diffstat (limited to 'src/lsp/lsp.zig')
| -rw-r--r-- | src/lsp/lsp.zig | 106 |
1 files changed, 86 insertions, 20 deletions
diff --git a/src/lsp/lsp.zig b/src/lsp/lsp.zig index 255a8200..0b4f2ba1 100644 --- a/src/lsp/lsp.zig +++ b/src/lsp/lsp.zig @@ -62,6 +62,21 @@ pub const Kind = enum { /// `gr` fills. Nothing is inserted. completion, + // The two-step hierarchy kinds, LSP 3.16/3.17: prepare at the cursor, + // then walk the item the server handed back. helix has none of these + // four (checked against helix-term/src/keymap/default.rs, which stops at + // the gotos), so they are pardes exceeding parity rather than matching + // it — possible here because the answers are LOCATIONS, and locations + // are the one thing this seam renders for free. + /// SPC l c — who calls the function under the cursor + incoming_calls, + /// SPC l C — everything the function under the cursor calls + outgoing_calls, + /// SPC l t — the types this one extends/implements + supertypes, + /// SPC l T — the types that extend/implement this one + subtypes, + // The two introspection kinds. A backend that answers nothing is // indistinguishable from a backend that is broken, so these exist to tell // those apart — they are the only Kinds whose answer is ABOUT the backend @@ -114,8 +129,8 @@ pub const Req = struct { /// where it is; `../../..` says neither, and the moment the row is read /// somewhere other than beside its own buffer it is wrong. /// -/// This is `look.grep`'s `shown` rule, spelled a second time — see the note -/// there; the two want to become one function. +/// This is also `look.grep`'s `shown` rule — it calls this function, so the +/// two spellings the docs used to complain about are one. pub fn rel(base: []const u8, path: []const u8) []const u8 { if (base.len == 0) return path; const home = std.mem.trimEnd(u8, base, "/"); @@ -173,6 +188,27 @@ pub fn edit(out: *std.Io.Writer, start: usize, end: usize) void { out.print("@edit {d} {d}\n", .{ start, end }) catch {}; } +/// The general mutating record: a half-open byte range REPLACED BY `text`, +/// which `@edit` cannot say (its replacement is the request's own arg, the +/// same for every range). Rename through a protocol server and `=` both need +/// per-range text, so this carries it — percent-encoded onto the one line a +/// record is allowed to be, because a TextEdit's newText is full of newlines +/// and the record stream is parsed line by line. The core decodes with +/// `parseLspEdits` and applies all records in one undo transaction; malformed, +/// overlapping or out-of-bounds records change nothing, exactly as for @edit. +pub fn put(out: *std.Io.Writer, start: usize, end: usize, text: []const u8) void { + out.print("@put {d} {d} ", .{ start, end }) catch {}; + for (text) |c| { + // '%' so the encoding round-trips; control bytes so the record stays + // one line; ' ' so the text is one token. Everything else is itself. + if (c == '%' or c == ' ' or c < 0x21) + out.print("%{X:0>2}", .{c}) catch {} + else + out.writeByte(c) catch {}; + } + out.writeByte('\n') catch {}; +} + /// Byte offset -> (line, column), both 0-based. Every backend needs it to turn /// an AST token into a row, so it lives here rather than three times over. pub fn lineCol(source: []const u8, offset: usize) struct { line: usize, col: usize } { @@ -195,29 +231,45 @@ pub fn lineCol(source: []const u8, offset: usize) struct { line: usize, col: usi /// answer writes nothing, and the core treats "no rows" as "no result", which /// is also what a language server still starting up looks like. pub fn query(gpa: std.mem.Allocator, arena: std.mem.Allocator, req: Req, out: *std.Io.Writer) void { - backend.query(gpa, arena, req, out); + // `status` is about the BACKENDS, plural: every one reports, in seam + // order, so `SPC l i` shows the analyser and the protocol client side by + // side and a machine with neither prints nothing at all. + if (req.kind == .status) { + inline for (backends) |b| b.query(gpa, arena, req, out); + return; + } + inline for (backends) |b| { + if (b.speaks(req.path) and b.supports.contains(req.kind)) + return b.query(gpa, arena, req, out); + } + // Nobody spoke the file. `explain` exists precisely to narrate a refusal, + // so it still goes to the first backend, whose trace says WHY it stopped + // ("not a .zig file", "no server for .md") instead of silently no-rowing. + if (req.kind == .explain and backends.len > 0) + backends[0].query(gpa, arena, req, out); } -/// ZLS, linked in as a module and called directly — no subprocess, no -/// JSON-RPC. See `lsp_zls.zig`. The web shell cannot build ZLS (no threads, -/// freestanding wasm), so there it is the empty backend the base tree shipped -/// with — and empty means SILENT rather than failing: `supports` below is then -/// an empty set, `lspRequest` returns on it before emitting, and the effect -/// never leaves the core. `web.zig`'s prong for it is waiting for a host that -/// links a backend, not dead weight for one that never will. -const backend = if (@import("pardes_config").zls_backend) @import("lsp_zls.zig") else struct { - pub fn query(_: std.mem.Allocator, _: std.mem.Allocator, _: Req, _: *std.Io.Writer) void {} - pub fn speaks(_: []const u8) bool { - return false; - } - pub const supports: std.EnumSet(Kind) = .initEmpty(); -}; +/// The compiled-in backends, asked in order; the first one that speaks the +/// file's language AND claims the kind answers. Two on a native build — ZLS +/// linked as a module for Zig (no process, cold is warm), and a real LSP +/// client (lsp_client.zig) speaking JSON-RPC to child servers for everything +/// else: rust-analyzer, clangd, gopls, whatever the spec table names. A +/// FREESTANDING core (web, esp32) compiles in neither: `supports` is then +/// empty, `lspRequest` returns before it emits, and the effect never exists. +const backends = if (@import("pardes_config").zls_backend) + .{ @import("lsp_zls.zig"), @import("lsp_client.zig") } +else + .{}; /// What this backend can actually answer, for the evaluation harness and for /// the core (a Kind that is not supported never leaves the keymap). An /// implementation narrows this to what it really does — claiming a feature it /// does not have shows up immediately in the harness's matrix. -pub const supports: std.EnumSet(Kind) = backend.supports; +pub const supports: std.EnumSet(Kind) = blk: { + var s: std.EnumSet(Kind) = .initEmpty(); + for (0..backends.len) |i| s.setUnion(backends[i].supports); + break :blk s; +}; /// Does the backend read this file's LANGUAGE at all? `supports` answers what /// a backend can do; this answers what it can do it TO, and it exists for the @@ -225,9 +277,23 @@ pub const supports: std.EnumSet(Kind) = backend.supports; /// diverts to `completion` after a `.`, so in a README — or in any pane the /// backend would refuse — it has to indent instead. The core asks rather than /// knowing, so the list of extensions stays the backend's business. -pub const speaks = backend.speaks; +pub fn speaks(path: []const u8) bool { + inline for (backends) |b| if (b.speaks(path)) return true; + return false; +} + +/// Where a shell registers the one function unsolicited SERVER STATE goes +/// through: "rust-analyzer indexing 3/120", "gopls exited". Called from the +/// client's reader threads, so a sink must be thread-safe and must copy +/// `text` before returning; both native shells post it to their event queue +/// and let the loop hand it to `Pardes.setMessage` — the same transient row a +/// save narrates into, because a server starting up is exactly that kind of +/// news. A build with no client accepts and ignores the registration. +pub fn setStatusSink(ctx: ?*anyopaque, cb: ?*const fn (ctx: ?*anyopaque, text: []const u8) void) void { + if (@import("pardes_config").zls_backend) backends[1].setStatusSink(ctx, cb); +} /// Name shown by the harness and in `SPC ?`. This is the SEAM's, not the /// backend's: a backend does not declare it, so renaming a backend means /// editing this line. -pub const backend_name = "zls-inproc"; +pub const backend_name = if (backends.len > 1) "zls-inproc+lsp-client" else "zls-inproc"; |
