diff options
Diffstat (limited to 'src/sam_edit.zig')
| -rw-r--r-- | src/sam_edit.zig | 56 |
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)); |
