summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/fs.md8
-rw-r--r--src/look.zig45
-rw-r--r--src/ninep/ctl.zig19
3 files changed, 68 insertions, 4 deletions
diff --git a/docs/fs.md b/docs/fs.md
index 49f4e4f1..fbf54c3e 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -466,9 +466,11 @@ unfinished was cut short. A fragment never runs on its
own. A line or Edit block held past 1 MiB is refused (EINVAL).
`@p<serial>:<address>` addresses the pane with that serial as `file:<address>`
-does a file, with any sam address (`/re/`, `#n`, `$`, `0/re/`, a range); a
-miss, and a serial no pane has, each log an `err`, and look reads back
-empty. A look takes acme's addresses after a colon (editors/acme/look.c:450): a
+does a file, with any sam address (`/re/`, `#n`, `$`, `0/re/`, a range),
+a terminal too: its body is then the text of its logical lines (the lines
+a `+Search` of it lists), addressed from its cursor, and the match is
+selected. A miss, and a serial no pane has (with a line, `@p77:3`, as
+with any other address), each log an `err`, and look reads back empty. 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. **The
address is evaluated from the file's dot**, as acme's is: `file:/re/`
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<terminal>:<address>`: any address over the terminal's body as its
+/// logical lines read (the lines a +Search lists as `@p<serial>:<line>`),
+/// 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<serial>:<address> takes any sam address; a miss and an unknown serial a
try testing.expectEqualStrings("", rd(p, root, 0, 64).bytes);
}
+test "@p<terminal>:<address> 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"));
+}