diff options
Diffstat (limited to 'src/file_pane.zig')
| -rw-r--r-- | src/file_pane.zig | 111 |
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; |
