// lesson: cursor-rules

Cursor Movement and the Goal Column

Cursor movement sounds like row += dr; col += dc. Open vim, move around for ten seconds, and count the rules that little formula misses. Movement is policy, refined over fifty years into conventions your fingers already know โ€” which means getting them wrong is instantly, viscerally noticeable.

The clamping rules

Where may the cursor be? On line r, valid columns run from 0 to len(r) โ€” inclusive. One past the last character is a legal and essential position: it's where you stand to append to a line. (Rendering-wise the cursor sits on the space after the text.) But len + 5 is not legal, and here's the classic case that generates it: cursor at column 40 of a long line, press Down onto a 10-column line. Keep the raw column and the cursor floats in the void; every editor instead snaps the column to the shorter line's length.

Vertical motion clamps the row to [0, nlines-1], then snaps the column as above.

Horizontal motion wraps. Left at column 0 moves to the end of the previous line; Right at end-of-line moves to column 0 of the next line. (At the very start/end of the buffer: no-op.) This is how arrow keys traverse a file as one continuous string of text instead of trapping you inside a line.

Home/End go to column 0 / column len. Page Up/Down move by a screenful of rows โ€” the viewport height, which is why the editor struct carries screen_rows โ€” clamping at the ends like any vertical move.

The goal column โ€” the rule everyone feels, nobody names

Snap-to-length has a follow-up problem. Cursor at column 40, press Down through these lines:

a very long line of text with the cursor out here somewhere
short
another very long line of text

Snapping alone puts you at column 5 on short โ€” fine โ€” but then at column 5 on the long line below. You feel this bug instantly: "I was at column 40; passing a short line shouldn't reset my lane."

Editors therefore remember a goal column (Emacs calls it exactly that): the column you want, as distinct from the column you've got.

  • Vertical moves never change the goal; they set the actual column to min(goal, len(new line)).
  • Horizontal moves and edits set the goal to wherever they put the cursor.

With the goal at 40: Down onto short shows column 5, Down again restores column 40. Two fields, three lines of discipline, and vertical motion feels right.

Where does this logic live?

Not in the key handler. The dispatch layer (a later lesson) will map KEY_ARROW_UP to editor_move(e, MOVE_UP) โ€” one call, no logic. All policy concentrates in one function with one switch, where every rule is visible next to its siblings and testable without a keyboard. The alternative โ€” clamps sprinkled through the key handler, the mouse handler, the search-jump code โ€” is how editors end up with the cursor in the void when you arrive at a line by a path nobody tested.

โ€บ Movement Rules

20 pts

Implement editor_move(e, m) for the eight motions, with all of the lesson's policy: inclusive end-of-line positions, vertical snap, goal-column memory, horizontal wrap, page moves by screen_rows, and total clamping (no motion may ever leave the cursor out of bounds, no matter the sequence).

Contract details the tests pin down:

  • MOVE_LEFT at (0,0) and MOVE_RIGHT at the last position of the last line: no-ops.
  • MOVE_UP on row 0 and MOVE_DOWN on the last row: no vertical motion, and the column also stays put (vim behavior).
  • Horizontal motions (LEFT, RIGHT, HOME, END) set goal_col to the resulting column; vertical motions (UP, DOWN, PAGE_*) leave goal_col alone and set cursor_col = min(goal_col, line len).

Log in to submit a solution and earn points.