Viewport logic for scrolling the visible region of a buffer.
The viewport defines which lines and columns are currently visible in the terminal. When the cursor moves outside the viewport, it scrolls to keep the cursor visible.
Summary
Functions
Scrolls so the cursor line is at the bottom of the viewport (zb in vim).
Returns a cache key that changes for logical and visual scrolling.
Centers the viewport on the given cursor line (zz in vim).
Clamps the current visual row offset against the visual row count of top.
Clamps the current visual row offset against the rows remaining to EOF.
Returns the number of columns available for buffer content after the gutter.
Returns the number of content rows (total rows minus reserved).
Computes how many buffer lines fit in display_rows screen rows,
accounting for decorations that consume extra display rows.
Adjusts the raw row count for a line spacing multiplier.
Number of rows reserved for the footer (modeline + minibuffer).
Computes the gutter width for line numbers based on total line count.
Returns the maximum visual row offset allowed for the rows remaining to EOF.
Creates a new viewport with the given dimensions and default reserved rows (2).
Creates a new viewport with the given dimensions and explicit reserved rows.
Stores a logical top line and resets wrapped row offset.
Stores a top logical line and clamps the visual row offset for that line.
Scrolls the viewport down by one line without moving the cursor.
Scrolls the viewport down by one line with an explicit scroll margin.
Scrolls the viewport up by one line without moving the cursor.
Scrolls the viewport up by one line with an explicit scroll margin.
Scrolls the viewport to keep the cursor visible.
Scrolls the viewport with a scroll margin.
Scrolls to a cursor visual row within its logical line when wrapping is active.
Scrolls down by one visual row when wrapping is active.
Scrolls up by one visual row when wrapping is active.
Scrolls so the cursor line is at the top of the viewport (zt in vim).
Returns the range of visible lines as {first_line, last_line} (inclusive).
Types
@type t() :: %MingaEditor.Viewport{ cols: pos_integer(), left: non_neg_integer(), reserved: non_neg_integer(), rows: pos_integer(), top: non_neg_integer(), visual_row_offset: non_neg_integer() }
A viewport representing the visible terminal region.
top— first visible buffer line (0-indexed)left— first visible display column (0-indexed, in terminal columns).Wide characters (CJK, emoji) occupy 2 display columns, so horizontal scroll advances by display columns, not grapheme counts.rows— total rows in this viewport (including reserved rows)cols— total columns in this viewportreserved— rows reserved for non-content elements (modeline, minibuffer).Defaults to `footer_rows()` (2) for the terminal-level viewport. Set to 0 for per-window viewports where Layout already excluded the modeline from the content rect.visual_row_offset— first visual row shown withintopwhen wrapping is active.`{top: 5, visual_row_offset: 2}` starts on the third visual row of logical line 5.
Functions
@spec bottom_on(t(), non_neg_integer(), non_neg_integer(), non_neg_integer()) :: t()
Scrolls so the cursor line is at the bottom of the viewport (zb in vim).
Respects scroll_margin by placing the cursor margin lines from the bottom.
@spec cache_key(t()) :: non_neg_integer()
Returns a cache key that changes for logical and visual scrolling.
@spec center_on(t(), non_neg_integer(), non_neg_integer()) :: t()
Centers the viewport on the given cursor line (zz in vim).
Returns the updated viewport.
@spec clamp_visual_row_offset(t(), pos_integer()) :: t()
Clamps the current visual row offset against the visual row count of top.
@spec clamp_visual_row_offset(t(), pos_integer(), pos_integer()) :: t()
Clamps the current visual row offset against the rows remaining to EOF.
@spec content_cols(t(), non_neg_integer()) :: pos_integer()
Returns the number of columns available for buffer content after the gutter.
Subtracts gutter_width(line_count) from the viewport's total columns,
clamped to at least 1.
@spec content_rows(t()) :: pos_integer()
Returns the number of content rows (total rows minus reserved).
@spec effective_page_lines( non_neg_integer(), pos_integer(), Minga.Core.Decorations.t(), non_neg_integer() ) :: pos_integer()
Computes how many buffer lines fit in display_rows screen rows,
accounting for decorations that consume extra display rows.
Walks forward from cursor_line, counting each buffer line as 1 display
row plus any virtual lines and block decorations attached to it. Stops
when the display row budget is exhausted. Returns the number of buffer
lines traversed.
When there are no decorations, this returns display_rows (the fast path).
@spec effective_rows(pos_integer(), number()) :: pos_integer()
Adjusts the raw row count for a line spacing multiplier.
When line_spacing > 1.0, each line takes more vertical space, so fewer
lines fit on screen. Returns floor(rows / line_spacing), clamped to at
least 1. Returns the input unchanged when spacing is 1.0 (the TUI default).
The caller reads Config.get(:line_spacing) and passes it here. This keeps
Viewport a pure module with no config dependency.
@spec gutter_width(non_neg_integer()) :: pos_integer()
Computes the gutter width for line numbers based on total line count.
Returns max(digits(line_count), 2) + 1 — at least 2 digits plus a
trailing space separator. For example: 1–99 lines → 3, 100–999 → 4.
@spec max_visual_row_offset(pos_integer(), pos_integer()) :: non_neg_integer()
Returns the maximum visual row offset allowed for the rows remaining to EOF.
@spec new(pos_integer(), pos_integer()) :: t()
Creates a new viewport with the given dimensions and default reserved rows (2).
@spec new(pos_integer(), pos_integer(), non_neg_integer()) :: t()
Creates a new viewport with the given dimensions and explicit reserved rows.
@spec put_top(t(), non_neg_integer()) :: t()
Stores a logical top line and resets wrapped row offset.
@spec put_top_visual(t(), non_neg_integer(), non_neg_integer(), pos_integer()) :: t()
Stores a top logical line and clamps the visual row offset for that line.
@spec scroll_line_down(t(), non_neg_integer(), non_neg_integer()) :: {t(), non_neg_integer()}
Scrolls the viewport down by one line without moving the cursor.
The cursor line is clamped to remain visible and respect scroll_margin.
When scrolling down, the cursor is pushed away from the top edge to
maintain the margin, matching vim's scrolloff behavior for Ctrl-E.
Returns {updated_viewport, clamped_cursor_line}.
@spec scroll_line_down(t(), non_neg_integer(), non_neg_integer(), non_neg_integer()) :: {t(), non_neg_integer()}
Scrolls the viewport down by one line with an explicit scroll margin.
@spec scroll_line_up(t(), non_neg_integer(), non_neg_integer()) :: {t(), non_neg_integer()}
Scrolls the viewport up by one line without moving the cursor.
The cursor line is clamped to remain visible and respect scroll_margin.
When scrolling up, the cursor is pushed away from the bottom edge to
maintain the margin, matching vim's scrolloff behavior for Ctrl-Y.
Returns {updated_viewport, clamped_cursor_line}.
@spec scroll_line_up(t(), non_neg_integer(), non_neg_integer(), non_neg_integer()) :: {t(), non_neg_integer()}
Scrolls the viewport up by one line with an explicit scroll margin.
@spec scroll_to_cursor( t(), {non_neg_integer(), non_neg_integer()} ) :: t()
Scrolls the viewport to keep the cursor visible.
Returns a new viewport adjusted so that the cursor position {line, col}
is within the visible area. col must be a display column (terminal
columns, not grapheme count) — wide characters count as 2. Reserves footer
rows for the modeline and minibuffer.
@spec scroll_to_cursor( t(), {non_neg_integer(), non_neg_integer()}, pid() | non_neg_integer() ) :: t()
Scrolls the viewport with a scroll margin.
Accepts either a buffer pid (reads scroll_margin from the buffer's
local options) or an explicit integer margin. The margin keeps n
lines visible above and below the cursor when possible. When the file
is shorter than 2 * margin + 1, the margin shrinks to fit.
@spec scroll_to_cursor_visual( t(), {non_neg_integer(), non_neg_integer()}, non_neg_integer(), pos_integer(), non_neg_integer() ) :: t()
Scrolls to a cursor visual row within its logical line when wrapping is active.
@spec scroll_visual_row_down(t(), pos_integer(), non_neg_integer(), non_neg_integer()) :: t()
Scrolls down by one visual row when wrapping is active.
@spec scroll_visual_row_up(t(), pos_integer(), non_neg_integer(), non_neg_integer()) :: t()
Scrolls up by one visual row when wrapping is active.
@spec top_on(t(), non_neg_integer(), non_neg_integer(), non_neg_integer()) :: t()
Scrolls so the cursor line is at the top of the viewport (zt in vim).
Respects scroll_margin by placing the cursor margin lines from the top.
@spec visible_range(t()) :: {non_neg_integer(), non_neg_integer()}
Returns the range of visible lines as {first_line, last_line} (inclusive).