diff options
Diffstat (limited to 'src/ninep/ctl.zig')
| -rw-r--r-- | src/ninep/ctl.zig | 83 |
1 files changed, 75 insertions, 8 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index b93d83c3..397f3339 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -52,7 +52,7 @@ fn noteResult(p: *Pardes, serial: u32) void { /// created; failing that, a look records the pane it focused and an exec /// the pane it acted on (even one it closed). pub fn run(p: *Pardes, id: usize, exec: bool, text: []const u8, in_tag: bool) void { - const context = p.panes[id].?.serial; + const context = if (p.panes[id]) |pane| pane.serial else 0; // A pane an event reader holds hears its clicks instead of acting on // them (editors/acme/look.c:53 and exec.c:173 send them to winevent), // and a line written to a look or exec is a click: on that pane, or for @@ -260,6 +260,7 @@ pub fn resultsLen(p: *Pardes) u64 { var n: u64 = 0; for (p.fs.results[0..p.fs.results_len]) |serial| { var digits: [16]u8 = undefined; + // unreachable: a u32 serial and a newline fit 16 n += (std.fmt.bufPrint(&digits, "{d}\n", .{serial}) catch unreachable).len; } return n; @@ -446,7 +447,7 @@ pub fn runBuiltin(p: *Pardes, req: Req, id: usize, line: []const u8) ?Reply { defer p.fs.no_prompt = false; // What a ctl write touched reads back from /look and /exec as a click's // does (`run`), so a script that made a pane with `Tty` finds it there. - const context = p.panes[id].?.serial; + const context = if (p.panes[id]) |pane| pane.serial else 0; const before = Serials.snapshot(p); _ = exec_line.executeBuiltinLine(p, id, line); const after = Serials.snapshot(p); @@ -580,7 +581,8 @@ pub fn writeRoot(p: *Pardes, req: Req) Reply { if (checkBuiltin(p, req, line, .session)) |refusal| return refusal; continue; } - if (p.panes[p.active] == null) return Reply.fail(req.tag, E.NOENT); + // An empty window (no pane at all) still takes its session words. + if (p.panes[p.active] == null and p.anyPane() != 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; @@ -1731,8 +1733,8 @@ test "Shell refuses a path that is no executable, and bare it goes back to the d const root_ctl = @intFromEnum(tree.TopFile.ctl); const refused = wr(p, root_ctl, "Shell /nonexistent/zzsh\n"); try testing.expectEqual(Status.err, refused.reply.status); - try testing.expect(std.mem.indexOf(u8, refused.reply.ename, "Shell: no shell \"/nonexistent/zzsh\"") != null); - try testing.expect(th.logHas(p, "no shell \"/nonexistent/zzsh\"")); + try testing.expect(std.mem.indexOf(u8, refused.reply.ename, "Shell: shell \"/nonexistent/zzsh\" not found") != null); + try testing.expect(th.logHas(p, "shell \"/nonexistent/zzsh\" not found")); try testing.expectEqual(E.IO, wr(p, root_ctl, "Shell /etc\n").errno()); try testing.expect(th.logHas(p, "Shell: not a shell: /etc is a directory")); try testing.expectEqualStrings("", p.settings.shell.requested.get()); @@ -1922,10 +1924,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: no match, 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: no match, 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); @@ -2096,7 +2098,7 @@ test "a language server question that finds nothing, cannot be answered, or is a p.lspRequest(id, .incoming_calls, ""); try testing.expect(p.lsp_wait != null); p.lspResponse(p.lsp_wait.?.id, ""); - try testing.expectEqualStrings("Callers: no callers, not found", p.fs.late_failure[0..p.fs.late_failure_len]); + try testing.expectEqualStrings("Callers: callers not found", p.fs.late_failure[0..p.fs.late_failure_len]); // A backend that cannot answer says why. p.fs.late_failure_len = 0; p.lspRequest(id, .outgoing_calls, ""); @@ -2140,6 +2142,23 @@ test "a corrupt image fails its look with an err, and no pane is made" { try testing.expectEqual(count, after); } +test "a truncated PDF fails its look with its path and why" { + if (comptime !pardes.pdf_enabled) return; + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + const bytes = try @import("mupdf").makeLinksTestPdf(testing.allocator); + defer testing.allocator.free(bytes); + try tmp.dir.writeFile(testing.io, .{ .sub_path = "cut.pdf", .data = bytes[0..40] }); + var dir_buf: [4096]u8 = undefined; + const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)]; + var line: [4200]u8 = undefined; + const r = wr(p, root_look, try std.fmt.bufPrint(&line, "{s}/cut.pdf\n", .{dir})); + try testing.expectEqual(Status.err, r.reply.status); + try testing.expect(std.mem.indexOf(u8, r.reply.ename, "cut.pdf: not a PDF pardes can read: ") != null); +} + test "a PDF's body reads its page's text layer, and takes no write" { if (comptime !pardes.pdf_enabled) return; const p = try withFile(testing.allocator, "x\n"); @@ -2201,6 +2220,20 @@ test "+Unsaved drops a pane once it has closed" { try testing.expect(std.mem.indexOf(u8, list.file.?.content, "/tmp/pardes-unsaved-a.txt: Modified") == null); } +test "a buffer whose file was deleted on disk is dirty, and Del asks first" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + const serial = serialOf(p); + try testing.expectEqualStrings("0", std.mem.trimEnd(u8, rd(p, Node.of(serial, .dirty), 0, 8).bytes, "\n")); + pardes.panes.File.deleted(p, @intCast(p.paneBySerial(serial).?)); + try testing.expectEqualStrings("1", std.mem.trimEnd(u8, rd(p, Node.of(serial, .dirty), 0, 8).bytes, "\n")); + try testing.expectEqual(Status.err, wr(p, Node.of(serial, .ctl), "Del\n").reply.status); + try testing.expect(p.paneBySerial(serial) != null); + _ = wr(p, Node.of(serial, .ctl), "Del\n"); + p.sync(); + try testing.expect(p.paneBySerial(serial) == null); +} + 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(); @@ -2512,6 +2545,33 @@ test "Recent keeps only files on disk: not a name never saved, nor the served tr try testing.expect(p.recent.get(names[2]) != null); } +test "a new file saved shows in /recent, open, while it is" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + var dir_buf: [4096]u8 = undefined; + const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)]; + // A host that writes files, as the real ones do. + const Writing = struct { + fn write(_: ?*anyopaque, _: u8, path: []const u8, bytes: []const u8) void { + std.Io.Dir.cwd().writeFile(testing.io, .{ .sub_path = path, .data = bytes }) catch {}; + } + }; + p.host = .{ .vtable = &.{ .write_file = Writing.write } }; + defer p.host = .{}; + const made = try th.newPane(p); + var line: [4200]u8 = undefined; + const path = try std.fmt.bufPrint(&line, "{s}/new.txt", .{dir}); + var name: [4200]u8 = undefined; + _ = wr(p, Node.of(made, .name), try std.fmt.bufPrint(&name, "{s}\n", .{path})); + _ = wr(p, Node.of(made, .body), "fresh\n"); + try testing.expectEqual(Status.ok, wr(p, Node.of(made, .ctl), "Save\n").reply.status); + p.perform(.{ .save_file = .{ .pane = @intCast(p.paneBySerial(made).?) } }); + var want: [4300]u8 = undefined; + try testing.expect(std.mem.indexOf(u8, rd(p, @intFromEnum(tree.TopFile.recent), 0, 8192).bytes, try std.fmt.bufPrint(&want, "open {s}\n", .{path})) != null); +} + test "/recent shows a name as /index does, a newline in it escaped" { const p = try withFile(testing.allocator, "x\n"); defer p.deinit(); @@ -2584,6 +2644,13 @@ test "a remembered place is clamped to the file as it is now, its selection's tw try testing.expect(back.body.vsel.active); // Its ends clamped to the last character, `two`'s `o`, not its newline. try testing.expect(back.body.cur_col <= 2 and back.body.vsel.col <= 2); + try testing.expect(std.mem.indexOf(u8, back.msg[0..back.msg_len], "c.txt has 2 lines now: at its last") != null); + // The same row with the file open: the same place, said the same way. + _ = wr(p, Node.of(rows.serial, .look), loc); + p.sync(); + try testing.expectEqual(back.serial, p.panes[p.active].?.serial); + try testing.expect(back.body.cur_row <= 1); + try testing.expect(std.mem.indexOf(u8, back.msg[0..back.msg_len], "at its last") != null); } test "size is monotonic: growing is never refused, and a size once taken is taken again" { |
