summaryrefslogtreecommitdiff
path: root/src/sam_edit.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/sam_edit.zig')
-rw-r--r--src/sam_edit.zig667
1 files changed, 667 insertions, 0 deletions
diff --git a/src/sam_edit.zig b/src/sam_edit.zig
new file mode 100644
index 00000000..c08c6cdd
--- /dev/null
+++ b/src/sam_edit.zig
@@ -0,0 +1,667 @@
+//! acme's Edit (editors/acme/edit.c, ecmd.c, elog.c): sam's command
+//! language over one pane's body. The commands are parsed as edit.c parses
+//! them, run as ecmd.c runs them against the text as it was before the Edit,
+//! and their changes collected in order, as elog.c logs them, then applied
+//! at once: an Edit is one undo step, and one that fails changes nothing.
+//!
+//! Addresses are the `addr` file's (src/ninep/addr.zig), so they take
+//! pardes's `line:col` too; regular expressions are src/regexp.zig's, mvzr
+//! searched as sam searches, with its step budget.
+//!
+//! Left out: the file commands (`b B D e r w f X Y`), the pipes (`< | >`),
+//! the `'` and `"` addresses, and `\1`..`\9` in `s` (mvzr has no
+//! submatches). `u` undoes or redoes only as a whole Edit. acme warns about
+//! changes out of sequence (elog.c:130) and applies them anyway; here the
+//! Edit is refused.
+const std = @import("std");
+const regexp = @import("regexp.zig");
+const addr_lang = @import("ninep/addr.zig");
+
+pub const Range = addr_lang.Range;
+
+pub const Op = struct { q0: u32, q1: u32, text: []const u8 };
+
+/// What an Edit came to: the changes in order, the dot it leaves (in the
+/// text before them), and what `p` and `=` printed.
+pub const Result = struct {
+ ops: []const Op,
+ dot: Range,
+ printed: []const u8,
+ /// A bare `u`/`u-3`: undo (positive) or redo (negative) that many.
+ undo: i32 = 0,
+};
+
+pub const Failure = error{ Edit, OutOfMemory };
+
+/// Why an Edit failed, in acme's words, when `run` answered error.Edit.
+pub const Why = struct {
+ buf: [200]u8 = undefined,
+ len: usize = 0,
+ pub fn text(w: *const Why) []const u8 {
+ return w.buf[0..w.len];
+ }
+};
+
+const Cmd = struct {
+ c: u8,
+ addr: ?[]const u8 = null,
+ re: ?[]const u8 = null,
+ text: []const u8 = "",
+ sub: ?*Cmd = null,
+ next: ?*Cmd = null,
+ num: i32 = 0,
+ global: bool = false,
+ to: ?[]const u8 = null,
+};
+
+/// Runs `command` over `text` from `dot`, the file named `name` (for `=`).
+/// Everything returned lives in `arena`.
+pub fn run(arena: std.mem.Allocator, text: []const u8, dot: Range, name: []const u8, command: []const u8, why: *Why) Failure!Result {
+ var src = try arena.alloc(u8, command.len + 1);
+ @memcpy(src[0..command.len], command);
+ src[command.len] = '\n';
+ if (command.len > 0 and command[command.len - 1] == '\n') src = src[0..command.len];
+ var ps: Parser = .{ .arena = arena, .src = src, .why = why };
+ var first: ?*Cmd = null;
+ var last: ?*Cmd = null;
+ while (try ps.parse(0)) |c| {
+ if (last) |l| l.next = c else first = c;
+ last = c;
+ }
+ const cmds = first orelse return .{ .ops = &.{}, .dot = dot, .printed = "" };
+ // acme's u runs at once (ecmd.c:532), outside the edit log; here it is
+ // an Edit of its own.
+ if (cmds.c == 'u') {
+ if (cmds.next != null) return fail(why, "u is only an Edit of its own here", .{});
+ return .{ .ops = &.{}, .dot = dot, .printed = "", .undo = cmds.num };
+ }
+ var ex: Exec = .{ .arena = arena, .text = text, .name = name, .why = why };
+ var d = dot;
+ var it: ?*Cmd = cmds;
+ while (it) |c| : (it = c.next) d = try ex.exec(c, d);
+ return .{ .ops = ex.ops.items, .dot = d, .printed = ex.out.items };
+}
+
+fn fail(why: *Why, comptime fmt: []const u8, args: anytype) Failure {
+ why.len = if (std.fmt.bufPrint(&why.buf, fmt, args)) |s| s.len else |_| 0;
+ return error.Edit;
+}
+
+// ---- parsing: edit.c ----
+
+const Parser = struct {
+ arena: std.mem.Allocator,
+ src: []const u8,
+ i: usize = 0,
+ /// The last pattern given, for an empty one (edit.c:562, getregexp).
+ lastpat: []const u8 = "",
+ why: *Why,
+
+ fn getch(ps: *Parser) ?u8 {
+ if (ps.i >= ps.src.len) return null;
+ ps.i += 1;
+ return ps.src[ps.i - 1];
+ }
+
+ fn nextc(ps: *const Parser) ?u8 {
+ return if (ps.i < ps.src.len) ps.src[ps.i] else null;
+ }
+
+ fn skipbl(ps: *Parser) ?u8 {
+ while (ps.nextc()) |c| {
+ if (c != ' ' and c != '\t') return c;
+ ps.i += 1;
+ }
+ return null;
+ }
+
+ fn atnl(ps: *Parser) Failure!void {
+ _ = ps.skipbl();
+ const c = ps.getch() orelse '\n';
+ if (c != '\n') return fail(ps.why, "newline expected (saw {c})", .{c});
+ }
+
+ fn okdelim(ps: *Parser, c: u8) Failure!void {
+ if (c == '\\' or std.ascii.isAlphanumeric(c)) return fail(ps.why, "bad delimiter {c}", .{c});
+ }
+
+ /// edit.c:216: a count, 1 when there is none; `-` allowed when signok > 1.
+ fn getnum(ps: *Parser, signok: u8) i32 {
+ var sign: i32 = 1;
+ if (signok > 1 and ps.nextc() == '-') {
+ sign = -1;
+ ps.i += 1;
+ }
+ const c = ps.nextc() orelse return sign;
+ if (c < '0' or c > '9') return sign;
+ var n: i32 = 0;
+ while (ps.nextc()) |d| {
+ if (d < '0' or d > '9') break;
+ n = n *| 10 +| (d - '0');
+ ps.i += 1;
+ }
+ return sign * n;
+ }
+
+ /// An address's text, blanks dropped, or null: the syntax of edit.c's
+ /// compoundaddr and simpleaddr, evaluated later by addr.zig.
+ fn address(ps: *Parser) Failure!?[]const u8 {
+ var out: std.ArrayList(u8) = .empty;
+ while (true) {
+ const c = ps.skipbl() orelse break;
+ switch (c) {
+ '0'...'9', '#', '.', '$', '+', '-', ',', ';' => {
+ ps.i += 1;
+ try out.append(ps.arena, c);
+ // pardes's line:col
+ if (c >= '0' and c <= '9' and ps.nextc() == ':' and ps.i + 1 < ps.src.len and
+ ps.src[ps.i + 1] >= '0' and ps.src[ps.i + 1] <= '9')
+ {
+ ps.i += 1;
+ try out.append(ps.arena, ':');
+ }
+ },
+ '/', '?' => {
+ ps.i += 1;
+ try out.append(ps.arena, c);
+ while (ps.nextc()) |d| {
+ if (d == '\n') break;
+ ps.i += 1;
+ try out.append(ps.arena, d);
+ if (d == '\\') {
+ if (ps.nextc()) |e| if (e != '\n') {
+ ps.i += 1;
+ try out.append(ps.arena, e);
+ };
+ continue;
+ }
+ if (d == c) break;
+ }
+ },
+ '\'' => return fail(ps.why, "can't handle '", .{}),
+ '"' => return fail(ps.why, "file addresses are not supported", .{}),
+ else => break,
+ }
+ }
+ return if (out.items.len == 0) null else out.items;
+ }
+
+ /// edit.c:562: up to the delimiter; `\delim` is the delimiter, `\\` stays.
+ fn getregexp(ps: *Parser, delim: u8) Failure![]const u8 {
+ var buf: std.ArrayList(u8) = .empty;
+ while (true) {
+ var c = ps.getch() orelse break;
+ if (c == '\\') {
+ if (ps.nextc() == delim) {
+ c = ps.getch().?;
+ } else if (ps.nextc() == '\\') {
+ try buf.append(ps.arena, c);
+ c = ps.getch().?;
+ }
+ } else if (c == delim or c == '\n') {
+ if (c == '\n') ps.i -= 1;
+ break;
+ }
+ try buf.append(ps.arena, c);
+ }
+ if (buf.items.len > 0) ps.lastpat = buf.items;
+ if (ps.lastpat.len == 0) return fail(ps.why, "no regular expression defined", .{});
+ return ps.lastpat;
+ }
+
+ /// edit.c:388: `\n` is a newline; for `s` every other escape is kept
+ /// for it to read (`\&`, `\\`).
+ fn getrhs(ps: *Parser, delim: u8, cmd: u8) Failure![]const u8 {
+ var s: std.ArrayList(u8) = .empty;
+ while (ps.getch()) |c0| {
+ var c = c0;
+ if (c == delim or c == '\n') {
+ ps.i -= 1;
+ break;
+ }
+ if (c == '\\') {
+ c = ps.getch() orelse return fail(ps.why, "bad right hand side", .{});
+ if (c == '\n') {
+ ps.i -= 1;
+ c = '\\';
+ } else if (c == 'n') {
+ c = '\n';
+ } else if (c != delim and (cmd == 's' or c != '\\')) {
+ try s.append(ps.arena, '\\');
+ }
+ }
+ try s.append(ps.arena, c);
+ }
+ return s.items;
+ }
+
+ /// edit.c:425: `/text/` on the line, or lines up to one that is `.`.
+ fn collecttext(ps: *Parser) Failure![]const u8 {
+ if (ps.skipbl() == '\n') {
+ ps.i += 1;
+ var s: std.ArrayList(u8) = .empty;
+ while (ps.i < ps.src.len) {
+ const end = std.mem.indexOfScalarPos(u8, ps.src, ps.i, '\n') orelse ps.src.len;
+ const line = ps.src[ps.i..end];
+ ps.i = @min(end + 1, ps.src.len);
+ if (std.mem.eql(u8, line, ".")) return s.items;
+ try s.appendSlice(ps.arena, line);
+ try s.append(ps.arena, '\n');
+ }
+ return s.items;
+ }
+ const delim = ps.getch() orelse return "";
+ try ps.okdelim(delim);
+ const s = try ps.getrhs(delim, 'a');
+ if (ps.nextc() == delim) ps.i += 1;
+ try ps.atnl();
+ return s;
+ }
+
+ /// edit.c:467, parsecmd.
+ fn parse(ps: *Parser, nest: u32) Failure!?*Cmd {
+ const a = try ps.address();
+ _ = ps.skipbl() orelse return null;
+ const c = ps.getch() orelse return null;
+ const cmd = try ps.arena.create(Cmd);
+ cmd.* = .{ .c = c, .addr = a };
+ switch (c) {
+ '\n' => {},
+ 'a', 'c', 'i' => cmd.text = try ps.collecttext(),
+ 'd', 'p' => try ps.atnl(),
+ '=' => {
+ _ = ps.skipbl();
+ const start = ps.i;
+ while (ps.nextc()) |d| : (ps.i += 1) if (d == '\n') break;
+ cmd.text = std.mem.trimEnd(u8, ps.src[start..ps.i], " \t");
+ ps.i = @min(ps.i + 1, ps.src.len);
+ if (cmd.text.len != 0 and !std.mem.eql(u8, cmd.text, "#")) return fail(ps.why, "newline expected", .{});
+ },
+ 's' => {
+ cmd.num = ps.getnum(1);
+ _ = ps.skipbl();
+ const delim = ps.getch() orelse '\n';
+ if (delim == '\n') return fail(ps.why, "no address", .{});
+ try ps.okdelim(delim);
+ cmd.re = try ps.getregexp(delim);
+ cmd.text = try ps.getrhs(delim, 's');
+ if (ps.nextc() == delim) {
+ ps.i += 1;
+ if (ps.nextc() == 'g') {
+ ps.i += 1;
+ cmd.global = true;
+ }
+ }
+ try ps.atnl();
+ },
+ 'x', 'y', 'g', 'v' => {
+ // x without a pattern is every line (ecmd.c:568, linelooper).
+ const n = ps.nextc();
+ if (c != 'x' or (n != ' ' and n != '\t' and n != '\n')) {
+ _ = ps.skipbl();
+ const delim = ps.getch() orelse '\n';
+ if (delim == '\n') return fail(ps.why, "no address", .{});
+ try ps.okdelim(delim);
+ cmd.re = try ps.getregexp(delim);
+ }
+ if (ps.skipbl() == '\n') {
+ ps.i += 1;
+ const p = try ps.arena.create(Cmd);
+ p.* = .{ .c = 'p' };
+ cmd.sub = p;
+ } else {
+ cmd.sub = (try ps.parse(nest)) orelse return fail(ps.why, "defcmd", .{});
+ }
+ },
+ 'm', 't' => {
+ cmd.to = (try ps.address()) orelse return fail(ps.why, "bad address", .{});
+ try ps.atnl();
+ },
+ 'u' => {
+ if (a != null) return fail(ps.why, "command takes no address", .{});
+ cmd.num = ps.getnum(2);
+ try ps.atnl();
+ },
+ '{' => {
+ var last: ?*Cmd = null;
+ while (true) {
+ if (ps.skipbl() == '\n') ps.i += 1;
+ const next = (try ps.parse(nest + 1)) orelse break;
+ if (last) |l| l.next = next else cmd.sub = next;
+ last = next;
+ }
+ },
+ '}' => {
+ try ps.atnl();
+ if (nest == 0) return fail(ps.why, "right brace with no left brace", .{});
+ return null;
+ },
+ 'b', 'B', 'D', 'e', 'r', 'w', 'f', 'X', 'Y', '<', '|', '>' => return fail(ps.why, "{c} is not supported in pardes", .{c}),
+ else => return fail(ps.why, "unknown command {c}", .{c}),
+ }
+ return cmd;
+ }
+};
+
+// ---- running: ecmd.c, logging: elog.c ----
+
+const Exec = struct {
+ arena: std.mem.Allocator,
+ text: []const u8,
+ name: []const u8,
+ why: *Why,
+ ops: std.ArrayList(Op) = .empty,
+ out: std.ArrayList(u8) = .empty,
+ nest: u32 = 0,
+
+ fn clip(n: usize) u32 {
+ return std.math.cast(u32, n) orelse std.math.maxInt(u32);
+ }
+
+ /// An address from `dot`, or `dot` when there is none (aDot).
+ fn address(ex: *Exec, expr: ?[]const u8, dot: Range) Failure!Range {
+ const e = expr orelse return dot;
+ var a: addr_lang.Addr = .{ .text = ex.text, .lim = null, .expr = e };
+ const r = a.address(dot) orelse return fail(ex.why, "{s}", .{a.err});
+ if (a.i < e.len) return fail(ex.why, "bad address syntax", .{});
+ if (r.q0 > r.q1) return fail(ex.why, "addresses out of order", .{});
+ return r;
+ }
+
+ /// elog.c's order: each change at or after the end of the one before,
+ /// since they all address the text as it was.
+ fn log(ex: *Exec, q0: u32, q1: u32, text: []const u8) Failure!void {
+ if (q0 == q1 and text.len == 0) return;
+ if (ex.ops.items.len > 0) {
+ const prev = ex.ops.items[ex.ops.items.len - 1];
+ if (q0 < prev.q1 or q0 < prev.q0) return fail(ex.why, "changes out of sequence", .{});
+ }
+ try ex.ops.append(ex.arena, .{ .q0 = q0, .q1 = q1, .text = text });
+ }
+
+ fn compile(ex: *Exec, pat: []const u8, c: u8) Failure!regexp.Regex {
+ return regexp.Regex.compile(pat) catch fail(ex.why, "bad regexp in {c} command", .{c});
+ }
+
+ fn find(ex: *Exec, rx: *regexp.Regex, from: usize, hi: usize) Failure!?regexp.Regex.Match {
+ if (from > hi) return null;
+ return rx.find(ex.text, from, hi, hi) catch fail(ex.why, "{s}", .{addr_lang.e_slow});
+ }
+
+ /// ecmd.c:62, cmdexec: runs `c` from `dot`, answering the dot it leaves.
+ fn exec(ex: *Exec, c: *Cmd, dot: Range) Failure!Range {
+ const r = try ex.address(c.addr, dot);
+ switch (c.c) {
+ '\n' => return r,
+ 'a' => {
+ try ex.log(r.q1, r.q1, c.text);
+ return .{ .q0 = r.q1, .q1 = r.q1 };
+ },
+ 'i' => {
+ try ex.log(r.q0, r.q0, c.text);
+ return .{ .q0 = r.q0, .q1 = r.q0 };
+ },
+ 'c' => {
+ try ex.log(r.q0, r.q1, c.text);
+ return .{ .q0 = r.q0, .q1 = r.q0 };
+ },
+ 'd' => {
+ try ex.log(r.q0, r.q1, "");
+ return .{ .q0 = r.q0, .q1 = r.q0 };
+ },
+ 'p' => {
+ try ex.out.appendSlice(ex.arena, ex.text[r.q0..r.q1]);
+ return r;
+ },
+ '=' => {
+ // ecmd.c:665, printposn
+ if (c.text.len > 0) {
+ try ex.out.print(ex.arena, "{s}:#{d}", .{ ex.name, r.q0 });
+ if (r.q1 != r.q0) try ex.out.print(ex.arena, ",#{d}", .{r.q1});
+ } else {
+ const l1 = 1 + std.mem.count(u8, ex.text[0..r.q0], "\n");
+ var l2 = l1 + std.mem.count(u8, ex.text[r.q0..r.q1], "\n");
+ if (r.q1 > r.q0 and ex.text[r.q1 - 1] == '\n') l2 -= 1;
+ try ex.out.print(ex.arena, "{s}:{d}", .{ ex.name, l1 });
+ if (l2 != l1) try ex.out.print(ex.arena, ",{d}", .{l2});
+ }
+ try ex.out.append(ex.arena, '\n');
+ return r;
+ },
+ 's' => return ex.substitute(c, r),
+ 'x', 'y' => {
+ const ranges = if (c.re) |pat| try ex.looper(pat, r, c.c == 'x') else try ex.lines(r);
+ ex.nest += 1;
+ defer ex.nest -= 1;
+ var d = r;
+ for (ranges) |each| d = try ex.exec(c.sub.?, each);
+ return d;
+ },
+ 'g', 'v' => {
+ var rx = try ex.compile(c.re.?, c.c);
+ const hit = (try ex.find(&rx, r.q0, r.q1)) != null;
+ if (hit != (c.c == 'v')) return ex.exec(c.sub.?, r);
+ return r;
+ },
+ 'm', 't' => {
+ const to = try ex.address(c.to, dot);
+ const bytes = ex.text[r.q0..r.q1];
+ if (c.c == 't') {
+ try ex.log(to.q1, to.q1, bytes);
+ } else if (r.q1 <= to.q0) {
+ // ecmd.c:408, move
+ try ex.log(r.q0, r.q1, "");
+ try ex.log(to.q1, to.q1, bytes);
+ } else if (r.q0 >= to.q1) {
+ try ex.log(to.q1, to.q1, bytes);
+ try ex.log(r.q0, r.q1, "");
+ } else if (!(r.q0 == to.q0 and r.q1 == to.q1)) {
+ return fail(ex.why, "move overlaps itself", .{});
+ }
+ return r;
+ },
+ 'u' => return fail(ex.why, "u is only an Edit of its own here", .{}),
+ '{' => {
+ var it = c.sub;
+ while (it) |each| : (it = each.next) _ = try ex.exec(each, r);
+ return r;
+ },
+ else => return fail(ex.why, "unknown command {c}", .{c.c}),
+ }
+ }
+
+ /// ecmd.c:443, s_cmd.
+ fn substitute(ex: *Exec, c: *Cmd, r: Range) Failure!Range {
+ var rx = try ex.compile(c.re.?, 's');
+ var n = c.num;
+ var op: ?usize = null;
+ var p1: usize = r.q0;
+ var did = false;
+ while (p1 <= r.q1) {
+ const m = (try ex.find(&rx, p1, r.q1)) orelse break;
+ if (m.start == m.end) {
+ if (op != null and m.start == op.?) {
+ p1 += 1;
+ continue;
+ }
+ p1 = m.end + 1;
+ } else p1 = m.end;
+ op = m.end;
+ n -= 1;
+ if (n > 0) continue;
+ var buf: std.ArrayList(u8) = .empty;
+ var i: usize = 0;
+ while (i < c.text.len) : (i += 1) {
+ const ch = c.text[i];
+ if (ch == '\\' and i + 1 < c.text.len) {
+ i += 1;
+ const e = c.text[i];
+ if (e >= '1' and e <= '9') return fail(ex.why, "no \\{c}: mvzr keeps no submatches", .{e});
+ try buf.append(ex.arena, e);
+ } else if (ch == '&') {
+ try buf.appendSlice(ex.arena, ex.text[m.start..m.end]);
+ } else try buf.append(ex.arena, ch);
+ }
+ try ex.log(clip(m.start), clip(m.end), buf.items);
+ did = true;
+ if (!c.global) break;
+ }
+ if (!did and ex.nest == 0) return fail(ex.why, "no substitution", .{});
+ return r;
+ }
+
+ /// ecmd.c:796, looper: the matches (x) or what lies between them (y).
+ fn looper(ex: *Exec, pat: []const u8, r: Range, xy: bool) Failure![]Range {
+ var rx = try ex.compile(pat, if (xy) 'x' else 'y');
+ var out: std.ArrayList(Range) = .empty;
+ var op: ?usize = if (xy) null else r.q0;
+ var p: usize = r.q0;
+ while (p <= r.q1) {
+ var tr: Range = undefined;
+ var sel_end: usize = undefined;
+ if (try ex.find(&rx, p, r.q1)) |m| {
+ if (m.start == m.end) {
+ if (op != null and m.start == op.?) {
+ p += 1;
+ continue;
+ }
+ p = m.end + 1;
+ } else p = m.end;
+ tr = if (xy) .{ .q0 = clip(m.start), .q1 = clip(m.end) } else .{ .q0 = clip(op.?), .q1 = clip(m.start) };
+ sel_end = m.end;
+ } else {
+ // no match, but y still takes what is left
+ if (xy or op.? > r.q1) break;
+ tr = .{ .q0 = clip(op.?), .q1 = r.q1 };
+ p = @as(usize, r.q1) + 1;
+ sel_end = r.q1;
+ }
+ op = sel_end;
+ try out.append(ex.arena, tr);
+ }
+ return out.items;
+ }
+
+ /// ecmd.c:840, linelooper: each line in the range, the first from where
+ /// the range starts, the last cut where it ends.
+ fn lines(ex: *Exec, r: Range) Failure![]Range {
+ var out: std.ArrayList(Range) = .empty;
+ var p: usize = r.q0;
+ while (p < r.q1) {
+ const nl = std.mem.indexOfScalarPos(u8, ex.text, p, '\n');
+ const e = @min(if (nl) |i| i + 1 else ex.text.len, r.q1);
+ if (e <= p) break;
+ try out.append(ex.arena, .{ .q0 = clip(p), .q1 = clip(e) });
+ p = e;
+ }
+ return out.items;
+ }
+};
+
+/// The text after `ops`, which are in order and do not overlap.
+pub fn apply(gpa: std.mem.Allocator, text: []const u8, ops: []const Op) ![]u8 {
+ var n: usize = text.len;
+ for (ops) |o| n = n - (o.q1 - o.q0) + o.text.len;
+ const out = try gpa.alloc(u8, n);
+ var at: usize = 0;
+ var from: usize = 0;
+ for (ops) |o| {
+ const keep = text[from..o.q0];
+ @memcpy(out[at..][0..keep.len], keep);
+ at += keep.len;
+ @memcpy(out[at..][0..o.text.len], o.text);
+ at += o.text.len;
+ from = o.q1;
+ }
+ @memcpy(out[at..], text[from..]);
+ return out;
+}
+
+/// Where `dot` lands after `ops`: each end moved by the changes before it;
+/// an empty dot where a change starts selects what it put there, as
+/// elogapply does (elog.c:283).
+pub fn moveDot(dot: Range, ops: []const Op) Range {
+ var shift: i64 = 0;
+ for (ops) |o| {
+ if (dot.q0 == dot.q1 and o.q0 == dot.q0 and (o.text.len > 0)) {
+ const q: u32 = @intCast(@as(i64, dot.q0) + shift);
+ return .{ .q0 = q, .q1 = q + @as(u32, @intCast(o.text.len)) };
+ }
+ if (o.q1 > dot.q0) break;
+ shift += @as(i64, @intCast(o.text.len)) - @as(i64, o.q1 - o.q0);
+ }
+ const q0: u32 = @intCast(@as(i64, dot.q0) + shift);
+ var shift1: i64 = 0;
+ for (ops) |o| {
+ if (o.q1 > dot.q1 or (o.q0 == o.q1 and o.q0 == dot.q1)) break;
+ shift1 += @as(i64, @intCast(o.text.len)) - @as(i64, o.q1 - o.q0);
+ }
+ const q1: u32 = @intCast(@max(@as(i64, q0), @as(i64, dot.q1) + shift1));
+ return .{ .q0 = q0, .q1 = q1 };
+}
+
+fn edited(text: []const u8, command: []const u8) ![]u8 {
+ var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena_state.deinit();
+ var why: Why = .{};
+ const res = run(arena_state.allocator(), text, .{}, "t", command, &why) catch |err| {
+ if (err == error.Edit) std.debug.print("Edit: {s}\n", .{why.text()});
+ return err;
+ };
+ return apply(std.testing.allocator, text, res.ops);
+}
+
+fn expectEdit(text: []const u8, command: []const u8, want: []const u8) !void {
+ const got = try edited(text, command);
+ defer std.testing.allocator.free(got);
+ try std.testing.expectEqualStrings(want, got);
+}
+
+test "sam's classic commands" {
+ try expectEdit("foo x foo y foo\n", ",x/foo/c/foobar/", "foobar x foobar y foobar\n");
+ try expectEdit("a b\nc d\n", ",x/ /c/_/", "a_b\nc_d\n");
+ // As in acme (ecmd.c:810), y also takes the empty stretch after the last match.
+ try expectEdit("one\ntwo\nthree\n", ",y/\\n/c/X/", "X\nX\nX\nX");
+ try expectEdit("aaa\n", ",s/a/b/g", "bbb\n");
+ try expectEdit("aaa\n", ",s/a/b/", "baa\n");
+ try expectEdit("aaa\n", ",s2/a/b/", "aba\n");
+ try expectEdit("cat dog\n", ",s/[a-z]+/<&>/g", "<cat> <dog>\n");
+ // x within x, g within x.
+ try expectEdit("int a;\nfloat b;\nint c;\n", ",x/.*\\n/g/^int/x/[a-z];/c/X;/", "int X;\nfloat b;\nint X;\n");
+ try expectEdit("keep 1\ndrop 2\nkeep 3\n", ",x g/drop/d", "keep 1\nkeep 3\n");
+ try expectEdit("keep 1\ndrop 2\nkeep 3\n", ",x v/keep/d", "keep 1\nkeep 3\n");
+ // Lines, a/i, grouping, m and t.
+ try expectEdit("a\nb\n", ",x a/;/", "a\n;b\n;");
+ try expectEdit("a\nb\n", "1 i/>/", ">a\nb\n");
+ try expectEdit("abc\n", "/b/{\ni/[/\na/]/\n}", "a[b]c\n");
+ try expectEdit("one\ntwo\n", "1 m $", "two\none\n");
+ try expectEdit("one\ntwo\n", "1 t $", "one\ntwo\none\n");
+ try expectEdit("one\ntwo\nthree\n", "2 d", "one\nthree\n");
+ // pardes's line:col.
+ try expectEdit("one\ntwo\n", "2:2 i/-/", "one\nt-wo\n");
+}
+
+test "an Edit that fails halfway changes nothing, and says why in acme's words" {
+ var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena_state.deinit();
+ for ([_][2][]const u8{
+ .{ ",x/foo/c/bar/\n/nomatch/d", "no match for regexp" },
+ .{ ",s/zzz/y/", "no substitution" },
+ .{ "k", "unknown command k" },
+ .{ ",x/a/{\na/1/\ni/2/\n}", "changes out of sequence" },
+ .{ "}", "right brace with no left brace" },
+ .{ "w /tmp/x", "w is not supported in pardes" },
+ .{ ",s/(a)/\\1/", "no \\1: mvzr keeps no submatches" },
+ .{ "1 m 1,2", "move overlaps itself" },
+ }) |c| {
+ var why: Why = .{};
+ try std.testing.expectError(error.Edit, run(arena_state.allocator(), "foo a\nfoo\n", .{}, "t", c[0], &why));
+ try std.testing.expectEqualStrings(c[1], why.text());
+ }
+}
+
+test "the dot an Edit leaves selects what a change put where it stood" {
+ const ops = [_]Op{.{ .q0 = 2, .q1 = 4, .text = "XYZ" }};
+ try std.testing.expectEqual(Range{ .q0 = 2, .q1 = 5 }, moveDot(.{ .q0 = 2, .q1 = 2 }, &ops));
+ try std.testing.expectEqual(Range{ .q0 = 6, .q1 = 7 }, moveDot(.{ .q0 = 5, .q1 = 6 }, &ops));
+}