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.zig83
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" {