summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-05 15:57:48 -0300
committerGabriel Schneider <[email protected]>2026-08-10 09:17:07 -0300
commit1891a51ed2b64eec3964d3af3807be90d08a14c2 (patch)
tree1f8e3afb3d2e765ade98ca8f41b3a416db396d24 /src
parent68d208e1a0ff37eaef4a2a97b04797a1a6df277c (diff)
downloadpardes-1891a51ed2b64eec3964d3af3807be90d08a14c2.tar.gz
pardes-1891a51ed2b64eec3964d3af3807be90d08a14c2.zip
soft-wrap long lines behind a toggle
Diffstat (limited to 'src')
-rw-r--r--src/builtins.zig10
-rw-r--r--src/config.zig6
-rw-r--r--src/file_pane.zig111
-rw-r--r--src/pardes.zig170
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);
}
};