summaryrefslogtreecommitdiff
path: root/src/lsp/lsp.zig
blob: b8356724ced93cc24f25e6d1ad2746f62cecaa26 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
//! 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";