summaryrefslogtreecommitdiff
path: root/src/modal.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 02:14:54 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit5492da0ba1d5f1c1cc0a880f82107961627c9ba6 (patch)
tree0230c6b9896d773c1e93722aa944a306184dd6a2 /src/modal.zig
parent69b6d714b7fc7788a1bf1c2305262a6e861253e8 (diff)
downloadpardes-5492da0ba1d5f1c1cc0a880f82107961627c9ba6.tar.gz
pardes-5492da0ba1d5f1c1cc0a880f82107961627c9ba6.zip
Addresses snap to runes, not grapheme clusters
sam and acme address runes; pardes snapped #n, line:col, dot and data's reads to grapheme clusters, so a lone combining mark or a CRLF's \r could not be addressed. #n, line:col, a search's match, dot (both ways) and data now land on rune boundaries (modal.runeStart and friends), and the tty's cell drawing is untouched. The README, fs.md and the skill say so. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/modal.zig')
-rw-r--r--src/modal.zig44
1 files changed, 44 insertions, 0 deletions
diff --git a/src/modal.zig b/src/modal.zig
index cc7f1ebd..dd7d2b57 100644
--- a/src/modal.zig
+++ b/src/modal.zig
@@ -1322,6 +1322,50 @@ pub fn lineEndOffset(text: []const u8, line: usize) usize {
}
/// gap offset -> (row, col) cell
+/// The start of the UTF-8 rune `off` falls in. Addresses (the 9P `addr`,
+/// `dot`, `#n`, `line:col`, a search's match) count runes, as sam's and
+/// 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);
+ 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;
+}
+
+/// `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;
+}
+
+/// The rune boundary after the one at `off`.
+pub fn nextRune(text: []const u8, off: usize) usize {
+ return if (off >= text.len) text.len else runeEnd(text, off + 1);
+}
+
+/// The rune boundary before `off`.
+pub fn prevRune(text: []const u8, off: usize) usize {
+ return if (off == 0) 0 else runeStart(text, @min(off, text.len) - 1);
+}
+
+/// `positionAt` snapped to a rune rather than a grapheme cluster.
+pub fn runePositionAt(text: []const u8, off: usize) Cursor {
+ const bounded = runeStart(text, off);
+ const s = if (std.mem.lastIndexOfScalar(u8, text[0..bounded], '\n')) |nl| nl + 1 else 0;
+ return .{ .row = lineAtOffset(text, bounded), .col = bounded - s };
+}
+
+/// `offsetAt` snapped to a rune rather than a grapheme cluster.
+pub fn runeOffsetAt(text: []const u8, c: Cursor) usize {
+ const row = @min(c.row, cursorLineCount(text) - 1);
+ const s = lineStartOffset(text, row);
+ const e = std.mem.indexOfScalarPos(u8, text, s, '\n') orelse text.len;
+ return s + runeStart(text[s..e], @min(c.col, e - s));
+}
+
pub fn positionAt(text: []const u8, off: usize) Cursor {
const bounded = @min(off, text.len);
// Find the line start backward without a second scan of the document prefix.