diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-05 18:02:16 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-10 09:17:07 -0300 |
| commit | 4ecf494b3af0ec8d8212dd9614b7e7d329a2251f (patch) | |
| tree | 5a60ea3d69c5fafa62cb259faf0b3a1f4cf85d9f /src | |
| parent | 79a8980e46d8ed1f9ef7e3f4e44db7ee58e9f88e (diff) | |
| download | pardes-4ecf494b3af0ec8d8212dd9614b7e7d329a2251f.tar.gz pardes-4ecf494b3af0ec8d8212dd9614b7e7d329a2251f.zip | |
wrap by default, at the window width, with a break marker
Diffstat (limited to 'src')
| -rw-r--r-- | src/config.zig | 12 | ||||
| -rw-r--r-- | src/file_pane.zig | 9 | ||||
| -rw-r--r-- | src/pardes.zig | 75 |
3 files changed, 70 insertions, 26 deletions
diff --git a/src/config.zig b/src/config.zig index b7ca9b11..a8e3639f 100644 --- a/src/config.zig +++ b/src/config.zig @@ -432,11 +432,13 @@ 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; +/// soft wrap (the Wrap builtin): the glyph a wrapped row ends with, in the one +/// column bodyText keeps free for it. A break is the one thing about a wrapped +/// line you cannot see — the text simply continues, and a missing line number +/// on the row below is an absence, which is a poor thing to read a document by. +/// So the break says so at the point it happens, in the chrome's own colour +/// because it is not in the file. +pub const wrap_marker = "↩"; /// 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 b76a7882..98a72ac4 100644 --- a/src/file_pane.zig +++ b/src/file_pane.zig @@ -191,13 +191,14 @@ pub fn refreshHighlights(p: *Pardes) void { /// 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. + // the pane's own content width, less the ONE column the break marker sits + // in — a wrapped row that filled its last cell would have nowhere to say it + // wrapped. 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))); + @max(1, @as(usize, pane.cols -| config.PREFIX_W) -| 1); 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 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); |
