summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/fs.md4
-rw-r--r--src/ninep/pane.zig35
-rw-r--r--src/ninep/tree.zig18
3 files changed, 50 insertions, 7 deletions
diff --git a/docs/fs.md b/docs/fs.md
index dc15ab91..03cdd1ee 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -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);
}