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.zig56
1 files changed, 53 insertions, 3 deletions
diff --git a/src/sam_edit.zig b/src/sam_edit.zig
index c08c6cdd..679823da 100644
--- a/src/sam_edit.zig
+++ b/src/sam_edit.zig
@@ -82,6 +82,19 @@ pub fn run(arena: std.mem.Allocator, text: []const u8, dot: Range, name: []const
return .{ .ops = ex.ops.items, .dot = d, .printed = ex.out.items };
}
+/// Whether `command` stops inside a `{` group or an `a`, `c` or `i` text
+/// block still waiting for its `.` line: a writer of lines (a ctl, exec)
+/// hands an Edit the lines after it until it does not.
+pub fn needsMore(arena: std.mem.Allocator, command: []const u8) bool {
+ var why: Why = .{};
+ const src = std.fmt.allocPrint(arena, "{s}\n", .{command}) catch return false;
+ var ps: Parser = .{ .arena = arena, .src = src, .why = &why };
+ while (true) {
+ const c = ps.parse(0) catch return std.mem.eql(u8, why.text(), "unmatched `{'") or ps.open_text;
+ if (c == null) return ps.open_text;
+ }
+}
+
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;
@@ -95,6 +108,10 @@ const Parser = struct {
i: usize = 0,
/// The last pattern given, for an empty one (edit.c:562, getregexp).
lastpat: []const u8 = "",
+ /// The last `parse` that answered null met a `}`, not the end.
+ braced: bool = false,
+ /// A text block of `a`, `c` or `i` ran out before its `.` line.
+ open_text: bool = false,
why: *Why,
fn getch(ps: *Parser) ?u8 {
@@ -248,6 +265,7 @@ const Parser = struct {
try s.appendSlice(ps.arena, line);
try s.append(ps.arena, '\n');
}
+ ps.open_text = true;
return s.items;
}
const delim = ps.getch() orelse return "";
@@ -290,7 +308,11 @@ const Parser = struct {
if (ps.nextc() == 'g') {
ps.i += 1;
cmd.global = true;
- }
+ } else if (ps.nextc()) |d| if (d >= '1' and d <= '9') {
+ // `s/a/b/2`, the count after, as some write it for
+ // sam's `s2/a/b/`.
+ cmd.num = ps.getnum(1);
+ };
}
try ps.atnl();
},
@@ -326,12 +348,18 @@ const Parser = struct {
var last: ?*Cmd = null;
while (true) {
if (ps.skipbl() == '\n') ps.i += 1;
- const next = (try ps.parse(nest + 1)) orelse break;
+ ps.braced = false;
+ const next = (try ps.parse(nest + 1)) orelse {
+ // sam reads on for the `}`; an Edit has no more to read.
+ if (!ps.braced) return fail(ps.why, "unmatched `{{'", .{});
+ break;
+ };
if (last) |l| l.next = next else cmd.sub = next;
last = next;
}
},
'}' => {
+ ps.braced = true;
try ps.atnl();
if (nest == 0) return fail(ps.why, "right brace with no left brace", .{});
return null;
@@ -380,7 +408,10 @@ const Exec = struct {
}
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});
+ return regexp.Regex.compile(pat) catch |err| switch (err) {
+ error.Anchor => fail(ex.why, "{s}", .{regexp.Regex.e_anchor}),
+ error.Bad => fail(ex.why, "bad regexp in {c} command", .{c}),
+ };
}
fn find(ex: *Exec, rx: *regexp.Regex, from: usize, hi: usize) Failure!?regexp.Regex.Match {
@@ -625,6 +656,7 @@ test "sam's classic commands" {
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("aaa\n", ",s/a/b/2", "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");
@@ -637,6 +669,8 @@ test "sam's classic commands" {
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");
+ // ^ at every line start in a pattern that spans lines (dogfood round 5).
+ try expectEdit("x = 1\ndef a\ndef b\n", ",x/^def .*\\n/a/X\\n/", "x = 1\ndef a\nX\ndef b\nX\n");
// pardes's line:col.
try expectEdit("one\ntwo\n", "2:2 i/-/", "one\nt-wo\n");
}
@@ -650,9 +684,12 @@ test "an Edit that fails halfway changes nothing, and says why in acme's words"
.{ "k", "unknown command k" },
.{ ",x/a/{\na/1/\ni/2/\n}", "changes out of sequence" },
.{ "}", "right brace with no left brace" },
+ .{ ",x/foo/{", "unmatched `{'" },
+ .{ ",x/foo/{\nd", "unmatched `{'" },
.{ "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" },
+ .{ ",x/(^|\\n)foo/d", "in a pattern with \\n, ^ can only come first and $ only just before a \\n" },
}) |c| {
var why: Why = .{};
try std.testing.expectError(error.Edit, run(arena_state.allocator(), "foo a\nfoo\n", .{}, "t", c[0], &why));
@@ -660,6 +697,19 @@ test "an Edit that fails halfway changes nothing, and says why in acme's words"
}
}
+test "an Edit wants the lines after it until its braces and text blocks close" {
+ var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena_state.deinit();
+ const a = arena_state.allocator();
+ try std.testing.expect(needsMore(a, ",x/foo/{"));
+ try std.testing.expect(needsMore(a, ",x/foo/{\ni/[/"));
+ try std.testing.expect(!needsMore(a, ",x/foo/{\ni/[/\n}"));
+ try std.testing.expect(needsMore(a, "$a\nline one"));
+ try std.testing.expect(!needsMore(a, "$a\nline one\n."));
+ try std.testing.expect(!needsMore(a, ",s/{/x/"));
+ try std.testing.expect(!needsMore(a, ",x/foo/c/bar/"));
+}
+
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));