summaryrefslogtreecommitdiff
path: root/src/output_pane.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-31 00:52:02 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:08 -0300
commita77e342eb7fa4add700a96a661226649e8c4763f (patch)
tree3ad4978cca350090463e58840dc94404ea8a1a33 /src/output_pane.zig
parentea228d1b832401db8efe33a5db4099ba41d42509 (diff)
downloadpardes-a77e342eb7fa4add700a96a661226649e8c4763f.tar.gz
pardes-a77e342eb7fa4add700a96a661226649e8c4763f.zip
output panes remember the command that opened them
An output buffer was a file pane with a flag, and everything special about it was re-derived from outside by asking the wrong question: the buffer NAME ("+Search", "+Help"), a parallel search_kind field, and f.output predicates sprinkled through kind-agnostic code. Now it carries its Origin — the Builtin, or the lsp.Kind for the five gotos helix binds as motions rather than words — plus that command's argument, and one Traits table answers every question the scattered code used to ask: what it is called, whether n/N steps it, whether it is a doc for layout, whether Esc dismisses it, whether it saves. The name is derived FROM the command instead of being the thing that identifies it. Adding a kind of output pane is adding a row. No golden moved.
Diffstat (limited to 'src/output_pane.zig')
-rw-r--r--src/output_pane.zig195
1 files changed, 180 insertions, 15 deletions
diff --git a/src/output_pane.zig b/src/output_pane.zig
index 64bfc630..45eb0931 100644
--- a/src/output_pane.zig
+++ b/src/output_pane.zig
@@ -1,28 +1,189 @@
//! 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). The ONLY thing that makes one different from a file pane is that
-//! it cannot be saved — every mode, motion, chord and look works because it IS
-//! a file pane, and that reuse is the point.
+//! buffers). It IS a file pane — every mode, motion, chord and look works for
+//! free — and that reuse is the point.
//!
-//! Thin on purpose, for now. The `f.output` tests still scattered through
-//! pardes.zig each ask a one-line question inside kind-agnostic code (does
-//! this pane claim a column? can it be saved? is it a doc?) and pulling them
-//! here would trade one line for a call. What belongs here is what an output
-//! pane KNOWS about itself, and today that is just how one is opened.
+//! 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");
-/// `content` is gpa-owned and adopted. The name carries `dir` so looks inside
-/// the buffer resolve like anywhere else.
-pub fn open(p: *Pardes, id: usize, dir: []const u8, name: []const u8, content: []u8) !*Pane {
- const path = try std.fmt.allocPrint(p.gpa, "{s}/{s}", .{ std.mem.trimEnd(u8, dir, "/"), name });
+/// 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);
- pane.file = .{ .path = path, .content = content, .output = true };
+ var out: Output = .{ .from = from };
+ setArg(&out, arg);
+ pane.file = .{ .path = path, .content = content, .output = out };
pane.cur_pinned = true;
return pane;
}
@@ -49,11 +210,15 @@ pub fn openHelp(p: *Pardes, id: usize, prefix: []const u8) void {
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;
- if (!hf.output or !std.mem.endsWith(u8, hf.path, "/" ++ config.help_buffer)) 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;
@@ -65,7 +230,7 @@ pub fn openHelp(p: *Pardes, id: usize, prefix: []const u8) void {
p.gpa.free(content);
return;
};
- const np = open(p, free, dir, config.help_buffer, content) catch {
+ const np = open(p, free, dir, .{ .cmd = .Help }, prefix, content) catch {
p.gpa.free(content);
return;
};