summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig38
1 files changed, 37 insertions, 1 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index e9cc746d..f95b4e69 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -4484,6 +4484,9 @@ pub const Pardes = struct {
surface: Surface = .{},
/// per-update scratch (paneCursorLines, selection text); reset each update
scratch: std.heap.ArenaAllocator,
+ /// the terminal motion surface, memoized against the pane it was built
+ /// for — see term_pane.RowsCache for the lifetime rule
+ shell_rows: term_pane.RowsCache = .{},
pub fn init(gpa: std.mem.Allocator, opts: Options) !*Pardes {
const image_gpa = opts.image_allocator orelse gpa;
@@ -4563,6 +4566,7 @@ pub const Pardes = struct {
if (p.last_dump) |d| gpa.free(d);
if (p.chord_arg) |a| gpa.free(a);
if (p.pipe_wait) |*wait| wait.deinit(gpa);
+ p.shell_rows.reset(gpa);
p.scratch.deinit();
gpa.free(p.surface.cells);
if (p.surface.images.len > 0) gpa.free(p.surface.images);
@@ -4594,6 +4598,7 @@ pub const Pardes = struct {
pub fn deinitPane(p: *Pardes, pane: *Pane) void {
std.debug.assert(pane.payloadValid());
+ p.shell_rows.dropPane(pane);
if (pane.image) |*iv| {
p.image_gpa.free(iv.path);
if (iv.rgba.len > 0) p.image_gpa.free(iv.rgba);
@@ -4808,6 +4813,10 @@ pub const Pardes = struct {
}
pub fn update(p: *Pardes, ev: Event) void {
+ // Free a motion surface that went bad during the LAST update, before
+ // anything in this one can ask for it. Nothing frees it mid-update:
+ // handlers hand `rows` around for the length of a single update.
+ p.shell_rows.sweep(p.gpa);
// A transient message is exactly as old as your last input: touch the
// keyboard or the mouse and it is gone, on every pane, because a
// message is a report and you have just proved you are back. Only
@@ -4862,6 +4871,7 @@ pub const Pardes = struct {
const pane = p.panes[o.pane] orelse return;
pane.appendTtyReplay(o.bytes);
pane.stream.nextSlice(o.bytes);
+ p.shell_rows.markStale(pane);
// follow new output to the bottom only in tty mode; in
// normal/insert the user is reading scrollback and the view
// holds still while output streams in. Tag edits hijack
@@ -6048,14 +6058,23 @@ pub const Pardes = struct {
return .{ .lines = ls, .row0 = 0 };
}
const rows = try term_pane.shellRows(p, pane);
+ // No edit buffer: the loop below would copy `rows` into a second array
+ // one slice at a time and return exactly what it was given. On a
+ // scrollback of any size that copy is the keystroke.
+ if (pane.ovl == null) return .{ .lines = rows, .row0 = 0 };
// the buffer may sit below the dump's last row (the grid's trailing
// blanks are trimmed off), so run the loop to whichever ends later
var last = rows.len;
if (pane.ovl) |o| last = @max(last, @as(usize, @intCast(@max(0, o.row + o.rows))));
var count = last;
if (pane.ovl) |o| {
+ // The split below writes ONE line for an empty buffer, but
+ // modal.lineCount("") is 0 — the same floor `n - 1` needs at the
+ // paste site. Without it, killing a whole line (`A<C-u>`, `d%`)
+ // sizes `ls` one short of what the loop writes, and on a buffer
+ // covering the last row that is a length of zero.
if (o.row >= 0 and @as(usize, @intCast(o.row)) < last)
- count = count - @min(@as(usize, @intCast(o.rows)), last - @as(usize, @intCast(o.row))) + modal.lineCount(o.text);
+ count = count - @min(@as(usize, @intCast(o.rows)), last - @as(usize, @intCast(o.row))) + @max(1, modal.lineCount(o.text));
}
const ls = try arena.alloc([]const u8, count);
var n: usize = 0;
@@ -6097,6 +6116,13 @@ pub const Pardes = struct {
fn flatSurface(p: *Pardes, pane: *Pane, pl: PaneLines) ![]const u8 {
if (pane.file) |f| return f.content;
if (hasPdf(pane)) return p.ensurePdfText(pane);
+ // The memo already holds these rows joined by '\n' — that is what its
+ // `text` IS. The identity check is the same argument hxOff makes below:
+ // `lines` came out of the cache untouched (no edit buffer stood in for
+ // any row), so the join cannot have drifted from it.
+ const c = &p.shell_rows;
+ if (!c.stale and c.pane == pane and
+ pl.lines.ptr == c.rows.ptr and pl.lines.len == c.rows.len) return c.text;
const arena = p.scratch.allocator();
var total: usize = if (pl.lines.len > 0) pl.lines.len - 1 else 0;
for (pl.lines) |ln| total += ln.len;
@@ -6127,6 +6153,15 @@ pub const Pardes = struct {
/// differential and 561 parity cases say so; the arithmetic below is
/// hxOff/hxPos transcribed, with line i's terminator read off idx[i+1]-1
/// instead of searched for.
+ ///
+ /// ponytail: a terminal's motion surface is NOT one of the things with no
+ /// index any more — shell_rows.rows are slices into shell_rows.text, so
+ /// their offsets ARE the line starts, for free. Claiming them here needs
+ /// `p`, which means threading it through paneOff/panePos/paneLineStart and
+ /// their callers (paneRange, setPaneRange, the three motion helpers). Left
+ /// undone deliberately: measured, that is a terminal key-down of 615us
+ /// against 140us, and 0.6ms is not a keystroke anybody feels. Do it the
+ /// day one of those scans shows up next to something that matters.
fn indexOf(pane: *Pane, text: []const u8) ?[]const usize {
const f = if (pane.file) |*ff| ff else return null;
if (text.ptr != f.content.ptr or text.len != f.content.len) return null;
@@ -12563,6 +12598,7 @@ pub const Pardes = struct {
// the size so bodyText renders the right number of rows
if (pane.isTerminal()) {
pane.vt.resize(p.gpa, .{ .cols = cols, .rows = rows }) catch {};
+ p.shell_rows.markStale(pane); // reflow moved every row
p.emit(.{ .resize_pty = .{ .pane = @intCast(id), .cols = cols, .rows = rows } });
}
pane.cols = cols;