summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 12:33:29 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commitd8b35f18817faaba340059cc2bd88ab3b3bc919b (patch)
treeb14b62889b3816a641918f48dc1eb3b2e17b2e4f
parenta5be027a0c05c8950bada14fa2f2e713ce7d6986 (diff)
downloadpardes-d8b35f18817faaba340059cc2bd88ab3b3bc919b.tar.gz
pardes-d8b35f18817faaba340059cc2bd88ab3b3bc919b.zip
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 <[email protected]>
-rw-r--r--.agents/skills/pardes-9p/SKILL.md3
-rw-r--r--docs/fs.md10
-rw-r--r--src/ninep/addr.zig44
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();