diff options
| -rw-r--r-- | docs/fs.md | 4 | ||||
| -rw-r--r-- | src/ninep/pane.zig | 35 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 18 |
3 files changed, 50 insertions, 7 deletions
@@ -738,7 +738,9 @@ The three flag files `dirty`, `mark` and `scroll` read `0` or `1` and take an undo point (writing `1` pushes one now), and whether a write scrolls the pane. The writes of one open of `data`, `xdata` or `body` are one undo step while `mark` is 1 (so `printf 'a\nb\n' > data` is one, though a -shell writes it a line at a time), and the next open starts another; to +shell writes it a line at a time), and the next open starts another. The +pane has one history, so two opens writing at once take turns in it: each +open's first write after the other's starts a step of its own; to make a loop's writes one step, write `1` (an undo point here), then `0`, the writes, then `1` again. diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index 9cb1dba6..9e832df6 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -56,6 +56,10 @@ pub const State = struct { /// `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, + /// The open whose writes `joined` joins (0: any, as after a truncation + /// or for an open with no record): an interleaved open's write starts + /// a step of its own. + joined_by: u32 = 0, noscroll: bool = false, tag_snap: std.ArrayList(u8) = .empty, /// An open reads pty/data, so output queues for it; a second is refused. @@ -446,6 +450,12 @@ pub fn write(p: *Pardes, req: Req, id: usize, pane: *Pane, file: PaneFile) Reply }; } +/// A write joins the step being made only when it is the same open's (or +/// the step is anyone's, `joined_by` 0): another open's starts its own. +fn joinOnly(pane: *Pane, handle: u32) void { + if (pane.fs.joined_by != 0 and pane.fs.joined_by != handle) pane.fs.joined = null; +} + fn writeBody(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { if (req.data.len == 0) return .{ .tag = req.tag, .written = 0 }; if (pane.file == null) { @@ -456,9 +466,11 @@ fn writeBody(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { return .{ .tag = req.tag, .written = @intCast(take) }; } const at = bodyOf(pane).len; + joinOnly(pane, req.handle); const take = spliceBody(p, pane, at, at, req.data) orelse return Reply.fail(req.tag, E.NOMEM); pane.fs.joined = fileOf(pane).?.revision; + pane.fs.joined_by = req.handle; if (!pane.fs.noscroll) showOffset(pane, at + take); return .{ .tag = req.tag, .written = @intCast(take) }; } @@ -511,9 +523,11 @@ fn writeData(p: *Pardes, req: Req, pane: *Pane) Reply { const q0: usize = pf.addr.q0; const q1: usize = @max(q0, @as(usize, pf.addr.q1)); const before = dotOf(pane); + joinOnly(pane, req.handle); const take = spliceBody(p, pane, q0, q1, req.data) orelse return Reply.fail(req.tag, E.NOMEM); pf.joined = fileOf(pane).?.revision; + pf.joined_by = req.handle; setDot(pane, .{ .q0 = shiftOne(before.q0, clip(q0), clip(q1 - q0), clip(take)), .q1 = shiftOne(before.q1, clip(q0), clip(q1 - q0), clip(take)), @@ -734,6 +748,7 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status { pf.joined = null; _ = spliceBody(p, pane, 0, bodyOf(pane).len, "") orelse return .err; pf.joined = fileOf(pane).?.revision; + pf.joined_by = 0; pf.addr = .{}; setDot(pane, .{}); }, @@ -745,6 +760,7 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status { pf.joined = null; _ = spliceBody(p, pane, q0, q1, "") orelse return .err; pf.joined = fileOf(pane).?.revision; + 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 }; }, @@ -1057,6 +1073,25 @@ test "the writes of one open are one undo step, as bash's line-buffered printf m } } +test "two opens writing one body are two undo steps, however their writes interleave" { + const p = try withFile(testing.allocator, "one\n"); + defer p.deinit(); + const serial = serialOf(p); + const body = Node.of(serial, .body); + const a = call(p, .{ .tag = 1, .op = .open, .node = body, .omode = 1 }).reply.handle; + const b = call(p, .{ .tag = 1, .op = .open, .node = body, .omode = 1 }).reply.handle; + try testing.expect(a != 0 and b != 0 and a != b); + _ = call(p, .{ .tag = 2, .op = .write, .node = body, .handle = a, .data = "a\n" }); + _ = call(p, .{ .tag = 2, .op = .write, .node = body, .handle = a, .data = "b\n" }); + _ = call(p, .{ .tag = 2, .op = .write, .node = body, .handle = b, .data = "c\n" }); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Undo").reply.status); + try testing.expectEqualStrings("one\na\nb\n", rd(p, 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, body, 0, 64).bytes); + _ = call(p, .{ .tag = 3, .op = .release, .node = body, .handle = a, .opened = true }); + _ = call(p, .{ .tag = 3, .op = .release, .node = body, .handle = b, .opened = true }); +} + test "a name cut across writes is one name, applied once at its newline or its close" { const p = try withFile(testing.allocator, "x\n"); defer p.deinit(); diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index 4b52be51..66f73e9c 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -131,6 +131,9 @@ pub const Open = struct { /// An open of a file that takes lines of commands (look, exec, a /// ctl, tagexec): its `pending` and `results`. lines, + /// An open writing a file pane's body, data or xdata: its writes are + /// one undo step (pane.zig `joined_by`), another open's its own. + writes, } = .{ .snapshot = null }, /// What a write to a file of command lines left unfinished: a last line /// with no newline yet, or an Edit block still open. A write through a @@ -180,7 +183,7 @@ pub const Open = struct { gpa.free(run.output); gpa.free(run.line); }, - .event, .pty_data, .ctl, .lines => {}, + .event, .pty_data, .ctl, .lines, .writes => {}, } o.pending.deinit(gpa); if (o.replay) |r| gpa.free(r.bytes); @@ -752,7 +755,8 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { pane_of = pn; const reader = req.omode & 3 != 1; // OWRITE is the one mode that never reads break :what switch (t.file) { - .body => if (pn.isTerminal()) .{ .snapshot = null } else return .{ .tag = req.tag }, + .body => if (pn.isTerminal()) .{ .snapshot = null } else if (req.omode & 3 == 0) return .{ .tag = req.tag } else .writes, + .data, .xdata => if (req.omode & 3 != 0) .writes else return .{ .tag = req.tag }, // Reads of these consume, so two readers would each see half // the stream; the second is refused rather than robbed. .event => if (reader and pn.fs.event_reader) return failText(req.tag, E.BUSY, e_in_use) else .{ .event = reader }, @@ -794,7 +798,7 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { pn.fs.event_reader = pn.fs.event_reader or reader; }, .pty_data => pane_of.?.fs.pty_reader = true, - .run, .ctl, .lines => {}, + .run, .ctl, .lines, .writes => {}, } return .{ .tag = req.tag, .handle = @intCast(i + 1) }; } @@ -831,9 +835,11 @@ 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`). + // The writes of one open are one undo step (pane.zig `joined`): its + // close ends that step. 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; + const pf = &p.panes[id].?.fs; + if (pf.joined_by == 0 or pf.joined_by == req.handle) pf.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| { @@ -871,7 +877,7 @@ fn releaseHandle(p: *Pardes, req: Req) void { .ctl => if (pn.fs.lock == req.handle) { pn.fs.lock = null; }, - .snapshot, .log, .lines => {}, + .snapshot, .log, .lines, .writes => {}, }; o.deinit(p.gpa); } |
