From 9ecabb15b648237cca94253a7abc4b556805fde0 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 07:11:20 -0300 Subject: `@p:
` takes any address over the terminal's logical lines Only a line number reached a terminal (the line way); any other address went to lookAddress, which wanted a file and said the terminal was no text. The body is now the lines a +Search of it lists, addressed from the cursor, and the match selected as a +Search row's range is. Co-Authored-By: Claude Opus 5.5 --- src/look.zig | 45 ++++++++++++++++++++++++++++++++++++++++++++- src/ninep/ctl.zig | 19 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/look.zig b/src/look.zig index 28986552..cfebd42d 100644 --- a/src/look.zig +++ b/src/look.zig @@ -1300,7 +1300,10 @@ fn lookAddress(p: *Pardes, id: usize, path: []const u8, expr: []const u8) void { if (target == before and target != id) return; } const pane = p.panes[target] orelse return; - const f = pane.file orelse return missed(p, id, "{s} is no text to address", .{path[0..@min(path.len, 64)]}); + const f = pane.file orelse { + if (by_serial and pane.isTerminal()) return lookTerminalAddress(p, id, target, expr); + return missed(p, id, "{s} is no text to address", .{path[0..@min(path.len, 64)]}); + }; if (!by_serial and 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 { @@ -1314,6 +1317,46 @@ fn lookAddress(p: *Pardes, id: usize, path: []const u8, expr: []const u8) void { p.active = target; } +/// `@p:
`: any address over the terminal's body as its +/// logical lines read (the lines a +Search lists as `@p:`), +/// from its cursor; the match is selected there, as a +Search row's is. +fn lookTerminalAddress(p: *Pardes, id: usize, target: usize, expr: []const u8) void { + const Addr = @import("ninep/addr.zig").Addr; + const pane = p.panes[target].?; + const lines = edit.paneCursorLines(p, &pane.body) catch return missed(p, id, "{s}", .{"out of memory"}); + var text: std.ArrayList(u8) = .empty; + const arena = p.scratch.allocator(); + const row: usize = @intCast(@max(0, pane.body.cur_row)); + var dot: u32 = 0; + for (lines, 0..) |ln, i| { + if (i == row) dot = @intCast(text.items.len + @min(ln.len, @as(usize, @intCast(@max(0, pane.body.cur_col))))); + text.appendSlice(arena, ln) catch return; + text.append(arena, '\n') catch return; + } + var a: Addr = .{ .text = text.items, .lim = null, .expr = expr }; + const r = a.address(.{ .q0 = dot, .q1 = dot }) orelse return missed(p, id, "{s}", .{a.err}); + if (a.i < expr.len or r.q0 > r.q1) + return missed(p, id, "{s}", .{if (r.q0 > r.q1) "addresses out of order" else "bad address syntax"}); + const from = spotAt(text.items, r.q0); + var at: Spot = .{ .line = from.line, .col = from.col }; + if (r.q1 > r.q0) { + // The last byte of the match, 1-based and inclusive, as a row's range. + const to = spotAt(text.items, r.q1 - 1); + at.end_line = to.line; + at.end_col = to.col; + } + clearNavigationSelection(p, pane); + focusPaneLine(p, target, at, .center); +} + +/// The 1-based line and column of byte `q` of `text`. +fn spotAt(text: []const u8, q: u32) struct { line: usize, col: usize } { + const upto = text[0..@min(q, text.len)]; + const line = std.mem.count(u8, upto, "\n") + 1; + const start = if (std.mem.lastIndexOfScalar(u8, upto, '\n')) |nl| nl + 1 else 0; + return .{ .line = line, .col = upto.len - start + 1 }; +} + pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void { const pane = p.panes[id] orelse return; p.noteHaptic(.look); diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 47ac2846..acb1b934 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -1969,3 +1969,22 @@ test "@p:
takes any sam address; a miss and an unknown serial a try testing.expectEqualStrings("", rd(p, root, 0, 64).bytes); } +test "@p:
takes any address over the terminal's lines, selecting the match" { + const p = try th.withTerm(testing.allocator); + defer p.deinit(); + const serial = serialOf(p); + const id = p.paneBySerial(serial).?; + p.update(.{ .output = .{ .pane = @intCast(id), .bytes = "one\r\ntwo MARK three\r\n" } }); + const pane = p.panes[id].?; + const root = @intFromEnum(tree.TopFile.look); + var line: [64]u8 = undefined; + try testing.expectEqual(Status.ok, wr(p, root, try std.fmt.bufPrint(&line, "@p{d}:0/MARK/\n", .{serial})).reply.status); + try testing.expectEqual(@as(i32, 1), pane.body.cur_row); + try testing.expect(pane.body.vsel.active); + // #n lands on that byte: #5 is the `w` of `two`. + try testing.expectEqual(Status.ok, wr(p, root, try std.fmt.bufPrint(&line, "@p{d}:#5\n", .{serial})).reply.status); + try testing.expectEqual(@as(i32, 1), pane.body.cur_row); + try testing.expectEqual(@as(i32, 1), pane.body.cur_col); + _ = wr(p, root, try std.fmt.bufPrint(&line, "@p{d}:/zzqq/\n", .{serial})); + try testing.expect(th.logHas(p, "look: no match for regexp")); +} -- cgit v1.3