summaryrefslogtreecommitdiff
path: root/src/edit_cmd.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/edit_cmd.zig')
-rw-r--r--src/edit_cmd.zig225
1 files changed, 181 insertions, 44 deletions
diff --git a/src/edit_cmd.zig b/src/edit_cmd.zig
index 7f5c6279..4e2c09fd 100644
--- a/src/edit_cmd.zig
+++ b/src/edit_cmd.zig
@@ -11,7 +11,8 @@
//! Shell, and the Edit is applied when they have all answered -- or not at
//! all, when one fails or a file it changes was edited meanwhile. One Edit
//! waits at a time; a 9P write that ran it is answered when it is done
-//! (9p_io, `fs.edit_answer_at`).
+//! (9p_io, `fs.edit_hold`), the connection serving on meanwhile; a flush
+//! of that write stops the commands and changes nothing.
const std = @import("std");
const pardes = @import("pardes.zig");
const sam = @import("sam_edit.zig");
@@ -67,6 +68,27 @@ const World = struct {
p: *Pardes,
from: usize,
+ fn check(ctx: *anyopaque, path: []const u8, why: *sam.Why) bool {
+ const w: *World = @ptrCast(@alignCast(ctx));
+ for (w.p.panes) |slot| {
+ const pane = slot orelse continue;
+ const f = ninep_pane.fileOf(pane) orelse continue;
+ if (std.mem.eql(u8, f.path, path)) return true;
+ }
+ if (comptime !pardes.hosted) return true;
+ const local = filesystem.localPath(path) orelse return true;
+ if (@import("exec.zig").isDirectory(local)) return said(bool, why, "B: {s} is a directory", .{path}) orelse false;
+ var buf: [4096]u8 = undefined;
+ if (filesystem.resolveOs(local, &buf) == null) return said(bool, why, "B: {s}: no such file", .{path}) orelse false;
+ return true;
+ }
+
+ fn refuseGet(ctx: *anyopaque, id: usize, why: *sam.Why) bool {
+ const w: *World = @ptrCast(@alignCast(ctx));
+ const words = @import("ninep/ctl.zig").getRefused(w.p, w.p.panes[id] orelse return false, "e") orelse return false;
+ return said(bool, why, "{s}", .{words}) orelse true;
+ }
+
fn open(ctx: *anyopaque, path: []const u8, why: *sam.Why) ?sam.File {
const w: *World = @ptrCast(@alignCast(ctx));
const p = w.p;
@@ -135,20 +157,27 @@ pub fn run(p: *Pardes, id: usize, command: []const u8) void {
defer if (!keep) pd.deinit(p.gpa);
const arena = pd.arena.allocator();
- // Every text pane, column by column as acme's allwindows goes, then any
- // not in a column; the Edit starts in its own.
+ // Every text pane in the order they were made, as /index lists them,
+ // so X goes the same way whatever the layout; the Edit starts in its own.
var files: std.ArrayList(sam.File) = .empty;
- var cur: usize = 0;
- var seen: [pardes.MAX_PANES]bool = @splat(false);
- for (0..p.ncol) |c| {
- for (p.col_panes[c][0..p.col_n[c]]) |slot| addFile(p, &files, arena, slot, id, &cur, &seen) catch |err| return p.reportError(id, "Edit", err);
+ for (p.panes, 0..) |slot, i| {
+ const q = slot orelse continue;
+ if (q.file == null) continue;
+ files.append(arena, fileOf(q, i)) catch |err| return p.reportError(id, "Edit", err);
}
- for (0..pardes.MAX_PANES) |slot| addFile(p, &files, arena, slot, id, &cur, &seen) catch |err| return p.reportError(id, "Edit", err);
+ std.mem.sort(sam.File, files.items, p, struct {
+ fn before(core: *Pardes, a: sam.File, b: sam.File) bool {
+ return core.panes[a.id].?.serial < core.panes[b.id].?.serial;
+ }
+ }.before);
+ const cur = for (files.items, 0..) |f, i| {
+ if (f.id == id) break i;
+ } else unreachable;
var world: World = .{ .p = p, .from = id };
var why: sam.Why = .{};
var text: [260]u8 = undefined;
- const res = sam.run(arena, files.items, cur, command, .{ .ctx = &world, .open = World.open, .read = World.read }, &why) catch |err| switch (err) {
+ const res = sam.run(arena, files.items, cur, command, .{ .ctx = &world, .check = World.check, .open = World.open, .read = World.read, .refuseGet = World.refuseGet }, &why) catch |err| switch (err) {
error.Edit => return p.reportFailure(id, std.fmt.bufPrint(&text, "Edit: {s}", .{why.text()}) catch "Edit: failed"),
error.OutOfMemory => return p.reportError(id, "Edit", err),
};
@@ -190,21 +219,45 @@ pub fn run(p: *Pardes, id: usize, command: []const u8) void {
.commands = commands,
.cwds = cwds,
.shell = arena.dupe(u8, p.shellBin()) catch |err| return p.reportError(id, "Edit", err),
+ .token = selection_pipe.newToken(),
};
keep = true;
p.pipe.edit_run = pd;
p.emit(.{ .pipe = .{ .id = pd.id } });
- // A 9P write that ran it is answered once it is done.
- if (p.fs.serving) p.fs.edit_answer_at = pardes.turn.edit_answers;
+ // A 9P write that ran it is held, and answered once it is done
+ // (9p_io, fs.edit_hold); the connection serves on meanwhile.
+ if (p.fs.serving) p.fs.edit_started = true;
+}
+
+/// The Edit waiting on its commands, stopped: its write was flushed, or
+/// its connection hung up (9p_io). Its commands are killed, and nothing
+/// changes.
+pub fn cancel(p: *Pardes) void {
+ p.fs.edit_hold = null;
+ const pd = p.pipe.edit_run orelse return;
+ p.pipe.edit_run = null;
+ selection_pipe.stop(pd.request.token);
+ if (p.panes[pd.origin]) |pane| if (pane.serial == pd.origin_serial)
+ p.setMessage(pd.origin, "Edit: interrupted; nothing changed");
+ pd.deinit(p.gpa);
}
-fn addFile(p: *Pardes, files: *std.ArrayList(sam.File), arena: std.mem.Allocator, slot: usize, from: usize, cur: *usize, seen: *[pardes.MAX_PANES]bool) !void {
- if (seen[slot]) return;
- seen[slot] = true;
- const pane = p.panes[slot] orelse return;
- if (pane.file == null) return;
- if (slot == from) cur.* = files.items.len;
- try files.append(arena, fileOf(pane, slot));
+/// The held 9P write that ran the Edit has its answer: done, failed in the
+/// words said on pane `at` when `failed`.
+fn answerHold(p: *Pardes, at: usize, failed: bool) void {
+ const h = if (p.fs.edit_hold) |*held| held else return;
+ h.done = true;
+ p.fs.news = true;
+ if (!failed) return;
+ const words = if (p.fs.said_at == p.fs.failures and p.fs.said_len > 0)
+ p.fs.said[0..p.fs.said_len]
+ else if (p.panes[at]) |pane| pane.msg[0..pane.msg_len] else "Edit: failed";
+ const kept = @import("Messages.zig").clip(words, h.said.len);
+ @memcpy(h.said[0..kept.len], kept);
+ h.said_len = @intCast(kept.len);
+ h.errno = @import("ninep/ctl.zig").failureErrno(kept);
+ // The write's err record, as a refusal answered at once has.
+ pardes.ctlfs.events.noteError(p, .{ .tag = h.tag, .op = .write, .node = h.node, .handle = h.handle }, @import("ninep/tree.zig").failText(h.tag, h.errno, h.said[0..h.said_len]));
}
/// The commands answered (pipe_resp): the Edit applied, or why not.
@@ -212,11 +265,10 @@ pub fn finish(p: *Pardes, success: bool, outputs: []const []const u8, failure: ?
const pd = p.pipe.edit_run orelse return;
p.pipe.edit_run = null;
defer pd.deinit(p.gpa);
- defer pardes.turn.editAnswered();
const at = if (p.panes[pd.origin]) |pane| (if (pane.serial == pd.origin_serial) pd.origin else p.active) else p.active;
// Whatever fails from here fails the 9P write that ran it too.
const failures = p.fs.failures;
- defer if (p.fs.failures != failures) p.noteLateFailure(at);
+ defer answerHold(p, at, p.fs.failures != failures);
if (success and outputs.len == pd.res.jobs.len) return commit(p, pd, outputs);
// Which command, in +Errors with its own words; the Edit changed nothing.
@@ -232,7 +284,9 @@ pub fn finish(p: *Pardes, success: bool, outputs: []const []const u8, failure: ?
Pipe.writeFailure(w, failure) catch break :report;
const why = std.mem.sliceTo(out.written()[why_at..], '\n');
const said = std.fmt.bufPrint(&line, "Edit: {c} {s}: {s}", .{ job.c, job.command[0..@min(job.command.len, 120)], why }) catch "Edit: a command failed";
+ const was = p.active;
_ = ninep_pane.appendErrors(p, at, out.written());
+ if (p.panes[was] != null) p.active = was;
return p.reportFailure(at, said);
}
p.reportFailure(at, "Edit: a command failed");
@@ -245,12 +299,21 @@ fn commit(p: *Pardes, pd: *Pending, outputs: []const []const u8) void {
const at = if (p.panes[pd.origin]) |pane| (if (pane.serial == pd.origin_serial) pd.origin else p.active) else p.active;
var text: [320]u8 = undefined;
for (res.files, 0..) |f, i| {
- if (f.ops.items.len == 0 and !f.close and !f.renamed and !f.moved) continue;
+ if (f.ops.items.len == 0 and !f.close and !f.renamed and !f.moved and f.get == null) continue;
const pane = p.panes[f.id] orelse return p.reportFailure(at, std.fmt.bufPrint(&text, "Edit: {s} closed while its commands ran; nothing changed", .{f.name}) catch "Edit: a pane closed");
if (pane.serial != pd.serials[i]) return p.reportFailure(at, std.fmt.bufPrint(&text, "Edit: {s} closed while its commands ran; nothing changed", .{f.name}) catch "Edit: a pane closed");
- if (f.ops.items.len > 0 and pane.file.?.revision != pd.revisions[i])
+ if ((f.ops.items.len > 0 or f.get != null) and pane.file.?.revision != pd.revisions[i])
return p.reportFailure(at, std.fmt.bufPrint(&text, "Edit: {s} changed while its commands ran; nothing changed", .{f.name}) catch "Edit: a file changed");
}
+ // e first, Get's way: read before anything is changed, so one that
+ // cannot be read changes nothing; its own undo step puts the old name
+ // back with the old text, and it leaves the pane clean.
+ for (res.files) |f| {
+ const path = f.get orelse continue;
+ var failed: anyerror = error.Unexpected;
+ const errno = @import("ninep/ctl.zig").getFile(p, p.panes[f.id].?, path, &failed);
+ if (errno != 0) return p.reportFailure(at, std.fmt.bufPrint(&text, "Edit: e {s}: {s}", .{ path, if (errno == @import("ninep/tree.zig").E.NOENT) "no such file" else "cannot be read" }) catch "Edit: e failed");
+ }
// `w`'s bytes, written after this step, before anything else is.
for (res.writes) |wr| {
const f = res.files[wr.file];
@@ -291,11 +354,6 @@ fn commit(p: *Pardes, pd: *Pending, outputs: []const []const u8) void {
panes.File.pushUndo(p, pane);
panes.File.setContent(p, pf, new);
}
- // e read its own file over it: what is on disk again.
- if (f.clean) {
- pf.saved_revision = pf.revision;
- pf.disk_gone = false;
- }
}
if (ops.len > 0 or f.moved) ninep_pane.setDot(pane, sam.moveDot(f.dot, ops));
if (f.renamed) ninep_pane.nameBuffer(p, f.id, f.name, false) catch |err| p.reportError(f.id, "Edit", err);
@@ -312,7 +370,11 @@ fn commit(p: *Pardes, pd: *Pending, outputs: []const []const u8) void {
from = job.at;
}
out.appendSlice(p.gpa, res.printed[from..]) catch return;
+ // Printed, the keyboard stays where it was: a +Errors made for it
+ // does not take it, so the next Edit lands where this one did.
+ const was = p.active;
if (out.items.len > 0) _ = ninep_pane.appendErrors(p, at, out.items);
+ if (p.panes[was] != null) p.active = was;
}
// D's panes last, each as Del closes one: an edited one is warned about
// once, and closes on the next.
@@ -344,6 +406,17 @@ fn runReal(p: *Pardes) !void {
p.update(.{ .pipe_resp = .{ .id = pd.id, .success = response.success, .outputs = response.outputs, .failure = response.failure } });
}
+/// Whether any +Errors holds `needle`.
+fn errorsHas(p: *Pardes, needle: []const u8) bool {
+ for (p.panes) |slot| {
+ const q = slot orelse continue;
+ const qf = q.file orelse continue;
+ const o = qf.output orelse continue;
+ if (std.meta.activeTag(o.from) == .errors and std.mem.indexOf(u8, qf.content, needle) != null) return true;
+ }
+ return false;
+}
+
fn errorsText(p: *Pardes) ?[]const u8 {
for (p.panes) |slot| {
const q = slot orelse continue;
@@ -358,8 +431,9 @@ test "Edit , | sort runs off the loop, through the session's shell in the file's
const p = try th.withFile(testing.allocator, "b\na\nc\n");
defer p.deinit();
const serial = th.serialOf(p);
- const answers = pardes.turn.edit_answers;
try testing.expectEqual(tree.Status.ok, th.wr(p, tree.Node.of(serial, .ctl), "Edit , | sort\n").reply.status);
+ // The listener is told to hold the write.
+ try testing.expect(p.fs.edit_started);
// Nothing changed yet: the command is out.
try testing.expectEqualStrings("b\na\nc\n", p.panes[0].?.file.?.content);
const req = p.pipe.pipeRequest(p.pipe.edit_run.?.id).?;
@@ -371,17 +445,55 @@ test "Edit , | sort runs off the loop, through the session's shell in the file's
try testing.expectEqual(tree.E.IO, th.wr(p, tree.Node.of(serial, .ctl), "Edit ,d\n").errno());
answer(p, &.{"a\nb\nc\n"});
try testing.expect(p.pipe.edit_run == null);
- try testing.expectEqual(answers +% 1, pardes.turn.edit_answers);
try testing.expectEqualStrings("a\nb\nc\n", p.panes[0].?.file.?.content);
try testing.expectEqual(tree.Status.ok, th.wr(p, tree.Node.of(serial, .ctl), "Undo\n").reply.status);
try testing.expectEqualStrings("b\na\nc\n", p.panes[0].?.file.?.content);
- // What follows it in the same write would run before its commands
- // answer: refused, and the Edit goes on.
- const after = th.wr(p, tree.Node.of(serial, .ctl), "Edit , | sort\nSave\n");
- try testing.expectEqual(tree.E.INVAL, after.errno());
- try testing.expectEqualStrings(@import("ninep/ctl.zig").e_after_edit, after.reply.ename);
- answer(p, &.{"a\nb\nc\n"});
- try testing.expectEqualStrings("a\nb\nc\n", p.panes[0].?.file.?.content);
+ // Another line in its write would run before its commands answer: the
+ // whole write is refused before any line runs, the one before it too.
+ for ([_][]const u8{ "Edit , | sort\nSave\n", "Msg before\nEdit , | sort\nMsg after\n" }) |two| {
+ const after = th.wr(p, tree.Node.of(serial, .ctl), two);
+ try testing.expectEqual(tree.E.INVAL, after.errno());
+ try testing.expectEqualStrings(@import("ninep/ctl.zig").e_after_edit, after.reply.ename);
+ try testing.expect(p.pipe.edit_run == null and !th.logHas(p, "before"));
+ }
+ try testing.expectEqual(tree.E.INVAL, th.wr(p, @intFromEnum(tree.TopFile.exec), "Msg before\nEdit , | sort\n").errno());
+}
+
+test "the held write asked again is parked again, then answered once, and a flush stops the Edit" {
+ const p = try th.withFile(testing.allocator, "b\na\n");
+ defer p.deinit();
+ const ctl = tree.Node.of(th.serialOf(p), .ctl);
+ const req: tree.Req = .{ .tag = 7, .op = .write, .node = ctl, .handle = 0, .data = "Edit , | sort\n" };
+ try testing.expectEqual(tree.Status.ok, p.serveFs(req).status);
+ // As 9p_io holds it.
+ p.fs.edit_hold = .{ .asker = @ptrCast(p), .slot = 0, .seq = 1, .tag = 7, .node = ctl, .handle = 0, .written = @intCast(req.data.len) };
+ // A retry of its park: still waiting, never run a second time.
+ try testing.expectEqual(tree.Status.again, p.serveFs(req).status);
+ try testing.expect(p.pipe.edit_run != null);
+ answer(p, &.{"a\nb\n"});
+ try testing.expect(p.fs.edit_hold.?.done);
+ const done = p.serveFs(req);
+ try testing.expectEqual(tree.Status.ok, done.status);
+ try testing.expectEqual(@as(u32, @intCast(req.data.len)), done.written);
+ try testing.expect(p.fs.edit_hold == null);
+ try testing.expectEqualStrings("a\nb\n", p.panes[0].?.file.?.content);
+ // A failure is the held write's answer, in the Edit's words.
+ _ = p.serveFs(.{ .tag = 8, .op = .write, .node = ctl, .data = "Edit , | false\n" });
+ p.fs.edit_hold = .{ .asker = @ptrCast(p), .slot = 0, .seq = 2, .tag = 8, .node = ctl, .handle = 0, .written = 15 };
+ p.update(.{ .pipe_resp = .{ .id = p.pipe.edit_run.?.id, .success = false, .outputs = &.{}, .failure = .{ .kind = .exit, .code = 1 } } });
+ const failed = filesystem.EditHold.answer(&p.fs);
+ try testing.expectEqual(tree.Status.err, failed.status);
+ try testing.expectEqualStrings("Edit: | false: exit status 1", failed.ename);
+ // Flushed (or hung up): its commands stopped, nothing changed, and the
+ // next Edit is free to run; the stopped one's late answer is no one's.
+ _ = p.serveFs(.{ .tag = 9, .op = .write, .node = ctl, .data = "Edit , | sort -r\n" });
+ const stale = p.pipe.edit_run.?.id;
+ p.fs.edit_hold = .{ .asker = @ptrCast(p), .slot = 0, .seq = 3, .tag = 9, .node = ctl, .handle = 0, .written = 18 };
+ cancel(p);
+ try testing.expect(p.pipe.edit_run == null and p.fs.edit_hold == null);
+ p.update(.{ .pipe_resp = .{ .id = stale, .success = true, .outputs = &.{"b\na\n"} } });
+ try testing.expectEqualStrings("a\nb\n", p.panes[0].?.file.?.content);
+ try testing.expectEqual(tree.Status.ok, th.wr(p, ctl, "Edit , | cat\n").reply.status);
}
test "Edit's < | and > with a real shell: a command per address, > printed to +Errors" {
@@ -399,6 +511,8 @@ test "Edit's < | and > with a real shell: a command per address, > printed to +E
try testing.expectEqualStrings("ONE TWO\nnew", p.panes[0].?.file.?.content);
try testing.expectEqual(tree.Status.ok, th.wr(p, ctl, "Edit , > wc -l\n").reply.status);
try runReal(p);
+ // The +Errors it opened did not take the keyboard.
+ try testing.expectEqual(@as(usize, 0), p.active);
try testing.expectEqualStrings("ONE TWO\nnew", p.panes[0].?.file.?.content);
try testing.expectEqualStrings("1", std.mem.trim(u8, errorsText(p).?, " \n"));
}
@@ -409,11 +523,12 @@ test "a failing command changes nothing and says why, its stderr in +Errors" {
defer p.deinit();
const ctl = tree.Node.of(th.serialOf(p), .ctl);
try testing.expectEqual(tree.Status.ok, th.wr(p, ctl, "Edit , | echo boom >&2; exit 3\n").reply.status);
- p.fs.late_failure_len = 0;
+ p.fs.edit_hold = .{ .asker = @ptrCast(p), .slot = 0, .seq = 1, .tag = 2, .node = ctl, .handle = 0, .written = 1 };
try runReal(p);
try testing.expectEqualStrings("keep\n", p.panes[0].?.file.?.content);
try testing.expectEqual(@as(usize, 0), p.panes[0].?.file.?.history.undo_len);
- try testing.expectEqualStrings("Edit: | echo boom >&2; exit 3: exit status 3", p.fs.late_failure[0..p.fs.late_failure_len]);
+ // The held write's answer is the failure, in its words.
+ try testing.expectEqualStrings("Edit: | echo boom >&2; exit 3: exit status 3", filesystem.EditHold.answer(&p.fs).ename);
const report = errorsText(p).?;
try testing.expect(std.mem.indexOf(u8, report, "Edit: | echo boom") != null);
try testing.expect(std.mem.indexOf(u8, report, "boom\n") != null);
@@ -422,12 +537,13 @@ test "a failing command changes nothing and says why, its stderr in +Errors" {
test "an Edit whose file is edited while its commands run is refused whole" {
const p = try th.withFile(testing.allocator, "abc\n");
defer p.deinit();
- try testing.expectEqual(tree.Status.ok, th.wr(p, tree.Node.of(th.serialOf(p), .ctl), "Edit , | cat\n").reply.status);
+ const ctl = tree.Node.of(th.serialOf(p), .ctl);
+ try testing.expectEqual(tree.Status.ok, th.wr(p, ctl, "Edit , | cat\n").reply.status);
+ p.fs.edit_hold = .{ .asker = @ptrCast(p), .slot = 0, .seq = 1, .tag = 2, .node = ctl, .handle = 0, .written = 1 };
panes.File.setContent(p, &p.panes[0].?.file.?, try testing.allocator.dupe(u8, "typed\n"));
- p.fs.late_failure_len = 0;
answer(p, &.{"ABC\n"});
try testing.expectEqualStrings("typed\n", p.panes[0].?.file.?.content);
- try testing.expectEqualStrings("Edit: /test.txt changed while its commands ran; nothing changed", p.fs.late_failure[0..p.fs.late_failure_len]);
+ try testing.expectEqualStrings("Edit: /test.txt changed while its commands ran; nothing changed", filesystem.EditHold.answer(&p.fs).ename);
}
test "X over open files, w, e, r, f, B and D on panes and the disk" {
@@ -439,6 +555,7 @@ test "X over open files, w, e, r, f, B and D on panes and the disk" {
try tmp.dir.writeFile(testing.io, .{ .sub_path = "b.h", .data = "foo\n" });
try tmp.dir.writeFile(testing.io, .{ .sub_path = "c.c", .data = "foo bar\n" });
try tmp.dir.writeFile(testing.io, .{ .sub_path = "data", .data = "DATA\n" });
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "n.c", .data = "new\n" });
var dir_buf: [4096]u8 = undefined;
const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)];
const p = try th.withFile(gpa, "scratch\n");
@@ -453,6 +570,11 @@ test "X over open files, w, e, r, f, B and D on panes and the disk" {
try testing.expectEqual(tree.Status.ok, th.wr(p, ctl, "Edit X/\\.[ch]$/ ,s/foo/FOO/g\n").reply.status);
try testing.expectEqualStrings("int FOO;\n", p.panes[a].?.file.?.content);
try testing.expectEqualStrings("scratch\n", p.panes[0].?.file.?.content);
+ // X alone lists them in the order they were opened, as /index does.
+ try testing.expectEqual(tree.Status.ok, th.wr(p, ctl, "Edit X\n").reply.status);
+ const listed = errorsText(p).?;
+ try testing.expect(std.mem.indexOf(u8, listed, "/test.txt").? < std.mem.indexOf(u8, listed, "/a.c").?);
+ try testing.expect(std.mem.indexOf(u8, listed, "/a.c").? < std.mem.indexOf(u8, listed, "/b.h").?);
// B opens a file, which a `"re"` address then edits, from pane 0.
var cmd: [4400]u8 = undefined;
try testing.expectEqual(tree.Status.ok, th.wr(p, ctl, try std.fmt.bufPrint(&cmd, "Edit B {s}/c.c\n", .{dir})).reply.status);
@@ -469,11 +591,26 @@ test "X over open files, w, e, r, f, B and D on panes and the disk" {
try testing.expect(!ninep_pane.dirtyOf(p.panes[a].?));
try testing.expectEqual(tree.Status.ok, th.wr(p, tree.Node.of(p.panes[c].?.serial, .ctl), "Edit 1 r data\n").reply.status);
try testing.expectEqualStrings("DATA\n", p.panes[c].?.file.?.content);
- // e of an edited file is refused; f names one and prints it.
- try testing.expectEqual(tree.E.IO, th.wr(p, tree.Node.of(p.panes[c].?.serial, .ctl), "Edit e\n").errno());
+ // e of an edited file is asked about once, as Get asks; again, it is
+ // read in Get's way, which leaves the pane clean and named after it.
+ const c_ctl = tree.Node.of(p.panes[c].?.serial, .ctl);
+ try testing.expect(th.wr(p, c_ctl, "Edit e data\n").reply.status == .err);
+ try testing.expectEqualStrings("DATA\n", p.panes[c].?.file.?.content);
+ try testing.expect(ninep_pane.dirtyOf(p.panes[c].?));
+ try testing.expectEqual(tree.Status.ok, th.wr(p, c_ctl, "Edit e data\n").reply.status);
+ try testing.expect(std.mem.endsWith(u8, p.panes[c].?.file.?.path, "/data"));
+ try testing.expect(!ninep_pane.dirtyOf(p.panes[c].?));
+ // B checks every name first: a directory among them opens none.
+ const before = p.next_serial;
+ try testing.expect(th.wr(p, ctl, try std.fmt.bufPrint(&cmd, "Edit B {s}/n.c {s}\n", .{ dir, dir })).reply.status == .err);
+ try testing.expectEqual(before, p.next_serial);
+ // f names one and prints it.
+ try testing.expectEqual(tree.Status.ok, th.wr(p, c_ctl, "Edit 1 c/edited/\n").reply.status);
try testing.expectEqual(tree.Status.ok, th.wr(p, tree.Node.of(p.panes[c].?.serial, .ctl), "Edit f d.c\n").reply.status);
try testing.expect(std.mem.endsWith(u8, p.panes[c].?.file.?.path, "/d.c"));
- try testing.expect(std.mem.indexOf(u8, errorsText(p).?, "'+. ") != null);
+ // In its directory's +Errors, or the log when the screen has no room
+ // left for one.
+ try testing.expect(errorsHas(p, "'+. ") or th.logHas(p, "'+. "));
// D closes a pane by name, an edited one (b.h, by the X) on the
// second asking, as Del does.
const b_serial = for (p.panes) |slot| {