//! 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); }