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.zig111
1 files changed, 93 insertions, 18 deletions
diff --git a/src/file_pane.zig b/src/file_pane.zig
index 36818d8d..b76a7882 100644
--- a/src/file_pane.zig
+++ b/src/file_pane.zig
@@ -179,10 +179,26 @@ pub fn refreshHighlights(p: *Pardes) void {
}
}
-/// the body a file pane renders: `pane.rows` content lines from the scroll
+/// 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.
-pub fn bodyText(arena: std.mem.Allocator, pane: *Pane, f: *File) ![]const u8 {
+///
+/// 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.
+pub fn bodyText(arena: std.mem.Allocator, pane: *Pane, f: *File, wrap: bool) ![]const u8 {
var out: std.ArrayList(u8) = .empty;
+ // min(WRAP_COLS, the pane's own content width), and 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, @min(config.WRAP_COLS, @as(usize, pane.cols -| config.PREFIX_W)));
+ pane.wrap_n = 0;
// start ON the first visible line instead of walking the file to it: this
// walk was O(f.scroll) and recolorSyntax below ran the identical one again
var flines = std.mem.splitScalar(u8, f.content[lineStart(pane.gpa, f, f.scroll)..], '\n');
@@ -190,23 +206,52 @@ pub fn bodyText(arena: std.mem.Allocator, pane: *Pane, f: *File) ![]const u8 {
// old walk left the iterator dry, so drop the one empty line a slice split
// still yields, or the body grows a phantom numbered row
if (f.scroll >= nlines(pane.gpa, f)) _ = flines.next();
+ // the line the NEXT row comes from and the byte column of it that row
+ // starts at — the two the map records, walked forward by the loop
+ var abs: i32 = @intCast(f.scroll);
+ var at: usize = 0;
+ var cur = flines.next();
var i: usize = 0;
while (i < pane.rows) : (i += 1) {
if (i > 0) try out.append(arena, '\n');
- const abs: i32 = @as(i32, @intCast(f.scroll)) + @as(i32, @intCast(i));
+ if (width > 0) {
+ pane.wrap_line[i] = abs;
+ pane.wrap_col[i] = @intCast(at);
+ pane.wrap_n = @intCast(i + 1);
+ }
const row_start = out.items.len;
- if (flines.next()) |text| {
+ if (cur) |text| {
var lbuf: [16]u8 = undefined;
// unsigned: {d} prints a leading '+' for signed ints
const lineno: usize = @intCast(abs + 1);
- const prefix = std.fmt.bufPrint(&lbuf, "{d: >4} ", .{lineno}) catch " ";
- var base: std.ArrayList(u8) = .empty;
- try base.appendSlice(arena, prefix);
- try base.appendSlice(arena, text);
- try out.appendSlice(arena, base.items);
- }
- // horizontal scroll: cut hscroll columns after the gutter
- if (pane.hscroll > 0) {
+ // the number belongs to the LINE, so only its first row carries
+ // one — repeated down a wrapped line it would read as several
+ // lines, which is exactly what this is not
+ const prefix = if (at > 0)
+ " "
+ else
+ std.fmt.bufPrint(&lbuf, "{d: >4} ", .{lineno}) catch " ";
+ try out.appendSlice(arena, prefix);
+ // A byte cut, like the hscroll one below, and it can land inside a
+ // multi-byte glyph for the same reason: columns here are BYTES.
+ // Surface.print decodes by hand and emits U+FFFD per undecodable
+ // byte, so a split glyph renders as a replacement char rather than
+ // panicking — see test/snapshots/badutf.snap. It cannot overflow
+ // the pane either: a UTF-8 sequence is never fewer bytes than the
+ // cells it draws in.
+ const take = if (width == 0) text.len - at else @min(width, text.len - at);
+ try out.appendSlice(arena, text[at .. at + take]);
+ if (width > 0 and at + take < text.len) {
+ at += take;
+ } else {
+ abs += 1;
+ at = 0;
+ cur = flines.next();
+ }
+ } else abs += 1;
+ // horizontal scroll: cut hscroll columns after the gutter. Wrapped
+ // there is nothing to scroll to, so the cut is not made at all.
+ if (pane.hscroll > 0 and width == 0) {
const cut_at = row_start + config.PREFIX_W;
if (out.items.len > cut_at) {
const cut = @min(@as(usize, @intCast(pane.hscroll)), out.items.len - cut_at);
@@ -227,10 +272,16 @@ pub fn drawGutter(p: *Pardes, pane: *Pane, r: pardes.Rect, tx: u16, tw: u16, bod
const goff = pane.scroll();
const gcur = pane.vt.screens.active.cursor;
const gcrow = if (pane.cur_pinned) pane.cur_row else @as(i32, @intCast(gcur.y)) + goff;
- const cur_vr: i32 = if (active and !pane.tag_edit) gcrow - goff else -1;
+ // the cursor's LINE, not its row: wrapped, one line owns a run of rows and
+ // the number sits on the first of them, so the whole run lights up — the
+ // gutter is naming the line you are on, and that is still one line
+ const cur_line: i32 = if (active and !pane.tag_edit) gcrow else std.math.minInt(i32);
var vr: u16 = 0;
while (vr < body_h) : (vr += 1) {
- const on_cursor = @as(i32, vr) == cur_vr;
+ const row_line: i32 = if (pane.wrap_n == 0)
+ goff + @as(i32, vr)
+ else if (vr < pane.wrap_n) pane.wrap_line[vr] else std.math.maxInt(i32);
+ const on_cursor = row_line == cur_line;
var c: u16 = 0;
while (c < config.PREFIX_W and c < tw) : (c += 1) {
const cell = s.at(tx + c, r.y + pardes.BOX_H + vr);
@@ -264,13 +315,37 @@ pub fn recolorSyntax(p: *Pardes, pane: *Pane, f: *File, r: pardes.Rect, tx: u16,
defer tz_recolor.end();
// indexed start, same as bodyText — an empty tail simply paints nothing
var flines = std.mem.splitScalar(u8, f.content[lineStart(p.gpa, f, f.scroll)..], '\n');
- const hs: usize = @intCast(@max(0, pane.hscroll));
+ const total = nlines(p.gpa, f);
var vr: u16 = 0;
while (vr < body_h) : (vr += 1) {
- const line = flines.next() orelse break;
- const base = @intFromPtr(line.ptr) - @intFromPtr(f.content.ptr);
+ // A colour has to land on the byte it belongs to, so this walk reads
+ // the same map the body was built from: wrapped, the screen row names
+ // its own line and the byte column it began at, and it ends where the
+ // NEXT row of that line begins. Unwrapped the rows ARE the lines in
+ // order and the split iterator is the cheaper walk.
+ var base: usize = undefined;
+ var line: []const u8 = undefined;
+ var hs: usize = @intCast(@max(0, pane.hscroll));
+ var limit: usize = undefined;
+ if (pane.wrap_n == 0) {
+ line = flines.next() orelse break;
+ base = @intFromPtr(line.ptr) - @intFromPtr(f.content.ptr);
+ limit = line.len;
+ } else {
+ if (vr >= pane.wrap_n) break;
+ const lrow: usize = @intCast(@max(0, pane.wrap_line[vr]));
+ if (lrow >= total) break;
+ base = lineStart(p.gpa, f, lrow);
+ const lend = if (lrow + 1 < total) lineStart(p.gpa, f, lrow + 1) -| 1 else f.content.len;
+ line = f.content[base..lend];
+ hs = @intCast(pane.wrap_col[vr]);
+ limit = if (vr + 1 < pane.wrap_n and pane.wrap_line[vr + 1] == pane.wrap_line[vr])
+ @min(line.len, @as(usize, @intCast(pane.wrap_col[vr + 1])))
+ else
+ line.len;
+ }
var c: usize = 0;
- while (hs + c < line.len and config.PREFIX_W + c < tw) : (c += 1) {
+ while (hs + c < limit and config.PREFIX_W + c < tw) : (c += 1) {
const idx = base + hs + c;
if (idx < f.highlight_start) continue;
const hidx = idx - f.highlight_start;