diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-31 00:52:02 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:08 -0300 |
| commit | a77e342eb7fa4add700a96a661226649e8c4763f (patch) | |
| tree | 3ad4978cca350090463e58840dc94404ea8a1a33 /src/pardes.zig | |
| parent | ea228d1b832401db8efe33a5db4099ba41d42509 (diff) | |
| download | pardes-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/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 103 |
1 files changed, 62 insertions, 41 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index 5412569b..01026347 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -600,12 +600,13 @@ pub const FileSnap = struct { pub const File = struct { path: []u8, content: []u8, - /// an OUTPUT buffer (acme's +Errors): a file pane with no file behind it, - /// where the core shows text it produced itself (search results). The ONLY - /// difference is that it cannot be saved — every mode, motion, chord and - /// look works because it IS a file pane. `path` still names it (and gives - /// its directory) so looks inside it resolve like anywhere else. - output: bool = false, + /// set = this is an OUTPUT buffer (acme's +Errors): a file pane with no + /// file behind it, showing text the core produced itself. It records the + /// COMMAND that opened it, and output_pane.zig's one table turns that into + /// every way it differs from a file — ask it, never this field's shape. + /// `path` still names it (and gives its directory) so looks inside it + /// resolve like anywhere else. + output: ?output_pane.Output = null, scroll: usize = 0, /// one syntax.Syn byte per content byte in the highlighted window highlights: []u8 = &.{}, @@ -678,15 +679,13 @@ pub const Pane = struct { /// (submit/cancel restore it, and the marker itself says WHICH search this /// is). Enter fills the output buffer in pane search_pane with one result /// per row; n/N walk it, search_row being the row they last landed on - /// (null = fresh results, nothing stepped to yet). + /// (null = fresh results, nothing stepped to yet). WHICH command filled + /// that buffer is the BUFFER's own record, never a second field here — + /// output_pane.resultsFrom is how `]d` asks. search_edit: bool = false, search_at: u16 = 0, search_pane: ?usize = null, search_row: ?usize = null, - /// which language query filled search_pane, or null when a plain `/`, - /// Find or Grep did. Read only by `]d`/`[d`, which step the diagnostics - /// list when it is already up and ask for one when it is not. - search_kind: ?lsp.Kind = null, /// the editable tag tail: seeded with the default builtins on first touch; /// edited in place by a modal one-line editor sharing the pane's mode tag_tail: std.ArrayList(u8) = .empty, @@ -928,8 +927,11 @@ pub const Pardes = struct { /// this one" — the id bump makes the older answer stale and lspResponse /// drops it. A queue would only buy the right to render an answer nobody /// is waiting for any more. + /// `arg` rides along only so the buffer the answer opens can record what + /// was asked (a rename's new name, a symbol query) — the query itself has + /// it in the effect already. lsp_seq: u32 = 0, - lsp_wait: ?struct { id: u32, kind: lsp.Kind, pane: usize } = null, + lsp_wait: ?struct { id: u32, kind: lsp.Kind, pane: usize, arg: Effect.Buf(128) } = null, /// pending effects, drained by the shell after each update. Growable: one /// update can emit unbounded writes (a big selection executed is chunked @@ -1217,10 +1219,10 @@ pub const Pardes = struct { } /// the editable tail: the user's edited buffer once touched, else defaults - /// (an output buffer has no file to Save, so it gets the plain tail) + /// (a buffer with nothing to Save gets the plain tail — the table decides) fn curTail(pane: *Pane) []const u8 { if (pane.tag_init) return pane.tag_tail.items; - if (pane.file) |f| if (!f.output) return file_pane_tail; + if (pane.file) |f| if (output_pane.fileTraits(f.output).saves) return file_pane_tail; return pane_tail; } @@ -2252,11 +2254,11 @@ pub const Pardes = struct { // Esc: exit select mode / drop pending prefixes + count. The // selection itself SURVIVES (helix; `;` collapses it) — motions - // replace it anyway. On an OUTPUT BUFFER (+Search, +Help) there is no - // file behind the pane and nothing worth keeping, so "never mind" - // means dismiss it: Esc IS Del, the same builtin its tag offers. + // replace it anyway. A buffer the table marks `dismiss` (every output + // buffer today) instead takes Esc as "never mind" and closes: Esc IS + // Del, the same builtin its tag offers. if (hit(key, config.escape)) { - if (pane.file) |f| if (f.output) return p.runBuiltin(.Del, p.active, "", null); + if (pane.file) |f| if (output_pane.fileTraits(f.output).dismiss) return p.runBuiltin(.Del, p.active, "", null); pane.select = false; pane.pending = 0; pane.pending2 = 0; @@ -2419,11 +2421,11 @@ pub const Pardes = struct { // n/N stepper — `d` differs from `n` only in what it guarantees is // showing. ]D / [D jump to the last/first, helix's pair. if (hit(key, config.goto_diagnostic)) { - if (pane.search_kind == .diagnostics and p.searchStep(p.active, if (fwd) 1 else -1)) return; + if (output_pane.resultsFrom(p, pane, .{ .query = .diagnostics }) and p.searchStep(p.active, if (fwd) 1 else -1)) return; return p.lspRequest(p.active, .diagnostics, ""); } if (hit(key, config.goto_diagnostic_end)) { - if (pane.search_kind != .diagnostics) return p.lspRequest(p.active, .diagnostics, ""); + if (!output_pane.resultsFrom(p, pane, .{ .query = .diagnostics })) return p.lspRequest(p.active, .diagnostics, ""); if (!fwd) { // [D = first pane.search_row = null; _ = p.searchStep(p.active, 1); @@ -2706,7 +2708,7 @@ pub const Pardes = struct { } else if (kind == .find) look.find(arena, dir, pat, &out) else { const pl = p.paneCursorLines(pane) catch return; // a real file names itself; a terminal or an output buffer has no path - const has_path = if (pane.file) |f| !f.output else false; + const has_path = if (pane.file) |f| f.output == null else false; var idbuf: [16]u8 = undefined; // relative to `dir`, like Find's rows — and `dir` IS this file's // own directory, so that is exactly its basename @@ -2732,7 +2734,14 @@ pub const Pardes = struct { p.gpa.free(content); return; }; - const np = output_pane.open(p, free, dir, config.search_buffer, content) catch { + // the buffer records WHICH search filled it, pattern and all: Find and + // Grep are builtins (words you can execute), the bare `/` is a key + const from: output_pane.Origin = switch (kind) { + .text => .search, + .find => .{ .cmd = .Find }, + .grep => .{ .cmd = .Grep }, + }; + const np = output_pane.open(p, free, dir, from, pat, content) catch { p.gpa.free(content); return; }; @@ -2740,7 +2749,6 @@ pub const Pardes = struct { p.active = id; pane.search_pane = free; pane.search_row = null; - pane.search_kind = null; } /// n/N: step to the next/previous row of this pane's results buffer and @@ -2753,8 +2761,9 @@ pub const Pardes = struct { const rid = pane.search_pane orelse return false; const rp = p.panes[rid] orelse return false; const rf = if (rp.file) |*f| f else return false; - // a freed slot can be reused by an unrelated pane: the name says ours - if (!rf.output or !std.mem.endsWith(u8, rf.path, "/" ++ config.search_buffer)) return false; + // one question covers both hazards: a freed slot can be reused by an + // unrelated pane, and a buffer of PROSE has nowhere to step to + if (!output_pane.fileTraits(rf.output).steps) return false; // fresh results: n starts at the first row, N has nothing behind it const nrows: i64 = @intCast(std.mem.count(u8, rf.content, "\n")); const step: i64 = if (pane.search_row) |c| @as(i64, @intCast(c)) + delta else if (delta > 0) 0 else -1; @@ -2795,7 +2804,7 @@ pub const Pardes = struct { // program's output and an output buffer is our own text. if (kind != .status) { const f = pane.file orelse return; - if (f.output) return; + if (f.output != null) return; } if (arg.len > 128) return; // the effect's arg is a Buf(128) const off = if (pane.file) |f| modal.hxOff(f.content, .{ @@ -2803,7 +2812,7 @@ pub const Pardes = struct { .col = @intCast(@max(0, pane.cur_col)), }) else 0; p.lsp_seq +%= 1; - p.lsp_wait = .{ .id = p.lsp_seq, .kind = kind, .pane = id }; + p.lsp_wait = .{ .id = p.lsp_seq, .kind = kind, .pane = id, .arg = .from(arg) }; p.emit(.{ .lsp = .{ .id = p.lsp_seq, .kind = kind, @@ -2826,8 +2835,9 @@ pub const Pardes = struct { const pane = p.panes[w.pane] orelse return; if (rows.len == 0) return; + const from: output_pane.Origin = .{ .query = w.kind }; const nrows = std.mem.count(u8, rows, "\n"); - if (w.kind.jumpsWhenSingle() and nrows == 1) { + if (output_pane.traits(from).jumps and nrows == 1) { const ln = std.mem.trimEnd(u8, rows, "\n"); var hi: usize = 0; while (hi < ln.len and config.isFileChar(ln[hi])) hi += 1; @@ -2845,18 +2855,18 @@ pub const Pardes = struct { p.gpa.free(content); return; }; - const np = output_pane.open(p, free, dir, w.kind.bufferName(), content) catch { + const np = output_pane.open(p, free, dir, from, w.arg.slice(), content) catch { p.gpa.free(content); return; }; p.placeDoc(w.pane, free, np); p.active = w.pane; // prose is not a list of locations: n/N over a hover blurb would step - // to nowhere, so only location buffers arm the stepper. - if (std.mem.eql(u8, w.kind.bufferName(), config.search_buffer)) { + // to nowhere, so only stepping buffers arm the stepper — and WHICH + // query filled it is now the buffer's own record, not a field here. + if (output_pane.traits(from).steps) { pane.search_pane = free; pane.search_row = null; - pane.search_kind = w.kind; } } @@ -4677,7 +4687,7 @@ pub const Pardes = struct { var k = f.idx; while (k > 0) : (k -= 1) { sib = p.col_terms[f.col][k - 1]; - if (p.panes[sib]) |pp| if (if (pp.file) |ff| !ff.output else true) break; + if (p.panes[sib]) |pp| if (if (pp.file) |ff| output_pane.fileTraits(ff.output).doc else true) break; } if (p.panes[sib]) |s| s.vweight += @as(f32, @floatFromInt(@max(1, p.rects[id].h))); } @@ -4722,10 +4732,10 @@ pub const Pardes = struct { }; src.vweight = @floatFromInt(BOX_H + keep); nw.vweight = @floatFromInt(@max(1, src_h -| (BOX_H + keep))); - // an output buffer is worth exactly its own text: a three-hit +Search + // a non-doc buffer is worth exactly its own text: a three-hit +Search // is four rows, not half the source. Nothing else can want the rows, // so they go straight back to the pane they were taken from. - if (nw.file) |f| if (f.output) { + if (nw.file) |f| if (!output_pane.fileTraits(f.output).doc) { // trimmed: every row ends in a newline, and the empty line after // the last one is not a result const want: f32 = @floatFromInt(BOX_H + file_pane.lineCount(std.mem.trimEnd(u8, f.content, "\n"))); @@ -5069,13 +5079,13 @@ pub const Pardes = struct { /// right) — nothing is displaced, the other columns just narrow. A full /// column bar, or a result list whose source died, stacks into the leftmost. pub fn placeDoc(p: *Pardes, from_id: usize, free: usize, nt: *Pane) void { - const doc = if (nt.file) |f| !f.output else true; // an image is a doc + const doc = if (nt.file) |f| output_pane.fileTraits(f.output).doc else true; // an image is a doc var src_id: ?usize = null; - if (p.panes[from_id]) |src| if (if (src.file) |f| !f.output else src.image != null) { + if (p.panes[from_id]) |src| if (if (src.file) |f| output_pane.fileTraits(f.output).doc else src.image != null) { src_id = from_id; }; if (doc and src_id == null) for (p.panes, 0..) |sl, i| { - if (sl) |pp| if (i != free and (if (pp.file) |f| !f.output else pp.image != null)) { + if (sl) |pp| if (i != free and (if (pp.file) |f| output_pane.fileTraits(f.output).doc else pp.image != null)) { src_id = i; break; }; @@ -5118,7 +5128,10 @@ pub const Pardes = struct { const tag = try p.tagText(arena, pane); const body = try p.bodyText(arena, pane); const scroll: usize = @intCast(@max(0, pane.scroll())); - const dp: dump.Pane = if (pane.file) |f| .{ + // BY POINTER: `origin_arg` is a slice into the File's own inline + // buffer, and a by-value capture would leave it pointing at a + // stack copy that dies before the ZON is written. + const dp: dump.Pane = if (pane.file) |*f| .{ .kind = .file, .tag = tag, .body = body, @@ -5130,7 +5143,8 @@ pub const Pardes = struct { .path = f.path, .content = f.content, .content_b64 = try dump.encodeBytes(arena, f.content), - .output = f.output, + .origin = if (f.output) |o| output_pane.word(o.from) else "", + .origin_arg = if (f.output) |*o| o.arg() else "", }, } else if (pane.image) |iv| blk: { const bytes = look.readFile(arena, iv.path) catch ""; @@ -5276,7 +5290,14 @@ pub const Pardes = struct { const path = try gpa.dupe(u8, f.path); errdefer gpa.free(path); const pane = try p.newDocPane(i); - pane.file = .{ .path = path, .content = content, .output = f.output, .scroll = src.scroll }; + // an unknown origin word is a dump from another build, not + // a reason to refuse the load: it comes back as a file + const out: ?output_pane.Output = if (output_pane.fromWord(f.origin)) |o| blk: { + var ov: output_pane.Output = .{ .from = o }; + output_pane.setArg(&ov, f.origin_arg); + break :blk ov; + } else null; + pane.file = .{ .path = path, .content = content, .output = out, .scroll = src.scroll }; pane.cur_pinned = true; pane.cur_row = @intCast(src.scroll); pane.cols = @max(1, src.cols); |
