summaryrefslogtreecommitdiff
path: root/src/ninep/addr.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 07:16:30 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commita0bca6764a6a425b71e1567ff9d49f9828c5813c (patch)
treee1f28e3cc7ad2aedc774f87073d60abe835b9e93 /src/ninep/addr.zig
parent03773fac517a2a26dc2f2813db9906d3bf7e91db (diff)
downloadpardes-a0bca6764a6a425b71e1567ff9d49f9828c5813c.tar.gz
pardes-a0bca6764a6a425b71e1567ff9d49f9828c5813c.zip
An empty match at a search's start is passed over, as sam's nextmatch does
Repeated `/^/` from #3 stayed at #3: the search found the empty match where it started and called that the next one. sam (editors/sam/ address.c:97-119) searches again a character on, or back, wrapping; so does addr now, and `/$/` and `/x*/` step as they should. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep/addr.zig')
-rw-r--r--src/ninep/addr.zig90
1 files changed, 68 insertions, 22 deletions
diff --git a/src/ninep/addr.zig b/src/ninep/addr.zig
index 989571a5..9498df19 100644
--- a/src/ninep/addr.zig
+++ b/src/ninep/addr.zig
@@ -231,6 +231,39 @@ pub const Addr = struct {
/// acme's regexp(): forward from the end of `r` to the limit, wrapping
/// to the start of the text when there is none; backward, the last
/// match ending by the start of `r`, else the last one in the text.
+ /// The first match at or after `from`, else, with no limit, the first
+ /// from the top (a search wraps); null (the outer) when too slow.
+ fn forward(a: *Addr, rx: *regexp_.Regex, from: usize, hi: usize) ??Range {
+ const ahead = rx.find(a.text, from, hi, hi) catch {
+ a.err = e_slow;
+ return null;
+ };
+ const m = ahead orelse (if (a.lim != null or from == 0) null else rx.find(a.text, 0, from - 1, hi) catch {
+ a.err = e_slow;
+ return null;
+ }) orelse return @as(?Range, null);
+ return @as(?Range, .{ .q0 = clip(m.start), .q1 = clip(m.end) });
+ }
+
+ /// The last match ending at or before `limit`, else the last of all
+ /// (a search wraps); null (the outer) when too slow.
+ fn backward(a: *Addr, rx: *regexp_.Regex, limit: u32) ??Range {
+ var last: ?Range = null;
+ var before: ?Range = null;
+ var at: usize = 0;
+ while (at <= a.text.len) {
+ const m = (rx.find(a.text, at, a.text.len, a.text.len) catch {
+ a.err = e_slow;
+ return null;
+ }) orelse break;
+ const found_range: Range = .{ .q0 = clip(m.start), .q1 = clip(m.end) };
+ if (found_range.q1 <= limit) before = found_range;
+ last = found_range;
+ at = if (m.end > m.start) m.end else m.end + 1;
+ }
+ return @as(?Range, before orelse last);
+ }
+
fn regexp(a: *Addr, r: Range, pat: []const u8, back: bool) ?Range {
if (pat.len == 0) {
a.err = "no previous regular expression";
@@ -240,33 +273,25 @@ pub const Addr = struct {
a.err = if (err == error.Anchor) regexp_.Regex.e_anchor else e_regexp;
return null;
};
+ // sam's nextmatch (editors/sam/address.c:97-119): an empty match
+ // right at the search's start is no progress, so the search is made
+ // again a character on (back: a character before), wrapping, and
+ // repeated `/^/` or `/$/` walks the lines.
const found = if (back) found: {
- var last: ?Range = null;
- var before: ?Range = null;
- var at: usize = 0;
- while (at <= a.text.len) {
- const m = (rx.find(a.text, at, a.text.len, a.text.len) catch {
- a.err = e_slow;
- return null;
- }) orelse break;
- const found_range: Range = .{ .q0 = clip(m.start), .q1 = clip(m.end) };
- if (found_range.q1 <= r.q0) before = found_range;
- last = found_range;
- at = if (m.end > m.start) m.end else m.end + 1;
- }
- break :found before orelse last;
+ var f = a.backward(&rx, r.q0) orelse return null;
+ if (f) |m| if (m.q0 == m.q1 and m.q1 == r.q0) {
+ f = a.backward(&rx, if (r.q0 == 0) clip(a.text.len) else r.q0 - 1) orelse return null;
+ };
+ break :found f;
} else found: {
const hi = if (a.lim) |l| @min(@as(usize, l.q1), a.text.len) else a.text.len;
const from = @min(@as(usize, r.q1), hi);
- const ahead = rx.find(a.text, from, hi, hi) catch {
- a.err = e_slow;
- return null;
+ var f = a.forward(&rx, from, hi) orelse return null;
+ if (f) |m| if (m.q0 == m.q1 and m.q0 == from) {
+ const next = if (from + 1 > hi) (if (a.lim != null) break :found null else 0) else from + 1;
+ f = a.forward(&rx, next, hi) orelse return null;
};
- const m = ahead orelse (if (a.lim != null or from == 0) null else rx.find(a.text, 0, from - 1, hi) catch {
- a.err = e_slow;
- return null;
- }) orelse break :found null;
- break :found Range{ .q0 = clip(m.start), .q1 = clip(m.end) };
+ break :found f;
};
// A byte pattern may match inside a rune: the match covers the
// runes it touches.
@@ -323,6 +348,27 @@ 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 "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();
+ const addr = Node.of(th.serialOf(p), .addr);
+ const Case = struct { start: []const u8, expr: []const u8, want: []const u32 };
+ for ([_]Case{
+ // From #3 (the start of line 2), /^/ goes on to line 3.
+ .{ .start = "#3", .expr = "/^/", .want = &.{6} },
+ .{ .start = "#2", .expr = "/$/", .want = &.{ 5, 8 } },
+ .{ .start = "#3", .expr = "/x*/", .want = &.{ 4, 5, 6 } },
+ .{ .start = "#3", .expr = "?^?", .want = &.{0} },
+ }) |c| {
+ _ = th.wr(p, addr, c.start);
+ for (c.want) |q| {
+ try testing.expectEqual(Status.ok, th.wr(p, addr, c.expr).reply.status);
+ try testing.expectEqual(q, p.panes[0].?.fs.addr.q0);
+ try testing.expectEqual(q, p.panes[0].?.fs.addr.q1);
+ }
+ }
+}
+
test "regular expressions search lines as sam's do, and a search wraps" {
const p = try th.withFile(testing.allocator, "alpha beta\nbeta gamma\ngamma\n");
defer p.deinit();