summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config.zig12
-rw-r--r--src/file_pane.zig9
-rw-r--r--src/pardes.zig75
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);