summaryrefslogtreecommitdiff
path: root/src/ninep/pane.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep/pane.zig')
-rw-r--r--src/ninep/pane.zig73
1 files changed, 58 insertions, 15 deletions
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index d2b2a791..13381066 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -29,14 +29,15 @@ const Node = tree.Node;
/// Filesystem state a pane carries beside its editor state.
pub const State = struct {
/// The range `data` and `xdata` read and write through. acme clears it
- /// when the first client opens `addr` (editors/acme/xfid.c:105), which
- /// suits a client that holds the fid open and leaves a shell reading back
- /// `0 0` from the address it just wrote. Here it is the pane's own
- /// register, cleared by truncating the file, so that `cp addr dot` and
+ /// when the first client opens `addr` (editors/acme/xfid.c:105-108),
+ /// which suits a client that holds the fid open; a shell cannot, and
+ /// would evaluate every `echo /re/ > addr` from #0 and never advance.
+ /// Here it is the pane's own register, cleared by neither an open nor
+ /// a truncation: `0` or `,` starts over, and `cp addr dot` and
/// `cat addr` answer what was written.
addr: Range = .{},
/// The last address written to `addr` failed, so there is none: `data`
- /// and `xdata` refuse until one is written or `addr` is truncated,
+ /// and `xdata` refuse until a good one is written,
/// rather than act at the address before it, which acme would do.
addr_failed: bool = false,
limit: ?Range = null,
@@ -46,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.
@@ -58,6 +63,9 @@ pub const State = struct {
lock: ?u32 = null,
/// The host started a shell it could not teach to mark its prompts.
unmarked: bool = false,
+ /// The pty's size as pty/ctl's winsize last set it, until the pane's own
+ /// grid resizes and gives the pty its size again.
+ winsize: ?[2]u16 = null,
/// Installed during this update; /log hears about it once the update ends
/// and the pane has its file, name and place.
unannounced: bool = false,
@@ -189,7 +197,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;
}
@@ -507,7 +517,7 @@ fn writeRange(req: Req, pane: *Pane, file: PaneFile) Reply {
fn writeFlag(p: *Pardes, req: Req, pane: *Pane, file: PaneFile) Reply {
const word = std.mem.trim(u8, req.data, " \t\r\n");
- if (!std.mem.eql(u8, word, "1") and !std.mem.eql(u8, word, "0")) return tree.failText(req.tag, E.INVAL, "takes 1 or 0");
+ if (!std.mem.eql(u8, word, "1") and !std.mem.eql(u8, word, "0")) return tree.failText(req.tag, E.INVAL, "bad flag: takes 1 or 0");
const on = word[0] == '1';
const pf = &pane.fs;
switch (file) {
@@ -515,9 +525,12 @@ fn writeFlag(p: *Pardes, req: Req, pane: *Pane, file: PaneFile) Reply {
const f = fileOf(pane) orelse return Reply.fail(req.tag, E.INVAL);
f.saved_revision = if (on) f.revision -% 1 else f.revision;
},
+ // acme's nomark joins the writes after it into one undo step. The
+ // step starts here: the text as it is now is what Undo brings back,
+ // and turning mark on again only ends it.
.mark => {
+ if (!on and !pf.nomark) panes.File.pushUndo(p, pane);
pf.nomark = !on;
- if (on) panes.File.pushUndo(p, pane);
},
.scroll => pf.noscroll = !on,
else => unreachable,
@@ -592,11 +605,11 @@ pub fn appendErrors(p: *Pardes, id: usize, text: []const u8) ?usize {
}
const free = p.freeSlot() orelse return null;
const content = p.gpa.dupe(u8, text) catch return null;
- const np = panes.Output.open(p, free, dir, .errors, "", content) catch {
+ _ = panes.Output.open(p, free, dir, .errors, "", content) catch {
p.gpa.free(content);
return null;
};
- exec.placeDoc(p, id, free, np);
+ exec.placeNew(p, id, id, free, .errors);
return text.len;
}
@@ -610,6 +623,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, .{});
},
@@ -619,6 +633,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 };
},
@@ -634,10 +649,10 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status {
pane.tag.vsel.active = false;
pane.tag.nsel = 0;
},
- .addr => {
- pf.addr = .{};
- pf.addr_failed = false;
- },
+ // A shell's `>` truncates before it writes: the address written
+ // is evaluated from where the last one left off, as with `>>`.
+ // `0` (or `,`) is how to start over. See State.addr.
+ .addr => {},
.limit => pf.limit = null,
.dot => if (fileOf(pane) != null) setDot(pane, .{}),
else => {},
@@ -875,7 +890,7 @@ test "the flag files read and write the state acme kept behind ctl verbs" {
try testing.expectEqual(@as(u64, 2), call(p, .{ .tag = 1, .op = .getattr, .node = node }).reply.attr.size);
for ([_][]const u8{ "", "yes", "2", "0 1", "true" }) |bad| {
try testing.expectEqual(E.INVAL, wr(p, node, bad).errno());
- try testing.expectEqualStrings("takes 1 or 0", wr(p, node, bad).reply.ename);
+ try testing.expectEqualStrings("bad flag: takes 1 or 0", wr(p, node, bad).reply.ename);
}
}
@@ -905,6 +920,34 @@ 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();
+ const serial = serialOf(p);
+ const body = Node.of(serial, .body);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .mark), "0").reply.status);
+ _ = wr(p, body, "two\n");
+ _ = wr(p, body, "three\n");
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .mark), "1").reply.status);
+ try testing.expectEqualStrings("one\ntwo\nthree\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);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Redo").reply.status);
+ try testing.expectEqualStrings("one\ntwo\nthree\n", rd(p, body, 0, 64).bytes);
+}
+
test "truncating the tag clears its editable tail" {
const p = try withFile(testing.allocator, "x\n");
defer p.deinit();