summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 13:08:11 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commit34cddaea7db5081743fca928e6e50842e23cc5db (patch)
treedecec6842c1b644d39646cf0a7afdfc210e36a58
parent4e224b7c7e67fafcbe7359fcf534048e254b701d (diff)
downloadpardes-34cddaea7db5081743fca928e6e50842e23cc5db.tar.gz
pardes-34cddaea7db5081743fca928e6e50842e23cc5db.zip
Typing tells g. where its edit ended, so no edit scans the whole file twice to find it
setEditText found the last edit's end by comparing the old and new texts from both ends and counting rows to it, twice over the file on every keystroke: edit-char grew with the file (+135% at 300k lines). Typing, deleting, Enter and Tab now pass the end they know (setEditTextEnd); the comparison stays for the edits that do not say (paste, pipes, s). perf edit-char: 50k lines 16661 -> 15286 us, 300k lines 30507 -> 22291 us (render alone ~13.6 ms of each, the render agent's). Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--src/edit.zig72
-rw-r--r--test/perf-baseline-gui-Debug.json2
-rw-r--r--test/perf-baseline-tty-Debug.json2
-rw-r--r--test/perf-baseline-tty-ReleaseFast.json2
4 files changed, 59 insertions, 19 deletions
diff --git a/src/edit.zig b/src/edit.zig
index a194ef48..183d6325 100644
--- a/src/edit.zig
+++ b/src/edit.zig
@@ -487,6 +487,19 @@ fn editTextEol(p: *Pardes, t: *Text, b: Bounds) ?panes.EditText {
/// install a rewritten editable text (frees the old one)
pub fn setEditText(p: *Pardes, t: *Text, new: []u8) void {
+ setEditTextEnd(p, t, new, null);
+}
+
+/// Where text `s` put in at `c` ends: `g.`'s place after typing it.
+fn insertedEnd(c: modal.Cursor, s: []const u8) modal.Cursor {
+ const nl = std.mem.lastIndexOfScalar(u8, s, '\n') orelse return .{ .row = c.row, .col = c.col + s.len };
+ return .{ .row = c.row + std.mem.count(u8, s, "\n"), .col = s.len - nl - 1 };
+}
+
+/// `setEditText` with where the edit ended in `new` (in the edit text's
+/// rows) when the caller knows it, as typing does: then nothing scans the
+/// whole text to find it. Null, it is found by comparing the texts.
+pub fn setEditTextEnd(p: *Pardes, t: *Text, new: []u8, known_end: ?modal.Cursor) void {
if (t.what == .tag) {
// A tag is edited as it is shown, but only what follows its
// prefix is its own: an edit that changed the prefix is refused.
@@ -512,10 +525,14 @@ pub fn setEditText(p: *Pardes, t: *Text, new: []u8) void {
const pane = t.pane() orelse return retireEditText(p, new);
// what changed ended where the texts agree again from their ends
const old: []const u8, const row0: i32 = if (pane.file) |f| .{ f.content, 0 } else if (pane.ovl) |o| .{ o.text, o.row } else .{ "", 0 };
- const front = std.mem.indexOfDiff(u8, old, new) orelse @min(old.len, new.len);
- var back: usize = 0;
- while (back < @min(old.len, new.len) - front and old[old.len - 1 - back] == new[new.len - 1 - back]) back += 1;
- const end = modal.positionAt(new, new.len - back);
+ const end = known_end orelse found: {
+ // ponytail: O(file), for the edits that do not say where they ended
+ // (paste, a pipe, s): typing passes its end and never comes here.
+ const front = std.mem.indexOfDiff(u8, old, new) orelse @min(old.len, new.len);
+ var back: usize = 0;
+ while (back < @min(old.len, new.len) - front and old[old.len - 1 - back] == new[new.len - 1 - back]) back += 1;
+ break :found modal.positionAt(new, new.len - back);
+ };
t.last_edit = .{ .row = @as(i32, @intCast(end.row)) + row0, .col = @intCast(end.col) };
if (pane.file) |*f| return panes.File.setContent(p, f, new);
panes.Terminal.setEditText(p, pane, new);
@@ -813,7 +830,7 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
const c = modal.Cursor{ .row = @intCast(@max(0, t.cur_row - eb.row0)), .col = @intCast(@max(0, t.cur_col)) };
if (key.text.len > 0 and !key.ctrl and !key.alt) {
const new = modal.insertAt(p.gpa, text, c, key.text) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, insertedEnd(c, key.text));
for (key.text) |ch| {
if (ch == '\n') {
t.cur_row += 1;
@@ -837,7 +854,7 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
const from = @min(wr.anchor, wr.head);
const fc = modal.positionAt(e2.text, from);
const new = modal.deleteSpan(p.gpa, e2.text, fc, c2) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, fc);
t.cur_row = @as(i32, @intCast(fc.row)) + e2.row0;
t.cur_col = @intCast(fc.col);
t.cur_pinned = true;
@@ -855,13 +872,13 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
const to = @max(wr.anchor, wr.head);
if (to <= g) return;
const new = modal.deleteSpan(p.gpa, e2.text, c2, modal.positionAt(e2.text, to)) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, c2);
t.cur_pinned = true;
return;
}
if (hit(key, config.kill_to_line_start)) {
const new = modal.deleteSpan(p.gpa, text, .{ .row = c.row, .col = 0 }, c) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, .{ .row = c.row, .col = 0 });
t.cur_col = 0;
t.cur_pinned = true;
t.show();
@@ -870,7 +887,7 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
if (hit(key, config.kill_to_line_end)) {
const llen = modal.lineSlice(text, c.row).len;
const new = modal.deleteSpan(p.gpa, text, c, .{ .row = c.row, .col = llen }) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, c);
t.cur_pinned = true;
return;
}
@@ -883,7 +900,7 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
// cursor go and the new line takes the indent
if (std.mem.indexOfNone(u8, before, " \t") == null) {
const new = modal.insertAt(p.gpa, text, .{ .row = c.row, .col = 0 }, "\n") catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, .{ .row = c.row + 1, .col = 0 });
t.cur_row += 1;
t.cur_pinned = true;
t.show();
@@ -900,7 +917,7 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
const cut = modal.deleteSpan(p.gpa, text, .{ .row = c.row, .col = kept }, c) catch return;
defer p.gpa.free(cut);
const new = modal.insertAt(p.gpa, cut, .{ .row = c.row, .col = kept }, block) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, .{ .row = c.row + 1, .col = indent });
t.cur_row += 1;
t.cur_col = @intCast(indent);
t.cur_pinned = true;
@@ -922,7 +939,7 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
while (drop > 0 and prev > 0 and line[prev - 1] == ' ') : (drop -= 1) prev -= 1;
}
const new = modal.deleteSpan(p.gpa, text, .{ .row = c.row, .col = prev }, c) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, .{ .row = c.row, .col = prev });
t.cur_col = @intCast(prev);
t.cur_pinned = true;
t.show();
@@ -936,7 +953,7 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
if (row == 0) return;
const prevlen = modal.lineSlice(e2.text, row - 1).len;
const new = modal.deleteSpan(p.gpa, e2.text, .{ .row = row - 1, .col = prevlen }, .{ .row = row, .col = 0 }) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, .{ .row = row - 1, .col = prevlen });
t.cur_row -= 1;
t.cur_col = @intCast(prevlen);
t.cur_pinned = true;
@@ -948,13 +965,13 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
const line = modal.lineSlice(text, c.row);
if (c.col < line.len) {
const new = modal.deleteChar(p.gpa, text, c) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, c);
} else {
const e2 = editText(p, t, t.cur_row, t.cur_row + 1, t.cur_col) orelse return;
const row: usize = @intCast(@max(0, t.cur_row - e2.row0));
if (row + 1 >= modal.lineCount(e2.text)) return;
const new = modal.deleteSpan(p.gpa, e2.text, .{ .row = row, .col = c.col }, .{ .row = row + 1, .col = 0 }) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, .{ .row = row, .col = c.col });
}
t.cur_pinned = true;
},
@@ -993,7 +1010,7 @@ pub fn insertTab(p: *Pardes, t: *Text) void {
};
const pad = modal.INDENT_W - (c.col % modal.INDENT_W);
const new = modal.insertAt(p.gpa, eb.text, c, " "[0..pad]) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, .{ .row = c.row, .col = c.col + pad });
t.cur_col += @intCast(pad);
t.cur_pinned = true;
t.show();
@@ -2365,3 +2382,26 @@ test "double-click selects as acme's does" {
const e = doubleClickRange(lines, 7);
try std.testing.expectEqualStrings("two\n", lines[e[0]..e[1]]);
}
+
+test "a typed edit's known end is where the texts' comparison would find it" {
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 20, .tty_only = true });
+ defer p.deinit();
+ const pane = try p.setTestFile("one\ntwo three\nfour\n");
+ const t = &pane.body;
+ const cases = [_]struct { at: modal.Cursor, s: []const u8 }{
+ .{ .at = .{ .row = 1, .col = 3 }, .s = "xy" },
+ .{ .at = .{ .row = 0, .col = 0 }, .s = "a\nbc" },
+ .{ .at = .{ .row = 2, .col = 4 }, .s = "\n" },
+ };
+ for (cases) |c| {
+ const base = try p.gpa.dupe(u8, pane.file.?.content);
+ defer p.gpa.free(base);
+ const scanned = try modal.insertAt(p.gpa, base, c.at, c.s);
+ setEditTextEnd(p, t, scanned, null);
+ const by_scan = t.last_edit.?;
+ const known = try modal.insertAt(p.gpa, base, c.at, c.s);
+ setEditTextEnd(p, t, known, insertedEnd(c.at, c.s));
+ try std.testing.expectEqual(by_scan.row, t.last_edit.?.row);
+ try std.testing.expectEqual(by_scan.col, t.last_edit.?.col);
+ }
+}
diff --git a/test/perf-baseline-gui-Debug.json b/test/perf-baseline-gui-Debug.json
index fd64ebf9..6bfe2c90 100644
--- a/test/perf-baseline-gui-Debug.json
+++ b/test/perf-baseline-gui-Debug.json
@@ -1 +1 @@
-{"harness":"67f85bf9692f1778","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"Debug","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseSafe","platform":"gui","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":1768,"med_us":2025,"p90_us":2122,"max_us":2125},{"op":"render","fixture":"medium","min_us":72,"med_us":75,"p90_us":76,"max_us":83},{"op":"key-down","fixture":"medium","min_us":73,"med_us":76,"p90_us":80,"max_us":84},{"op":"key-right","fixture":"medium","min_us":74,"med_us":77,"p90_us":81,"max_us":84},{"op":"page-down","fixture":"medium","min_us":73,"med_us":74,"p90_us":77,"max_us":79},{"op":"wheel","fixture":"medium","min_us":71,"med_us":75,"p90_us":81,"max_us":81},{"op":"edit-char","fixture":"medium","min_us":2413,"med_us":2674,"p90_us":2943,"max_us":3111}]}
+{"harness":"67f85bf9692f1778","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"Debug","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseSafe","platform":"gui","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":1887,"med_us":2147,"p90_us":2277,"max_us":2326},{"op":"render","fixture":"medium","min_us":73,"med_us":76,"p90_us":84,"max_us":96},{"op":"key-down","fixture":"medium","min_us":73,"med_us":81,"p90_us":84,"max_us":86},{"op":"key-right","fixture":"medium","min_us":73,"med_us":77,"p90_us":91,"max_us":173},{"op":"page-down","fixture":"medium","min_us":75,"med_us":78,"p90_us":80,"max_us":84},{"op":"wheel","fixture":"medium","min_us":72,"med_us":76,"p90_us":79,"max_us":83},{"op":"edit-char","fixture":"medium","min_us":1269,"med_us":1437,"p90_us":1680,"max_us":1697}]}
diff --git a/test/perf-baseline-tty-Debug.json b/test/perf-baseline-tty-Debug.json
index 682ea028..6ffd034c 100644
--- a/test/perf-baseline-tty-Debug.json
+++ b/test/perf-baseline-tty-Debug.json
@@ -1 +1 @@
-{"harness":"67f85bf9692f1778","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"Debug","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseSafe","platform":"tty","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":1944,"med_us":2130,"p90_us":2232,"max_us":2394},{"op":"render","fixture":"medium","min_us":71,"med_us":75,"p90_us":76,"max_us":91},{"op":"key-down","fixture":"medium","min_us":72,"med_us":79,"p90_us":93,"max_us":102},{"op":"key-right","fixture":"medium","min_us":73,"med_us":76,"p90_us":78,"max_us":81},{"op":"page-down","fixture":"medium","min_us":72,"med_us":73,"p90_us":76,"max_us":76},{"op":"wheel","fixture":"medium","min_us":71,"med_us":85,"p90_us":96,"max_us":102},{"op":"edit-char","fixture":"medium","min_us":2612,"med_us":3112,"p90_us":3238,"max_us":3618}]}
+{"harness":"67f85bf9692f1778","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"Debug","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseSafe","platform":"tty","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":1907,"med_us":2037,"p90_us":2283,"max_us":2371},{"op":"render","fixture":"medium","min_us":72,"med_us":80,"p90_us":87,"max_us":87},{"op":"key-down","fixture":"medium","min_us":73,"med_us":78,"p90_us":82,"max_us":88},{"op":"key-right","fixture":"medium","min_us":72,"med_us":75,"p90_us":87,"max_us":87},{"op":"page-down","fixture":"medium","min_us":73,"med_us":81,"p90_us":93,"max_us":94},{"op":"wheel","fixture":"medium","min_us":73,"med_us":75,"p90_us":84,"max_us":88},{"op":"edit-char","fixture":"medium","min_us":1333,"med_us":1712,"p90_us":1956,"max_us":2636}]}
diff --git a/test/perf-baseline-tty-ReleaseFast.json b/test/perf-baseline-tty-ReleaseFast.json
index 4fd96d92..5d68e6b2 100644
--- a/test/perf-baseline-tty-ReleaseFast.json
+++ b/test/perf-baseline-tty-ReleaseFast.json
@@ -1 +1 @@
-{"harness":"67f85bf9692f1778","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"ReleaseFast","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseFast","platform":"tty","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":1971,"med_us":2259,"p90_us":2308,"max_us":2339},{"op":"render","fixture":"medium","min_us":71,"med_us":75,"p90_us":78,"max_us":79},{"op":"key-down","fixture":"medium","min_us":73,"med_us":77,"p90_us":79,"max_us":95},{"op":"key-right","fixture":"medium","min_us":72,"med_us":77,"p90_us":80,"max_us":88},{"op":"page-down","fixture":"medium","min_us":72,"med_us":76,"p90_us":81,"max_us":83},{"op":"wheel","fixture":"medium","min_us":71,"med_us":75,"p90_us":80,"max_us":91},{"op":"edit-char","fixture":"medium","min_us":2662,"med_us":2999,"p90_us":3375,"max_us":3622}]}
+{"harness":"67f85bf9692f1778","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"ReleaseFast","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseFast","platform":"tty","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":1771,"med_us":2022,"p90_us":2188,"max_us":2228},{"op":"render","fixture":"medium","min_us":75,"med_us":76,"p90_us":76,"max_us":86},{"op":"key-down","fixture":"medium","min_us":73,"med_us":79,"p90_us":80,"max_us":80},{"op":"key-right","fixture":"medium","min_us":72,"med_us":78,"p90_us":81,"max_us":82},{"op":"page-down","fixture":"medium","min_us":72,"med_us":76,"p90_us":80,"max_us":84},{"op":"wheel","fixture":"medium","min_us":75,"med_us":76,"p90_us":78,"max_us":79},{"op":"edit-char","fixture":"medium","min_us":1106,"med_us":1436,"p90_us":1692,"max_us":1721}]}