summaryrefslogtreecommitdiff
path: root/src/modal.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 03:01:30 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commitbeea3aaa8d35c00145c78a746393d71b8b14e658 (patch)
tree85497b5c2097d595e3f86f33d269cf0c3336ef9f /src/modal.zig
parentd0500fe7ecd6149ffafe6a04ff2b8206ff6c52df (diff)
downloadpardes-beea3aaa8d35c00145c78a746393d71b8b14e658.tar.gz
pardes-beea3aaa8d35c00145c78a746393d71b8b14e658.zip
Edit's x, y and s match whole runes, as addresses do
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 <[email protected]>
Diffstat (limited to 'src/modal.zig')
-rw-r--r--src/modal.zig22
1 files changed, 15 insertions, 7 deletions
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`.