diff options
Diffstat (limited to 'src/ninep/pane.zig')
| -rw-r--r-- | src/ninep/pane.zig | 73 |
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(); |
