summaryrefslogtreecommitdiff
path: root/src/lsp/lsp.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/lsp/lsp.zig')
-rw-r--r--src/lsp/lsp.zig106
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";