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.zig241
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");