summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 19:59:47 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit1f8de151e382755c1ce597f166ea4552e7f7e8d1 (patch)
tree1ed05b6c528dd9db099a249504fdc3b4083fb72f
parent16fe974c7426925f1b7ffc516f149cc1f4ffa390 (diff)
downloadpardes-1f8de151e382755c1ce597f166ea4552e7f7e8d1.tar.gz
pardes-1f8de151e382755c1ce597f166ea4552e7f7e8d1.zip
A look with a blank in its pattern addresses, file:N selects the line, and a miss is said, logged and reads back empty
Round 6: look calc.py:/return a/ was a word search, since a blank ended the address; calc.py:99 past the end focused the file and said nothing; and /look still read back the pane before a miss. A pattern after the colon may now hold blanks; file:N selects the line, newline included, as acme's does; a line past the end opens and focuses nothing; and every miss (no match, a bad address, a line past the end) says so, logs err <serial> look: why, keeps the selection, and leaves look reading back empty. Documented, with file:0/re/ for the first match, since acme evaluates the address from the file's dot. Five goldens change: a look at file:N now selects the line. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--.agents/skills/pardes-9p/SKILL.md4
-rw-r--r--docs/fs.md18
-rw-r--r--src/look.zig88
-rw-r--r--src/ninep/ctl.zig15
-rw-r--r--src/pardes.zig2
-rw-r--r--test/snapshots/look-center.golden6
-rw-r--r--test/snapshots/look-file-1col.golden2
-rw-r--r--test/snapshots/look-file.golden4
-rw-r--r--test/snapshots/lookloaded.golden4
-rw-r--r--test/snapshots/rsearch.golden2
10 files changed, 108 insertions, 37 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md
index d34fa8b3..1be910c1 100644
--- a/.agents/skills/pardes-9p/SKILL.md
+++ b/.agents/skills/pardes-9p/SKILL.md
@@ -54,7 +54,9 @@ $m/README the served guide, worth reading first
$m/index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name
$m/status pid, version, panes
$m/look write a line = a right click on it at the active pane (file:/re/, file:#n, :/re/
- select by address, as acme's look; a miss changes nothing)
+ select by address, as acme's look, from the file's dot: file:0/re/ for the
+ first match; file:N selects the line; a miss changes nothing, logs err, and
+ look reads back empty)
$m/exec write a line = a middle click: an editor command word, or a shell line
$m/pane/<n>/pty/run write one line, read `exit N` + its output, or `busy` / `error ...`, on the same open:
exec 3<>$m/pane/$n/pty/run; echo make >&3; cat <&3
diff --git a/docs/fs.md b/docs/fs.md
index f6516ea1..a489e1ea 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -309,12 +309,18 @@ not the hits buffer it opened.
A look takes acme's addresses after a colon (editors/acme/look.c:450): a
line written to `look` as `file:/re/`, `file:#n`, `file:$` or any address
-opens (or finds) the file and selects what the address names, evaluated
-from its dot; `:addr` does the same in the pane itself. `file:12` and
-`file:12:5` stay line and column. A bare `/re/` is a path, as in acme, and
-failing that a search for its text. A look that finds nothing, or an
-address that does not evaluate, says so on the message row and changes
-nothing: no buffer opens and the selection stays.
+opens (or finds) the file and selects what the address names. **The
+address is evaluated from the file's dot**, as acme's is: `file:/re/`
+finds the next match after the current selection, not the first in the
+file. For the first, start at the top: `file:0/re/` (or `file:#0/re/`).
+`:addr` does the same in the pane itself, and a pattern may hold blanks
+(`calc.py:/return a/`). `file:12` selects line 12, its newline included,
+as acme's does; `file:12:5` puts the caret at line 12, column 5. A bare
+`/re/` is a path, as in acme, and failing that a search for its text. A
+look that finds nothing, an address that does not evaluate, or a line past
+the file's end (`calc.py:99`) says so on the message row and in the log
+(`err <serial> look: ...`), focuses and opens nothing, keeps the
+selection, and leaves `look` reading back empty.
`/pane/<n>/name` reads the pane's file name (a terminal's directory) and
writing it renames the buffer; a relative name resolves against the pane's
diff --git a/src/look.zig b/src/look.zig
index 020aacb8..6cf01dd2 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -1142,14 +1142,49 @@ pub fn followPdfLink(p: *Pardes, id: usize, probe: *const panes.Pdf.WordProbe) v
}
}
+/// A look that found nothing: said on the message row, logged as an `err`
+/// record, and marked so that /look reads back nothing (ninep/ctl.zig run).
+fn missed(p: *Pardes, id: usize, comptime fmt: []const u8, args: anytype) void {
+ p.look_missed = true;
+ var said: [160]u8 = undefined;
+ const why = std.fmt.bufPrint(&said, fmt, args) catch "no match";
+ var msg: [176]u8 = undefined;
+ p.setMessage(id, std.fmt.bufPrint(&msg, "Look: {s}", .{why}) catch "Look: no match");
+ const serial = if (p.panes[id]) |pane| pane.serial else 0;
+ var rec: [200]u8 = undefined;
+ pardes.ctlfs.events.notePath(p, "err", std.fmt.bufPrint(&rec, "{d} look: {s}", .{ serial, why }) catch return);
+}
+
+/// After a look at `path:N` focused its pane: a line past the end focuses
+/// nothing, leaves no pane it opened, and is a miss; a bare line number
+/// selects the line, its newline included, as acme's does.
+fn settleLine(p: *Pardes, id: usize, path: []const u8, at: Spot, before: usize, opened: ?usize) void {
+ const shown = p.panes[p.active] orelse return;
+ const f = if (shown.file) |*file| file else return;
+ if (at.line > panes.File.nlines(p.gpa, f)) {
+ if (opened) |free| p.removePane(free, null) catch {};
+ if (p.panes[before] != null) p.active = before;
+ return missed(p, id, "{s} has no line {d}", .{ path, at.line });
+ }
+ if (at.line > 0 and at.col == 0 and at.end_line == 0) {
+ const start = modal.lineStartOffset(f.content, at.line - 1);
+ const next = std.mem.indexOfScalarPos(u8, f.content, start, '\n');
+ const end = if (next) |i| i + 1 else f.content.len;
+ @import("ninep/pane.zig").setDot(shown, .{ .q0 = @intCast(start), .q1 = @intCast(end) });
+ }
+}
+
/// `file:addr` or `:addr`, acme's look.c:450-543: a colon followed by an
/// address character (addr.c:28, isaddrc) that is not a line spot this
/// parser already reads (`file:12`, `file:12:5`). Not a URL.
fn addressed(tok: []const u8) ?struct { path: []const u8, expr: []const u8 } {
if (std.mem.indexOf(u8, tok, "://") != null) return null;
- if (std.mem.indexOfAny(u8, tok, " \t") != null) return null;
const colon = std.mem.indexOfScalar(u8, tok, ':') orelse return null;
const expr = tok[colon + 1 ..];
+ // A written look is a whole line: the path has no blanks, but a
+ // pattern in the address may (`calc.py:/return a/`).
+ if (std.mem.indexOfAny(u8, tok[0..colon], " \t") != null) return null;
+ if (std.mem.indexOfAny(u8, expr, " \t") != null and std.mem.indexOfAny(u8, expr, "/?") == null) return null;
if (expr.len == 0 or std.mem.indexOfScalar(u8, "0123456789+-/$.#,;", expr[0]) == null) return null;
if (std.ascii.isDigit(expr[0])) {
const pl = parsePathLine(tok);
@@ -1177,12 +1212,10 @@ fn lookAddress(p: *Pardes, id: usize, path: []const u8, expr: []const u8) void {
if (path.len > 0 and (f.output != null or !std.mem.endsWith(u8, f.path, std.fs.path.basename(path)))) return;
var a: Addr = .{ .text = f.content, .lim = null, .expr = expr };
const r = a.address(ninep_pane.dotOf(pane)) orelse {
- var said: [128]u8 = undefined;
- return p.setMessage(id, std.fmt.bufPrint(&said, "Look: {s}", .{a.err}) catch "Look: bad address");
+ return missed(p, id, "{s}", .{a.err});
};
if (a.i < expr.len or r.q0 > r.q1) {
- var said: [128]u8 = undefined;
- return p.setMessage(id, std.fmt.bufPrint(&said, "Look: {s}", .{if (r.q0 > r.q1) "addresses out of order" else "bad address syntax"}) catch "Look: bad address");
+ return missed(p, id, "{s}", .{if (r.q0 > r.q1) "addresses out of order" else "bad address syntax"});
}
clearNavigationSelection(p, pane);
ninep_pane.setDot(pane, r);
@@ -1209,6 +1242,10 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void {
if (comptime pdf_enabled) if (panes.Pdf.lookSection(p, id, pl.path, pl.at)) return;
var realbuf: [4096]u8 = undefined;
const found = resolveLookTarget(p, id, txt, &realbuf);
+ // A line past the end of an open pane is a miss (the target's line is
+ // already clamped, so the one written is asked).
+ if (found == .pane and found.pane.id < MAX_PANES) if (p.panes[found.pane.id]) |shown| if (shown.file) |*f| if (pl.at.line > panes.File.nlines(p.gpa, f))
+ return missed(p, id, "{s} has no line {d}", .{ trimmed, pl.at.line });
// A miss changes nothing, the selection included; anything else starts
// from a clean one.
if (found != .none) clearNavigationSelection(p, pane);
@@ -1219,10 +1256,7 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void {
p.reportError(id, "search", err);
return;
};
- if (rows == 0) {
- var said: [96]u8 = undefined;
- return p.setMessage(id, std.fmt.bufPrint(&said, "Look: no match for \"{s}\"", .{@import("Messages.zig").clip(trimmed, 48)}) catch "Look: no match");
- }
+ if (rows == 0) return missed(p, id, "no match for \"{s}\"", .{@import("Messages.zig").clip(trimmed, 48)});
clearNavigationSelection(p, pane);
const at = pane.search_row;
_ = searchStep(p, id, 1);
@@ -1235,8 +1269,10 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void {
.pane => |t| {
if (t.id >= MAX_PANES) return;
const target = p.panes[t.id] orelse return;
+ const before = p.active;
clearNavigationSelection(p, target);
focusPaneLine(p, t.id, t.at, .center);
+ settleLine(p, id, trimmed, t.at, before, null);
},
.url => |u| if (u.len <= 256) p.emit(.{ .open_link = .from(u) }),
.dir => |dir| {
@@ -1267,20 +1303,30 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void {
exec.placeDoc(p, id, free, nt);
return;
};
+ // An open pane on this path too short for the line: a miss that
+ // touches nothing.
+ if (openPaneTarget(p, target.path, target.at)) |open| if (p.panes[open.pane.id].?.file) |*f|
+ if (target.at.line > panes.File.nlines(p.gpa, f))
+ return missed(p, id, "{s} has no line {d}", .{ target.path, target.at.line });
// focus an existing pane on this path (rescrolled), else open
- if (focusPaneByPath(p, target.path, target.at)) return;
- const free = p.freeSlot() orelse return p.reportError(id, "look", error.NoPaneSlots);
- const nt = panes.File.open(p, free, target.path, target.at.line) catch |err|
- return p.reportError(id, "look", err);
- if (target.at.col > 0) nt.body.cur_col = @intCast(target.at.col - 1);
- exec.placeDoc(p, id, free, nt);
- layout.compute(p);
- nt.file.?.scroll -|= @max(1, p.rects[free].h -| BOX_H) / 2;
- if (target.at.end_line != 0) {
- const centered = nt.file.?.scroll;
- selectSpan(p, nt, target.at);
- nt.file.?.scroll = centered;
+ const before = p.active;
+ var opened: ?usize = null;
+ if (!focusPaneByPath(p, target.path, target.at)) {
+ const free = p.freeSlot() orelse return p.reportError(id, "look", error.NoPaneSlots);
+ const nt = panes.File.open(p, free, target.path, target.at.line) catch |err|
+ return p.reportError(id, "look", err);
+ if (target.at.col > 0) nt.body.cur_col = @intCast(target.at.col - 1);
+ exec.placeDoc(p, id, free, nt);
+ layout.compute(p);
+ nt.file.?.scroll -|= @max(1, p.rects[free].h -| BOX_H) / 2;
+ if (target.at.end_line != 0) {
+ const centered = nt.file.?.scroll;
+ selectSpan(p, nt, target.at);
+ nt.file.?.scroll = centered;
+ }
+ opened = free;
}
+ settleLine(p, id, target.path, target.at, before, opened);
},
.image => |target| {
if (focusPaneByPath(p, target.path, .{})) return;
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index b11692e0..edf81cd9 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -69,10 +69,13 @@ 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) {
@@ -892,6 +895,18 @@ test "a look takes acme's :addr and file:addr, and a miss changes nothing" {
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" {
diff --git a/src/pardes.zig b/src/pardes.zig
index 55c8de8e..a85c97ab 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -4102,6 +4102,8 @@ pub const Pardes = struct {
last_click: ?struct { id: usize, on_tag: bool, row: i32, col: i32, ms: i64 } = null,
/// A B1 chord's edits share the undo point it took (mouse.zig).
chord_hold: bool = false,
+ /// The look just run found nothing (look.zig missed).
+ look_missed: bool = false,
look_hover_wait: ?look.LookHoverWait = null,
look_hover_preview: ?look.LookHoverPreview = null,
pdf_hover_preview: if (pdf_enabled) ?look.PdfWordPreview else void = if (pdf_enabled) null else {},
diff --git a/test/snapshots/look-center.golden b/test/snapshots/look-center.golden
index 3eaa819b..e5db101e 100644
--- a/test/snapshots/look-center.golden
+++ b/test/snapshots/look-center.golden
@@ -1,4 +1,4 @@
-== snap center-open grid=150x31 cursor=7,12
+== snap center-open grid=150x31 cursor=15,12
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
| /tmp/pardes-snap/look-center/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/look-center/cwd Tty+bash Save Mode Filter Collapse Del
@@ -19,7 +19,7 @@
| /tmp/pardes-snap/look-center/cwd Tty+bash Save Mode Filter Collapse Del
| ls
| big.txt
-== snap center-dedup grid=150x31 cursor=7,12
+== snap center-dedup grid=150x31 cursor=15,12
|7: 145 line 145
|8: 146 line 146 big.txt:150 big.txt:2
|9: 147 line 147
@@ -30,7 +30,7 @@
|14: 152 line 152
|15: 153 line 153
|16: 154 line 154
-== snap center-clamp grid=150x31 cursor=7,8
+== snap center-clamp grid=150x31 cursor=13,8
|7: 1 line 1
|8: 2 line 2 big.txt:150 big.txt:2
|9: 3 line 3
diff --git a/test/snapshots/look-file-1col.golden b/test/snapshots/look-file-1col.golden
index f70f787b..45f9f20f 100644
--- a/test/snapshots/look-file-1col.golden
+++ b/test/snapshots/look-file-1col.golden
@@ -1,4 +1,4 @@
-== snap file-open-1col grid=100x31 cursor=7,19
+== snap file-open-1col grid=100x31 cursor=14,19
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
| /tmp/pardes-snap/look-file-1col/cwd Tty+bash Save Mode Filter Collapse Del
diff --git a/test/snapshots/look-file.golden b/test/snapshots/look-file.golden
index 19b7899d..f0690264 100644
--- a/test/snapshots/look-file.golden
+++ b/test/snapshots/look-file.golden
@@ -1,4 +1,4 @@
-== snap file-open grid=150x31 cursor=7,12
+== snap file-open grid=150x31 cursor=14,12
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
| /tmp/pardes-snap/look-file/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/look-file/cwd Tty+bash Save Mode Filter Collapse Del
@@ -32,7 +32,7 @@
|9: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|10: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|11: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-14 #f5f7ee,#3c614a, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|13: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|14: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|15: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
diff --git a/test/snapshots/lookloaded.golden b/test/snapshots/lookloaded.golden
index 8883609a..48dffe49 100644
--- a/test/snapshots/lookloaded.golden
+++ b/test/snapshots/lookloaded.golden
@@ -1,4 +1,4 @@
-== snap file-open grid=100x31 cursor=7,12
+== snap file-open grid=100x31 cursor=14,12
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
| /tmp/pardes-snap/lookloaded/cwd Tty+bash Save M /tmp/pardes-snap/lookloaded/cwd Tty+bash Save M
@@ -22,7 +22,7 @@
== snap removed grid=100x31 cursor=54,7
|5: rm big.txt; echo GO''NE
|6: /tmp/pardes-snap/lookloaded/cwd/big.txt Save Tt GONE
-== snap look-noFS grid=100x31 cursor=7,12
+== snap look-noFS grid=100x31 cursor=14,12
|7: 5 line 5
|8: 6 line 6 big.txt:10
|9: 7 line 7
diff --git a/test/snapshots/rsearch.golden b/test/snapshots/rsearch.golden
index 8312e1bc..ccf8067b 100644
--- a/test/snapshots/rsearch.golden
+++ b/test/snapshots/rsearch.golden
@@ -18,7 +18,7 @@
|23: /tmp/pardes-snap/rsearch/cwd/+Search Save Tty Collapse Del LocationsConfig
|24: 1 code.txt:4:1-8
|25: 2 pick two words
-== snap path-open grid=210x31 cursor=7,16
+== snap path-open grid=210x31 cursor=16,16
|13: /tmp/pardes-snap/rsearch/cwd/sub/dep.txt Save Tty Collapse Del
|14: 1 d1
|15: 2 d2