diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-05 15:57:48 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-10 09:17:07 -0300 |
| commit | 1891a51ed2b64eec3964d3af3807be90d08a14c2 (patch) | |
| tree | 1f8e3afb3d2e765ade98ca8f41b3a416db396d24 /src | |
| parent | 68d208e1a0ff37eaef4a2a97b04797a1a6df277c (diff) | |
| download | pardes-1891a51ed2b64eec3964d3af3807be90d08a14c2.tar.gz pardes-1891a51ed2b64eec3964d3af3807be90d08a14c2.zip | |
soft-wrap long lines behind a toggle
Diffstat (limited to 'src')
| -rw-r--r-- | src/builtins.zig | 10 | ||||
| -rw-r--r-- | src/config.zig | 6 | ||||
| -rw-r--r-- | src/file_pane.zig | 111 | ||||
| -rw-r--r-- | src/pardes.zig | 170 |
4 files changed, 248 insertions, 49 deletions
diff --git a/src/builtins.zig b/src/builtins.zig index 9bd955bd..aaeab5fa 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -181,6 +181,16 @@ pub const Colors = struct { } }; +/// Soft wrap: a file line too long for the pane renders across several body +/// rows instead of running off the right edge. Global like Colors, and off by +/// default for the same reason — it changes what every file pane looks like, +/// so it is a switch you flip once rather than something a pane carries. +pub const Wrap = struct { + pub fn run(c: Ctx) void { + c.p.wrap_on = !c.p.wrap_on; + } +}; + /// One step along the ring. With 228 themes in it this is no longer a way to /// REACH a theme — ThemeSel is — but it is still the way to browse one, and the /// browse got better rather than worse: the generated half is sorted by name, so diff --git a/src/config.zig b/src/config.zig index f8a3767b..b7ca9b11 100644 --- a/src/config.zig +++ b/src/config.zig @@ -120,6 +120,7 @@ pub const leader_path = paths: { .Delcol = "cd", .Debug = "td", .Colors = "tc", + .Wrap = "tw", .NextColor = "tn", .Crt = "tr", // the theme picker joins the toggles it belongs with; `Theme` itself takes @@ -431,6 +432,11 @@ test "wheel drift guard" { /// the file pane's line-number gutter, in columns pub const PREFIX_W: u16 = 5; +/// soft wrap (the Wrap builtin): the widest a wrapped row may get. The rule is +/// min(this, the pane's own content width), so a narrow pane breaks at its own +/// right edge and a wide one stops here — a line dragged across a 300-column +/// monitor is not readable text, it is a scanline. +pub const WRAP_COLS: usize = 100; /// vim 'scrolloff': keyboard cursor moves keep this many context rows visible /// above/below the cursor (clamped at file boundaries and short panes), and /// the same count of COLUMNS horizontally 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; diff --git a/src/pardes.zig b/src/pardes.zig index ad530319..3acf5765 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -3505,6 +3505,28 @@ pub const Pane = struct { /// the goal is just being able to read long lines. Byte columns, like the /// rest of the file-pane code. hscroll: i32 = 0, + /// THE WRAP MAP, and the whole of what soft line breaks are: for every body + /// row of the LAST frame, the document line it showed and the byte column + /// of that line the row started at. `wrap_n == 0` says the body was NOT + /// wrapped, i.e. the rows are the lines from `scroll()` down one each — + /// which is exactly what wrapAt/wrapRow below fall back to, so with the + /// toggle off not one reader computes anything it did not compute before. + /// + /// INVALIDATION, the part that rots if nobody says it out loud: written in + /// EXACTLY ONE PLACE, file_pane.bodyText, on every build of a file pane's + /// body. So it is at worst one frame old — which is what a mouse click + /// wants (you click the character you can SEE), and it is fresh for the + /// render passes, every one of which runs after bodyText inside the same + /// renderPane call. Nothing else may write it; a second writer is a second + /// truth, and the first click on a stale row is how you find out. + /// + /// ponytail: a fixed 256 rows. A pane taller than that does not wrap at + /// all — bodyText leaves wrap_n at 0 and clips the way it always did — + /// rather than half-recording a mapping every site here would then have to + /// distrust. Grow the arrays the day a 256-row window turns up. + wrap_line: [256]i32 = undefined, + wrap_col: [256]i32 = undefined, + wrap_n: u16 = 0, sel: [3]Sel = @splat(.{}), /// terminals only: the typed-text buffer standing in for shell rows ovl: ?Ovl = null, @@ -3562,6 +3584,46 @@ pub const Pane = struct { return pane.surfRow(@intCast(pane.vt.screens.active.pages.scrollbar().offset)); } + /// The document position a BODY ROW begins at — `vr` 0 is the first row + /// under the tag. The screen->document half of the wrap map, and the half + /// the mouse asks: a click lands on the character the user can see, which + /// is last frame's arrangement, which is what the map holds. + pub fn wrapAt(pane: *Pane, vr: i32) struct { line: i32, at: i32 } { + if (pane.wrap_n > 0 and vr >= 0 and vr < @as(i32, pane.wrap_n)) + return .{ .line = pane.wrap_line[@intCast(vr)], .at = pane.wrap_col[@intCast(vr)] }; + // unwrapped: rows ARE lines, and the byte column a row starts at is the + // horizontal scroll (always 0 on a terminal, which never has one) + return .{ .line = pane.scroll() + vr, .at = pane.hscroll }; + } + + /// ...and back: the body row `line`:`col` renders on, plus the byte column + /// that row starts at — subtract it from a document column to get a screen + /// one. `row` is -1 when the position is not on screen, which only a + /// wrapped body ever says: unwrapped the arithmetic answers for any line at + /// all and the callers' own bounds checks do the rejecting, as before. + pub fn wrapRow(pane: *Pane, line: i32, col: i32) struct { row: i32, at: i32 } { + if (pane.wrap_n == 0) return .{ .row = line - pane.scroll(), .at = pane.hscroll }; + var i: u16 = 0; + while (i < pane.wrap_n) : (i += 1) { + if (pane.wrap_line[i] != line) continue; + // the LAST row of a line owns every column past its start, so a + // cursor parked on the trailing newline still has somewhere to draw + if (i + 1 < pane.wrap_n and pane.wrap_line[i + 1] == line and col >= pane.wrap_col[i + 1]) continue; + return .{ .row = @intCast(i), .at = pane.wrap_col[i] }; + } + return .{ .row = -1, .at = 0 }; + } + + /// ponytail: `delta` is LOGICAL LINES, wrapped or not — one `j` past the + /// bottom scrolls a whole line even when that line is five screen rows, and + /// a wheel tick or a Ctrl-d page counts lines rather than rows. So a body + /// full of long lines scrolls in jumps, and the view can never sit at the + /// MIDDLE of a wrapped line. That is the ceiling the whole feature buys its + /// smallness with: wrap is render + hit-test and nothing else in the editor + /// knows about it. The upgrade is to make f.scroll a (line, row-within-line) + /// pair, which every reader of it — this, the scrollbar, the syntax window, + /// bodyText, ensureCursorVisible, the gutter click — would then have to + /// learn; do that when scrolling long lines actually annoys someone. fn scrollBy(pane: *Pane, delta: i32) void { if (pane.file) |*f| { const max: i64 = @intCast(file_pane.nlines(pane.gpa, f) -| 1); @@ -3590,9 +3652,22 @@ pub const Pane = struct { pub fn ensureCursorVisible(pane: *Pane) void { // scrolloff margin, shrunk on short panes so the band stays non-empty - const margin: i32 = @min(config.scroll_off, @divTrunc(@as(i32, pane.rows) - 1, 2)); + var margin: i32 = @min(config.scroll_off, @divTrunc(@as(i32, pane.rows) - 1, 2)); const off = pane.scroll(); - const last = off + @as(i32, pane.rows) - 1; + // A WRAPPED body shows fewer LINES than it has rows, and scrolling is + // still by line — so the bottom of the view is the last line the map + // recorded, not off+rows-1, or every long line would scroll the file + // out from under itself. Clamped to the file's own last line because + // the rows past EOF go on counting, and the margin shrinks with it for + // the same reason the short-pane clamp above exists: a band wider than + // the view has no stable position and the two branches below then take + // turns moving it, one keystroke each, for ever. + var last = off + @as(i32, pane.rows) - 1; + if (pane.wrap_n > 0) if (pane.file) |*f| { + const eof: i32 = @intCast(file_pane.nlines(pane.gpa, f) -| 1); + last = @min(pane.wrap_line[pane.wrap_n - 1], eof); + margin = @min(margin, @divTrunc(@max(0, last - off), 2)); + }; if (pane.cur_row < off + margin) { pane.scrollBy(pane.cur_row - margin - off); // scrollBy clamps at line 0 } else if (pane.cur_row > last - margin) { @@ -3608,8 +3683,11 @@ pub const Pane = struct { } // the horizontal mirror, files only: keep scroll_off columns of // context around the cursor (wheel-driven hscroll is exempt — it - // never moves the cursor, and a cursor move pulls the view back) - if (pane.file != null) { + // never moves the cursor, and a cursor move pulls the view back). + // A wrapped body has nothing to scroll sideways, and its hscroll is + // left ALONE rather than zeroed: turn the wrap back off and the view + // you had is still there. + if (pane.file != null and pane.wrap_n == 0) { const w: i32 = @max(1, @as(i32, pane.cols) - @as(i32, config.PREFIX_W)); const hmargin: i32 = @min(config.scroll_off, @divTrunc(w - 1, 2)); if (pane.cur_col < pane.hscroll + hmargin) @@ -3855,6 +3933,10 @@ pub const Pardes = struct { /// its first theme. No frame is rendered in that interval. animate_theme_changes: bool = false, colors_on: bool = true, + /// soft line breaks in file panes (the Wrap builtin). Off by default: with + /// it off every row math in here is the one-row-per-line arithmetic it has + /// always been, and nothing on screen moves by a cell. + wrap_on: bool = false, show_debug: bool = false, /// the Crt builtin's CRT post-process, rendered by the gui shell only /// (the tty and web shells never read it) @@ -4731,8 +4813,9 @@ pub const Pardes = struct { /// the word under the modal cursor as a pane-local selection (paneText /// coords: row 0 is the tag; file panes carry the line-number prefix) fn cursorWordSel(pane: *Pane) Sel { - const vrow = (pane.cur_row - pane.scroll()) + @as(i32, BOX_H); - const vcol = if (pane.file != null) pane.cur_col - pane.hscroll + @as(i32, config.PREFIX_W) else pane.cur_col; + const w = pane.wrapRow(pane.cur_row, pane.cur_col); + const vrow = w.row + @as(i32, BOX_H); + const vcol = if (pane.file != null) pane.cur_col - w.at + @as(i32, config.PREFIX_W) else pane.cur_col; return .{ .state = .done, .c0 = vcol, .c1 = vcol, .r0 = vrow, .r1 = vrow }; } @@ -8539,7 +8622,8 @@ pub const Pardes = struct { p.pdfHorizontalWheel(pane, if (m.button == .wheel_right) 1 else -1); // ponytail: no right clamp — overscroll shows blank and the // next cursor move or left wheel pulls it back - } else if (pane.file != null) { + } else if (pane.file != null and !p.wrap_on) { + // wrapped there is nothing off to the right to reach pane.hscroll = @max(0, pane.hscroll + (if (m.button == .wheel_right) config.wheel_cols else -config.wheel_cols)); } }, @@ -8874,8 +8958,13 @@ pub const Pardes = struct { if (!dragged) pane.sel[sel_slot].state = .none; const body_vis = sl.r1 - @as(i32, BOX_H); if (body_vis >= 0 and pane.mode != .tty) { - pane.cur_row = body_vis + pane.scroll(); - pane.cur_col = if (pane.file != null) @max(0, sl.c1 - @as(i32, config.PREFIX_W)) + pane.hscroll else sl.c1; + // the row the user clicked, as the LAST FRAME drew it: + // which line it showed and the byte column it started + // at, so a click lands on the character under the + // pointer whether or not that row is a continuation + const w = pane.wrapAt(body_vis); + pane.cur_row = w.line; + pane.cur_col = if (pane.file != null) @max(0, sl.c1 - @as(i32, config.PREFIX_W)) + w.at else sl.c1; pane.cur_pinned = true; if (!pane.isTerminal()) pane.mode = .normal; pane.msel.active = false; @@ -8900,9 +8989,10 @@ pub const Pardes = struct { // position, the way the left button converts its drag end. const clk = pane.sel[b]; const cvis = clk.r0 - @as(i32, BOX_H); - const crow = cvis + pane.scroll(); + const cw = pane.wrapAt(cvis); + const crow = cw.line; const ccol = if (pane.file != null) - @max(0, clk.c0 - @as(i32, config.PREFIX_W)) + pane.hscroll + @max(0, clk.c0 - @as(i32, config.PREFIX_W)) + cw.at else clk.c0; // A click INSIDE a live selection takes the WHOLE selection @@ -9045,11 +9135,14 @@ pub const Pardes = struct { const sl = pane.sel[sel_slot]; pane.sel[sel_slot].state = .none; const pfx: i32 = if (pane.file != null) config.PREFIX_W else 0; - const hs: i32 = if (pane.file != null) pane.hscroll else 0; - const row0 = @max(0, sl.r0 - @as(i32, BOX_H)) + pane.scroll(); - const row1 = @max(0, sl.r1 - @as(i32, BOX_H)) + pane.scroll(); - const col0 = @max(0, sl.c0 - pfx) + hs; - const col1 = @max(0, sl.c1 - pfx) + hs; + // both ends of the sweep through the same last-frame map, so a + // chord over wrapped rows cuts the text that was under it + const w0 = pane.wrapAt(@max(0, sl.r0 - @as(i32, BOX_H))); + const w1 = pane.wrapAt(@max(0, sl.r1 - @as(i32, BOX_H))); + const row0 = w0.line; + const row1 = w1.line; + const col0 = @max(0, sl.c0 - pfx) + w0.at; + const col1 = @max(0, sl.c1 - pfx) + w1.at; pane.cur_row = row1; pane.cur_col = col1; pane.cur_pinned = true; @@ -11859,11 +11952,15 @@ pub const Pardes = struct { } // modal line selection (helix `x`): whole body rows, normal mode only if (pane.mode == .normal and pane.msel.active) { - const lo = @min(pane.msel.r0, pane.msel.r1) - off + @as(i32, BOX_H); - const hi = @max(pane.msel.r0, pane.msel.r1) - off + @as(i32, BOX_H); + const lo = @min(pane.msel.r0, pane.msel.r1); + const hi = @max(pane.msel.r0, pane.msel.r1); var row: u16 = BOX_H; // never paint the tag row while (row < r.h) : (row += 1) { - if (@as(i32, row) < lo or @as(i32, row) > hi) continue; + // walked by SCREEN row and asked what LINE each one shows, + // because a wrapped line is several rows. wrapAt degenerates to + // `off + row` when nothing wrapped, which is what this was. + const ar = pane.wrapAt(@as(i32, row) - @as(i32, BOX_H)).line; + if (ar < lo or ar > hi) continue; var col: u16 = if (pane.file != null) config.PREFIX_W else 0; while (col < tw) : (col += 1) { const cell = s.at(tx + col, r.y + row); @@ -11890,7 +11987,6 @@ pub const Pardes = struct { const show_extra = pane.mode != .tty and pane.nsel > 0; if (show_prim or show_extra) { const vpfx: i32 = if (pane.file != null) config.PREFIX_W else 0; - const vhs: i32 = if (pane.file != null) pane.hscroll else 0; var si: usize = 0; while (si <= pane.nsel) : (si += 1) { const primary = si == pane.nsel; @@ -11912,12 +12008,19 @@ pub const Pardes = struct { // is readable on the themes whose selection is already close to // the page. const bg = if (primary) th.sel_bg else mix(th.sel_bg, page_rgb); - var ar: i32 = bnd.lo_row; - while (ar <= bnd.hi_row) : (ar += 1) { - const prow = ar - off + @as(i32, BOX_H); - if (prow < BOX_H or prow >= @as(i32, r.h)) continue; - const cstart: i32 = if (ar == bnd.lo_row) bnd.lo_col - vhs + vpfx else vpfx; - const cend: i32 = if (ar == bnd.hi_row) bnd.hi_col - vhs + vpfx else @as(i32, tw) - 1; + // ...and this walks SCREEN rows too, asking the map which line + // and which byte column of it each one shows. Unwrapped that is + // `off + vr` / `hscroll`, i.e. the arithmetic this was, and it + // is also the cheaper loop: a linewise selection over a whole + // file used to iterate once per LINE to reject all but a + // screenful of them. + var vr: i32 = 0; + while (vr + @as(i32, BOX_H) < @as(i32, r.h)) : (vr += 1) { + const w = pane.wrapAt(vr); + if (w.line < bnd.lo_row or w.line > bnd.hi_row) continue; + const prow = vr + @as(i32, BOX_H); + const cstart: i32 = if (w.line == bnd.lo_row) bnd.lo_col - w.at + vpfx else vpfx; + const cend: i32 = if (w.line == bnd.hi_row) bnd.hi_col - w.at + vpfx else @as(i32, tw) - 1; var col: i32 = @max(cstart, vpfx); while (col <= cend and col < tw) : (col += 1) { const cell = s.at(tx + @as(u16, @intCast(col)), r.y + @as(u16, @intCast(prow))); @@ -11927,8 +12030,9 @@ pub const Pardes = struct { } } if (primary and !preview) continue; // the hardware cursor IS the primary's - const crow = sr.row - off + @as(i32, BOX_H); - const ccol = sr.col - vhs + vpfx; + const cw = pane.wrapRow(sr.row, sr.col); + const crow = cw.row + @as(i32, BOX_H); + const ccol = sr.col - cw.at + vpfx; if (crow >= BOX_H and crow < @as(i32, r.h) and ccol >= vpfx and ccol < tw) { const cell = s.at(tx + @as(u16, @intCast(ccol)), r.y + @as(u16, @intCast(crow))); cell.default = false; @@ -11946,10 +12050,14 @@ pub const Pardes = struct { const goff: i32 = @intCast(pane.vt.screens.active.pages.scrollbar().offset); const crow = if (pane.cur_pinned) pane.cur_row else pane.surfRow(@as(i32, @intCast(cur.y)) + goff); const ccol = if (pane.cur_pinned) pane.cur_col else @as(i32, @intCast(cur.x)); - const prow = crow - off + @as(i32, BOX_H); + // which ROW of a wrapped line the cursor is on, and which byte + // column that row starts at — `off`/`hscroll` when nothing + // wrapped, so this is the same two lines it always was + const cwp = pane.wrapRow(crow, ccol); + const prow = cwp.row + @as(i32, BOX_H); // files: cur_col is content-relative, add the lineno prefix const cx = if (pane.file != null) - @as(i32, config.PREFIX_W) + ccol - pane.hscroll + @as(i32, config.PREFIX_W) + ccol - cwp.at else ccol; if (prow >= BOX_H and cx >= 0 and prow < r.h and cx < tw) @@ -12253,7 +12361,7 @@ pub const Pardes = struct { } return out.items; } - if (pane.file) |*f| return file_pane.bodyText(arena, pane, f); + if (pane.file) |*f| return file_pane.bodyText(arena, pane, f, p.wrap_on); return term_pane.bodyText(arena, pane); } }; |
