summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 19:00:49 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit8948c9ec1b8ade3d1e4ed7103fd4f6d17a8ea23e (patch)
tree0e7ca727fd79fd0c1605a166203917db3d968566
parent5a5c9c9698c2621d3509c687880d22e904f25139 (diff)
downloadpardes-8948c9ec1b8ade3d1e4ed7103fd4f6d17a8ea23e.tar.gz
pardes-8948c9ec1b8ade3d1e4ed7103fd4f6d17a8ea23e.zip
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 <[email protected]>
-rw-r--r--src/fs.zig3
-rw-r--r--src/ninep/pane.zig22
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();