diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 16:12:29 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | 07a3f53993a27c886ccb3ed665acb9c8b34a2873 (patch) | |
| tree | 4a4cb5c3c21c7da4989c65a5db0ed01cbb0aa652 | |
| parent | cc66593d1432813c9af029ce3779ccba9ded8883 (diff) | |
| download | pardes-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.zig | 2 | ||||
| -rw-r--r-- | src/edit.zig | 13 | ||||
| -rw-r--r-- | src/pardes.zig | 3 | ||||
| -rw-r--r-- | test/panes.zig | 13 |
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(); |
