From 8948c9ec1b8ade3d1e4ed7103fd4f6d17a8ea23e Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 19:00:49 -0300 Subject: A replace through data or body, truncate then write, is one undo step A shell's printf x > data truncates the addressed range and then writes, and each pushed its own undo point, so one replace took two Undos. A truncation of data, xdata or body now lets the write that follows it at once join its step, as acme's replace is one. Co-Authored-By: Claude Opus 5.5 --- src/fs.zig | 3 ++- src/ninep/pane.zig | 22 +++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/fs.zig b/src/fs.zig index 42d761ca..0a4473d6 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -598,7 +598,8 @@ test "virtual body writes can read their input from the same pane" { const path = try std.fmt.bufPrint(&path_buffer, "/virtual/pane/{d}/body", .{pane.serial}); try write(p, path, pane.file.?.content); try std.testing.expectEqualStrings("same pane contents\n", pane.file.?.content); - try std.testing.expectEqual(@as(usize, 2), pane.file.?.history.undo_len); + // The truncation and the write are one undo step. + try std.testing.expectEqual(@as(usize, 1), pane.file.?.history.undo_len); const events = &p.panes[p.active].?.fs.events; try std.testing.expect(std.mem.startsWith(u8, events.peek().?, "ED")); events.pop(); diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index a5acccbc..50fc220e 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -47,6 +47,10 @@ pub const State = struct { event_reader: bool = false, events: events.Queue = .{}, nomark: bool = false, + /// The revision a truncation of `data`, `xdata` or `body` left: the + /// write that follows it at once joins its undo step, so a shell's + /// `printf x > data` is one step, as acme's replace is. + joined: ?u32 = null, noscroll: bool = false, tag_snap: std.ArrayList(u8) = .empty, /// An open reads pty/data, so output queues for it; a second is refused. @@ -190,7 +194,9 @@ pub fn spliceBody(p: *Pardes, pane: *Pane, q0: usize, q1: usize, bytes: []const @memcpy(new[0..lo], f.content[0..lo]); @memcpy(new[lo..][0..take], bytes[0..take]); @memcpy(new[lo + take ..], f.content[hi..]); - if (!pane.fs.nomark) panes.File.pushUndo(p, pane); + const join = pane.fs.joined == f.revision; + pane.fs.joined = null; + if (!pane.fs.nomark and !join) panes.File.pushUndo(p, pane); panes.File.setContent(p, f, new); return take; } @@ -614,6 +620,7 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status { switch (file) { .body => if (fileOf(pane) != null) { _ = spliceBody(p, pane, 0, bodyOf(pane).len, "") orelse return .err; + pf.joined = fileOf(pane).?.revision; pf.addr = .{}; setDot(pane, .{}); }, @@ -623,6 +630,7 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status { const q1 = @max(q0, pf.addr.q1); const before = dotOf(pane); _ = spliceBody(p, pane, q0, q1, "") orelse return .err; + pf.joined = fileOf(pane).?.revision; setDot(pane, .{ .q0 = shiftOne(before.q0, q0, q1 - q0, 0), .q1 = shiftOne(before.q1, q0, q1 - q0, 0) }); pf.addr = .{ .q0 = q0, .q1 = q0 }; }, @@ -909,6 +917,18 @@ test "the flag files read and write the state acme kept behind ctl verbs" { try testing.expectEqual(E.INVAL, wr(term, Node.of(serialOf(term), .dirty), "1").errno()); } +test "a replace through data, truncate then write, is one undo step" { + const p = try withFile(testing.allocator, "one two\n"); + defer p.deinit(); + const serial = serialOf(p); + _ = wr(p, Node.of(serial, .addr), "/two/"); + _ = call(p, .{ .tag = 1, .op = .setattr, .node = Node.of(serial, .data), .truncate = true }); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .data), "2").reply.status); + try testing.expectEqualStrings("one 2\n", rd(p, Node.of(serial, .body), 0, 64).bytes); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Undo").reply.status); + try testing.expectEqualStrings("one two\n", rd(p, Node.of(serial, .body), 0, 64).bytes); +} + test "writes under mark 0 are one undo step, even with no history before them" { const p = try withFile(testing.allocator, "one\n"); defer p.deinit(); -- cgit v1.3