// lesson: viewport

The Viewport

Your terminal shows 24 rows; your file has 10,000 lines. The editor's window onto the file is the viewport, and its entire state is two numbers:

int rowoff;   /* index of the first VISIBLE file row    */
int coloff;   /* index of the first VISIBLE file column */

File row r appears on screen row r - rowoff (when 0 <= r - rowoff < screen_rows); file column c appears on screen column c - coloff. That translation โ€” file coordinates minus offset โ€” is the whole theory. Everything else is deciding when the offsets change.

Scroll-to-fit: the invariant

The editor never scrolls speculatively; it scrolls to maintain one invariant: the cursor is always visible. Derive the vertical rule from the inequality itself. Visibility means:

rowoff <= cursor_row < rowoff + screen_rows

Two ways to violate it, two one-line repairs:

  • Cursor above the window (cursor_row < rowoff) โ€” you pressed Up at the top edge: slide the window up to put the cursor on the first row: rowoff = cursor_row.
  • Cursor below the window (cursor_row >= rowoff + screen_rows) โ€” Down at the bottom edge: slide the window down to put the cursor on the last row: rowoff = cursor_row - screen_rows + 1.

Check the fencepost in that second line with a concrete case (always check fenceposts with concrete cases): 10 screen rows, cursor lands on file row 10 โ€” one past visible rows 0โ€“9. New rowoff = 10 - 10 + 1 = 1, visible rows 1โ€“10, cursor on the last screen row. Off by one in either direction and you'll either scroll a row early or leave the cursor hanging one row off-screen.

The horizontal axis is the same two lines with cursor_col, coloff, screen_cols โ€” long lines scroll sideways as the cursor pushes past the edges. (This is how nano and kilo handle long lines; vim wraps them by default instead. Wrapping is a much deeper change โ€” one file line becomes several screen rows and the row translation stops being subtraction โ€” which is exactly why we chose sideways scrolling.)

Where does this run? Once per frame, after input handling and before rendering โ€” one editor_scroll(e) that enforces the invariant no matter who moved the cursor or how (keys, search jump, file load). Nobody else touches the offsets; one writer, one invariant, no drift.

Tabs, or: the cursor is not where the bytes say

There's a second coordinate subtlety hiding in rendering: the byte column and the screen column disagree the moment a line contains a tab. \tx is two bytes, but on an 8-column tab display the x sits at screen column 8, not 1.

So an editor tracks two notions (kilo calls them cx and rx):

  • cursor_col โ€” index into the line's bytes. Editing operates here.
  • render column โ€” where that byte lands on screen. The terminal cursor gets positioned here, and horizontal scrolling must be computed here (it's visual overflow, not byte overflow).

The conversion walks the line's prefix, expanding each tab to the next multiple of the tab width:

rx = 0;
for each byte before cursor_col:
    if tab: rx += TABSTOP - (rx % TABSTOP);   /* jump to next stop */
    else:   rx += 1;

Note rx % TABSTOP, not a fixed 8: a tab consumes up to 8 columns, less if text precedes it โ€” ab\t puts the next glyph at column 8, not 10. The reverse mapping (screen column โ†’ byte column, needed when a mouse click or a search highlight gives you a visual position) runs the same walk until it meets or passes the target.

โ€บ Scroll to Keep the Cursor Visible

20 pts

Implement editor_scroll(e): enforce both axes of the invariant, exactly as derived โ€” including never letting the offsets go negative, and treating the cursor's render column (provided helper) as the thing that must be horizontally visible. The tests walk a cursor around a large document and check the offsets at every boundary fencepost.

Log in to submit a solution and earn points.

โ€บ Tabs and Render Columns

15 pts

The two coordinate conversions, this time yours:

  • editor_cx_to_rx(line, cx) โ€” byte column โ†’ screen column, tabs expanding to the next multiple of TABSTOP (8).
  • editor_rx_to_cx(line, rx) โ€” screen column โ†’ byte column: walk until the render position meets or passes rx; if rx lies past the end of the line, return the line length.

Log in to submit a solution and earn points.