diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 02:14:54 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | 5492da0ba1d5f1c1cc0a880f82107961627c9ba6 (patch) | |
| tree | 0230c6b9896d773c1e93722aa944a306184dd6a2 /src/ninep/pane.zig | |
| parent | 69b6d714b7fc7788a1bf1c2305262a6e861253e8 (diff) | |
| download | pardes-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/ninep/pane.zig')
| -rw-r--r-- | src/ninep/pane.zig | 45 |
1 files changed, 38 insertions, 7 deletions
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index c594c7a3..902f4f2a 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -128,11 +128,12 @@ pub fn clip(n: usize) u32 { pub fn dotOf(pane: *Pane) State.Range { const text = bodyOf(pane); - const head = modal.offsetAt(text, .{ .row = @intCast(@max(0, pane.body.cur_row)), .col = @intCast(@max(0, pane.body.cur_col)) }); + // In runes, as every address is (modal.runeStart). + const head = modal.runeOffsetAt(text, .{ .row = @intCast(@max(0, pane.body.cur_row)), .col = @intCast(@max(0, pane.body.cur_col)) }); if (!pane.body.vsel.active) return .{ .q0 = clip(head), .q1 = clip(head) }; - const anchor = modal.offsetAt(text, .{ .row = @intCast(@max(0, pane.body.vsel.row)), .col = @intCast(@max(0, pane.body.vsel.col)) }); + const anchor = modal.runeOffsetAt(text, .{ .row = @intCast(@max(0, pane.body.vsel.row)), .col = @intCast(@max(0, pane.body.vsel.col)) }); var hi = @max(head, anchor); - if (hi < text.len) hi = modal.nextGrapheme(text, hi); + if (hi < text.len) hi = modal.nextRune(text, hi); return .{ .q0 = clip(@min(head, anchor)), .q1 = clip(hi) }; } @@ -140,9 +141,9 @@ pub fn setDot(pane: *Pane, r: State.Range) void { const text = bodyOf(pane); const q0 = @min(@as(usize, r.q0), text.len); const q1 = @max(q0, @min(@as(usize, r.q1), text.len)); - const a = modal.positionAt(text, q0); + const a = modal.runePositionAt(text, q0); pane.body.vsel = .{ .active = q1 > q0, .row = @intCast(a.row), .col = @intCast(a.col), .explicit = true }; - const h = modal.positionAt(text, if (q1 > q0) modal.prevGrapheme(text, q1) else q0); + const h = modal.runePositionAt(text, if (q1 > q0) modal.prevRune(text, q1) else q0); pane.body.cur_row = @intCast(h.row); pane.body.cur_col = @intCast(h.col); pane.body.cur_pinned = true; @@ -369,7 +370,7 @@ fn readData(req: Req, id: usize, pane: *Pane, pf: *State, stop_at_end: bool) Rep const q0: usize = pf.addr.q0; const hi: usize = if (stop_at_end) @max(q0, @as(usize, pf.addr.q1)) else text.len; var end = @min(hi, q0 +| req.size); - end = @max(q0, modal.graphemeStart(text, end)); + end = @max(q0, modal.runeStart(text, end)); pf.addr.q0 = clip(end); if (!stop_at_end) pf.addr.q1 = clip(end); if (pane.file == null) return .{ .tag = req.tag }; @@ -858,7 +859,7 @@ test "data and xdata read from addr, move it, and write through it" { try testing.expectEqual(@as(u32, 4), p.panes[0].?.fs.addr.q0); } -test "data never splits a grapheme, in either direction" { +test "data never splits a rune, in either direction" { const gpa = testing.allocator; const p = try withFile(gpa, "\u{00e9}x\n"); defer p.deinit(); @@ -1205,3 +1206,33 @@ test "setattr truncation empties the body and answers fresh attributes" { const noop = call(p, .{ .tag = 8, .op = .setattr, .node = Node.of(serial, .body) }); try testing.expectEqual(@as(u64, 9), noop.reply.attr.size); } + +test "addresses are in runes: a combining mark and a lone \\r are places of their own" { + const gpa = testing.allocator; + // e, U+0301 (two bytes, 1-2), x, \r, \n + const p = try withFile(gpa, "e\u{0301}x\r\n"); + defer p.deinit(); + const serial = serialOf(p); + const addr_node = Node.of(serial, .addr); + const dot = Node.of(serial, .dot); + const Case = struct { expr: []const u8, q0: u32, q1: u32 }; + for ([_]Case{ + .{ .expr = "#1,#3", .q0 = 1, .q1 = 3 }, // the mark alone + .{ .expr = "#2", .q0 = 1, .q1 = 1 }, // inside it: back to its start + .{ .expr = "#4,#5", .q0 = 4, .q1 = 5 }, // the \r alone + .{ .expr = "1:2", .q0 = 1, .q1 = 1 }, // line:col the same + .{ .expr = "1:3", .q0 = 1, .q1 = 1 }, + .{ .expr = "0/\xCC/", .q0 = 1, .q1 = 3 }, // a match inside a rune covers it + }) |c| { + try testing.expectEqual(Status.ok, wr(p, addr_node, c.expr).reply.status); + try testing.expectEqual(c.q0, p.panes[0].?.fs.addr.q0); + try testing.expectEqual(c.q1, p.panes[0].?.fs.addr.q1); + } + // Copied to dot, it reads back the same. + _ = wr(p, addr_node, "#1,#3"); + try testing.expectEqual(Status.ok, wr(p, dot, " 1 3 ").reply.status); + p.sync(); + try testing.expectEqualStrings(" 1 3 ", rd(p, dot, 0, 100).bytes); + _ = wr(p, addr_node, "#4,#5"); + try testing.expectEqualStrings("\r", rd(p, Node.of(serial, .xdata), 0, 100).bytes); +} |
