diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 11:16:17 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:16 -0300 |
| commit | be11a4e58e86d9b299accd3688c89d3c3a7b4b0c (patch) | |
| tree | a5932c021958eaa201dfa417ef4752a787a70ae5 /src | |
| parent | 536df266a4af38cf20ecb340337a6de2922e8902 (diff) | |
| download | pardes-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.zig | 31 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 4 |
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; |
