diff options
| -rw-r--r-- | src/look.zig | 82 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 33 | ||||
| -rw-r--r-- | src/ninep/testing.zig | 7 |
3 files changed, 88 insertions, 34 deletions
diff --git a/src/look.zig b/src/look.zig index a29b27b5..fe5a3ac1 100644 --- a/src/look.zig +++ b/src/look.zig @@ -1302,6 +1302,41 @@ fn addressed(tok: []const u8) ?struct { path: []const u8, expr: []const u8 } { return .{ .path = tok[0..colon], .expr = expr }; } +/// A look at text that names nothing to open: a `./` name is a missing +/// file, anything else a search for the text as written. +fn lookUnresolved(p: *Pardes, id: usize, pane: *Pane, trimmed: []const u8) void { + // A `./` or `../` name opened nothing: said, ENOENT, not searched + // for as a word. What is missing is the pane's directory when + // that is gone, else the file named. + if (filesystem.dotRelative(trimmed)) { + const dir = Pardes.paneDir(pane); + var said: [4200]u8 = undefined; + const gone = if (comptime pardes.hosted) if (filesystem.localPath(dir)) |local| !exec.isDirectory(local) else false else true; + if (gone) return p.reportFailure(id, std.fmt.bufPrint(&said, "look: {s}: no such directory", .{dir[0..@min(dir.len, 4096)]}) catch "look: no such directory"); + var cut: [miss_path_cap + "…".len]u8 = undefined; + return p.reportFailure(id, std.fmt.bufPrint(&said, "look: {s}: no such file", .{missText(trimmed, &cut)}) catch "look: no such file"); + } + // A raw terminal's clicks are its program's; a look written over + // 9P is no keyboard input, and searches whatever the key mode. + if (pane.image != null or (pane.body.mode == .tty and !p.fs.serving)) return; + if (p.settings.look_word == .search and pane.file != null) return lookWord(p, id, pane, trimmed); + const rows = runSearch(p, id, trimmed, .text, .cursor, false) catch |err| { + p.reportError(id, "search", err); + return; + }; + if (rows == 0) { + var cut: [miss_path_cap + "…".len]u8 = undefined; + return missed(p, id, "no match for \"{s}\"", .{missText(trimmed, &cut)}); + } + clearNavigationSelection(p, pane); + const at = pane.search_row; + _ = searchStep(p, id, 1); + if (at != null and pane.search_row == at) { + pane.search_row = null; + _ = searchStep(p, id, 1); + } +} + /// Opens (or finds) `path` as Look would, `id`'s own pane when it is empty, /// and selects what `expr` addresses there from its dot. An address that /// does not evaluate moves nothing and says why (acme: no jump). @@ -1321,9 +1356,18 @@ fn lookAddress(p: *Pardes, id: usize, path: []const u8, expr: []const u8) void { const by_serial = std.mem.startsWith(u8, path, config.pane_addr); if (by_serial and open == null) return missed(p, id, "no pane {s}", .{path[0..@min(path.len, 32)]}); if (open) |o| target = o else if (path.len > 0) { + // A path that names nothing to open: the whole of what was written + // is looked for, as `zzq:2` is, and a miss quotes it as written. + if (!by_serial and resolveLookTarget(p, id, path, &realbuf) == .none) + return lookUnresolved(p, id, p.panes[id] orelse return, path.ptr[0 .. path.len + 1 + expr.len]); const before = p.active; + const failures = p.fs.failures; + const was_missed = p.look_missed; lookAt(p, id, path); target = p.active; + // Look said why it opened nothing (its placement refused, say): + // that one failure is the look's, no miss of the address after it. + if (p.fs.failures != failures or (p.look_missed and !was_missed)) return; // The path opened nothing (a search, or no such file): Look said so. if (target == before and target != id) return; } @@ -1484,39 +1528,7 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void { pane.sel = @splat(.{}); } switch (found) { - .none => { - // A `./` or `../` name opened nothing: said, ENOENT, not searched - // for as a word. What is missing is the pane's directory when - // that is gone, else the file named. - if (filesystem.dotRelative(trimmed)) { - const dir = Pardes.paneDir(pane); - var said: [4200]u8 = undefined; - const gone = if (comptime pardes.hosted) if (filesystem.localPath(dir)) |local| !exec.isDirectory(local) else false else true; - if (gone) return p.reportFailure(id, std.fmt.bufPrint(&said, "look: {s}: no such directory", .{dir[0..@min(dir.len, 4096)]}) catch "look: no such directory"); - var cut: [miss_path_cap + "…".len]u8 = undefined; - return p.reportFailure(id, std.fmt.bufPrint(&said, "look: {s}: no such file", .{missText(trimmed, &cut)}) catch "look: no such file"); - } - // A raw terminal's clicks are its program's; a look written over - // 9P is no keyboard input, and searches whatever the key mode. - if (pane.image != null or (pane.body.mode == .tty and !p.fs.serving)) return; - if (p.settings.look_word == .search and pane.file != null) return lookWord(p, id, pane, trimmed); - const rows = runSearch(p, id, trimmed, .text, .cursor, false) catch |err| { - p.reportError(id, "search", err); - return; - }; - if (rows == 0) { - var cut: [miss_path_cap + "…".len]u8 = undefined; - return missed(p, id, "no match for \"{s}\"", .{missText(trimmed, &cut)}); - } - clearNavigationSelection(p, pane); - const at = pane.search_row; - _ = searchStep(p, id, 1); - if (at != null and pane.search_row == at) { - pane.search_row = null; - _ = searchStep(p, id, 1); - } - - }, + .none => lookUnresolved(p, id, pane, trimmed), .pane => |t| { // `@p<serial>` names no open pane: a miss, said. if (t.id >= MAX_PANES or p.panes[t.id] == null) return missed(p, id, "no pane {s}", .{pl.path[0..@min(pl.path.len, 32)]}); @@ -1573,7 +1585,11 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void { const nt = panes.File.open(p, free, target.path, target.at.line) catch |err| return p.reportError(id, if (@import("Messages.zig").dialReason(err) != null) target.path else "look", err); if (target.at.col > 0) nt.body.cur_col = @intCast(target.at.col - 1); + const failures = p.fs.failures; exec.placeNew(p, id, id, free, .doc); + // Refused a place: that is the look's one failure, not a + // miss of its line too. + if (p.fs.failures != failures) return; layout.compute(p); nt.file.?.scroll -|= @max(1, p.rects[free].h -| BOX_H) / 2; if (target.at.end_line != 0) { diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 59a80f36..5bf354eb 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -1752,6 +1752,39 @@ test "a look after Newcol fills the empty column the keyboard is on" { try testing.expectEqual(p.active, p.col_panes[p.ncol - 1][0]); } +test "a look at file:addr whose file is not there is one miss, quoting what was written, in a terminal too" { + const p = try th.withTerm(testing.allocator); + defer p.deinit(); + const looked = Node.of(serialOf(p), .look); + for ([_][]const u8{ "zzq:#3", "zzq:2", "zzq:/re/" }) |written| { + var line: [64]u8 = undefined; + _ = wr(p, looked, try std.fmt.bufPrint(&line, "{s}\n", .{written})); + var want: [64]u8 = undefined; + try testing.expectEqual(@as(usize, 1), th.logCount(p, try std.fmt.bufPrint(&want, "look: no match for \"{s}\"\n", .{written}))); + } + try testing.expectEqual(@as(usize, 3), th.logCount(p, "\nerr ")); +} + +test "a look whose pane is refused a place fails with that alone, not a missing line too" { + const gpa = testing.allocator; + // A screen with rows for one pane only. + const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 80, .rows = 5 }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + _ = try p.setTestFile("x\n"); + while (p.nextEffect()) |_| {} + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + try tmp.dir.writeFile(testing.io, .{ .sub_path = "f.txt", .data = "one\ntwo\n" }); + var dir: [4096]u8 = undefined; + const at = dir[0..try tmp.dir.realPath(testing.io, &dir)]; + var line: [4200]u8 = undefined; + const r = wr(p, root_look, try std.fmt.bufPrint(&line, "{s}/f.txt:2\n", .{at})); + try testing.expectEqual(Status.err, r.reply.status); + try testing.expect(std.mem.indexOf(u8, r.reply.ename, "no space for a pane") != null); + try testing.expectEqual(@as(usize, 1), th.logCount(p, "\nerr ")); +} + test "a look under pardes's placement, from a pane pane/new made after Delcol, opens its file" { const gpa = testing.allocator; const p = try withFile(gpa, "x\n"); diff --git a/src/ninep/testing.zig b/src/ninep/testing.zig index 927552e2..b8eaa02b 100644 --- a/src/ninep/testing.zig +++ b/src/ninep/testing.zig @@ -58,11 +58,16 @@ pub fn rd(p: *Pardes, node: u64, off: u64, size: u32) Answer { /// Whether the log, read whole from a fresh open, holds `needle`. pub fn logHas(p: *Pardes, needle: []const u8) bool { + return logCount(p, needle) > 0; +} + +/// How many times the log, read whole from a fresh open, holds `needle`. +pub fn logCount(p: *Pardes, needle: []const u8) usize { const log = @intFromEnum(tree.TopFile.log); const h = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle; defer _ = call(p, .{ .tag = 3, .op = .release, .node = log, .handle = h }); const text = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = h, .size = 1 << 16 }).bytes; - return std.mem.indexOf(u8, text, needle) != null; + return std.mem.count(u8, text, needle); } pub fn wr(p: *Pardes, node: u64, data: []const u8) Answer { |
