Skip to content

fix: Ctrl+End scrolls viewport to show trailing empty line#1067

Merged
sinelaw merged 6 commits intomasterfrom
fix0
Feb 19, 2026
Merged

fix: Ctrl+End scrolls viewport to show trailing empty line#1067
sinelaw merged 6 commits intomasterfrom
fix0

Conversation

@sinelaw
Copy link
Copy Markdown
Owner

@sinelaw sinelaw commented Feb 19, 2026

Fixes #992

When line wrapping is enabled and the file ends with a trailing newline, Ctrl+End moves the cursor to the correct byte position (document end) but the viewport didn't scroll far enough — the empty final line was hidden below the visible area.

Root cause: ViewLineIterator intentionally skipped the trailing empty view line (produced after the last source newline), so the cursor at buffer_len had no corresponding view line. find_view_line_for_byte mapped it to the previous content line instead.

Fix:

  • ViewLineIterator now emits one trailing empty ViewLine when the last token was a source newline (representing a real document line)
  • find_view_line_for_byte maps cursor positions past all source bytes to the trailing empty view line
  • Backward visual-row scan in ensure_visible uses prev() directly instead of prev()+next_line()+prev() to avoid pending_trailing_empty_line interference in LineIterator

sinelaw and others added 5 commits February 19, 2026 17:45
Fixes #992

When line wrapping is enabled and the file ends with a trailing newline,
Ctrl+End moves the cursor to the correct byte position (document end)
but the viewport didn't scroll far enough — the empty final line was
hidden below the visible area.

Root cause: ViewLineIterator intentionally skipped the trailing empty
view line (produced after the last source newline), so the cursor at
buffer_len had no corresponding view line. find_view_line_for_byte
mapped it to the previous content line instead.

Fix:
- ViewLineIterator now emits one trailing empty ViewLine when the last
  token was a source newline (representing a real document line)
- find_view_line_for_byte maps cursor positions past all source bytes
  to the trailing empty view line
- Backward visual-row scan in ensure_visible uses prev() directly
  instead of prev()+next_line()+prev() to avoid pending_trailing_empty_line
  interference in LineIterator

Co-Authored-By: Claude Opus 4.6 <[email protected]>
After Ctrl+End → Left → Down, the cursor should return to the empty
trailing line.  Without the ViewLineIterator fix this fails because
the trailing empty line has no view line for the cursor to land on.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
After Ctrl+End → Left → Down, the cursor should reach the trailing
empty line.  At wider terminal sizes (135x37) with the real CSV content,
Down does nothing — the cursor stays on the last visual row of the
previous line.  Uses the actual olney-book-1.csv to reproduce.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Two issues prevented the Down key from moving the cursor to the trailing
empty line (after a file's final '\n') when line wrapping is on:

1. The trailing empty ViewLine got line_end_byte = prev_line_end_byte
   (the \n byte) instead of buffer_len.  This made move_visual_line
   return the same position the cursor was already on — a no-op.
   Fix: detect LineStart::AfterSourceNewline and set line_end_byte to
   buffer_len.

2. When the screen is full and the trailing empty line isn't rendered,
   no ViewLineMapping existed for it, so move_visual_line returned None
   at the boundary.  Fix: always push the mapping when the last rendered
   line ends with a newline.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
When the ViewLineIterator emits the trailing empty line (after a file's
trailing '\n'), resolve_cursor_fallback placed the cursor at
last_line_end.y + 1 — assuming the last rendered line was the content
line before the empty line.  But with the iterator fix, the trailing
empty line IS the last rendered line, so + 1 overshoots to a tilde row.

Fix: only add 1 when terminated_with_newline is true (last rendered line
was the newline-terminated content line); use the position directly when
the trailing empty line was already rendered.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@sinelaw sinelaw force-pushed the fix0 branch 2 times, most recently from 6bc6b82 to 4284952 Compare February 19, 2026 21:20
…s in viewport slices

ViewLineIterator unconditionally emitted a trailing empty ViewLine after
the last source newline, even when the tokens represented a viewport
window into the middle of a buffer.  This broke tests that relied on
exact line counts for mid-buffer views.

Add an `at_buffer_end` parameter to ViewLineIterator::new().  Only emit
the trailing empty line when the token stream actually covers the buffer
end.  Also reset next_line_start when tokens are exhausted without a
Newline/Break so content like "\nX" doesn't spuriously trigger the
trailing line.

In the renderer, detect the iterator's trailing empty ViewLine and skip
the duplicate implicit-line rendering, preserve last_line_end tracking,
and fix the cursor-at-end fallback (use buffer.len() instead of 0 for
empty lines).

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@sinelaw sinelaw merged commit 27dd489 into master Feb 19, 2026
8 checks passed
@sinelaw sinelaw deleted the fix0 branch February 19, 2026 22:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Odd behavior using Ctrl-End and scrolling

1 participant