//! 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. //! //! Location answers render as `+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, formatting) rides the same buffer. Rename //! is the one mutating answer: it emits byte ranges through `edit`, and the core //! applies them atomically only while the source revision is still current. //! //! `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, /// Tab in insert mode, with a `.` immediately before the cursor. NOT an /// autocomplete popup — the seam returns locations, so this answers "what /// could go here, and where is each of those DEFINED": one row per /// candidate, pointing at its declaration, in the same `+Search` buffer /// `gr` fills. Nothing is inserted. completion, // 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 = "", }; /// How a row SPELLS a path: relative to `base` if it lives UNDER it, its full /// absolute self otherwise. /// /// `base` is `Req.root` — the directory of the file the query was asked about /// — which is also the directory the results buffer is opened in, so a row /// shortened here reads as the name that window would have typed and still /// resolves when looked. `gr` over one file was otherwise the same /// forty-character absolute prefix repeated down the whole pane, with the part /// you came to read pushed off the right edge. /// /// UNDER, not "shorter": a path outside that tree is left absolute rather than /// walked up to with `../`. An absolute path resolves from anywhere and says /// 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. pub fn rel(base: []const u8, path: []const u8) []const u8 { if (base.len == 0) return path; const home = std.mem.trimEnd(u8, base, "/"); if (path.len > home.len and std.mem.startsWith(u8, path, home) and path[home.len] == '/') return path[home.len + 1 ..]; return path; } /// 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). /// `path` has already been through `rel`: the caller holds the base. 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 {}; } /// Emit one half-open byte range for a mutating response. Rename is the only /// current user: every other answer remains human-readable rows. Byte offsets /// avoid converting the displayed 1-based locations back into source offsets /// in the core, and the prefix makes malformed or mixed responses fail closed. pub fn edit(out: *std.Io.Writer, start: usize, end: usize) void { out.print("@edit {d} {d}\n", .{ start, end }) 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 fn speaks(_: []const u8) bool { return false; } 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; /// 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 /// one key that must not be eaten when the answer is no: insert-mode Tab /// 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; /// Name shown by the harness and in `SPC ?`. Each implementation renames it. pub const backend_name = "zls-inproc";