summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 16:12:29 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit07a3f53993a27c886ccb3ed665acb9c8b34a2873 (patch)
tree4a4cb5c3c21c7da4989c65a5db0ed01cbb0aa652
parentcc66593d1432813c9af029ce3779ccba9ded8883 (diff)
downloadpardes-07a3f53993a27c886ccb3ed665acb9c8b34a2873.tar.gz
pardes-07a3f53993a27c886ccb3ed665acb9c8b34a2873.zip
A tag's edited text stays alive until the next edit, so ms and md in a tag read it whole
setEditText freed the text a tag edit was handed once it had copied the tag's own part out of it, but every caller reads that text back to place the cursor, and surround goes on to edit it again: ms and md in a tag read freed memory. The same held for a terminal without an edit buffer and a text with no pane. The text is now kept until the next such edit (edit.retireEditText), which covers every caller at once; the callers the audit found reading it back are surround's two, the paste and replace paths, the number adjust, and the insert-mode Enter. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--src/Terminal.zig2
-rw-r--r--src/edit.zig13
-rw-r--r--src/pardes.zig3
-rw-r--r--test/panes.zig13
4 files changed, 28 insertions, 3 deletions
diff --git a/src/Terminal.zig b/src/Terminal.zig
index e3378007..4f94533d 100644
--- a/src/Terminal.zig
+++ b/src/Terminal.zig
@@ -1382,7 +1382,7 @@ pub fn editText(p: *Pardes, pane: *Pane, lo: i32, hi: i32, col: i32) ?EditText {
/// Consume a rewritten overlay, freeing the terminal edit text it replaces.
pub fn setEditText(p: *Pardes, pane: *Pane, new: []u8) void {
- const overlay = if (pane.ovl) |*value| value else return p.gpa.free(new);
+ const overlay = if (pane.ovl) |*value| value else return pardes.edit.retireEditText(p, new);
for (0..pane.sel.len) |button| {
pane.clearPointerSelection(button);
pane.sel[button].state = .none;
diff --git a/src/edit.zig b/src/edit.zig
index 8f6f09c6..3e12f169 100644
--- a/src/edit.zig
+++ b/src/edit.zig
@@ -463,7 +463,9 @@ pub fn setEditText(p: *Pardes, t: *Text, new: []u8) 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.
- defer p.gpa.free(new);
+ // The shown text stays alive until the next such edit, since every
+ // caller reads the new text back, as a body's is kept.
+ defer retireEditText(p, new);
const pane = t.pane() orelse return;
const prefix = tagline.pathPrefix(p.scratch.allocator(), pane) catch return;
if (!std.mem.startsWith(u8, new, prefix)) {
@@ -480,11 +482,18 @@ pub fn setEditText(p: *Pardes, t: *Text, new: []u8) void {
t.own = new;
return;
}
- const pane = t.pane() orelse return p.gpa.free(new);
+ const pane = t.pane() orelse return retireEditText(p, new);
if (pane.file) |*f| return panes.File.setContent(p, f, new);
panes.Terminal.setEditText(p, pane, new);
}
+/// Take a text an edit made that nothing keeps, holding it until the next
+/// one: the edit's caller still reads it to place the cursor.
+pub fn retireEditText(p: *Pardes, new: []u8) void {
+ if (p.retired_edit) |old| p.gpa.free(old);
+ p.retired_edit = new;
+}
+
pub const InsertAt = enum { at, append, line_start, line_end, open_below, open_above };
/// Files insert only inside existing rows and at grapheme boundaries.
diff --git a/src/pardes.zig b/src/pardes.zig
index e4645d70..c9d93979 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -4096,6 +4096,8 @@ pub const Pardes = struct {
in_len: usize = 0,
registers: Registers = .{},
+ /// an edited text nothing kept (a tag's shown text): see edit.retireEditText
+ retired_edit: ?[]u8 = null,
clip_pending: ?edit.ClipRequest = null,
/// the last serialized dump (gpa-owned), read by the write_dump effect
dump_out: ?[]u8 = null,
@@ -4267,6 +4269,7 @@ pub const Pardes = struct {
for (&p.col_tags) |*t| t.deinit(gpa);
p.global_tag.deinit(gpa);
p.registers.deinit(gpa);
+ if (p.retired_edit) |r| gpa.free(r);
if (p.dump_out) |d| gpa.free(d);
p.fallback.deinit();
if (p.last_dump) |d| gpa.free(d);
diff --git a/test/panes.zig b/test/panes.zig
index 4e98f449..0b9909ad 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -314,6 +314,19 @@ const TagNavigationTests = struct {
try std.testing.expect(pane.prompt == .none and pane.focus == .body);
}
+ test "ms and md edit a tag's own text, and the text the edit made is read back whole" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 20 });
+ defer p.deinit();
+ const pane = try p.setTestFile("body\n");
+ p.update(.tick);
+ key(p, ':'); // the tag, on `Save` the first time
+ for ("miwms(") |cp| key(p, cp);
+ try std.testing.expect(std.mem.indexOf(u8, pardes.tagline.curTail(pane), "(Save)") != null);
+ for ("md(") |cp| key(p, cp);
+ try std.testing.expect(std.mem.indexOf(u8, pardes.tagline.curTail(pane), " Save ") != null);
+ try std.testing.expect(std.mem.indexOf(u8, pardes.tagline.curTail(pane), "(") == null);
+ }
+
test "the keyboard reaches a tag's path, which it can select and yank but not edit" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 20 });
defer p.deinit();