diff options
Diffstat (limited to 'src/lsp.zig')
| -rw-r--r-- | src/lsp.zig | 141 |
1 files changed, 141 insertions, 0 deletions
diff --git a/src/lsp.zig b/src/lsp.zig new file mode 100644 index 00000000..62b5e1bb --- /dev/null +++ b/src/lsp.zig @@ -0,0 +1,141 @@ +//! The language-intelligence seam. +//! +//! The core never speaks a protocol and never blocks. It emits an `lsp` Effect +//! naming a Kind, a file and a byte offset; a shell runs `query` on a worker +//! and posts the answer back as an `lsp_resp` Event. That is the whole async +//! execution model — the same shape the pty readers already use, because a +//! language query is just another thing that answers later. +//! +//! Every backend renders into ONE format: `+Search` rows. A location is +//! `path:LINE:COL text`, which is what look.zig already resolves and what n/N +//! already steps, so a multi-result answer IS helix's picker and a single +//! result IS a jump — with no picker UI written for it. Free text (hover, +//! a rename's diff) rides the same buffer as plain lines. +//! +//! `query` is the ONLY thing an implementation supplies. Swapping backends is +//! swapping this one function, which is also how the three competing +//! implementations are measured against each other: same core, same harness, +//! same rows, different `query`. +const std = @import("std"); + +/// What the caller wants to know. The helix command each one backs is named +/// alongside, because the keymap is helix's and these are its verbs. +pub const Kind = enum { + /// gd + definition, + /// gD + declaration, + /// gy + type_definition, + /// gi + implementation, + /// gr + references, + /// SPC k + hover, + /// SPC s + document_symbols, + /// SPC S (arg = the query) + workspace_symbols, + /// SPC d, and the list that ]d / [d step + diagnostics, + /// SPC D + workspace_diagnostics, + /// SPC r (arg = the new name) + rename, + /// SPC a + code_action, + /// = + format, + /// SPC h + select_refs, + + /// Whether an answer of exactly one row should JUMP rather than open a + /// results buffer. Helix: the five gotos jump on a single location and + /// show a picker on several; a symbol list is always a picker. + pub fn jumpsWhenSingle(k: Kind) bool { + return switch (k) { + .definition, .declaration, .type_definition, .implementation, .references => true, + else => false, + }; + } + + /// The buffer an answer opens. Kept distinct from `+Search` only where the + /// content is not a list of locations — n/N over prose is nonsense. + pub fn bufferName(k: Kind) []const u8 { + return switch (k) { + .hover => "+Hover", + .code_action, .format, .rename => "+Lsp", + else => "+Search", + }; + } +}; + +/// One question. `source` is a snapshot of the buffer taken by the shell +/// before the worker starts — the core keeps editing while this is in flight, +/// so a backend must never reach back into core memory. +pub const Req = struct { + kind: Kind, + /// absolute path of the file the offset is in + path: []const u8, + /// the buffer's bytes, NUL-terminated (std.zig.Ast and zls both want a + /// sentinel, and every backend has to parse this same text) + source: [:0]const u8, + /// cursor position, a byte offset into `source` + offset: u32, + /// kind-specific argument: the new name for a rename, the query for + /// workspace symbols. Empty otherwise. + arg: []const u8 = "", + /// where the project starts — the directory of the pane that asked. A + /// backend that indexes more than one file walks from here. + root: []const u8 = "", +}; + +/// Emit one `path:LINE:COL text` row. Line and column are 1-based, the way +/// every other row in a `+Search` buffer is (and the way look.zig parses one). +pub fn row( + gpa: std.mem.Allocator, + out: *std.ArrayList(u8), + path: []const u8, + line: usize, + col: usize, + text: []const u8, +) void { + out.print(gpa, "{s}:{d}:{d} {s}\n", .{ + path, line + 1, col + 1, std.mem.trim(u8, text, " \t\r\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 } { + const upto = source[0..@min(offset, source.len)]; + const line = std.mem.count(u8, upto, "\n"); + const bol = if (std.mem.lastIndexOfScalar(u8, upto, '\n')) |i| i + 1 else 0; + return .{ .line = line, .col = upto.len - bol }; +} + +/// Answer `req`, appending rows to `out`. Runs on a worker thread with no +/// access to the core: everything it may read is in `req`. +/// +/// `arena` is freed wholesale when the query returns; `gpa` owns only what +/// goes into `out`. Errors are not reported — a backend that cannot answer +/// appends nothing, and the core treats "no rows" as "no result", which is +/// also what a language server that is still starting up looks like. +pub fn query(gpa: std.mem.Allocator, arena: std.mem.Allocator, req: Req, out: *std.ArrayList(u8)) void { + // ponytail: the base tree has no backend on purpose — this is the seam the + // competing implementations fill in, and an empty answer is a legal one. + _ = gpa; + _ = arena; + _ = req; + _ = out; +} + +/// 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) = .initEmpty(); + +/// Name shown by the harness and in `SPC ?`. Each implementation renames it. +pub const backend_name = "none"; |
