From 4ecf494b3af0ec8d8212dd9614b7e7d329a2251f Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 5 Aug 2026 18:02:16 -0300 Subject: wrap by default, at the window width, with a break marker --- src/pardes.zig | 75 +++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 17 deletions(-) (limited to 'src/pardes.zig') diff --git a/src/pardes.zig b/src/pardes.zig index 7ff5ba1f..b8b2de33 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -2600,6 +2600,15 @@ pub const Surface = struct { if (cp == '\r') continue; const width: u16 = if (cp < 0x80) 1 else uucode.get(.width, cp); if (width == 0) continue; + // a DOUBLE-width glyph with one column left is not drawn at all. + // Writing it puts one cell in the surface and two on the glass, and + // when that column is the screen's last the terminal wraps the tail + // onto the next row — where our own model says "space", so the diff + // render never repaints it and the smear outlives the frame. A + // blank at the edge is what every terminal does with the same + // problem. Reachable from any byte cut through wide text: hscroll's + // and soft wrap's both. + if (width == 2 and col + 1 >= end) break; s.set(col, y, cp_slice, style); if (width == 2 and col + 1 < end) { // spacer: empty cell under the wide glyph's tail @@ -3663,19 +3672,29 @@ pub const Pane = struct { var margin: i32 = @min(config.scroll_off, @divTrunc(@as(i32, pane.rows) - 1, 2)); const off = pane.scroll(); // 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. + // still by line, so the bottom of the view is not off+rows-1 — a long + // line at the bottom would leave the cursor below the last row it can + // actually see. What the map is asked for is the COUNT of lines that + // fit, not which ones: this runs on every cursor move and the map is + // last FRAME's, but several keys can arrive between two renders (an + // autorepeated j, a paste) and then its absolute line numbers name a + // scroll offset that has already moved on — reading them cost a + // batched j four extra lines of scroll per keystroke. A count is + // scroll-independent, and when nothing wrapped it is exactly `rows`, + // so this whole block is a no-op on an unwrapped body and the margin + // clamp below reduces to the short-pane one above it. + // + // ponytail: last frame's line count applied to this frame's offset. It + // is exact whenever a render happened in between (the normal case) and + // an estimate mid-batch, self-correcting on the next key. The exact + // answer is to re-walk the lines from `off` accumulating wrapped + // heights — do that when a batch visibly lands the cursor off screen. 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.wrap_n > 0) { + const lines_shown = pane.wrap_line[pane.wrap_n - 1] - pane.wrap_line[0]; + last = off + lines_shown; + margin = @min(margin, @divTrunc(@max(0, lines_shown), 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) { @@ -3941,10 +3960,13 @@ 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, + /// soft line breaks in file panes (the Wrap builtin). ON by default: a line + /// running off the right edge is text you cannot read without scrolling + /// sideways for it, and the reason to reach for the toggle is the rare + /// file — a table, a fixture, a minified blob — whose columns mean + /// something. Turned off, every row math in here is the one-row-per-line + /// arithmetic it always was and hscroll comes back. + wrap_on: bool = true, 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) @@ -8796,7 +8818,6 @@ pub const Pardes = struct { // a topbar word runs on the PRESS — there is no // drag to chord into, so the argument is simply // whatever is selected right now: select a word, - // then click Grep, and it greps that word. if (word.len > 0) _ = p.execute(p.active, p.withArg(word, p.heldSelection(p.active))); } return; @@ -12023,6 +12044,26 @@ pub const Pardes = struct { if (pane.file) |*f| { file_pane.drawGutter(p, pane, r, tx, tw, body_h, active); if (p.colors_on) file_pane.recolorSyntax(p, pane, f, r, tx, tw, body_h); + // the break marker, in the column bodyText kept free for it: a row + // that continues says so where it broke. Drawn after the syntax + // pass so nothing recolors it (it is chrome, not file bytes) and + // before the selection passes, which still win over it — a swept + // wrapped line should highlight whole. + // + // ponytail: a row continues iff the NEXT row shows the same line, + // so the bottom row of a pane never carries one — what it continues + // into is off screen anyway. Record the cut in bodyText next to the + // map the day that reads as a lie. + if (tw > config.PREFIX_W + 1) { + var wr: u16 = 0; + while (wr + 1 < pane.wrap_n and wr + 1 < body_h) : (wr += 1) { + if (pane.wrap_line[wr + 1] != pane.wrap_line[wr]) continue; + s.set(tx + tw - 1, r.y + BOX_H + wr, config.wrap_marker, .{ + .fg = .{ .rgb = chrome.lineno }, + .bg = pane_bg, + }); + } + } } const off = pane.scroll(); if (p.colors_on and pane.isTerminal()) term_pane.recolorAnsi(p, pane, r, tx, tw, body_h, off); -- cgit v1.3