diff options
Diffstat (limited to 'src/file_pane.zig')
| -rw-r--r-- | src/file_pane.zig | 61 |
1 files changed, 48 insertions, 13 deletions
diff --git a/src/file_pane.zig b/src/file_pane.zig index 42e5dba9..80c31edb 100644 --- a/src/file_pane.zig +++ b/src/file_pane.zig @@ -559,25 +559,60 @@ pub fn refreshHighlights(p: *Pardes) void { } } +/// The width a wrapped row of THIS pane holds, in cells, or 0 when the pane is +/// not wrapping — the render decision, named once so motion cannot disagree +/// with paint. One column is left for the break marker: a row that filled its +/// last cell would have nowhere to say it continues. A pane taller than the +/// map refuses to wrap rather than record part of itself (see Pane.wrap_line). +pub fn wrapWidth(pane: *const Pane, wrap: bool) usize { + if (!wrap or pane.rows > pane.wrap_line.len) return 0; + return @max(1, @as(usize, pane.cols -| config.PREFIX_W) -| 1); +} + +pub const VisualRow = struct { start: usize, end: usize }; + +/// The visual row of `line` holding byte `col`: `[start, end)`, where `end` is +/// where the next visual row of the same line begins and equals `line.len` on +/// the last one. This is the same walk `fillBody` renders with, so `gj`/`gk` +/// step exactly the breaks a reader sees. `width == 0` (not wrapping) makes +/// the whole line one visual row, which is what collapses visual motion onto +/// textual motion instead of special-casing it upstream. +pub fn visualRow(line: []const u8, col: usize, width: usize) VisualRow { + if (width == 0) return .{ .start = 0, .end = line.len }; + var start: usize = 0; + while (true) { + const end = fitEnd(line, start, width); + if (col < end or end >= line.len) return .{ .start = start, .end = end }; + start = end; + } +} + +test "visual rows partition a line at the breaks the body renders" { + const line = "abcdefgh"; + try std.testing.expectEqual(VisualRow{ .start = 0, .end = line.len }, visualRow(line, 5, 0)); + try std.testing.expectEqual(VisualRow{ .start = 0, .end = 3 }, visualRow(line, 0, 3)); + try std.testing.expectEqual(VisualRow{ .start = 0, .end = 3 }, visualRow(line, 2, 3)); + try std.testing.expectEqual(VisualRow{ .start = 3, .end = 6 }, visualRow(line, 3, 3)); + // Past the end (a cursor on the newline) names the LAST row, and a short + // line is one row however narrow the pane is. + try std.testing.expectEqual(VisualRow{ .start = 6, .end = 8 }, visualRow(line, line.len, 3)); + try std.testing.expectEqual(VisualRow{ .start = 0, .end = 0 }, visualRow("", 0, 3)); + // A grapheme wider than the row still occupies exactly one row. + try std.testing.expectEqual(VisualRow{ .start = 0, .end = 4 }, visualRow("👩x", 0, 1)); +} + /// the body a file pane renders: `pane.rows` SCREEN rows from the scroll /// offset, each behind its right-aligned line number, then cut by hscroll. /// /// With `wrap` on a line too long for the pane takes several rows instead of /// running off the right edge, and this is where that happens — it is a render -/// property and nothing else in the editor learns about it. What every other -/// site needs is the map back: which line each row showed and at which byte -/// column it began, recorded into pane.wrap_line/wrap_col as the rows are -/// built. `wrap_n` stays 0 for an unwrapped body, and that is the value the -/// readers treat as "rows are lines", so the off path never consults an array. +/// property, and the one thing the rest of the editor reads back is the map: +/// which line each row showed and at which byte column it began, recorded into +/// pane.wrap_line/wrap_col as the rows are built. `wrap_n` stays 0 for an +/// unwrapped body, and that is the value the readers treat as "rows are +/// lines", so the off path never consults an array. pub fn bodyText(arena: std.mem.Allocator, pane: *Pane, f: *State, wrap: bool) ![]const u8 { - // the pane's own content width, less the ONE column the break marker sits - // in — a wrapped row that filled its last cell would have nowhere to say it - // wrapped. 0 means don't wrap. A pane taller than the map refuses too, - // rather than recording part of itself — see Pane.wrap_line. - const width: usize = if (!wrap or pane.rows > pane.wrap_line.len) - 0 - else - @max(1, @as(usize, pane.cols -| config.PREFIX_W) -| 1); + const width = wrapWidth(pane, wrap); pane.wrap_n = 0; // Count the exact rendered bytes first. Unwrapped source lines are not |
