diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 07:16:30 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:16 -0300 |
| commit | a0bca6764a6a425b71e1567ff9d49f9828c5813c (patch) | |
| tree | e1f28e3cc7ad2aedc774f87073d60abe835b9e93 | |
| parent | 03773fac517a2a26dc2f2813db9906d3bf7e91db (diff) | |
| download | pardes-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]>
| -rw-r--r-- | src/ninep/addr.zig | 90 |
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(); |
