From b726f10d03f3c1141d99bf20e99a982026da785f Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 30 Sep 2026 21:11:49 -0300 Subject: A shell's > into an empty pane's body is one undo step, as into any other The truncation of an empty body is no edit and pushes no step, but it still marked the open's writes as joining the step it would have pushed, so the text written after it had no undo step at all. A truncation that changed nothing (of body, data or xdata) now leaves the step to the write that follows. Co-Authored-By: Claude Opus 5.5 --- src/ninep/pane.zig | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index d276b771..14877f0f 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -304,6 +304,21 @@ pub fn spliceBody(p: *Pardes, pane: *Pane, q0: usize, q1: usize, bytes: []const return take; } +test "a shell's > into an empty pane's body is one undo step, as into any other" { + const p = try withFile(testing.allocator, ""); + defer p.deinit(); + const pane = p.panes[0].?; + const f = &pane.file.?; + const body = Node.of(pane.serial, .body); + const steps = f.history.undo_len; + const h = call(p, .{ .tag = 1, .op = .open, .node = body, .omode = 1 }).reply.handle; + _ = call(p, .{ .tag = 2, .op = .setattr, .node = body, .truncate = true }); + try testing.expectEqual(tree.Status.ok, call(p, .{ .tag = 3, .op = .write, .node = body, .handle = h, .data = "fresh\n" }).reply.status); + _ = call(p, .{ .tag = 4, .op = .release, .node = body, .handle = h, .opened = true }); + try testing.expectEqualStrings("fresh\n", f.content); + try testing.expectEqual(steps + 1, f.history.undo_len); +} + test "a write that changes nothing, an empty range deleted or a range replaced by itself, makes no undo step" { const p = try withFile(testing.allocator, "abc\ndef\n"); defer p.deinit(); @@ -1017,8 +1032,11 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status { switch (file) { .body => if (fileOf(pane) != null) { pf.joined = null; + const was = fileOf(pane).?.revision; _ = spliceBody(p, pane, 0, bodyOf(pane).len, "") orelse return .err; - pf.joined = fileOf(pane).?.revision; + // An empty body truncated is no edit: the write after it makes + // the step, rather than join one that was never pushed. + pf.joined = if (fileOf(pane).?.revision != was) fileOf(pane).?.revision else null; pf.joined_by = 0; pf.addr = .{}; setDot(pane, .{}); @@ -1029,8 +1047,9 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status { const q1 = @max(q0, pf.addr.q1); const before = dotOf(pane); pf.joined = null; + const was = fileOf(pane).?.revision; _ = spliceBody(p, pane, q0, q1, "") orelse return .err; - pf.joined = fileOf(pane).?.revision; + pf.joined = if (fileOf(pane).?.revision != was) fileOf(pane).?.revision else null; pf.joined_by = 0; setDot(pane, .{ .q0 = shiftOne(before.q0, q0, q1 - q0, 0), .q1 = shiftOne(before.q1, q0, q1 - q0, 0) }); pf.addr = .{ .q0 = q0, .q1 = q0 }; -- cgit v1.3