//! The address language of a pane's `addr` file: acme's, with mvzr regexps. const std = @import("std"); const mvzr = @import("mvzr"); const modal = @import("../modal.zig"); const pane_files = @import("pane.zig"); pub const Range = pane_files.State.Range; fn clip(n: usize) u32 { return std.math.cast(u32, n) orelse std.math.maxInt(u32); } fn safePattern(pat: []const u8) bool { var i: usize = 0; while (i < pat.len) : (i += 1) { if (pat[i] != '\\') continue; if (i + 1 >= pat.len) return false; i += 1; } return true; } pub const Addr = struct { text: []const u8, lim: ?Range, expr: []const u8, i: usize = 0, depth: u8 = 0, const max_depth = 32; const Size = enum { char, line }; pub fn address(a: *Addr, ar_in: Range) ?Range { const start = a.i; var ar = ar_in; var r = ar_in; var dir: u8 = 0; var size: Size = .line; var c: u8 = 0; while (a.i < a.expr.len) { const prevc = c; c = a.expr[a.i]; a.i += 1; switch (c) { ',', ';' => { if (c == ';') ar = r; if (prevc == 0) r.q0 = 0; // lhs defaults to 0 if (a.i >= a.expr.len) { r.q1 = clip(a.text.len); // rhs defaults to $ } else { if (a.depth >= max_depth) return null; a.depth += 1; const nr = a.address(ar) orelse return null; a.depth -= 1; r.q1 = nr.q1; } return r; }, '+', '-' => { if (prevc == '+' or prevc == '-') { const nc = if (a.i < a.expr.len) a.expr[a.i] else 0; if (nc != '#' and nc != '/' and nc != '?') r = a.number(r, 1, prevc, .line) orelse return null; } dir = c; }, '.', '$' => { if (a.i != start + 1) { a.i -= 1; return r; } r = if (c == '.') ar else .{ .q0 = clip(a.text.len), .q1 = clip(a.text.len) }; dir = if (a.i < a.expr.len) '+' else 0; }, '#', '0'...'9' => { var digit = c; if (c == '#') { if (a.i >= a.expr.len or a.expr[a.i] < '0' or a.expr[a.i] > '9') { a.i -= 1; return r; } digit = a.expr[a.i]; a.i += 1; size = .char; } var n: u64 = digit - '0'; while (a.i < a.expr.len) : (a.i += 1) { const d = a.expr[a.i]; if (d < '0' or d > '9') break; n = @min(n * 10 + (d - '0'), std.math.maxInt(u32)); } r = a.number(r, @intCast(n), dir, size) orelse return null; dir = 0; size = .line; }, '/', '?' => { const back = c == '?'; r = a.regexp(r, a.pattern(c), back) orelse return null; dir = 0; size = .line; }, else => { a.i -= 1; return r; }, } } if (dir != 0) r = a.number(r, 1, dir, .line) orelse return null; return r; } fn pattern(a: *Addr, delim: u8) []const u8 { const s = a.i; while (a.i < a.expr.len) { const c = a.expr[a.i]; if (c == '\n') break; a.i += 1; if (c == '\\') { if (a.i < a.expr.len) a.i += 1; continue; } if (c == delim) return a.expr[s .. a.i - 1]; } return a.expr[s..a.i]; } fn number(a: *Addr, r_in: Range, n: u32, dir: u8, size: Size) ?Range { var r = r_in; if (size == .char) { var off: i64 = n; if (dir == '+') { off = @as(i64, r.q1) + n; } else if (dir == '-') { if (r.q0 == 0 and n > 0) r.q0 = clip(a.text.len); off = @as(i64, r.q0) - n; } if (off < 0 or off > @as(i64, @intCast(a.text.len))) return null; const g = clip(modal.graphemeStart(a.text, @intCast(off))); return .{ .q0 = g, .q1 = g }; } var line: i64 = n; var q0: usize = r.q0; var q1: usize = r.q1; switch (dir) { '-' => { if (q0 < a.text.len) while (q0 > 0 and a.text[q0 - 1] != '\n') { q0 -= 1; }; q1 = q0; while (line > 0 and q0 > 0) { if (a.text[q0 - 1] == '\n') { line -= 1; q1 = q0; } q0 -= 1; } if (line > 1) return null; 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; }, } 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; } } if (line > 0) return null; return .{ .q0 = clip(q0), .q1 = clip(q1) }; } fn regexp(a: *Addr, r: Range, pat: []const u8, back: bool) ?Range { if (pat.len == 0 or !safePattern(pat)) return null; const re = mvzr.compile(pat) orelse return null; if (back) { const hi = @min(@as(usize, r.q0), a.text.len); var best: ?mvzr.Match = null; var at: usize = 0; while (at < hi) { const m = re.matchPos(at, a.text[0..hi]) orelse break; best = m; at = if (m.end > m.start) m.end else m.end + 1; } const m = best orelse return null; return .{ .q0 = clip(m.start), .q1 = clip(m.end) }; } 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 m = re.match(a.text[from..hi]) orelse return null; return .{ .q0 = clip(from + m.start), .q1 = clip(from + m.end) }; } }; const testing = std.testing; const th = @import("testing.zig"); const tree = @import("tree.zig"); const Node = tree.Node; const E = tree.E; const Status = tree.Status; test "the address language, form by form" { const gpa = testing.allocator; const p = try th.withFile(gpa, "one\ntwo\nthree\n"); defer p.deinit(); const serial = th.serialOf(p); const addr = Node.of(serial, .addr); const Case = struct { expr: []const u8, q0: u32, q1: u32 }; for ([_]Case{ .{ .expr = "#0", .q0 = 0, .q1 = 0 }, .{ .expr = "#5", .q0 = 5, .q1 = 5 }, .{ .expr = "0", .q0 = 0, .q1 = 0 }, .{ .expr = "1", .q0 = 0, .q1 = 4 }, .{ .expr = "2", .q0 = 4, .q1 = 8 }, .{ .expr = "$", .q0 = 14, .q1 = 14 }, .{ .expr = ",", .q0 = 0, .q1 = 14 }, .{ .expr = "1,2", .q0 = 0, .q1 = 8 }, .{ .expr = "#1,#4", .q0 = 1, .q1 = 4 }, .{ .expr = "2+1", .q0 = 8, .q1 = 14 }, .{ .expr = "$-1", .q0 = 8, .q1 = 14 }, .{ .expr = "/two/", .q0 = 4, .q1 = 7 }, .{ .expr = "/t.o/", .q0 = 4, .q1 = 7 }, .{ .expr = "1\n", .q0 = 0, .q1 = 4 }, }) |c| { _ = th.wr(p, addr, "#0"); const w = th.wr(p, addr, c.expr); try testing.expectEqual(Status.ok, w.reply.status); const got = th.rd(p, addr, 0, 64); var want: [32]u8 = undefined; try testing.expectEqualStrings( try std.fmt.bufPrint(&want, "{d:>11} {d:>11} ", .{ c.q0, c.q1 }), got.bytes, ); } _ = th.wr(p, addr, "1"); _ = th.wr(p, addr, "."); try testing.expectEqual(@as(u32, 0), p.fs.panes[0].addr.q0); try testing.expectEqual(@as(u32, 4), p.fs.panes[0].addr.q1); _ = th.wr(p, addr, "$"); _ = th.wr(p, addr, "?o?"); try testing.expectEqual(@as(u32, 6), p.fs.panes[0].addr.q0); // the `o` in "two" try testing.expectEqual(@as(u32, 7), p.fs.panes[0].addr.q1); // limit is its own file: copying addr onto it bounds the search, and // truncating it lifts the bound again. const limit = Node.of(serial, .limit); _ = th.wr(p, addr, "1"); try testing.expectEqualStrings("", th.rd(p, limit, 0, 64).bytes); var copied: [64]u8 = undefined; const pair = th.rd(p, addr, 0, 64).bytes; @memcpy(copied[0..pair.len], pair); _ = th.wr(p, limit, copied[0..pair.len]); try testing.expectEqual(@as(u32, 4), p.fs.panes[0].limit.?.q1); try testing.expectEqualStrings(" 0 4 ", th.rd(p, limit, 0, 64).bytes); _ = th.wr(p, addr, "#0"); try testing.expectEqual(E.INVAL, th.wr(p, addr, "/three/").errno()); _ = th.call(p, .{ .tag = 6, .op = .setattr, .node = limit, .truncate = true }); try testing.expect(p.fs.panes[0].limit == null); _ = th.wr(p, addr, "#0"); try testing.expectEqual(Status.ok, th.wr(p, addr, "/three/").reply.status); for ([_][]const u8{ "zzz", "#", "//", "/nomatch/", "1 2 3", "99", "/a\\" }) |bad| { _ = th.wr(p, addr, "#0"); try testing.expectEqual(E.INVAL, th.wr(p, addr, bad).errno()); } const nested = "," ** 4096; _ = th.wr(p, addr, "#0"); try testing.expectEqual(E.INVAL, th.wr(p, addr, nested).errno()); }