summaryrefslogtreecommitdiff
path: root/src/file_pane.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/file_pane.zig')
-rw-r--r--src/file_pane.zig61
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