summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 11:16:17 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commitbe11a4e58e86d9b299accd3688c89d3c3a7b4b0c (patch)
treea5932c021958eaa201dfa417ef4752a787a70ae5 /src
parent536df266a4af38cf20ecb340337a6de2922e8902 (diff)
downloadpardes-be11a4e58e86d9b299accd3688c89d3c3a7b4b0c.tar.gz
pardes-be11a4e58e86d9b299accd3688c89d3c3a7b4b0c.zip
The writes of one open of data, body or xdata are one undo step
bash line-buffers its stdout, so printf 'x\ny\nz\n' > data arrives as three writes and took three Undos. A write now leaves the join mark a truncation already left, so the next write on the file joins its step until the open is released; a truncation and any other edit start a step of their own. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/ninep/pane.zig31
-rw-r--r--src/ninep/tree.zig4
2 files changed, 32 insertions, 3 deletions
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index 4370d401..0b6f04a4 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -50,9 +50,11 @@ 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.
+ /// The revision a truncation or a write of `data`, `xdata` or `body`
+ /// left: the write that follows it at once joins its undo step, until
+ /// that open is released (tree.zig releaseHandle). So a shell's
+ /// `printf 'x\ny\n' > data` is one step, as acme's replace is, though
+ /// bash writes it a line at a time; any other edit between breaks it.
joined: ?u32 = null,
noscroll: bool = false,
tag_snap: std.ArrayList(u8) = .empty,
@@ -454,6 +456,7 @@ fn writeBody(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
const at = bodyOf(pane).len;
const take = spliceBody(p, pane, at, at, req.data) orelse
return Reply.fail(req.tag, E.NOMEM);
+ pane.fs.joined = fileOf(pane).?.revision;
if (!pane.fs.noscroll) showOffset(pane, at + take);
return .{ .tag = req.tag, .written = @intCast(take) };
}
@@ -508,6 +511,7 @@ fn writeData(p: *Pardes, req: Req, pane: *Pane) Reply {
const before = dotOf(pane);
const take = spliceBody(p, pane, q0, q1, req.data) orelse
return Reply.fail(req.tag, E.NOMEM);
+ pf.joined = fileOf(pane).?.revision;
setDot(pane, .{
.q0 = shiftOne(before.q0, clip(q0), clip(q1 - q0), clip(take)),
.q1 = shiftOne(before.q1, clip(q0), clip(q1 - q0), clip(take)),
@@ -725,6 +729,7 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status {
const pf = &pane.fs;
switch (file) {
.body => if (fileOf(pane) != null) {
+ pf.joined = null;
_ = spliceBody(p, pane, 0, bodyOf(pane).len, "") orelse return .err;
pf.joined = fileOf(pane).?.revision;
pf.addr = .{};
@@ -735,6 +740,7 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status {
const q0 = pf.addr.q0;
const q1 = @max(q0, pf.addr.q1);
const before = dotOf(pane);
+ pf.joined = null;
_ = 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) });
@@ -1030,6 +1036,25 @@ 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 "the writes of one open are one undo step, as bash's line-buffered printf makes them; the next open is another" {
+ const p = try withFile(testing.allocator, "one\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ for ([_]u64{ Node.of(serial, .data), Node.of(serial, .body), Node.of(serial, .xdata) }) |node| {
+ _ = wr(p, Node.of(serial, .addr), "$");
+ for ([_][]const u8{ "x\n", "y\n", "z\n" }) |piece| try testing.expectEqual(Status.ok, wr(p, node, piece).reply.status);
+ _ = call(p, .{ .tag = 3, .op = .release, .node = node });
+ // A second open: its own step.
+ _ = wr(p, Node.of(serial, .addr), "$");
+ try testing.expectEqual(Status.ok, wr(p, node, "w\n").reply.status);
+ _ = call(p, .{ .tag = 3, .op = .release, .node = node });
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Undo").reply.status);
+ try testing.expectEqualStrings("one\nx\ny\nz\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\n", rd(p, Node.of(serial, .body), 0, 64).bytes);
+ }
+}
+
test "a replace through data, truncate then write, is one undo step" {
const p = try withFile(testing.allocator, "one two\n");
defer p.deinit();
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index 64089c01..a8b86a96 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -831,6 +831,10 @@ fn releaseHandle(p: *Pardes, req: Req) void {
p.panes[id].?.fs.tag_rewrite = false;
p.panes[id].?.fs.tag_held_newline = false;
};
+ // The writes of one open are one undo step (pane.zig `joined`).
+ if (t == .pane and (t.pane.file == .body or t.pane.file == .data or t.pane.file == .xdata)) if (p.paneBySerial(t.pane.serial)) |id| {
+ p.panes[id].?.fs.joined = null;
+ };
// `: > limit` clears it for good once its open goes.
if (t == .pane and t.pane.file == .limit) if (p.paneBySerial(t.pane.serial)) |id| {
p.panes[id].?.fs.limit_truncated = null;