summaryrefslogtreecommitdiff
path: root/src/term_pane.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/term_pane.zig')
-rw-r--r--src/term_pane.zig118
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