From beea3aaa8d35c00145c78a746393d71b8b14e658 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 03:01:30 -0300 Subject: Edit's x, y and s match whole runes, as addresses do MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mvzr matches bytes, and only the addr path widened a match to runes, so ,x/./a/|/ on é split it into invalid UTF-8 and ,s/./X/g gave a combining sequence four X's. Edit's matches now go through the same rune alignment (and an empty match steps a rune, not a byte). runeStart no longer walks a stray continuation byte back onto the ASCII before it: an invalid byte is a place of its own. Co-Authored-By: Claude Opus 5.5 --- src/modal.zig | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) (limited to 'src/modal.zig') diff --git a/src/modal.zig b/src/modal.zig index dd7d2b57..11eda7b9 100644 --- a/src/modal.zig +++ b/src/modal.zig @@ -1327,18 +1327,26 @@ pub fn lineEndOffset(text: []const u8, line: usize) usize { /// acme's do, not grapheme clusters: a lone combining mark or a `\r` is a /// place of its own, whatever a terminal draws in one cell. pub fn runeStart(text: []const u8, off: usize) usize { - var o = @min(off, text.len); + const o = @min(off, text.len); + if (o == text.len or text[o] & 0xC0 != 0x80) return o; + var s = o; var steps: u8 = 0; - while (o > 0 and o < text.len and text[o] & 0xC0 == 0x80 and steps < 3) : (steps += 1) o -= 1; - return o; + while (s > 0 and text[s] & 0xC0 == 0x80 and steps < 3) : (steps += 1) s -= 1; + // Only a lead byte whose sequence reaches `o`, its bytes all + // continuations, makes `o` the inside of a rune; a stray continuation + // byte (invalid UTF-8) is a place of its own. + const n = std.unicode.utf8ByteSequenceLength(text[s]) catch return o; + if (s + n <= o or s + n > text.len) return o; + for (text[s + 1 .. s + n]) |c| if (c & 0xC0 != 0x80) return o; + return s; } /// `off`, or the end of the rune it falls inside. pub fn runeEnd(text: []const u8, off: usize) usize { - var o = @min(off, text.len); - var steps: u8 = 0; - while (o < text.len and text[o] & 0xC0 == 0x80 and steps < 3) : (steps += 1) o += 1; - return o; + const o = @min(off, text.len); + const s = runeStart(text, o); + if (s == o) return o; + return s + (std.unicode.utf8ByteSequenceLength(text[s]) catch 1); } /// The rune boundary after the one at `off`. -- cgit v1.3