//! 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` — or `path:LINE:COL-ENDCOL text` where the protocol //! answered with a real range, which a look then SELECTS — and that 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, // 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 // rather than about the code. /// SPC l i — configuration, capabilities and the recent-query log status, /// SPC l w — why the query at the cursor answers what it does. Narrates /// the REAL resolution path rather than re-deriving it, so it cannot drift /// away from what `gd` actually did. explain, // What an ANSWER becomes — which buffer it opens, whether a single row // jumps instead, whether n/N walk it — is not here: it is one row per Kind // in output_pane.traits, beside the same questions asked of `/`, Find, // Grep and Help. A Kind added above will not compile until it has one. }; /// 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( out: *std.Io.Writer, path: []const u8, line: usize, col: usize, text: []const u8, ) void { out.print("{s}:{d}:{d} {s}\n", .{ path, line + 1, col + 1, std.mem.trim(u8, text, " \t\r\n"), }) catch {}; } /// The same row for a protocol RANGE: `path:LINE:COL-ENDCOL`, which a look /// SELECTS rather than parking on its first cell — so `gd` lands on the whole /// name and a references list steps symbol by symbol with each one highlighted /// (config.range_sep spells the dash; `-` is written out here for the same /// reason `:` is). /// /// `end_col` is the protocol's own EXCLUSIVE end character, which is already /// the 1-based inclusive column pardes wants, so the conversion is the absence /// of one. A span that is empty or crosses lines falls back to the point row: /// the only multi-line ranges here are whole declarations, and a goto onto one /// wants the cursor at its name, not its body painted. pub fn spanRow( out: *std.Io.Writer, path: []const u8, line: usize, col: usize, end_line: usize, end_col: usize, text: []const u8, ) void { if (end_line != line or end_col <= col) return row(out, path, line, col, text); out.print("{s}:{d}:{d}-{d} {s}\n", .{ path, line + 1, col + 1, end_col, 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`, writing rows to `out`. Runs on a worker thread with no /// access to the core: everything it may read is in `req`. /// /// `out` is a plain `std.Io.Writer` — the shell owns the buffer behind it (an /// `Io.Writer.Allocating`), so a backend never allocates the result, never /// frees it, and cannot get the allocator wrong. Write failures are the /// writer's problem; a backend may ignore them. /// /// `arena` is freed wholesale when the query returns; `gpa` is for a backend's /// own longer-lived scratch. Errors are not reported — a backend that cannot /// 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); } /// ZLS, linked in as a module and called directly — no subprocess, no /// JSON-RPC. See `lsp_zls.zig`. The web shell has no threads, never emits the /// effect, and cannot build ZLS anyway, so there it is the empty backend the /// base tree shipped with. 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 const supports: std.EnumSet(Kind) = .initEmpty(); }; /// 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; /// Name shown by the harness and in `SPC ?`. Each implementation renames it. pub const backend_name = "zls-inproc";