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.zig59
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}!");
+}