diff options
Diffstat (limited to 'src/ninep/addr.zig')
| -rw-r--r-- | src/ninep/addr.zig | 188 |
1 files changed, 140 insertions, 48 deletions
diff --git a/src/ninep/addr.zig b/src/ninep/addr.zig index 1e47183b..9498df19 100644 --- a/src/ninep/addr.zig +++ b/src/ninep/addr.zig @@ -13,6 +13,7 @@ fn clip(n: usize) u32 { pub const e_no_match = "no match for regexp"; pub const e_range = "address out of range"; +pub const e_col_zero = "address out of range: a column counts from 1"; pub const e_regexp = "bad regular expression"; /// Not "took too long": 9ns reads errors by their words, and that would be /// ENAMETOOLONG. @@ -94,7 +95,7 @@ pub const Addr = struct { // pardes's `line:col`, Look's `file:12:5` spelling (look.zig // parsePathLine): the point at that column of that line, the // column in bytes from 1, clamped to the line's end and snapped - // to a character's start as Look's cursor is. Not sam's. + // to a rune's start, as every address is. Not sam's. if (size == .line and dir == 0 and 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') { @@ -104,10 +105,10 @@ pub const Addr = struct { col = @min(col * 10 + (a.expr[a.i] - '0'), std.math.maxInt(u32)); const lines = modal.cursorLineCount(a.text); if (n == 0 or n > lines or col == 0) { - a.err = e_range; + a.err = if (col == 0) e_col_zero else e_range; return null; } - const q = clip(modal.offsetAt(a.text, .{ .row = @intCast(n - 1), .col = @intCast(col - 1) })); + const q = clip(modal.runeOffsetAt(a.text, .{ .row = @intCast(n - 1), .col = @intCast(col - 1) })); r = .{ .q0 = q, .q1 = q }; continue; } @@ -162,7 +163,7 @@ pub const Addr = struct { a.err = e_range; return null; } - const g = clip(modal.graphemeStart(a.text, @intCast(off))); + const g = clip(modal.runeStart(a.text, @intCast(off))); return .{ .q0 = g, .q1 = g }; } var line: i64 = n; @@ -188,35 +189,81 @@ pub const Addr = struct { while (q0 > 0 and a.text[q0 - 1] != '\n') q0 -= 1; return .{ .q0 = clip(q0), .q1 = clip(q1) }; }, - '+' => { - if (q1 > 0) while (q1 < a.text.len and a.text[q1 - 1] != '\n') { - q1 += 1; - }; - q0 = q1; - }, - else => { - q0 = 0; - q1 = 0; - }, + else => {}, } - while (line > 0 and q1 < a.text.len) { - const ch = a.text[q1]; - q1 += 1; - if (ch == '\n' or q1 == a.text.len) { - line -= 1; - if (line > 0) q0 = q1; + // Forward, sam's lineaddr (editors/sam/address.c:180-215), not acme's + // number(): line n is found by counting newlines, and a line just + // past the last newline is an empty one there, so `1` of an empty + // text is #0,#0 and `2` of "a\n" is #2,#2; only past that is it out + // of range. + const nc = a.text.len; + var p: usize = 0; + var p1: usize = 0; + if (n == 0) { + if (dir == 0 or q1 == 0) return .{ .q0 = 0, .q1 = 0 }; + p1 = q1; + p = q1 - 1; + } else { + var k: u32 = 1; + if (dir != 0 and q1 > 0) { + p = q1 - 1; + k = @intFromBool(a.text[p] == '\n'); + p += 1; + } + while (k < n) { + if (p >= nc) { + a.err = e_range; + return null; + } + if (a.text[p] == '\n') k += 1; + p += 1; } + p1 = p; } - if (line > 0) { - a.err = e_range; - return null; + while (p < nc) { + const ch = a.text[p]; + p += 1; + if (ch == '\n') break; } - return .{ .q0 = clip(q0), .q1 = clip(q1) }; + return .{ .q0 = clip(p1), .q1 = clip(p) }; } /// 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"; @@ -226,34 +273,29 @@ 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. + if (found) |f| return .{ .q0 = clip(modal.runeStart(a.text, f.q0)), .q1 = clip(modal.runeEnd(a.text, f.q1)) }; return found orelse { a.err = e_no_match; return null; @@ -306,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(); @@ -357,7 +420,8 @@ test "a failed address leaves none, so data refuses rather than act at the last try testing.expectEqualStrings(e_no_match, th.wr(p, addr, "/zzz/").reply.ename); try testing.expectEqualStrings(pane_files.e_addr_failed, th.wr(p, data, "ONE").reply.ename); try testing.expectEqualStrings(pane_files.e_addr_failed, th.rd(p, data, 0, 64).reply.ename); - try testing.expectEqualStrings(pane_files.e_addr_failed, th.rd(p, addr, 0, 64).reply.ename); + try testing.expectEqual(Status.ok, th.rd(p, addr, 0, 64).reply.status); + try testing.expectEqualStrings("", th.rd(p, addr, 0, 64).bytes); try testing.expectEqual(E.INVAL, th.call(p, .{ .tag = 1, .op = .setattr, .node = Node.of(serial, .xdata), .truncate = true }).errno()); try testing.expectEqualStrings("one\ntwo\n", p.panes[0].?.file.?.content); // A good address gives it one again; truncating addr does not. @@ -466,7 +530,7 @@ test "the address language, form by form" { for ([_][2][]const u8{ .{ "zzz", e_syntax }, .{ "/nomatch/", e_no_match }, .{ "99", e_range }, .{ "#999", e_range }, .{ "/a[/", e_regexp }, .{ "/(a/", e_regexp }, - .{ "/*a/", e_regexp }, .{ "99:1", e_range }, .{ "2:0", e_range }, + .{ "/*a/", e_regexp }, .{ "99:1", e_range }, .{ "2:0", e_col_zero }, }) |c| { _ = th.wr(p, addr, "#0"); try testing.expectEqualStrings(c[1], th.wr(p, addr, c[0]).reply.ename); @@ -476,3 +540,31 @@ test "the address language, form by form" { _ = th.wr(p, addr, "#0"); try testing.expectEqual(E.INVAL, th.wr(p, addr, nested).errno()); } + +test "a line just past the last newline is an empty one there, as sam's lineaddr has it" { + const Case = struct { text: []const u8, expr: []const u8, q0: u32, q1: u32 }; + for ([_]Case{ + .{ .text = "", .expr = "1", .q0 = 0, .q1 = 0 }, + .{ .text = "a\n", .expr = "2", .q0 = 2, .q1 = 2 }, + .{ .text = "a\nb", .expr = "2", .q0 = 2, .q1 = 3 }, + .{ .text = "a\nb\n", .expr = "1", .q0 = 0, .q1 = 2 }, + .{ .text = "a\nb\n", .expr = "0", .q0 = 0, .q1 = 0 }, + .{ .text = "a\nb\n", .expr = "1+1", .q0 = 2, .q1 = 4 }, + .{ .text = "a\nb\n", .expr = "$", .q0 = 4, .q1 = 4 }, + }) |c| { + var a: Addr = .{ .text = c.text, .lim = null, .expr = c.expr }; + const r = a.address(.{ .q0 = 0, .q1 = 0 }).?; + try testing.expectEqual(c.q0, r.q0); + try testing.expectEqual(c.q1, r.q1); + } + for ([_][2][]const u8{ .{ "", "2" }, .{ "a\n", "3" } }) |c| { + var a: Addr = .{ .text = c[0], .lim = null, .expr = c[1] }; + try testing.expectEqual(null, a.address(.{ .q0 = 0, .q1 = 0 })); + try testing.expectEqualStrings(e_range, a.err); + } + // Edit's addresses are these: 1i on an empty file inserts. + const p = try th.withFile(testing.allocator, ""); + defer p.deinit(); + try testing.expectEqual(Status.ok, th.wr(p, Node.of(th.serialOf(p), .ctl), "Edit 1i/header/\n").reply.status); + try testing.expectEqualStrings("header", p.panes[0].?.file.?.content); +} |
