summaryrefslogtreecommitdiff
path: root/src/ninep/ctl.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep/ctl.zig')
-rw-r--r--src/ninep/ctl.zig344
1 files changed, 319 insertions, 25 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 397f3339..5bf354eb 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -251,8 +251,9 @@ pub fn captured(p: *Pardes, req: Req, id: usize, exec: bool, text: []const u8, i
/// EIO.
fn failureErrno(failure: []const u8) u16 {
if (std.mem.indexOf(u8, failure, "no such") != null or std.mem.indexOf(u8, failure, "not found") != null) return E.NOENT;
- // A pattern refused (Edit's too) is malformed input, as 9ns reads it.
- if (std.mem.indexOf(u8, failure, "bad regular expression") != null) return E.INVAL;
+ // A pattern refused (Edit's too) is malformed input, as 9ns reads it;
+ // so is a word this build has not (`invalid: acme's Put ...`).
+ if (std.mem.indexOf(u8, failure, "bad regular expression") != null or std.mem.startsWith(u8, failure, "invalid")) return E.INVAL;
return E.IO;
}
@@ -330,13 +331,13 @@ fn writeQuoted(w: *std.Io.Writer, s: []const u8) !void {
try w.writeByte('\'');
}
-pub const pane_text_cap = 7 * 12 + 2 * 256 + 4 + "notcurrent ".len + 48;
+pub const pane_text_cap = 9 * 12 + 2 * 256 + 4 + "notcurrent ".len + 48;
-/// acme's winctlprint: serial, tag length, body length, a reserved zero, the
-/// dirty flag, then the width in cells, the font and the tab width. Then
-/// rio's word for whether the window has the keyboard, `current` or
-/// `notcurrent` (rio(4), wctl), which acme's line has no room for because
-/// acme has one window for input at a time only by the mouse.
+/// acme's winctlprint (plan9port wind.c), field for field: serial, tag
+/// length, body length, isdir (0), the dirty flag, the width in cells, the
+/// font, the tab width, whether Undo has a step, whether Redo has one. Then
+/// pardes's own: rio's word for whether the window has the keyboard,
+/// `current` or `notcurrent` (rio(4), wctl), and a REPL's id.
pub fn paneText(p: *Pardes, pane: *Pane, buf: []u8) []const u8 {
var w = std.Io.Writer.fixed(buf);
w.print("{d:>11} {d:>11} {d:>11} {d:>11} {d:>11} {d:>11} ", .{
@@ -352,7 +353,10 @@ pub fn paneText(p: *Pardes, pane: *Pane, buf: []u8) []const u8 {
writeQuoted(&w, if (font.len == 0) "default" else font) catch return w.buffered();
// While a column's or the workspace's tag has the keyboard, no pane does.
const current = p.panes[p.active] == pane and !p.header_focus;
- w.print(" {d:>11} {s} ", .{ config.tab_width, if (current) "current" else "notcurrent" }) catch {};
+ const f = pane_files.fileOf(pane);
+ const undo = if (f) |file| file.history.undo_len > 0 else false;
+ const redo = if (f) |file| file.history.redo_len > 0 else false;
+ w.print(" {d:>11} {d:>11} {d:>11} {s} ", .{ config.tab_width, @intFromBool(undo), @intFromBool(redo), if (current) "current" else "notcurrent" }) catch {};
// A terminal bound as a REPL ends with its id, `python-a`.
var idbuf: [48]u8 = undefined;
const id = exec_line.replId(&idbuf, pane);
@@ -387,7 +391,13 @@ pub fn refuseTo(p: *Pardes, req: Req, why: []const u8, line: []const u8, ctl: []
/// as on a tag; acme's lowercase verbs are the pane ctl's and alias none.
fn checkBuiltin(p: *Pardes, req: Req, line: []const u8, scope: builtins.Scope) ?Reply {
const sp = std.mem.indexOfAny(u8, line, " \t");
- const word = line[0 .. sp orelse line.len];
+ const typed = line[0 .. sp orelse line.len];
+ // acme's words: checked as the pardes word they run as, or refused.
+ const word = for (pardes.exec.acme_words) |w| {
+ if (!std.mem.eql(u8, typed, w.acme)) continue;
+ const now = w.pardes orelse return tree.failText(req.tag, E.INVAL, std.fmt.bufPrint(&p.fs.ename, "invalid: acme's {s} is not a pardes builtin{s}", .{ w.acme, w.hint }) catch "invalid: an acme word pardes has not");
+ break now;
+ } else typed;
const arg = if (sp) |s| std.mem.trim(u8, line[s + 1 ..], " \t") else "";
const b = std.meta.stringToEnum(Builtin, word) orelse {
// A setting this build's frontend cannot show (Lift, GripWidth on a
@@ -777,10 +787,19 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply {
if (apply) p.fs.results_len = 0;
var held = !other and pane.fs.lock != null;
var it = Messages.init(p, req.data);
- while (it.next()) |line| {
- if (line.len == 0) continue;
+ while (it.next()) |line_in| {
+ if (line_in.len == 0) continue;
// A line before this one may have closed the pane (`Del`).
if (apply and p.paneBySerial(serial) == null) return Reply.fail(req.tag, E.NOENT);
+ // acme's ctl words: done by the file that replaces each, or the
+ // builtin that does it, checked with the rest before any runs.
+ var line = line_in;
+ switch (acmeCtl(p, req, serial, line_in, apply)) {
+ .no => {},
+ .done => continue,
+ .refused => |r| return r,
+ .builtin => |word| line = word,
+ }
if (std.mem.eql(u8, line, "get")) {
if (!apply) continue;
// Quoting the line, as a builtin's refusal (Save's) does.
@@ -854,6 +873,72 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply {
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
+/// acme's pane ctl vocabulary (editors/acme/xfid.c xfidctlwrite): each word
+/// done here by the file that replaces it, or run as the builtin that does
+/// it; those with no place here refused with why.
+const AcmeCtl = union(enum) { no, done, refused: Reply, builtin: []const u8 };
+
+fn acmeCtl(p: *Pardes, req: Req, serial: u32, line: []const u8, apply: bool) AcmeCtl {
+ const id = p.paneBySerial(serial) orelse return .no;
+ const pane = p.panes[id].?;
+ const word = line[0 .. std.mem.indexOfScalar(u8, line, ' ') orelse line.len];
+ const rest = std.mem.trim(u8, line[word.len..], " ");
+ const File = tree.PaneFile;
+ // A word written to one of the pane's own files, as acme's is to its.
+ const Into = struct { file: File, data: []const u8 };
+ const into: ?Into = if (std.mem.eql(u8, word, "name") and rest.len > 0)
+ .{ .file = .name, .data = rest }
+ else if (std.mem.eql(u8, line, "clean"))
+ .{ .file = .dirty, .data = "0" }
+ else if (std.mem.eql(u8, line, "dirty"))
+ .{ .file = .dirty, .data = "1" }
+ else if (std.mem.eql(u8, line, "mark"))
+ .{ .file = .mark, .data = "1" }
+ else if (std.mem.eql(u8, line, "nomark"))
+ .{ .file = .mark, .data = "0" }
+ else
+ null;
+ if (into) |w| {
+ if (!apply) return .done;
+ const r = pane_files.write(p, .{ .tag = req.tag, .op = .write, .node = tree.Node.of(serial, w.file), .data = w.data }, id, pane, w.file);
+ return if (r.status == .err) .{ .refused = r } else .done;
+ }
+ if (std.mem.eql(u8, line, "put")) return .{ .builtin = "Save" };
+ if (std.mem.eql(u8, line, "del")) return .{ .builtin = "Del" };
+ if (std.mem.eql(u8, line, "delete")) return .{ .builtin = "Delete" };
+ const pf = &pane.fs;
+ if (std.mem.eql(u8, line, "dot=addr")) {
+ if (apply) pane_files.setDot(pane, pf.addr);
+ return .done;
+ }
+ if (std.mem.eql(u8, line, "addr=dot")) {
+ if (apply) pf.addr = pane_files.dotOf(pane);
+ return .done;
+ }
+ if (std.mem.eql(u8, line, "limit=addr")) {
+ if (apply) pf.limit = pf.addr;
+ return .done;
+ }
+ if (std.mem.eql(u8, line, "show")) {
+ if (apply) pane_files.showOffset(pane, pane_files.dotOf(pane).q0);
+ return .done;
+ }
+ if (std.mem.eql(u8, line, "cleartag")) {
+ if (apply) _ = pane_files.truncate(p, pane, .tag);
+ return .done;
+ }
+ const none: ?[]const u8 = if (std.mem.eql(u8, word, "dump") or std.mem.eql(u8, word, "dumpdir"))
+ "a pane is dumped with the session: Dump and DumpDir on /ctl"
+ else if (std.mem.eql(u8, word, "font"))
+ "the font is the session's: Font on /ctl"
+ else if (std.mem.eql(u8, line, "menu") or std.mem.eql(u8, line, "nomenu"))
+ "pardes has no menu"
+ else
+ null;
+ if (none) |why| return .{ .refused = tree.failText(req.tag, E.INVAL, std.fmt.bufPrint(&p.fs.ename, "invalid: acme's {s}: {s}", .{ word, why }) catch "invalid: acme's ctl word") };
+ return .no;
+}
+
const e_not_locked = "pane not locked by this open";
/// Who holds it is another open of this ctl, whose client pardes cannot name.
const e_locked = tree.e_in_use ++ ": another open of this ctl holds the lock";
@@ -922,7 +1007,7 @@ const root_status = @intFromEnum(tree.TopFile.status);
const root_look = @intFromEnum(tree.TopFile.look);
const root_exec = @intFromEnum(tree.TopFile.exec);
-test "pane ctl read is index's five fields plus width in cells, font, tab width and whether it is current" {
+test "pane ctl read is acme's fields -- index's five, width in cells, font, tab width, undo, redo -- then whether it is current" {
const gpa = testing.allocator;
const p = try withFile(gpa, "x\n");
defer p.deinit();
@@ -932,8 +1017,8 @@ test "pane ctl read is index's five fields plus width in cells, font, tab width
try testing.expectEqual(Status.ok, a.reply.status);
var want: std.ArrayList(u8) = .empty;
defer want.deinit(gpa);
- try want.print(gpa, "{d:>11} {d:>11} {d:>11} {d:>11} {d:>11} {d:>11} {s} {d:>11} current ", .{
- pane.serial, pane_files.tagOf(p, pane).len, @as(usize, 2), 0, 0, pane.cols, "default", config.tab_width,
+ try want.print(gpa, "{d:>11} {d:>11} {d:>11} {d:>11} {d:>11} {d:>11} {s} {d:>11} {d:>11} {d:>11} current ", .{
+ pane.serial, pane_files.tagOf(p, pane).len, @as(usize, 2), 0, 0, pane.cols, "default", config.tab_width, 0, 0,
});
try testing.expectEqualStrings(want.items, a.bytes);
try testing.expectEqual(@as(u64, a.bytes.len), call(p, .{ .tag = 1, .op = .getattr, .node = Node.of(pane.serial, .ctl) }).reply.attr.size);
@@ -1034,13 +1119,9 @@ test "the pane ctl takes acme's verbs and the pane's builtins, and refuses the r
defer p.deinit();
const serial = serialOf(p);
const ctl_node = Node.of(serial, .ctl);
- // acme's other ctl words have files of their own here, and a builtin is
- // its own capitalised word: `Get`, `DEL` and `del` are none of them.
- for ([_][]const u8{
- "menu", "nomenu", "dump echo hi", "font Go Mono", "lock x", "bogus", "DEL",
- "put", "del", "delete", "clean", "dirty", "show", "mark",
- "Get", "limit=addr", "get x", "look /tmp", "exec Del",
- }) |bad| {
+ // A builtin is its own capitalised word: `GET` and `DEL` are none;
+ // acme's own words are done or refused as acme's (the test below).
+ for ([_][]const u8{ "lock x", "bogus", "DEL", "GET", "get x", "look /tmp", "exec Del" }) |bad| {
const refused = wr(p, ctl_node, bad);
try testing.expectEqual(E.INVAL, refused.errno());
var want: [64]u8 = undefined;
@@ -1671,6 +1752,59 @@ test "a look after Newcol fills the empty column the keyboard is on" {
try testing.expectEqual(p.active, p.col_panes[p.ncol - 1][0]);
}
+test "a look at file:addr whose file is not there is one miss, quoting what was written, in a terminal too" {
+ const p = try th.withTerm(testing.allocator);
+ defer p.deinit();
+ const looked = Node.of(serialOf(p), .look);
+ for ([_][]const u8{ "zzq:#3", "zzq:2", "zzq:/re/" }) |written| {
+ var line: [64]u8 = undefined;
+ _ = wr(p, looked, try std.fmt.bufPrint(&line, "{s}\n", .{written}));
+ var want: [64]u8 = undefined;
+ try testing.expectEqual(@as(usize, 1), th.logCount(p, try std.fmt.bufPrint(&want, "look: no match for \"{s}\"\n", .{written})));
+ }
+ try testing.expectEqual(@as(usize, 3), th.logCount(p, "\nerr "));
+}
+
+test "a look whose pane is refused a place fails with that alone, not a missing line too" {
+ const gpa = testing.allocator;
+ // A screen with rows for one pane only.
+ const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 80, .rows = 5 });
+ defer p.deinit();
+ while (p.nextEffect()) |_| {}
+ _ = try p.setTestFile("x\n");
+ while (p.nextEffect()) |_| {}
+ var tmp = testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "f.txt", .data = "one\ntwo\n" });
+ var dir: [4096]u8 = undefined;
+ const at = dir[0..try tmp.dir.realPath(testing.io, &dir)];
+ var line: [4200]u8 = undefined;
+ const r = wr(p, root_look, try std.fmt.bufPrint(&line, "{s}/f.txt:2\n", .{at}));
+ try testing.expectEqual(Status.err, r.reply.status);
+ try testing.expect(std.mem.indexOf(u8, r.reply.ename, "no space for a pane") != null);
+ try testing.expectEqual(@as(usize, 1), th.logCount(p, "\nerr "));
+}
+
+test "a look under pardes's placement, from a pane pane/new made after Delcol, opens its file" {
+ const gpa = testing.allocator;
+ const p = try withFile(gpa, "x\n");
+ defer p.deinit();
+ var tmp = testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "f.txt", .data = "file\n" });
+ var dir: [4096]u8 = undefined;
+ const at = dir[0..try tmp.dir.realPath(testing.io, &dir)];
+ var line: [4200]u8 = undefined;
+ const first = serialOf(p);
+ try testing.expectEqual(tree.Status.ok, wr(p, Node.of(first, .exec), "Placement\n").reply.status);
+ try testing.expectEqual(tree.Status.ok, wr(p, Node.of(first, .exec), "Delcol\n").reply.status);
+ const made = try th.newPane(p);
+ try testing.expectEqual(tree.Status.ok, wr(p, Node.of(made, .look), try std.fmt.bufPrint(&line, "{s}/f.txt:#4\n", .{at})).reply.status);
+ // The pane pane/new made is placed, and so is the one its look opened.
+ for (p.panes, 0..) |slot, i| if (slot != null) try testing.expect(layout.findPane(p, i) != null);
+ try testing.expect(p.panes[p.active].?.serial != made);
+}
+
test "a Restore of a file that is no dump fails the write before any warning, and a failed Dump fails its write" {
if (comptime !pardes.hosted) return error.SkipZigTest;
const p = try withFile(testing.allocator, "x\n");
@@ -1924,10 +2058,10 @@ test "Grep with no hit says grep: no match and opens nothing, a missing pane dir
for (p.panes) |slot| count += @intFromBool(slot != null);
const missed = wr(p, Node.of(serialOf(p), .exec), "Grep zzqqnomatchzz\n");
try testing.expectEqual(E.NOENT, missed.errno());
- try testing.expectEqualStrings("grep: pattern not found", missed.reply.ename);
+ try testing.expectEqualStrings("Grep: pattern not found", missed.reply.ename);
const unfound = wr(p, Node.of(serialOf(p), .exec), "Find zzqqnomatchzz\n");
try testing.expectEqual(E.NOENT, unfound.errno());
- try testing.expectEqualStrings("find: name not found", unfound.reply.ename);
+ try testing.expectEqualStrings("Find: name not found", unfound.reply.ename);
var after: usize = 0;
for (p.panes) |slot| after += @intFromBool(slot != null);
try testing.expectEqual(count, after);
@@ -2234,6 +2368,133 @@ test "a buffer whose file was deleted on disk is dirty, and Del asks first" {
try testing.expect(p.paneBySerial(serial) == null);
}
+test "a failure said while a 9P write waits on it posts no msg, only the write's err" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ p.fs.write_waits = true;
+ p.saveFailed(@intCast(p.active), "/nowhere/zz.txt", error.FileNotFound);
+ p.fs.write_waits = false;
+ try testing.expect(!th.logHas(p, "msg "));
+ try testing.expect(p.fs.late_failure_len > 0);
+}
+
+test "Mini with no argument is wrong #args, EINVAL, as every word that needs one" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const r = wr(p, Node.of(serialOf(p), .ctl), "Mini\n");
+ try testing.expectEqual(E.INVAL, r.errno());
+ try testing.expectEqualStrings("wrong #args in control message \"Mini\"", r.reply.ename);
+}
+
+test "a look that misses file:/re/ names the file, and is logged as its pane's" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ var tmp = testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "f.txt", .data = "alpha\n" });
+ var dir_buf: [4096]u8 = undefined;
+ const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)];
+ var line: [4200]u8 = undefined;
+ _ = wr(p, root_look, try std.fmt.bufPrint(&line, "{s}/f.txt\n", .{dir}));
+ p.sync();
+ const f_serial = p.panes[p.active].?.serial;
+ _ = wr(p, Node.of(serialOf(p), .look), try std.fmt.bufPrint(&line, "{s}/f.txt:/zzqq/\n", .{dir}));
+ var want: [4300]u8 = undefined;
+ try testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "err {d} look: {s}/f.txt: no match for regexp", .{ f_serial, dir })));
+}
+
+test "acme's words run as pardes's (Put is Save, Delete a Del that does not ask) or are refused, never run as commands" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ for ([_][]const u8{ "Snarf\n", "Putall\n", "Zerox\n", "Get\n" }) |line| {
+ for ([_]u64{ Node.of(serial, .ctl), Node.of(serial, .exec) }) |node| {
+ const r = wr(p, node, line);
+ try testing.expectEqual(E.INVAL, r.errno());
+ try testing.expect(std.mem.indexOf(u8, r.reply.ename, "is not a pardes builtin") != null);
+ }
+ }
+ // Put saves, as Save does.
+ _ = wr(p, Node.of(serial, .name), "/tmp/pardes-put.txt\n");
+ const put = wr(p, Node.of(serial, .exec), "Put\n");
+ try testing.expectEqual(Status.ok, put.reply.status);
+ try testing.expect(put.saved);
+ // Delete closes an edited pane without asking.
+ const made = try th.newPane(p);
+ _ = wr(p, Node.of(made, .name), "/tmp/pardes-delete.txt\n");
+ _ = wr(p, Node.of(made, .body), "edited\n");
+ try testing.expectEqual(Status.ok, wr(p, Node.of(made, .ctl), "Delete\n").reply.status);
+ p.sync();
+ try testing.expect(p.paneBySerial(made) == null);
+}
+
+test "a pane's ctl takes acme's words: done by the files that replace them, or refused with why" {
+ const p = try withFile(testing.allocator, "one two\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ const ctl_node = Node.of(serial, .ctl);
+ const pane = p.panes[p.paneBySerial(serial).?].?;
+ try testing.expectEqual(Status.ok, wr(p, ctl_node, "name /tmp/pardes-acme-ctl.txt\n").reply.status);
+ try testing.expectEqualStrings("/tmp/pardes-acme-ctl.txt", pane.file.?.path);
+ try testing.expectEqual(Status.ok, wr(p, ctl_node, "dirty\n").reply.status);
+ try testing.expectEqualStrings("1", std.mem.trimEnd(u8, rd(p, Node.of(serial, .dirty), 0, 8).bytes, "\n"));
+ try testing.expectEqual(Status.ok, wr(p, ctl_node, "clean\n").reply.status);
+ try testing.expectEqualStrings("0", std.mem.trimEnd(u8, rd(p, Node.of(serial, .dirty), 0, 8).bytes, "\n"));
+ _ = wr(p, Node.of(serial, .addr), "/two/");
+ try testing.expectEqual(Status.ok, wr(p, ctl_node, "dot=addr\nshow\nnomark\nmark\nlimit=addr\n").reply.status);
+ try testing.expectEqual(@as(u32, 4), pane_files.dotOf(pane).q0);
+ try testing.expect(pane.fs.limit != null);
+ try testing.expectEqual(Status.ok, wr(p, ctl_node, "cleartag\n").reply.status);
+ for ([_][]const u8{ "menu", "nomenu", "dump echo hi", "dumpdir /tmp", "font Go Mono" }) |line| {
+ const r = wr(p, ctl_node, line);
+ try testing.expectEqual(E.INVAL, r.errno());
+ try testing.expect(std.mem.startsWith(u8, r.reply.ename, "invalid: acme's "));
+ }
+ // Checked whole: a refused word anywhere and nothing ran.
+ try testing.expectEqual(E.INVAL, wr(p, ctl_node, "dirty\nmenu\n").errno());
+ try testing.expectEqualStrings("0", std.mem.trimEnd(u8, rd(p, Node.of(serial, .dirty), 0, 8).bytes, "\n"));
+}
+
+test "ctl reads acme's fields in acme's order, undo and redo flags included, then pardes's own" {
+ const p = try withFile(testing.allocator, "one\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ const ctl_node = Node.of(serial, .ctl);
+ const fields = struct {
+ fn of(text: []const u8, buf: *[16][]const u8) [][]const u8 {
+ var it = std.mem.tokenizeScalar(u8, text, ' ');
+ var n: usize = 0;
+ while (it.next()) |t| : (n += 1) buf[n] = t;
+ return buf[0..n];
+ }
+ };
+ var buf: [16][]const u8 = undefined;
+ var f = fields.of(rd(p, ctl_node, 0, 4096).bytes, &buf);
+ try testing.expectEqualStrings("0", f[8]); // nothing to undo
+ try testing.expectEqualStrings("0", f[9]); // nothing to redo
+ try testing.expect(std.mem.eql(u8, f[10], "current") or std.mem.eql(u8, f[10], "notcurrent"));
+ _ = wr(p, Node.of(serial, .body), "two\n");
+ f = fields.of(rd(p, ctl_node, 0, 4096).bytes, &buf);
+ try testing.expectEqualStrings("1", f[8]);
+ _ = wr(p, ctl_node, "Undo\n");
+ f = fields.of(rd(p, ctl_node, 0, 4096).bytes, &buf);
+ try testing.expectEqualStrings("1", f[9]);
+}
+
+test "a write to addr, limit or mark asks for no frame; one to body does" {
+ const p = try withFile(testing.allocator, "one\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ for ([_]tree.PaneFile{ .addr, .limit, .mark }) |file| {
+ p.needs_frame = false;
+ _ = p.serveFs(.{ .tag = 1, .op = .write, .node = Node.of(serial, file), .data = if (file == .mark) "1" else "#0" });
+ try testing.expect(!p.needs_frame);
+ }
+ p.needs_frame = false;
+ _ = p.serveFs(.{ .tag = 1, .op = .write, .node = Node.of(serial, .body), .data = "two\n" });
+ try testing.expect(p.needs_frame);
+}
+
test "every EINVAL a write gets says why, in its err record too; DEL is a control character in a line" {
const p = try withFile(testing.allocator, "x\n");
defer p.deinit();
@@ -2960,7 +3221,7 @@ test "@p<serial>:<address> takes any sam address; a miss and an unknown serial a
}
var line: [64]u8 = undefined;
_ = wr(p, root, try std.fmt.bufPrint(&line, "@p{d}:/zzqq/\n", .{serial}));
- try testing.expect(th.logHas(p, "look: no match for regexp"));
+ try testing.expect(th.logHas(p, ": no match for regexp"));
try testing.expectEqualStrings("", rd(p, root, 0, 64).bytes);
_ = wr(p, root, "@p999:/foo/\n");
try testing.expect(th.logHas(p, "look: no pane @p999"));
@@ -3020,3 +3281,36 @@ test "@p<terminal>:<address> takes any address over the terminal's lines, select
try testing.expectEqual(row_before, pane.body.cur_row);
try testing.expectEqualStrings("", rd(p, root, 0, 64).bytes);
}
+
+test "a bad command inside an Edit block written whole is named, not the } after it" {
+ const p = try withFile(testing.allocator, "abc\n");
+ defer p.deinit();
+ const r = wr(p, Node.of(serialOf(p), .ctl), "Edit {\n,s/a/b/\nzq\n}\n");
+ try testing.expectEqual(Status.err, r.reply.status);
+ try testing.expect(std.mem.indexOf(u8, r.reply.ename, "\"}\"") == null);
+ try testing.expect(std.mem.indexOf(u8, r.reply.ename, "z") != null);
+ try testing.expectEqualStrings("abc\n", p.panes[0].?.file.?.content);
+}
+
+test "a refused Delcol changes nothing: no pane, no focus" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ _ = wr(p, @intFromEnum(tree.TopFile.ctl), "Newcol\n");
+ const made = try th.newPane(p);
+ _ = wr(p, Node.of(made, .name), "/tmp/pardes-delcol-refused.txt\n");
+ _ = wr(p, Node.of(made, .body), "edit\n");
+ p.sync();
+ const col = layout.findPane(p, p.paneBySerial(made).?).?.col;
+ const active = p.active;
+ const active_column = p.active_column;
+ var count: usize = 0;
+ for (p.panes) |slot| count += @intFromBool(slot != null);
+ try testing.expectEqual(Status.err, wr(p, Node.ofCol(layout.columnSerial(p, col), .ctl), "Delcol\n").reply.status);
+ p.sync();
+ var after: usize = 0;
+ for (p.panes) |slot| after += @intFromBool(slot != null);
+ try testing.expectEqual(count, after);
+ try testing.expectEqual(active, p.active);
+ try testing.expectEqual(active_column, p.active_column);
+ try testing.expect(th.logHas(p, "unsaved "));
+}