From d8b35f18817faaba340059cc2bd88ab3b3bc919b Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 12:33:29 -0300 Subject: addr takes a row's range, L:C-L2:C2 or L:C-C2, so a Recent, +Search or Jumplist row pastes in The rows spell a range L:C-L2:C2 (inclusive), which addr read as L:C then back L2 lines. After an L:C, a - and a number is now that range, through its last character; the docs set both spellings side by side. Co-Authored-By: Claude Opus 5.5 --- .agents/skills/pardes-9p/SKILL.md | 3 ++- docs/fs.md | 10 ++++++++- src/ninep/addr.zig | 44 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md index 4a9c0940..57b7e32c 100644 --- a/.agents/skills/pardes-9p/SKILL.md +++ b/.agents/skills/pardes-9p/SKILL.md @@ -282,7 +282,8 @@ Truncating `data` is pardes's own (acme ignores OTRUNC and always inserts). is why copying one onto another is all that acme's `addr=dot`, `dot=addr` and `limit=addr` ever were; a write may also be an address expression (`#0,#5`, `/pattern/`, `2+1`, or pardes's `12:5`: line 12, byte column 5, composing as -`12:5,14:1`), whose regexps are mvzr's searched as sam searches: +`12:5,14:1`; a Recent, +Search or Jumplist row's `12:5-14:2` is taken too, +through 14:2 inclusive), whose regexps are mvzr's searched as sam searches: `^`/`$` match at any line's start and end, `.` and `[^...]` never match a newline, the leftmost match wins (the first alternative there, not the longest). In a pattern with `\n`, `^` works only first (`^def .*\n` finds diff --git a/docs/fs.md b/docs/fs.md index 53082b11..a7e91fb3 100644 --- a/docs/fs.md +++ b/docs/fs.md @@ -659,7 +659,15 @@ column 0, is `address out of range`. Since the column counts bytes, a column a tool gives in characters (pytest's, a compiler's) is the same only on an ASCII line; elsewhere address the line and search it (`12/name/`) or use `#n`. sam would read `12:5` as a syntax -error. +error. The rows of Recent, a +Search and the Jumplist spell a range +`12:5-14:2` (or `12:5-9` on one line), and `addr` takes that too, so a row +pastes in; the two spellings side by side: + + 12:5,14:2 sam's range: from line 12 column 5 up to the point at 14:2 + 12:5-14:2 a row's range: 12:5 through the character at 14:2, inclusive + +A `-` right after `L:C` and a digit is this range, not sam's "back N +lines" from that point. A write to `data` or `xdata` replaces the range `addr` names, as acme's does (editors/acme/xfid.c:491-523: it deletes the range, then inserts), so `echo NEW > data` and `echo NEW >> data` both replace that range, and diff --git a/src/ninep/addr.zig b/src/ninep/addr.zig index 9498df19..09ef2b66 100644 --- a/src/ninep/addr.zig +++ b/src/ninep/addr.zig @@ -110,6 +110,35 @@ pub const Addr = struct { } const q = clip(modal.runeOffsetAt(a.text, .{ .row = @intCast(n - 1), .col = @intCast(col - 1) })); r = .{ .q0 = q, .q1 = q }; + // The rows' range, `L:C-L2:C2` or `L:C-C2` (Recent, + // +Search, Jumplist): through its last character, + // inclusive, as the rows mean it -- so a row pastes in. + if (a.i + 1 < a.expr.len and a.expr[a.i] == '-' and a.expr[a.i + 1] >= '1' and a.expr[a.i + 1] <= '9') { + a.i += 1; + var n2: u64 = 0; + while (a.i < a.expr.len and a.expr[a.i] >= '0' and a.expr[a.i] <= '9') : (a.i += 1) + n2 = @min(n2 * 10 + (a.expr[a.i] - '0'), std.math.maxInt(u32)); + var end_line = n; + var end_col = n2; + if (a.i + 1 < a.expr.len and a.expr[a.i] == ':' and a.expr[a.i + 1] >= '0' and a.expr[a.i + 1] <= '9') { + a.i += 1; + end_line = n2; + end_col = 0; + while (a.i < a.expr.len and a.expr[a.i] >= '0' and a.expr[a.i] <= '9') : (a.i += 1) + end_col = @min(end_col * 10 + (a.expr[a.i] - '0'), std.math.maxInt(u32)); + } + if (end_line == 0 or end_line > lines or end_col == 0) { + a.err = if (end_col == 0) e_col_zero else e_range; + return null; + } + const last = modal.runeOffsetAt(a.text, .{ .row = @intCast(end_line - 1), .col = @intCast(end_col - 1) }); + const e = clip(@min(a.text.len, last + (std.unicode.utf8ByteSequenceLength(if (last < a.text.len) a.text[last] else 'x') catch 1))); + if (e < q) { + a.err = e_order; + return null; + } + r.q1 = e; + } continue; } r = a.number(r, @intCast(n), dir, size) orelse return null; @@ -348,6 +377,21 @@ test "an address is evaluated from the current one, as acme's are" { try testing.expectEqualStrings(e_order, th.wr(q, qaddr, "/a/,/b/").reply.ename); } +test "a row's range, L:C-L2:C2 or L:C-C2, is an address through its last character" { + const p = try th.withFile(testing.allocator, "one\ntwo three\nfour\n"); + defer p.deinit(); + const addr = Node.of(th.serialOf(p), .addr); + const data = Node.of(th.serialOf(p), .xdata); + try testing.expectEqual(Status.ok, th.wr(p, addr, "2:5-2:9").reply.status); + try testing.expectEqualStrings("three", th.rd(p, data, 0, 64).bytes); + try testing.expectEqual(Status.ok, th.wr(p, addr, "2:1-3").reply.status); + try testing.expectEqualStrings("two", th.rd(p, data, 0, 64).bytes); + try testing.expectEqual(Status.ok, th.wr(p, addr, "1:2-3:2").reply.status); + try testing.expectEqualStrings("ne\ntwo three\nfo", th.rd(p, data, 0, 64).bytes); + try testing.expectEqualStrings(e_order, th.wr(p, addr, "2:5-1:1").reply.ename); + try testing.expectEqualStrings(e_range, th.wr(p, addr, "2:1-9:1").reply.ename); +} + test "an empty match at the search's start is passed over, as sam's nextmatch does" { const p = try th.withFile(testing.allocator, "ab\ncd\nef\n"); defer p.deinit(); -- cgit v1.3