From b660adabce36e90cbfbb52d95715528983376ab5 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 22:02:04 -0300 Subject: A pane whose whole text fits in its rows never scrolls to keep a margin The scroll margin reveals text; with none off screen there is nothing to reveal. ensureCursorVisible and a centred look both leave such a pane at its top. A longer file keeps helix's clamp (hxdiff and hxparity unchanged). Co-Authored-By: Claude Opus 5.5 --- src/look.zig | 3 ++- src/panes.zig | 18 +++++++++++++++++- src/pardes.zig | 18 ++++++++++++++++++ test/snapshots/find.golden | 11 +---------- test/snapshots/lookrange.golden | 38 ++++++-------------------------------- test/snapshots/lsp-client.golden | 16 +--------------- test/snapshots/rsearch.golden | 11 ++--------- test/snapshots/rsearch.snap | 19 +++++++++---------- 8 files changed, 56 insertions(+), 78 deletions(-) diff --git a/src/look.zig b/src/look.zig index b6a89b64..a413bf50 100644 --- a/src/look.zig +++ b/src/look.zig @@ -968,7 +968,8 @@ pub fn focusPaneLine(p: *Pardes, id: usize, at: Spot, landing: enum { center, ke if (pane.file) |*f| { if (at.line > panes.File.nlines(p.gpa, f)) return; if (landing == .center) { - const next = (at.line - 1) -| pane.rows / 2; // center, clamp at top + // center, clamp at top; a text that fits is shown whole + const next = if (pane.textFits()) 0 else (at.line - 1) -| pane.rows / 2; if (next != f.scroll) { f.scroll = next; f.syntax_dirty = true; diff --git a/src/panes.zig b/src/panes.zig index a7e5c668..47582692 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -512,6 +512,16 @@ pub const Pane = struct { } } + /// Every line of the file's text fits in the body's rows; the empty + /// line after a final newline is no text. + pub fn textFits(pane: *Pane) bool { + if (pane.file) |*f| { + const lines = File.nlines(pane.gpa, f) - @intFromBool(std.mem.endsWith(u8, f.content, "\n")); + return lines <= pane.rows; + } + return false; + } + pub fn ensureCursorVisible(pane: *Pane) void { const off = pane.scroll(); var in_context = false; @@ -539,7 +549,13 @@ pub const Pane = struct { pane.scrollBy(pane.body.cur_row - margin - off); } else if (pane.body.cur_row > last - margin) { var to = pane.body.cur_row + margin; - if (pane.file) |*f| to = @min(to, @as(i32, @intCast(File.nlines(pane.gpa, f) -| 1))); + if (pane.file) |*f| { + // The margin only reveals text: a pane its whole text + // fits in never scrolls for it. A longer one keeps + // helix's clamp at the buffer's last line. + const lines: i32 = @intCast(File.nlines(pane.gpa, f)); + to = @min(to, if (off == 0 and pane.textFits()) @max(pane.body.cur_row, lines - 1 - @intFromBool(std.mem.endsWith(u8, f.content, "\n"))) else lines - 1); + } if (comptime Pdf.enabled) { if (pane.pdf) |pv| to = @min(to, @as(i32, @intCast(modal.lineCount(pv.text) -| 1))); diff --git a/src/pardes.zig b/src/pardes.zig index 23341904..18eedccb 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -1354,6 +1354,24 @@ test "BootShell keep leaves the untouched boot shell a dragged document joins; r } } +test "a pane its whole text fits in never scrolls to keep a margin" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 30 }); + defer p.deinit(); + const pane = try p.setTestFile("one\ntwo\nthree\n"); + // A second pane under it, as acme's placement puts one: right under the + // three lines, which leaves the file exactly three body rows. + const below = p.freeSlot().?; + _ = try p.openTutorView(below); + exec.placeNew(p, 0, 0, below, .doc); + p.active = 0; + p.sync(); + try std.testing.expectEqual(@as(u16, 3), p.rects[0].h - pane.tag_rows); + for ("jj") |c| p.update(.{ .key = .{ .cp = c, .text = &.{c} } }); + p.sync(); + try std.testing.expectEqual(@as(i32, 2), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, 0), pane.scroll()); +} + test "dragging a column's only pane to another leaves that column in place, empty" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 32 }); defer p.deinit(); diff --git a/test/snapshots/find.golden b/test/snapshots/find.golden index 50557fad..e6e1bac1 100644 --- a/test/snapshots/find.golden +++ b/test/snapshots/find.golden @@ -65,15 +65,9 @@ |12: /tmp/pardes-snap/find/cwd/sub/nest/zqmark-gamma.txt Save Tty Collapse Del |13: 1 gamma body == snap third grid=120x41 cursor=7,16 -|6: 2 sub/nest/zqmark-gamma.txt -|7: 3 zqmark-alpha.txt -|8: 4 |15: /tmp/pardes-snap/find/cwd/zqmark-alpha.txt Save Tty Collapse Del |16: 1 alpha body == snap back grid=120x41 cursor=7,13 -|6: 1 sub/ZQMARK-beta.txt -|7: 2 sub/nest/zqmark-gamma.txt -|8: 3 zqmark-alpha.txt == snap word grid=120x41 cursor=7,13 |3: 1 Find Find |4: Find / @@ -81,11 +75,8 @@ |3: 1 Find Find / |4: |13: 1 gamma body Last -== snap nomatch grid=120x41 cursor=7,7 +== snap nomatch grid=120x41 cursor=7,8 |3: 1 Find -|6: 2 sub/nest/zqmark-gamma.txt -|7: 3 zqmark-alpha.txt -|8: 4 |13: 1 gamma body |18: /tmp/pardes-snap/find/cwd/+Search Save Tty Collapse Del |19: 1 diff --git a/test/snapshots/lookrange.golden b/test/snapshots/lookrange.golden index 19a83f2c..7d802b0b 100644 --- a/test/snapshots/lookrange.golden +++ b/test/snapshots/lookrange.golden @@ -85,37 +85,11 @@ |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-9 #e5eee5,#0d1410, 10-15 #f7e4a5,#5e4923, 16-18 #e5eee5,#0d1410, 19-149 d,#0d1410, |20: 0-0 d,#608269, 1-1 d,#0d1410, 2-18 #59675c,#0d1410, 19-149 d,#0d1410, |21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-11 #e5eee5,#0d1410, 12-17 #f7e4a5,#5e4923, 18-149 d,#0d1410, -== snap hit2 grid=150x31 cursor=17,12 -|8: 4 dddd -|9: 5 eeee -|10: 6 ffff -|11: 7 gg NEEDLE gg -|12: 8 hhhh NEEDLE -|13: 9 iiii -|14: -|15: -|16: +== snap hit2 grid=150x31 cursor=17,15 == style hit2 grid=150x31 -|8: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410, -|9: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410, -|10: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410, -|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-18 #e5eee5,#0d1410, 19-149 d,#0d1410, -|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-11 #e5eee5,#0d1410, 12-17 #f5f7ee,#3c614a, 18-149 d,#0d1410, -|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410, -|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410, -|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410, -== snap back grid=150x31 cursor=15,12 -|8: 3 cccc dccc -|9: 4 dddd -|10: 5 eeee -|11: 6 ffff -|12: 7 gg NEEDLE gg -|13: 8 hhhh NEEDLE -|14: 9 iiii +|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-18 #e5eee5,#0d1410, 19-149 d,#0d1410, +|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-11 #e5eee5,#0d1410, 12-17 #f5f7ee,#3c614a, 18-149 d,#0d1410, +== snap back grid=150x31 cursor=15,14 == style back grid=150x31 -|8: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-15 #e5eee5,#0d1410, 16-149 d,#0d1410, -|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410, -|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410, -|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-9 #e5eee5,#0d1410, 10-15 #f5f7ee,#3c614a, 16-18 #e5eee5,#0d1410, 19-149 d,#0d1410, -|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #e5eee5,#0d1410, 18-149 d,#0d1410, -|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410, +|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-9 #e5eee5,#0d1410, 10-15 #f5f7ee,#3c614a, 16-18 #e5eee5,#0d1410, 19-149 d,#0d1410, +|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #e5eee5,#0d1410, 18-149 d,#0d1410, diff --git a/test/snapshots/lsp-client.golden b/test/snapshots/lsp-client.golden index e4ea4c90..17eb0d40 100644 --- a/test/snapshots/lsp-client.golden +++ b/test/snapshots/lsp-client.golden @@ -25,19 +25,5 @@ |20: 5 main.rs:5:13-15 |21: 6 let y = add(3, 4); |22: 7 -== snap gr-opened grid=100x25 cursor=21,6 -|3: 2 -|4: 3 fn main() { -|5: 4 let x = add(1, 2); -|6: 5 let y = add(3, 4); -|7: 6 let _ = x + y; -|8: 7 } -|9: 8 +== snap gr-opened grid=100x25 cursor=21,7 == snap gr-back grid=100x25 cursor=21,6 -|3: 1 mod lib; -|4: 2 -|5: 3 fn main() { -|6: 4 let x = add(1, 2); -|7: 5 let y = add(3, 4); -|8: 6 let _ = x + y; -|9: 7 } diff --git a/test/snapshots/rsearch.golden b/test/snapshots/rsearch.golden index 6e078612..872693bb 100644 --- a/test/snapshots/rsearch.golden +++ b/test/snapshots/rsearch.golden @@ -17,17 +17,14 @@ |14: 3 code.txt:3:1-7 |15: 4 count_x again |16: 5 -== snap select grid=210x31 cursor=14,9 -|7: 2 open sub/dep.txt:3 here -|8: 3 count_x again -|9: 4 pick two words +== snap select grid=210x31 cursor=14,10 |10: ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ |16: /tmp/pardes-snap/rsearch/cwd/+Search Save Tty Collapse Del LocationsConfig |17: 1 code.txt:4:1-8 |18: 2 pick two words |19: 3 == snap path-open grid=210x31 cursor=7,22 -|10: +|10: 4 pick two words |19: /tmp/pardes-snap/rsearch/cwd/sub/dep.txt Save Tty Collapse Del |20: 1 d1 |21: 2 d2 @@ -36,10 +33,6 @@ |24: 5 d5 |25: 6 d6 == snap step1 grid=210x31 cursor=13,9 -|7: 1 run count_x now -|8: 2 open sub/dep.txt:3 here -|9: 3 count_x again -|10: 4 pick two words == snap step2 grid=210x31 cursor=13,9 == snap step3 grid=210x31 cursor=13,9 == snap selclick grid=210x31 cursor=14,10 diff --git a/test/snapshots/rsearch.snap b/test/snapshots/rsearch.snap index d880e557..0ce257bd 100644 --- a/test/snapshots/rsearch.snap +++ b/test/snapshots/rsearch.snap @@ -50,17 +50,16 @@ snap select # right-CLICK the path `sub/dep.txt:3` (line 2, col 18): isFileChar keeps / . : # so the click expands the whole token and it RESOLVES — a look, not a search — # opening dep.txt scrolled to line 3 (DEPTARGET). The "don't break paths" case. -press right 18 8 -release right 18 8 +press right 18 9 +release right 18 9 wait 10000 DEPTARGET stable 700 15000 snap path-open # A click does not merely LIST the hits, it goes to one: this re-search of -# `count_x` (line 3, col 12: acme sized code.txt to its four lines, and the -# first search scrolled line 1 off) also STEPS onto hit 1, so the cursor lands -# in the match, not where the click was. -press right 12 9 -release right 12 9 +# `count_x` (line 1, col 15) also STEPS onto hit 1, so the cursor lands in the +# match, not where the click was. +press right 15 8 +release right 15 8 stable 700 15000 snap step1 # ...and the same word clicked in its OTHER place (line 3, col 10) opens NO @@ -75,9 +74,9 @@ press right 10 10 release right 10 10 stable 700 15000 snap step3 -# acme sized code.txt to its four lines, and on so tight a pane a click on -# line 4 scrolls it to keep its context rows: drag its last row (the resize -# handle) down four rows first, so the rows below stay where they are clicked. +# acme sized code.txt to its four lines, so line 4 is on its last row, and a +# pane's last row is its resize handle: a press there would drag the seam, not +# sweep text. Drag that handle down four rows first. press left 40 11 drag left 40 15 release left 40 15 -- cgit v1.3