diff options
Diffstat (limited to 'src/ninep/ctl.zig')
| -rw-r--r-- | src/ninep/ctl.zig | 252 |
1 files changed, 224 insertions, 28 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index b28e02fa..b4144fd2 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -69,13 +69,19 @@ fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void { const before = Serials.snapshot(p); // An exec answers the pane it went to: a command pane it reused, or // the terminal it typed into. + p.look_missed = false; const went: ?usize = if (exec) pardes.exec.execute(p, id, text) else blk: { look.lookAt(p, id, text); break :blk null; }; + // A look that found nothing answers nothing, not the pane before it. + if (!exec and p.look_missed) return; const after = Serials.snapshot(p); var created = false; for (after.list[0..after.len]) |serial| if (std.mem.indexOfScalar(u32, before.list[0..before.len], serial) == null) { + // A look's hits buffer is how it found the text, not where: it + // answers the pane the hit is selected in. + if (!exec) if (p.paneBySerial(serial)) |made| if (p.panes[made].?.file) |f| if (f.output) |o| if (o.from == .search) continue; noteResult(p, serial); created = true; }; @@ -84,6 +90,37 @@ fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void { noteResult(p, if (exec) context else (p.panes[p.active] orelse return).serial); } +/// A write's messages, a line each, except that an `Edit` line takes the +/// lines after it while its `{` group or `a`/`c`/`i` text block is open +/// (sam_edit.needsMore): a block written whole reaches Edit whole, never +/// as lines of their own. +const Messages = struct { + p: *Pardes, + data: []const u8, + at: usize = 0, + + fn init(p: *Pardes, data: []const u8) Messages { + return .{ .p = p, .data = data }; + } + + fn next(m: *Messages) ?[]const u8 { + if (m.at >= m.data.len) return null; + const start = m.at; + var end = std.mem.indexOfScalarPos(u8, m.data, start, '\n') orelse m.data.len; + m.at = end + 1; + const first = std.mem.trim(u8, m.data[start..end], " \t\r"); + const edit = "Edit"; + if (!std.mem.startsWith(u8, first, edit) or (first.len > edit.len and first[edit.len] != ' ' and first[edit.len] != '\t')) + return first; + const sam = @import("../sam_edit.zig"); + while (m.at < m.data.len and sam.needsMore(m.p.scratch.allocator(), std.mem.trim(u8, m.data[start..end], " \t\r")[edit.len..])) { + end = std.mem.indexOfScalarPos(u8, m.data, m.at, '\n') orelse m.data.len; + m.at = end + 1; + } + return std.mem.trim(u8, m.data[start..end], " \t\r"); + } +}; + // ---- look and exec ---- /// One command per line, checked whole before any line runs: a line is the @@ -93,11 +130,11 @@ fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void { pub fn command(p: *Pardes, req: Req, serial: ?u32, exec: bool) Reply { for ([2]bool{ false, true }) |apply| { if (apply) p.fs.results_len = 0; - var it = std.mem.splitScalar(u8, req.data, '\n'); - while (it.next()) |raw| { - const text = std.mem.trim(u8, raw, " \t\r"); + var it = Messages.init(p, req.data); + while (it.next()) |text| { if (text.len == 0) continue; - for (text) |c| if (c < ' ' and c != '\t') return Reply.fail(req.tag, E.INVAL); + // Only an Edit block holds newlines (Messages). + for (text) |c| if (c < ' ' and c != '\t' and c != '\n') return Reply.fail(req.tag, E.INVAL); if (!apply) continue; const id = if (serial) |s| p.paneBySerial(s) orelse break else p.active; if (p.panes[id] == null) return Reply.fail(req.tag, E.NOENT); @@ -205,6 +242,13 @@ fn refuse(p: *Pardes, req: Req, why: []const u8, line: []const u8) Reply { return tree.failText(req.tag, E.INVAL, text); } +/// `refuse`, saying which ctl takes the message instead. +fn refuseTo(p: *Pardes, req: Req, why: []const u8, line: []const u8, ctl: []const u8) Reply { + const room = p.fs.ename.len -| (why.len + ctl.len + 18); + const text = std.fmt.bufPrint(&p.fs.ename, "{s} \"{s}\": write it to {s}", .{ why, line[0..@min(line.len, room)], ctl }) catch why; + return tree.failText(req.tag, E.INVAL, text); +} + /// Checks one line written to a ctl as a builtin of `scope` before any line /// of the write runs: a word the registry knows, of this ctl's scope, given /// an argument if and only if it takes or requires one, and for a setting a @@ -216,8 +260,9 @@ fn checkBuiltin(p: *Pardes, req: Req, line: []const u8, scope: builtins.Scope) ? const word = line[0 .. sp orelse line.len]; const arg = if (sp) |s| std.mem.trim(u8, line[s + 1 ..], " \t") else ""; const b = std.meta.stringToEnum(Builtin, word) orelse return refuse(p, req, "unknown control message", line); - if (builtins.registry.scope(b) != scope) - return refuse(p, req, if (scope == .pane) "not a window control message" else "not a session control message", line); + // Edit is the one pane word the root takes too: at the active pane. + if (builtins.registry.scope(b) != scope and b != .Edit) + return refuseTo(p, req, if (scope == .pane) "not a window control message" else "not a session control message", line, if (scope == .pane) "/ctl" else "pane/<n>/ctl"); if (arg.len > 0 and !builtins.registry.takesArg(b)) return refuse(p, req, "wrong #args in control message", line); if (arg.len == 0 and builtins.registry.requiresArg(b)) return refuse(p, req, "wrong #args in control message", line); const setting = config.Runtime.find(word) orelse return null; @@ -298,6 +343,9 @@ fn writeSettings(p: *Pardes, w: *std.Io.Writer) !void { .tagline_size => try w.print("{s} {d}\n", .{ word, s.font.tagline_percent }), .window_opacity => try w.print("{s} {d}\n", .{ word, s.window_opacity }), .window_blur => try w.print("{s} {d}\n", .{ word, s.window_blur }), + .choice => |which| switch (which) { + inline else => |field| try w.print("{s} {s}\n", .{ word, @tagName(@field(s, @tagName(field))) }), + }, .message_ms => |which| try w.print("{s} {d}\n", .{ word, @field(s, "message_" ++ @tagName(which) ++ "_ms") }), // Bare, it names the default directory. .dump_dir => if (s.dump_dir.len > 0) try w.print("{s} {s}\n", .{ word, s.dump_dir.get() }) else try w.print("{s}\n", .{word}), @@ -336,15 +384,16 @@ pub fn readRoot(p: *Pardes, req: Req) Reply { pub fn writeRoot(p: *Pardes, req: Req) Reply { for ([2]bool{ false, true }) |apply| { if (apply) p.fs.results_len = 0; - var it = std.mem.splitScalar(u8, req.data, '\n'); - while (it.next()) |raw| { - const line = std.mem.trim(u8, raw, " \t\r"); + var it = Messages.init(p, req.data); + while (it.next()) |line| { if (line.len == 0) continue; if (!apply) { if (checkBuiltin(p, req, line, .session)) |refusal| return refusal; continue; } if (p.panes[p.active] == null) return Reply.fail(req.tag, E.NOENT); + p.fs.session_write = true; + defer p.fs.session_write = false; if (runBuiltin(p, req, p.active, line)) |refusal| return refusal; } } @@ -445,25 +494,33 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply { for ([2]bool{ false, true }) |apply| { if (apply) p.fs.results_len = 0; var held = !other and pane.fs.lock != null; - var it = std.mem.splitScalar(u8, req.data, '\n'); - while (it.next()) |raw| { - const line = std.mem.trim(u8, raw, " \t\r"); + var it = Messages.init(p, req.data); + while (it.next()) |line| { if (line.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); if (std.mem.eql(u8, line, "get")) { if (!apply) continue; + if (getRefused(p, pane)) |said| return tree.failText(req.tag, E.IO, said); const errno = get(p, pane); if (errno != 0) return Reply.fail(req.tag, errno); } else if (std.mem.eql(u8, line, "lock")) { if (mine == null) return tree.failText(req.tag, E.INVAL, tree.e_bad_ctl); - if (other) return tree.failText(req.tag, E.BUSY, tree.e_in_use); + if (other) return tree.failText(req.tag, E.BUSY, e_locked); held = true; if (apply) pane.fs.lock = mine; } else if (std.mem.eql(u8, line, "unlock")) { if (!held) return tree.failText(req.tag, E.INVAL, e_not_locked); held = false; if (apply) pane.fs.lock = null; + } else if (std.mem.startsWith(u8, line, "answer ")) { + // The pane's question (its `ask` in the log), answered. + const choice = std.mem.trim(u8, line["answer ".len..], " "); + if (choice.len != 1) return refuse(p, req, "answer takes one choice, or -", line); + const id = p.paneBySerial(serial).?; + if (p.panes[id].?.prompt != .del_side and p.panes[id].?.prompt != .repl_choice) + return refuse(p, req, "no question asked", line); + if (apply) _ = p.answerQuestion(id, if (choice[0] == '-') null else choice[0]); } else if (!apply) { if (checkBuiltin(p, req, line, .pane)) |refusal| return refusal; } else if (runBuiltin(p, req, p.paneBySerial(serial).?, line)) |refusal| { @@ -475,6 +532,23 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply { } const e_not_locked = "window 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"; + +/// acme's get asks winclean first (exec.c:513): unsaved text is warned +/// about once, and the same get again, nothing edited since, discards it. +fn getRefused(p: *Pardes, pane: *Pane) ?[]const u8 { + const f = pane_files.fileOf(pane) orelse return null; + if (!panes.Output.fileTraits(f.output).saves or f.revision == f.saved_revision) return null; + // Dirty by a rename alone is no text to lose: only edits are asked about. + const saved = f.saved_hash orelse return null; + if (std.hash.Wyhash.hash(0, f.content) == saved) return null; + if (pane.discard_warned) |w| if (w.revision == f.revision and w.by == .get) return null; + pane.discard_warned = .{ .revision = f.revision, .by = .get }; + const said = std.fmt.bufPrint(&p.fs.ename, "{s}: Modified (get again to discard)", .{f.path}) catch "Modified (get again to discard)"; + p.setMessage(p.paneBySerial(pane.serial).?, said); + return said; +} fn get(p: *Pardes, pane: *Pane) u16 { const f = pane_files.fileOf(pane) orelse return 0; @@ -486,6 +560,7 @@ fn get(p: *Pardes, pane: *Pane) u16 { panes.File.pushUndo(p, pane); panes.File.setContent(p, f, bytes); f.saved_revision = f.revision; + f.saved_hash = std.hash.Wyhash.hash(0, f.content); return 0; } @@ -546,10 +621,23 @@ test "commands lists every builtin with its argument and the ctl that takes it" try testing.expect(words.next() == null); } try testing.expectEqual(std.enums.values(Builtin).len, lines); - for ([_][]const u8{ "\nDel arg pane\n", "\nNewcol root\n", "\nSave arg pane\n", "\nVerbose arg root\n" }) |row| + for ([_][]const u8{ "\nDel arg pane\n", "\nNewcol root\n", "\nSave arg pane\n", "\nVerbose arg root\n", "\nUndo pane\n", "\nRedo pane\n" }) |row| try testing.expect(std.mem.indexOf(u8, listed.bytes, row) != null); } +test "Undo and Redo on a pane's ctl step its body through its edits" { + const p = try withFile(testing.allocator, "one\n"); + defer p.deinit(); + const serial = serialOf(p); + const body = Node.of(serial, .body); + try testing.expectEqual(Status.ok, wr(p, body, "two\n").reply.status); + try testing.expectEqualStrings("one\ntwo\n", rd(p, body, 0, 64).bytes); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Undo").reply.status); + try testing.expectEqualStrings("one\n", rd(p, body, 0, 64).bytes); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Redo").reply.status); + try testing.expectEqualStrings("one\ntwo\n", rd(p, body, 0, 64).bytes); +} + test "focus reads the pane with the keyboard, and a serial written to it moves the keyboard there" { const p = try withFile(testing.allocator, "x\n"); defer p.deinit(); @@ -604,8 +692,8 @@ test "the pane ctl takes acme's verbs and the pane's builtins, and refuses the r var want: [64]u8 = undefined; try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "unknown control message \"{s}\"", .{bad}), refused.reply.ename); } - try testing.expectEqualStrings("not a window control message \"Newcol\"", wr(p, ctl_node, "Newcol").reply.ename); - try testing.expectEqualStrings("not a window control message \"Verbose off\"", wr(p, ctl_node, "Verbose off").reply.ename); + try testing.expectEqualStrings("not a window control message \"Newcol\": write it to /ctl", wr(p, ctl_node, "Newcol").reply.ename); + try testing.expectEqualStrings("not a window control message \"Verbose off\": write it to /ctl", wr(p, ctl_node, "Verbose off").reply.ename); try testing.expectEqualStrings("wrong #args in control message \"Collapse now\"", wr(p, ctl_node, "Collapse now").reply.ename); // Checked whole first: a bad line anywhere and nothing ran. try testing.expectEqual(E.INVAL, wr(p, ctl_node, "Msg first\nbogus").errno()); @@ -657,7 +745,7 @@ test "the root ctl reads the settings as a write takes them, and takes the sessi try testing.expectEqual(Status.ok, wr(p, root_ctl, "Newcol\n").reply.status); try testing.expectEqual(columns + 1, p.ncol); - try testing.expectEqualStrings("not a session control message \"Del\"", wr(p, root_ctl, "Del").reply.ename); + try testing.expectEqualStrings("not a session control message \"Del\": write it to pane/<n>/ctl", wr(p, root_ctl, "Del").reply.ename); try testing.expectEqualStrings("unknown control message \"Nonsense 1\"", wr(p, root_ctl, "Nonsense 1").reply.ename); try testing.expectEqualStrings("bad value in control message \"Verbose maybe\"", wr(p, root_ctl, "Verbose maybe").reply.ename); const no_theme = wr(p, root_ctl, "Theme no-such-theme").reply.ename; @@ -700,7 +788,7 @@ test "a second lock is refused until the holder unlocks or closes, and binds nob try testing.expectEqual(Status.ok, w.ctl(p, ctl_node, a, "lock\n").reply.status); // Another open's lock is refused at once, and so is its unlock; writes // to the pane's other files are not. - try testing.expectEqualStrings(tree.e_in_use, w.ctl(p, ctl_node, b, "lock\n").reply.ename); + try testing.expectEqualStrings(e_locked, w.ctl(p, ctl_node, b, "lock\n").reply.ename); try testing.expectEqual(E.BUSY, w.ctl(p, ctl_node, b, "lock\n").errno()); try testing.expectEqualStrings(e_not_locked, w.ctl(p, ctl_node, b, "unlock\n").reply.ename); try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .addr), "1").reply.status); @@ -730,6 +818,112 @@ test "a second lock is refused until the holder unlocks or closes, and binds nob for (p.fs.opens) |o| try testing.expect(o.node == 0); } +test "Edit through a pane's ctl renames every foo in one undo step, and a failed one changes nothing" { + const p = try withFile(testing.allocator, "foo x foo y foo\n"); + defer p.deinit(); + const serial = serialOf(p); + const ctl_node = Node.of(serial, .ctl); + const body = Node.of(serial, .body); + try testing.expectEqual(Status.ok, wr(p, ctl_node, "Edit ,x/foo/c/foobar/\n").reply.status); + try testing.expectEqualStrings("foobar x foobar y foobar\n", rd(p, body, 0, 64).bytes); + const failed = wr(p, ctl_node, "Edit ,s/nothing/x/\n"); + try testing.expectEqual(E.IO, failed.errno()); + try testing.expectStringStartsWith(failed.reply.ename, "Edit: no substitution"); + try testing.expectEqualStrings("foobar x foobar y foobar\n", rd(p, body, 0, 64).bytes); + // The whole rename is one undo step. + try testing.expectEqual(Status.ok, wr(p, ctl_node, "Undo").reply.status); + try testing.expectEqualStrings("foo x foo y foo\n", rd(p, body, 0, 64).bytes); +} + +test "a write of an Edit block carries it whole, to a pane's ctl, the root's ctl and exec" { + const p = try withFile(testing.allocator, "foo x\nfoo y\n"); + defer p.deinit(); + const serial = serialOf(p); + const body = Node.of(serial, .body); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Edit ,x/foo/{\ni/[/\na/]/\n}\n").reply.status); + try testing.expectEqualStrings("[foo] x\n[foo] y\n", rd(p, body, 0, 64).bytes); + p.active = p.paneBySerial(serial).?; + try testing.expectEqual(Status.ok, wr(p, @intFromEnum(tree.TopFile.ctl), "Edit ,x/\\[/{\nc/</\n}\n").reply.status); + try testing.expectEqualStrings("<foo] x\n<foo] y\n", rd(p, body, 0, 64).bytes); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .exec), "Edit $a\nend\n.\n").reply.status); + try testing.expectEqualStrings("<foo] x\n<foo] y\nend\n", rd(p, body, 0, 64).bytes); + // An unclosed group is refused, and nothing after it runs as a line. + try testing.expectEqual(E.IO, wr(p, Node.of(serial, .ctl), "Edit ,x/foo/{\nd\n").errno()); + try testing.expectEqualStrings("<foo] x\n<foo] y\nend\n", rd(p, body, 0, 64).bytes); +} + +test "dirty goes back to 0 when the text is the saved text again" { + const p = try withFile(testing.allocator, "one\n"); + defer p.deinit(); + const serial = serialOf(p); + const dirty = Node.of(serial, .dirty); + try testing.expectEqualStrings("0\n", rd(p, dirty, 0, 8).bytes); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .body), "two\n").reply.status); + try testing.expectEqualStrings("1\n", rd(p, dirty, 0, 8).bytes); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Undo").reply.status); + try testing.expectEqualStrings("0\n", rd(p, dirty, 0, 8).bytes); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Redo").reply.status); + try testing.expectEqualStrings("1\n", rd(p, dirty, 0, 8).bytes); +} + +test "a message from a root ctl write is the session's in the log" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + p.update(.tick); // the pane's `new` is logged before the log is opened + while (p.nextEffect()) |_| {} + const log = @intFromEnum(tree.TopFile.log); + const f = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle; + const frozen = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = f, .size = 8192 }).bytes.len; + _ = call(p, .{ .tag = 3, .op = .write, .node = log, .handle = f, .data = "follow" }); + try testing.expectEqual(Status.ok, wr(p, @intFromEnum(tree.TopFile.ctl), "Kill\n").reply.status); + // The builtin's name, then what it said: both the session's. + try testing.expectEqualStrings("msg - Kill\n", call(p, .{ .tag = 4, .op = .read, .node = log, .handle = f, .off = frozen, .size = 4096 }).bytes); + try testing.expectEqualStrings("msg - Kill: nothing running\n", call(p, .{ .tag = 5, .op = .read, .node = log, .handle = f, .off = frozen, .size = 4096 }).bytes); +} + +test "a look takes acme's :addr and file:addr, and a miss changes nothing" { + const gpa = testing.allocator; + const p = try withFile(gpa, "foo x foo y\nbar\n"); + defer p.deinit(); + const serial = serialOf(p); + const look_node = Node.of(serial, .look); + const dot = Node.of(serial, .dot); + // :addr is an address in this pane, evaluated from its dot. + try testing.expectEqual(Status.ok, wr(p, look_node, ":/y/\n").reply.status); + try testing.expectEqualStrings(" 10 11 ", rd(p, dot, 0, 64).bytes); + try testing.expectEqual(Status.ok, wr(p, look_node, ":#4\n").reply.status); + try testing.expectEqualStrings(" 4 4 ", rd(p, dot, 0, 64).bytes); + try testing.expectEqual(Status.ok, wr(p, look_node, ":/foo/\n").reply.status); // from #4: the second foo + try testing.expectEqualStrings(" 6 9 ", rd(p, dot, 0, 64).bytes); + // file:addr names the pane's own file here. + var line: [256]u8 = undefined; + const path = p.panes[0].?.file.?.path; + try testing.expectEqual(Status.ok, wr(p, look_node, try std.fmt.bufPrint(&line, "{s}:/bar/\n", .{path})).reply.status); + try testing.expectEqualStrings(" 12 15 ", rd(p, dot, 0, 64).bytes); + // A miss: no buffer, the selection kept, and it says so. + const panes_before = p.panes.len - std.mem.count(?*pardes.Pane, &p.panes, &.{null}); + try testing.expectEqual(Status.ok, wr(p, look_node, "zzqq\n").reply.status); + try testing.expectEqual(panes_before, p.panes.len - std.mem.count(?*pardes.Pane, &p.panes, &.{null})); + try testing.expectEqualStrings(" 12 15 ", rd(p, dot, 0, 64).bytes); + try testing.expectStringStartsWith(p.panes[0].?.msg[0..p.panes[0].?.msg_len], "Look: no match"); + // A hit answers the pane the text is selected in, not its hits buffer. + try testing.expectEqual(Status.ok, wr(p, look_node, "foo\n").reply.status); + var own: [16]u8 = undefined; + try testing.expectEqualStrings(try std.fmt.bufPrint(&own, "{d}\n", .{serial}), rd(p, look_node, 0, 64).bytes); + // A pattern with a blank in it; 0/re/ searches from the top, whatever dot is. + try testing.expectEqual(Status.ok, wr(p, look_node, ":/foo y/\n").reply.status); + try testing.expectEqualStrings(" 6 11 ", rd(p, dot, 0, 64).bytes); + try testing.expectEqual(Status.ok, wr(p, look_node, try std.fmt.bufPrint(&line, "{s}:0/foo/\n", .{path})).reply.status); + try testing.expectEqualStrings(" 0 3 ", rd(p, dot, 0, 64).bytes); + // file:N selects the line; a line past the end moves nothing and reads back empty. + try testing.expectEqual(Status.ok, wr(p, look_node, try std.fmt.bufPrint(&line, "{s}:2\n", .{path})).reply.status); + try testing.expectEqualStrings(" 12 16 ", rd(p, dot, 0, 64).bytes); + try testing.expectEqual(Status.ok, wr(p, look_node, try std.fmt.bufPrint(&line, "{s}:99\n", .{path})).reply.status); + try testing.expectEqualStrings(" 12 16 ", rd(p, dot, 0, 64).bytes); + try testing.expectEqualStrings("", rd(p, look_node, 0, 64).bytes); + try testing.expectStringStartsWith(p.panes[0].?.msg[0..p.panes[0].?.msg_len], "Look: "); +} + test "exec runs a builtin at the pane and records the pane it acted on" { const gpa = testing.allocator; const p = try withFile(gpa, "Msg fs-ran\n"); @@ -886,6 +1080,10 @@ test "get reloads the pane from disk and reports a missing file without losing d _ = wr(p, Node.of(serial, .name), try std.fmt.bufPrint(&name, "{s}/missing.txt\n", .{directory})); _ = wr(p, Node.of(serial, .body), "unsaved\n"); + // Unsaved text is asked about once, as acme's get asks winclean. + const asked = wr(p, ctl_node, "get\n"); + try testing.expectEqual(E.IO, asked.errno()); + try testing.expect(std.mem.endsWith(u8, asked.reply.ename, ": Modified (get again to discard)")); try testing.expectEqual(E.NOENT, wr(p, ctl_node, "get\n").errno()); try testing.expect(p.paneBySerial(serial) != null); try testing.expectEqualStrings("from disk\nunsaved\n", pane.file.?.content); @@ -999,7 +1197,7 @@ test "Joincol with no column to the right and Theme with no such theme say so" { try testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "no theme \"drak\"") != null); } -test "a look that fills a column's stillborn scratch is answered with the scratch already gone" { +test "a look after Newcol fills the empty column the keyboard is on" { const gpa = testing.allocator; const p = try withFile(gpa, "x\n"); defer p.deinit(); @@ -1009,14 +1207,12 @@ test "a look that fills a column's stillborn scratch is answered with the scratc var dir: [4096]u8 = undefined; const at = dir[0..try tmp.dir.realPath(testing.io, &dir)]; var line: [4200]u8 = undefined; - // Newcol makes a column of one empty +New; a file looked from it - // joins that column and takes the scratch's place. + // Newcol makes an empty column and gives its tag the keyboard; no pane + // has it, so focus reads empty. try testing.expectEqual(tree.Status.ok, wr(p, @intFromEnum(tree.TopFile.ctl), "Newcol\n").reply.status); - const scratch = p.panes[p.active].?.serial; - try testing.expectEqual(tree.Status.ok, wr(p, Node.of(scratch, .look), try std.fmt.bufPrint(&line, "{s}/f.txt\n", .{at})).reply.status); - try testing.expect(p.paneBySerial(scratch) == null); - var want: [32]u8 = undefined; - const gone = try std.fmt.bufPrint(&want, "{d} ", .{scratch}); - var rows = std.mem.splitScalar(u8, rd(p, @intFromEnum(tree.TopFile.index), 0, 4096).bytes, '\n'); - while (rows.next()) |row| try testing.expect(!std.mem.startsWith(u8, row, gone)); + try testing.expectEqual(@as(usize, 0), p.col_n[p.ncol - 1]); + try testing.expectEqualStrings("", rd(p, @intFromEnum(tree.TopFile.focus), 0, 64).bytes); + try testing.expectEqual(tree.Status.ok, wr(p, @intFromEnum(tree.TopFile.look), try std.fmt.bufPrint(&line, "{s}/f.txt\n", .{at})).reply.status); + try testing.expectEqual(@as(usize, 1), p.col_n[p.ncol - 1]); + try testing.expectEqual(p.active, p.col_panes[p.ncol - 1][0]); } |
