summaryrefslogtreecommitdiff
path: root/src/output_pane.zig
blob: 45eb093156ac4108d041df9cbbdc1e9107968201 (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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
//! Output panes: acme's +Errors, a file pane with no file behind it, holding
//! text the core produced itself (+Search results, +Help, the LSP answer
//! buffers). It IS a file pane — every mode, motion, chord and look works for
//! free — and that reuse is the point.
//!
//! What it is NOT any more is a file pane with a bool on it. An output buffer
//! remembers THE COMMAND THAT OPENED IT (`Origin`), and every special case it
//! gets is one `traits` lookup on that field. Before this, a dozen places
//! re-derived what a pane was from the outside, each asking a different wrong
//! question: `endsWith(path, "+Search")` (the NAME decided what a pane WAS,
//! which is backwards — a name is a consequence), a `search_kind` field on the
//! pane that ran the search rather than on the buffer that answered it, and
//! `f.output` booleans sprinkled through kind-agnostic layout code. Now the
//! buffer knows, and the table below is the whole answer: one screen you read
//! top to bottom to see every way an output pane differs from a file, and one
//! row to add to introduce another kind.
const std = @import("std");
const pardes = @import("pardes.zig");
const Pardes = pardes.Pardes;
const Pane = pardes.Pane;
const File = pardes.File;
const file_pane = @import("file_pane.zig");
const builtins = @import("builtins.zig");
const Builtin = builtins.Builtin();
const config = @import("config.zig");
const lsp = @import("lsp/lsp.zig");

/// What opened this buffer — THE field, and the only input to `traits`.
///
/// Two vocabularies because the core has exactly two: a `Builtin` is a word
/// you can execute anywhere, and that covers Find, Grep, Help and every
/// language query that has a name (Hover, Diagnostics...). The rest are KEYS —
/// helix binds the five gotos and `=` as motions and `/` as a search input,
/// and a motion has no word to click. Recording the key's `lsp.Kind` (or
/// `.search` for the bare `/`) is not a parallel tag enum: both are the values
/// the caller already holds when it opens the buffer.
pub const Origin = union(enum) {
    cmd: Builtin,
    query: lsp.Kind,
    /// the bare `/` — the pane's own text, searched in core
    search,
};

/// The cap on a recorded argument, matching the one lspRequest already puts on
/// an effect payload. ponytail: a longer pattern is TRUNCATED here, because
/// `arg` is a record of what was asked and never the text anything re-runs; if
/// something ever re-runs it, this becomes gpa-owned like `content`.
pub const max_arg = 128;

/// An output buffer's own state, hung off `File.output`.
pub const Output = struct {
    from: Origin,
    /// the command's ARGUMENT: the pattern a Grep matched, the new name a
    /// Rename took, the SPC prefix a Help lists. Inline rather than allocated
    /// — it is a fixed-size record, and an owned slice would buy a free in
    /// deinitPane and an errdefer at every open site for nothing.
    arg_buf: [max_arg]u8 = undefined,
    arg_len: u8 = 0,

    pub fn arg(o: *const Output) []const u8 {
        return o.arg_buf[0..o.arg_len];
    }
};

pub fn setArg(o: *Output, text: []const u8) void {
    o.arg_len = @intCast(@min(text.len, max_arg));
    @memcpy(o.arg_buf[0..o.arg_len], text[0..o.arg_len]);
}

/// Every way an output pane differs from a file pane. One struct so the
/// QUESTIONS are visible even where today every buffer answers alike: a column
/// that never varies still says "this is decided here", which is what makes
/// the next kind of buffer a row rather than a hunt.
pub const Traits = struct {
    /// the buffer's name (`+Search`...). DERIVED from the command, never the
    /// thing that identifies it — that inversion is what this file undoes.
    name: []const u8,
    /// n/N walk the rows: each is a `path:LINE:COL text` location the ordinary
    /// look path resolves, so the buffer IS helix's picker. Prose (a hover
    /// blurb, a rename diff) has nowhere to step to.
    steps: bool = false,
    /// an answer of exactly ONE row jumps straight there instead of opening
    /// this buffer at all — helix: the gotos jump on a single location and
    /// show a picker on several, a symbol list is always a picker.
    jumps: bool = false,
    /// a DOCUMENT for layout purposes: claims a column of its own, is a split
    /// parent, pays for a split. A result list is not — it belongs to the pane
    /// that asked for it, lands directly below it and takes its rows from
    /// there, so opening or closing one never resizes a bystander.
    doc: bool = false,
    /// Esc dismisses it: there is no file behind the pane and nothing worth
    /// keeping, so "never mind" means close it — Esc IS the Del its tag offers.
    dismiss: bool = true,
    /// there is a file behind it to write. Also what its tag says: no Save to
    /// offer means the plain pane tail rather than the file one.
    saves: bool = false,
};

/// A REAL file pane, as a row of the same table — so kind-agnostic code asks
/// one question and gets one answer whichever it is holding. `name` is unused:
/// a file already has a path.
const file_row: Traits = .{ .name = "", .doc = true, .dismiss = false, .saves = true };

/// THE TABLE. Everything above, answered from the command that opened the
/// buffer. Exhaustive on purpose: a new `lsp.Kind` or a new output-opening
/// builtin should not compile until someone has said what its buffer does.
pub fn traits(o: Origin) Traits {
    return switch (o) {
        // rows are `location text`, so n/N walk them
        .search => .{ .name = config.search_buffer, .steps = true },
        .cmd => |b| switch (b) {
            .Help => .{ .name = config.help_buffer },
            // Find (file names) and Grep (file contents) both list locations;
            // no other builtin opens a buffer, and the day one does it lands
            // here rather than in a call site.
            else => .{ .name = config.search_buffer, .steps = true },
        },
        .query => |k| switch (k) {
            .hover => .{ .name = config.hover_buffer },
            // prose: an action list, a diff, a report about the backend
            .code_action, .format, .rename, .status, .explain => .{ .name = config.lsp_buffer },
            .definition, .declaration, .type_definition, .implementation, .references => .{
                .name = config.search_buffer,
                .steps = true,
                .jumps = true,
            },
            .document_symbols, .workspace_symbols, .diagnostics, .workspace_diagnostics, .select_refs => .{
                .name = config.search_buffer,
                .steps = true,
            },
        },
    };
}

/// The same table asked of a file pane's `output` field, null (a real file)
/// included. This is what the kind-agnostic code in pardes.zig calls.
pub fn fileTraits(out: ?Output) Traits {
    return traits((out orelse return file_row).from);
}

/// How the dump spells an origin. A WORD, never an integer, for the reason the
/// dump already stores tag words: reordering builtins.zig stays free. Nothing
/// collides — a builtin is CamelCase, an lsp.Kind is snake_case, and `/` is
/// neither.
pub fn word(o: Origin) []const u8 {
    return switch (o) {
        .cmd => |b| @tagName(b),
        .query => |k| @tagName(k),
        .search => "/",
    };
}

/// the inverse; null for "" (a real file) and for a word this build no longer
/// has, which is a dump from another version and not a reason to fail a load
pub fn fromWord(w: []const u8) ?Origin {
    if (w.len == 0) return null;
    if (std.mem.eql(u8, w, "/")) return .search;
    if (std.meta.stringToEnum(Builtin, w)) |b| return .{ .cmd = b };
    if (std.meta.stringToEnum(lsp.Kind, w)) |k| return .{ .query = k };
    return null;
}

/// Is the results buffer `pane`'s n/N is armed on the one `from` filled?
/// `]d`/`[d` are the only keys that care WHICH search is showing — they step
/// the diagnostics list when it is up and ask for one when it is not — and
/// this is how they ask now that the buffer remembers: `search_pane` is a
/// SLOT, so this doubles as the check that the slot is still ours.
pub fn resultsFrom(p: *Pardes, pane: *Pane, from: Origin) bool {
    const rp = p.panes[pane.search_pane orelse return false] orelse return false;
    const f = rp.file orelse return false;
    const o = f.output orelse return false;
    return std.meta.eql(o.from, from);
}

/// Open one. `content` is gpa-owned and adopted; the NAME comes from the table
/// (the caller says what ran, not what to call it) and carries `dir` so looks
/// inside the buffer resolve like anywhere else.
pub fn open(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []const u8, content: []u8) !*Pane {
    const path = try std.fmt.allocPrint(p.gpa, "{s}/{s}", .{
        std.mem.trimEnd(u8, dir, "/"), traits(from).name,
    });
    errdefer p.gpa.free(path);
    const pane = try p.newDocPane(id);
    var out: Output = .{ .from = from };
    setArg(&out, arg);
    pane.file = .{ .path = path, .content = content, .output = out };
    pane.cur_pinned = true;
    return pane;
}

/// The Help builtin: every builtin reachable from `prefix` (empty = all
/// of them) with the keys that run it, written into an output buffer
/// (acme's +Errors) — ordinary text, so the names in it are LIVE: middle-
/// click `Tutor` there and the tutor opens. Reuses the open +Help buffer
/// instead of piling panes up, and focus follows: you asked to read it.
pub fn openHelp(p: *Pardes, id: usize, prefix: []const u8) void {
    const pane = p.panes[id] orelse return;
    const arena = p.scratch.allocator();
    var out: std.ArrayList(u8) = .empty;
    if (prefix.len == 0) {
        out.appendSlice(arena, "pardes builtins - SPC then the keys,\nor execute the name anywhere\n\n") catch return;
    } else {
        out.appendSlice(arena, "pardes builtins under SPC") catch return;
        for (prefix) |c| out.appendSlice(arena, &[_]u8{ ' ', c }) catch return;
        out.appendSlice(arena, "\n\n") catch return;
    }
    for (pardes.leader_rows) |row| {
        if (!std.mem.startsWith(u8, row.path, prefix)) continue;
        out.appendSlice(arena, row.line) catch return;
        out.append(arena, '\n') catch return;
    }
    const content = p.gpa.dupe(u8, out.items) catch return;
    // the buffer says what made it, so finding the open one is asking that and
    // not matching its name
    for (p.panes, 0..) |slot, i| {
        const hp = slot orelse continue;
        const hf = if (hp.file) |*f| f else continue;
        const ho = if (hf.output) |*o| o else continue;
        if (!std.meta.eql(ho.from, Origin{ .cmd = .Help })) continue;
        file_pane.setContent(p, hf, content);
        setArg(ho, prefix);
        hf.scroll = 0;
        hp.cur_row = 0;
        hp.msel.active = false;
        p.active = i;
        return;
    }
    const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice();
    const free = p.freeSlot() orelse {
        p.gpa.free(content);
        return;
    };
    const np = open(p, free, dir, .{ .cmd = .Help }, prefix, content) catch {
        p.gpa.free(content);
        return;
    };
    p.placeDoc(id, free, np);
}