diff options
Diffstat (limited to 'src/sam_edit.zig')
| -rw-r--r-- | src/sam_edit.zig | 59 |
1 files changed, 54 insertions, 5 deletions
diff --git a/src/sam_edit.zig b/src/sam_edit.zig index 29dec726..cd2998ef 100644 --- a/src/sam_edit.zig +++ b/src/sam_edit.zig @@ -16,6 +16,13 @@ const std = @import("std"); const regexp = @import("regexp.zig"); const addr_lang = @import("ninep/addr.zig"); +const modal = @import("modal.zig"); + +/// The next place after `p` a search may start: a rune on, and past the +/// text's end once there, which ends a loop over `p <= end`. +fn stepRune(text: []const u8, p: usize) usize { + return if (p >= text.len) p + 1 else modal.nextRune(text, p); +} pub const Range = addr_lang.Range; @@ -68,6 +75,11 @@ pub fn run(arena: std.mem.Allocator, text: []const u8, dot: Range, name: []const if (last) |l| l.next = c else first = c; last = c; } + // An a, c or i text that never met its `.` line is refused, as an + // unclosed `{` is: sam takes the end of input for one, but a block that + // reaches Edit unfinished was cut short (a write, an open closed), and + // half of it inserted is worse than none. + if (ps.open_text) return fail(why, "a, c or i text not ended by a . line", .{}); 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. @@ -85,6 +97,25 @@ pub fn run(arena: std.mem.Allocator, text: []const u8, dot: Range, name: []const /// 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. +/// What an Edit command still waits for: nothing, the `.` line that ends +/// an a, c or i text block, or the `}` of a group. +pub const Wait = enum { none, text, group }; + +/// As `needsMore`, saying which: while a text block is open only a `.` +/// line can end it, so a caller need not parse the block again for every +/// line of text in it (ninep/ctl.zig, editEnd), which was quadratic. +pub fn waitsFor(arena: std.mem.Allocator, command: []const u8) Wait { + var why: Why = .{}; + const src = std.fmt.allocPrint(arena, "{s}\n", .{command}) catch return .none; + defer arena.free(src); + var ps: Parser = .{ .arena = arena, .src = src, .why = &why }; + while (true) { + const c = ps.parse(0) catch + return if (ps.open_text) .text else if (std.mem.eql(u8, why.text(), "unmatched `{'")) .group else .none; + if (c == null) return if (ps.open_text) .text else .none; + } +} + 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; @@ -418,9 +449,16 @@ const Exec = struct { }; } + /// A match covers the runes it touches, as an address's does + /// (addr.zig): mvzr matches bytes, so `.` would take one byte of `é` + /// and x, y and s would split it. From `from` on only, so a match never + /// reaches back into what the last one took. 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}); + const m = (rx.find(ex.text, from, hi, hi) catch return fail(ex.why, "{s}", .{addr_lang.e_slow})) orelse return null; + var start = modal.runeStart(ex.text, m.start); + if (start < from) start = modal.runeEnd(ex.text, m.start); + return .{ .start = start, .end = @max(start, modal.runeEnd(ex.text, m.end)) }; } /// ecmd.c:62, cmdexec: runs `c` from `dot`, answering the dot it leaves. @@ -516,10 +554,10 @@ const Exec = struct { 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; + p1 = stepRune(ex.text, p1); continue; } - p1 = m.end + 1; + p1 = stepRune(ex.text, m.end); } else p1 = m.end; op = m.end; n -= 1; @@ -560,10 +598,10 @@ const Exec = struct { if (try ex.find(&rx, p, r.q1)) |m| { if (m.start == m.end) { if (op != null and m.start == op.?) { - p += 1; + p = stepRune(ex.text, p); continue; } - p = m.end + 1; + p = stepRune(ex.text, m.end); } 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; @@ -723,3 +761,14 @@ test "the dot an Edit leaves selects what a change put where it stood" { 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)); } + +test "x, y and s match whole runes: a byte pattern never splits one" { + // é is c3 a9: `.` takes it whole, as it takes a lone combining mark. + try expectEdit("\u{e9}", ",x/./a/|/", "\u{e9}|"); + try expectEdit("e\u{301}t", ",s/./X/g", "XXX"); + try expectEdit("a\u{e9}b", ",y/\u{e9}/c/-/", "-\u{e9}-"); + try expectEdit("\u{e9}\u{e9}", ",x/\\xC3/c/E/", "EE"); + // An invalid byte is a place of its own. + try expectEdit("a\x81b", ",x/./a/|/", "a|\x81|b|"); + try expectEdit("\u{e9}", ",s/$/!/", "\u{e9}!"); +} |
