summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-05 18:02:16 -0300
committerGabriel Schneider <[email protected]>2026-08-10 09:17:07 -0300
commit4ecf494b3af0ec8d8212dd9614b7e7d329a2251f (patch)
tree5a60ea3d69c5fafa62cb259faf0b3a1f4cf85d9f /src/pardes.zig
parent79a8980e46d8ed1f9ef7e3f4e44db7ee58e9f88e (diff)
downloadpardes-4ecf494b3af0ec8d8212dd9614b7e7d329a2251f.tar.gz
pardes-4ecf494b3af0ec8d8212dd9614b7e7d329a2251f.zip
wrap by default, at the window width, with a break marker
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig75
1 files changed, 58 insertions, 17 deletions
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);