diff options
Diffstat (limited to 'src/term_pane.zig')
| -rw-r--r-- | src/term_pane.zig | 118 |
1 files changed, 109 insertions, 9 deletions
diff --git a/src/term_pane.zig b/src/term_pane.zig index 5e50e15d..9f13832f 100644 --- a/src/term_pane.zig +++ b/src/term_pane.zig @@ -19,34 +19,134 @@ const EditSnap = pardes.EditSnap; const Ovl = pardes.Ovl; const modal = @import("modal.zig"); +/// The memo behind `shellRows`. ONE entry for the editor, because the motion +/// surface is built for the pane the cursor is in and a second pane asking +/// would only double a multi-megabyte buffer for a slot it is about to lose +/// again. gpa-owned rather than scratch-arena: the whole point is to outlive +/// the update that built it. +/// +/// LIFETIME, the part that would rot silently: `rows` is handed out to +/// callers, so the buffers are freed in exactly two places — `sweep`, at the +/// TOP of an update before any handler can be holding them, and `reset` when +/// the editor goes away. Everything that notices the entry has gone bad +/// (output arrived, the grid reflowed, the pane died, another pane wants the +/// slot) only marks it `stale`; nothing frees mid-update. That is the same +/// guarantee the scratch arena gave, spelled out. +pub const RowsCache = struct { + /// whose grid this describes; null = the slot is free + pane: ?*const Pane = null, + /// the rows joined by '\n' — `flatSurface` hands this back verbatim + /// instead of rebuilding the join on every keystroke + text: []const u8 = &.{}, + /// slices INTO `text`, absolute grid rows from 0 + rows: [][]const u8 = &.{}, + /// `text` is a prefix of this: blanking a prompt row shortens the join, + /// and the slack is not worth a second allocation to reclaim + text_alloc: []u8 = &.{}, + stale: bool = false, + + pub fn reset(c: *RowsCache, gpa: std.mem.Allocator) void { + if (c.text_alloc.len > 0) gpa.free(c.text_alloc); + if (c.rows.len > 0) gpa.free(c.rows); + c.* = .{}; + } + + /// Free a stale entry. Called at the top of `update`, and nowhere else. + pub fn sweep(c: *RowsCache, gpa: std.mem.Allocator) void { + if (c.stale) c.reset(gpa); + } + + /// `pane`'s grid moved: the entry no longer describes it. + pub fn markStale(c: *RowsCache, pane: *const Pane) void { + if (c.pane == pane) c.stale = true; + } + + /// `pane` is being destroyed. Drop the pointer now — a freed pane's + /// address can come back from the allocator as a different pane, and an + /// entry still naming it would answer for the wrong grid — but leave the + /// buffers to the next sweep, as ever. + pub fn dropPane(c: *RowsCache, pane: *const Pane) void { + if (c.pane != pane) return; + c.pane = null; + c.stale = true; + } +}; + +const Rows = struct { + text_alloc: []u8, + text: []const u8, + rows: [][]const u8, +}; + /// A terminal's shell rows as the surface sees them: the WHOLE /// history+active grid, prompt rows blanked (OSC 133), absolute grid rows /// from 0. The raw material the motion surface is composed from — the /// edit buffer is NOT applied here, so it is also what seeding the buffer -/// reads. Scratch-arena backed. +/// reads. /// ghostty's dump trims the grid's trailing blank rows; ONE of them is /// kept back, the row the cursor sits on below the last line of output. /// That row is a file's final newline: without it the surface would have /// one line fewer than the same text in a document, and every motion and /// linewise edit at the bottom would diverge. -/// ponytail: O(scrollback) dump+scan per keystroke — window it around the -/// viewport if a multi-MB scrollback ever makes navigation lag. +/// +/// Building it is O(scrollback) — a dump of the whole history — and a +/// keystroke asks for it once or twice, so the result is memoized against the +/// pane until its grid changes. A pane sitting on 16 MiB of agent transcript +/// paid that dump per press of `j` before the cache; now it pays it once per +/// chunk of output. pub fn shellRows(p: *Pardes, pane: *Pane) ![]const []const u8 { - const arena = p.scratch.allocator(); - const full = try pane.vt.screens.active.dumpStringAlloc(arena, .{ .screen = .{} }); + const c = &p.shell_rows; + if (!c.stale and c.pane == pane) return c.rows; + if (c.pane != null) { + // Another pane holds the slot. Take it for the NEXT update (the sweep + // frees what is there) and answer this one from scratch: whoever owns + // the live entry may still be holding the rows it handed out. + c.stale = true; + return (try buildRows(p.scratch.allocator(), p, pane)).rows; + } + const built = try buildRows(p.gpa, p, pane); + c.* = .{ + .pane = pane, + .text = built.text, + .rows = built.rows, + .text_alloc = built.text_alloc, + }; + return c.rows; +} + +fn buildRows(alloc: std.mem.Allocator, p: *Pardes, pane: *Pane) !Rows { + const full = try pane.vt.screens.active.dumpStringAlloc(p.scratch.allocator(), .{ .screen = .{} }); var pit = pane.vt.screens.active.pages.rowIterator(.right_down, .{ .screen = .{} }, null); // split yields one more item than delimiters; the extra final slot is the // cursor row retained below. - const rows = try arena.alloc([]const u8, std.mem.count(u8, full, "\n") + 2); + const n_rows = std.mem.count(u8, full, "\n") + 2; + const rows = try alloc.alloc([]const u8, n_rows); + errdefer alloc.free(rows); + // Blanking a prompt row only ever SHORTENS it and the retained cursor row + // adds one separator, so the dump's length plus one bounds the join. + const text = try alloc.alloc(u8, full.len + 1); + errdefer alloc.free(text); + var at: usize = 0; var n: usize = 0; var it = std.mem.splitScalar(u8, full, '\n'); while (it.next()) |raw| { const is_prompt = if (pit.next()) |pin| pin.rowAndCell().row.semantic_prompt != .none else false; - rows[n] = if (is_prompt) "" else raw; + const shown = if (is_prompt) "" else raw; + if (n > 0) { + text[at] = '\n'; + at += 1; + } + @memcpy(text[at..][0..shown.len], shown); + rows[n] = text[at..][0..shown.len]; + at += shown.len; n += 1; } - rows[n] = ""; - return rows; + text[at] = '\n'; + at += 1; + rows[n] = text[at..][0..0]; + n += 1; + std.debug.assert(n == n_rows); + return .{ .text_alloc = text, .text = text[0..at], .rows = rows }; } /// The body a terminal renders: the viewport's shell rows (prompt rows blanked |
