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 ++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) (limited to 'src') 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(); -- cgit v1.3