diff options
| author | Gabriel Schneider <[email protected]> | 2026-10-01 20:50:48 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 22:07:48 -0300 |
| commit | 406f483288c7689bdd2e1f3f68b276c61ca9412a (patch) | |
| tree | b6398fb9974b91441563fd655d18ddaf182a2a5a /src/edit_cmd.zig | |
| parent | b55cb01d2777760ed79b545dfca7115837591577 (diff) | |
| download | pardes-406f483288c7689bdd2e1f3f68b276c61ca9412a.tar.gz pardes-406f483288c7689bdd2e1f3f68b276c61ca9412a.zip | |
An Edit's held write: < | > commands no longer block their 9P connection (answered like a held read, so a status read or a filter reading the session's own mount runs beside it), a Tflush or hang-up of the write kills the commands' process groups and changes nothing; e loads by Get's way (asked once, clean after, undo puts the name back); ~ in e r w f B; B checks every name first; an Edit that runs commands is a write of its own, refused up front with other lines; Edit's +Errors output keeps the keyboard where it was; X goes in pane order; a refused write open says its errno; the reference's Coming from acme rows say what Get file's undo, failure and directory refusal, Putall's answer, Zerox across Dump and Incl's directories now do
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/edit_cmd.zig')
| -rw-r--r-- | src/edit_cmd.zig | 225 |
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| { |
