diff options
Diffstat (limited to 'src/ninep/pane.zig')
| -rw-r--r-- | src/ninep/pane.zig | 241 |
1 files changed, 213 insertions, 28 deletions
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index b37ae645..42c0c48e 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -1,5 +1,6 @@ -//! The files under /pane/<serial>/: name, body, tag, ctl, addr, data, xdata, -//! sel, errors and event. Terminal panes add pty/ (pty.zig). +//! The files under /pane/<serial>/: name, body, tag, ctl, the three range +//! files addr, dot and limit, data, xdata, sel, the three flag files dirty, +//! mark and scroll, errors, event, look and exec. Terminals add pty/ (pty.zig). const std = @import("std"); const pardes = @import("../pardes.zig"); const panes = @import("../panes.zig"); @@ -187,10 +188,15 @@ pub fn spliceBody(p: *Pardes, id: usize, pane: *Pane, q0: usize, q1: usize, byte return take; } -// ---- sizes reported by stat ---- +// ---- what stat reports ---- + +/// A range file's text: acme's addr format, two right-aligned offsets, so +/// that copying one range file onto another is the whole of `dot=addr`. +pub const range_len: u64 = 24; pub fn fileSize(p: *Pardes, id: usize, f: PaneFile) u64 { const pane = p.panes[id] orelse return 0; + const pf = &p.fs.panes[id]; return switch (f) { .body, .data, .xdata => bodyOf(pane).len, .tag => tagOf(p, pane).len, @@ -203,7 +209,24 @@ pub fn fileSize(p: *Pardes, id: usize, f: PaneFile) u64 { const d = dotOf(pane); break :sel @max(d.q0, d.q1) - d.q0; }, - .dir, .addr, .errors, .event, .pty, .pty_ctl, .pty_status, .pty_data => 0, + .addr, .dot => range_len, + .limit => if (pf.limit == null) 0 else range_len, + .dirty, .mark, .scroll => 2, + .look, .exec => ctl.resultsLen(p), + .event => events.pending(&pf.events), + .pty_status => pty.status_len, + .pty_data => events.pending(&pf.pty_out), + .dir, .errors, .pty, .pty_ctl => 0, + }; +} + +/// The qid version, which a client polls to watch a file for change. Only +/// the pane's text can promise one: its revision counts every edit, and +/// nothing else here changes in step with it. +pub fn versionOf(pane: *const Pane, f: PaneFile) u32 { + return switch (f) { + .body, .data, .xdata => if (pane.file) |*file| file.revision else 0, + else => 0, }; } @@ -246,13 +269,17 @@ pub fn read(p: *Pardes, req: Req, id: usize, pane: *Pane, file: PaneFile) Reply .ctl => ctl.readPane(p, req, pane), .addr => addr: { clampAddr(pf, bodyOf(pane).len); - const out = p.fs.stage(p.gpa); - out.print(p.gpa, "{d:>11} {d:>11} ", .{ pf.addr.q0, pf.addr.q1 }) catch {}; - break :addr tree.stagedReply(p, req); + break :addr readRange(p, req, pf.addr); }, + .dot => readRange(p, req, dotOf(pane)), + .limit => readRange(p, req, pf.limit), .data => readData(req, id, pane, pf, false), .xdata => readData(req, id, pane, pf, true), .sel => readSel(req, id, pane), + .dirty => readFlag(p, req, dirtyOf(pane)), + .mark => readFlag(p, req, !pf.nomark), + .scroll => readFlag(p, req, !pf.noscroll), + .look, .exec => ctl.readResults(p, req), .event => events.readQueue(p, req, &pf.events), .pty_status => pty.readStatus(p, req, id, pane), .pty_data => pty.readData(p, req, pf), @@ -260,6 +287,20 @@ pub fn read(p: *Pardes, req: Req, id: usize, pane: *Pane, file: PaneFile) Reply }; } +/// An unset limit reads empty, which is how a client tells it from one that +/// happens to bound nothing. +fn readRange(p: *Pardes, req: Req, r: ?State.Range) Reply { + const out = p.fs.stage(p.gpa); + if (r) |range| out.print(p.gpa, "{d:>11} {d:>11} ", .{ range.q0, range.q1 }) catch {}; + return tree.stagedReply(p, req); +} + +fn readFlag(p: *Pardes, req: Req, on: bool) Reply { + const out = p.fs.stage(p.gpa); + out.appendSlice(p.gpa, if (on) "1\n" else "0\n") catch {}; + return tree.stagedReply(p, req); +} + fn readBody(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { if (pane.file != null) { const text = bodyOf(pane); @@ -322,10 +363,12 @@ pub fn write(p: *Pardes, req: Req, id: usize, pane: *Pane, file: PaneFile) Reply .name => writeName(p, req, id, pane), .body => writeBody(p, req, id, pane), .tag => writeTag(p, req, pane), - .ctl => ctl.writePane(p, req, pane.serial), - .addr => writeAddr(p, req, id, pane), + .ctl => ctl.writePane(p, req, pane), + .addr, .dot, .limit => writeRange(p, req, id, pane, file), .data, .xdata => writeData(p, req, id, pane), .sel => writeSel(p, req, id, pane), + .dirty, .mark, .scroll => writeFlag(p, req, id, pane, file), + .look, .exec => ctl.command(p, req, pane.serial, file == .exec), .event => events.writeEvent(p, req, id), .errors => if (appendErrors(p, id, req.data)) |took| .{ .tag = req.tag, .written = @intCast(took) } @@ -389,15 +432,68 @@ fn writeSel(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { return .{ .tag = req.tag, .written = @intCast(take) }; } -fn writeAddr(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { +/// The pair of offsets a range file reads back, so that copying `addr` onto +/// `dot` or `limit` does what acme spelled `dot=addr` and `limit=addr`. +fn pairOf(text: []const u8) ?State.Range { + var it = std.mem.tokenizeAny(u8, text, " \t"); + const q0 = std.fmt.parseInt(u32, it.next() orelse return null, 10) catch return null; + const q1 = std.fmt.parseInt(u32, it.next() orelse return null, 10) catch return null; + if (it.next() != null) return null; + return .{ .q0 = q0, .q1 = @max(q0, q1) }; +} + +/// A range file takes an address expression, or that pair of offsets. +fn rangeOf(pf: *State, text: []const u8, data: []const u8) ?State.Range { + const expr = std.mem.trimEnd(u8, data, "\n"); + if (pairOf(expr)) |r| { + const n = clip(text.len); + return .{ .q0 = @min(r.q0, n), .q1 = @min(r.q1, n) }; + } + var a: addressing.Addr = .{ .text = text, .lim = pf.limit, .expr = expr }; + const r = a.address(pf.addr) orelse return null; + return if (a.i < expr.len) null else r; +} + +fn writeRange(p: *Pardes, req: Req, id: usize, pane: *Pane, file: PaneFile) Reply { const pf = &p.fs.panes[id]; const text = bodyOf(pane); clampAddr(pf, text.len); - const expr = std.mem.trimEnd(u8, req.data, "\n"); - var a: addressing.Addr = .{ .text = text, .lim = pf.limit, .expr = expr }; - const r = a.address(pf.addr) orelse return Reply.fail(req.tag, E.INVAL); - if (a.i < expr.len) return Reply.fail(req.tag, E.INVAL); - pf.addr = r; + const r = rangeOf(pf, text, req.data) orelse return Reply.fail(req.tag, E.INVAL); + switch (file) { + .addr => pf.addr = r, + .limit => pf.limit = r, + // Setting dot scrolls to it, which is the whole of acme's `show`. + .dot => { + if (fileOf(pane) == null) return Reply.fail(req.tag, E.INVAL); + setDot(pane, r); + }, + else => unreachable, + } + return .{ .tag = req.tag, .written = @intCast(req.data.len) }; +} + +fn flagOf(data: []const u8) ?bool { + const word = std.mem.trim(u8, data, " \t\r\n"); + if (std.mem.eql(u8, word, "1")) return true; + if (std.mem.eql(u8, word, "0")) return false; + return null; +} + +fn writeFlag(p: *Pardes, req: Req, id: usize, pane: *Pane, file: PaneFile) Reply { + const on = flagOf(req.data) orelse return Reply.fail(req.tag, E.INVAL); + const pf = &p.fs.panes[id]; + switch (file) { + .dirty => { + const f = fileOf(pane) orelse return Reply.fail(req.tag, E.INVAL); + f.saved_revision = if (on) f.revision -% 1 else f.revision; + }, + .mark => { + pf.nomark = !on; + if (on) panes.File.pushUndo(p, pane); + }, + .scroll => pf.noscroll = !on, + else => unreachable, + } return .{ .tag = req.tag, .written = @intCast(req.data.len) }; } @@ -475,14 +571,24 @@ pub fn appendErrors(p: *Pardes, id: usize, text: []const u8) ?usize { return text.len; } -/// Truncation empties a body; every other setattr just answers fresh attributes. +/// Truncation empties whatever the file holds: the body, the tag's editable +/// tail (acme's `cleartag`), a range. Every other setattr just answers fresh +/// attributes. pub fn truncate(p: *Pardes, id: usize, pane: *Pane, file: PaneFile) tree.Status { + const pf = &p.fs.panes[id]; switch (file) { .body, .data, .xdata => if (fileOf(pane) != null) { _ = spliceBody(p, id, pane, 0, bodyOf(pane).len, "") orelse return .err; - p.fs.panes[id].addr = .{}; + pf.addr = .{}; setDot(pane, .{}); }, + .tag => { + pane.tag_tail_len = 0; + pane.tag_init = true; + }, + .addr => pf.addr = .{}, + .limit => pf.limit = null, + .dot => if (fileOf(pane) != null) setDot(pane, .{}), else => {}, } return .ok; @@ -509,15 +615,19 @@ test "index lists serial kind dirty flag and name and stat reports its length" { const a = rd(p, index, 0, 4096); try testing.expectEqual(Status.ok, a.reply.status); var want: [64]u8 = undefined; - try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d} text 0 /test.txt\n", .{pane.serial}), a.bytes); + const clean_row = try std.fmt.bufPrint(&want, "{d} text 0 /test.txt\n", .{pane.serial}); + try testing.expect(std.mem.indexOf(u8, a.bytes, clean_row) != null); + const at = std.mem.indexOf(u8, a.bytes, clean_row).?; const stat = call(p, .{ .tag = 1, .op = .getattr, .node = index }); try testing.expectEqual(@as(u64, a.bytes.len), stat.reply.attr.size); try testing.expectEqual(@as(u16, 0o444), stat.reply.attr.mode); pane.file.?.saved_revision = pane.file.?.revision -% 1; - try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d} text 1 /test.txt\n", .{pane.serial}), rd(p, index, 0, 4096).bytes); - const mid = rd(p, index, 2, 4); - try testing.expectEqualStrings("text", mid.bytes); + var dirty_want: [64]u8 = undefined; + const dirty_row = try std.fmt.bufPrint(&dirty_want, "{d} text 1 /test.txt\n", .{pane.serial}); + try testing.expect(std.mem.indexOf(u8, rd(p, index, 0, 4096).bytes, dirty_row) != null); + const kind = at + std.mem.indexOfScalar(u8, clean_row, ' ').? + 1; + try testing.expectEqualStrings("text", rd(p, index, kind, 4).bytes); const serial = try th.newPane(p); const both = rd(p, index, 0, 4096); @@ -533,10 +643,11 @@ test "a terminal is listed as term with its directory as name" { const pane = p.panes[0].?; p.setCwd(0, "/work/dir"); var want: [64]u8 = undefined; - try testing.expectEqualStrings( - try std.fmt.bufPrint(&want, "{d} term 0 /work/dir\n", .{pane.serial}), + try testing.expect(std.mem.indexOf( + u8, rd(p, @intFromEnum(tree.TopFile.index), 0, 4096).bytes, - ); + try std.fmt.bufPrint(&want, "{d} term 0 /work/dir\n", .{pane.serial}), + ) != null); try testing.expectEqualStrings("/work/dir\n", rd(p, Node.of(pane.serial, .name), 0, 4096).bytes); try testing.expectEqual(E.PERM, wr(p, Node.of(pane.serial, .name), "/elsewhere\n").errno()); } @@ -566,6 +677,13 @@ test "body reads at any offset and writes append" { try testing.expectEqual(@as(u64, "one\ntwo\nthree\na".len), stat.reply.attr.size); try testing.expectEqual(@as(u16, 0o666), stat.reply.attr.mode); try testing.expect(stat.reply.attr.mtime >= p.fs.started); + + // The qid version is the pane's revision, so a stat sees an edit land. + try testing.expectEqual(p.panes[0].?.file.?.revision, stat.reply.attr.version); + _ = wr(p, body, "more\n"); + const later = call(p, .{ .tag = 7, .op = .getattr, .node = body }); + try testing.expect(later.reply.attr.version != stat.reply.attr.version); + try testing.expectEqual(@as(u32, 0), call(p, .{ .tag = 8, .op = .getattr, .node = Node.of(serial, .tag) }).reply.attr.version); } test "a body write to a terminal pane types at its shell" { @@ -653,18 +771,29 @@ test "sel reads the selection and writing sel replaces it" { const p = try withFile(gpa, "one\ntwo\n"); defer p.deinit(); const serial = serialOf(p); - const ctl_node = Node.of(serial, .ctl); + const addr_node = Node.of(serial, .addr); + const dot = Node.of(serial, .dot); const sel = Node.of(serial, .sel); - _ = wr(p, Node.of(serial, .addr), "#0,#3"); - try testing.expectEqual(Status.ok, wr(p, ctl_node, "dot=addr\n").reply.status); + // Copying addr onto dot is the whole of acme's `dot=addr`, and back again + // of `addr=dot`; each range file reads exactly what the other takes. + _ = wr(p, addr_node, "#0,#3"); + try testing.expectEqual(Status.ok, wr(p, dot, " 0 3 ").reply.status); + try testing.expectEqualStrings(" 0 3 ", rd(p, dot, 0, 100).bytes); + try testing.expectEqual(range_len, call(p, .{ .tag = 1, .op = .getattr, .node = dot }).reply.attr.size); try testing.expectEqualStrings("one", rd(p, sel, 0, 100).bytes); try testing.expectEqual(@as(u64, 3), call(p, .{ .tag = 1, .op = .getattr, .node = sel }).reply.attr.size); - _ = wr(p, ctl_node, "addr=dot\n"); + _ = wr(p, addr_node, "#0"); + _ = wr(p, addr_node, " 0 3 "); try testing.expectEqual(@as(u32, 0), p.fs.panes[0].addr.q0); try testing.expectEqual(@as(u32, 3), p.fs.panes[0].addr.q1); + // A truncation empties dot; an address expression addresses it. + _ = call(p, .{ .tag = 9, .op = .setattr, .node = dot, .truncate = true }); + try testing.expectEqualStrings("", rd(p, sel, 0, 100).bytes); + try testing.expectEqual(Status.ok, wr(p, dot, "#0,#3").reply.status); + try testing.expectEqual(Status.ok, wr(p, sel, "ONE").reply.status); try testing.expectEqualStrings("ONE\ntwo\n", p.panes[0].?.file.?.content); _ = wr(p, sel, "!"); @@ -672,6 +801,62 @@ test "sel reads the selection and writing sel replaces it" { try testing.expectEqual(@as(u64, 0), call(p, .{ .tag = 2, .op = .getattr, .node = sel }).reply.attr.size); } +test "the flag files read and write the state acme kept behind ctl verbs" { + const gpa = testing.allocator; + const p = try withFile(gpa, "one\ntwo\n"); + defer p.deinit(); + const serial = serialOf(p); + const pane = p.panes[0].?; + const pf = &p.fs.panes[0]; + const dirty = Node.of(serial, .dirty); + const mark = Node.of(serial, .mark); + const scroll = Node.of(serial, .scroll); + + for ([_]u64{ dirty, mark, scroll }) |node| { + 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("0\n", rd(p, dirty, 0, 8).bytes); + try testing.expectEqualStrings("1\n", rd(p, mark, 0, 8).bytes); + try testing.expectEqualStrings("1\n", rd(p, scroll, 0, 8).bytes); + + try testing.expectEqual(Status.ok, wr(p, dirty, "1\n").reply.status); + try testing.expect(dirtyOf(pane)); + try testing.expectEqualStrings("1\n", rd(p, dirty, 0, 8).bytes); + _ = wr(p, dirty, "0"); + try testing.expect(!dirtyOf(pane)); + + _ = wr(p, mark, "0"); + _ = wr(p, scroll, "0"); + try testing.expect(pf.nomark and pf.noscroll); + try testing.expectEqualStrings("0\n", rd(p, mark, 0, 8).bytes); + try testing.expectEqualStrings("0\n", rd(p, scroll, 0, 8).bytes); + _ = wr(p, mark, "1"); + _ = wr(p, scroll, "1"); + try testing.expect(!pf.nomark and !pf.noscroll); + + // A terminal has no file, so its dirty flag is readable but not writable. + const term = try th.withTerm(gpa); + defer term.deinit(); + try testing.expectEqualStrings("0\n", rd(term, Node.of(serialOf(term), .dirty), 0, 8).bytes); + try testing.expectEqual(E.INVAL, wr(term, Node.of(serialOf(term), .dirty), "1").errno()); +} + +test "truncating the tag clears its editable tail" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + const pane = p.panes[0].?; + const tag = Node.of(pane.serial, .tag); + _ = wr(p, tag, " Mine"); + try testing.expect(pane.tag_tail_len > 0); + const cleared = call(p, .{ .tag = 1, .op = .setattr, .node = tag, .truncate = true }); + try testing.expectEqual(Status.ok, cleared.reply.status); + try testing.expectEqual(@as(usize, 0), pane.tag_tail_len); + try testing.expect(std.mem.indexOf(u8, rd(p, tag, 0, 4096).bytes, " Mine") == null); +} + test "name reads the file name and writing it promotes a scratch without touching its body" { const gpa = testing.allocator; const p = try withFile(gpa, "opener\n"); |
