// lesson: motions
Motions โ the Nouns
In vi's grammar, a motion is a noun phrase: a place in the text,
named relative to the cursor. w โ the next word. $ โ the end of the
line. G โ the last line. Pressed alone, a motion moves the cursor
there. The deeper payoff comes next lesson, when the same nouns become
the objects of verbs (d w: delete to the next word) โ which is
exactly why motions must be pure functions (buffer, position, count) โ position, with no side effects: the operator machinery will
call them to measure text without moving anything.
One invariant governs everything: in normal mode the cursor sits on
a character, never past the last one. Valid columns are 0 .. max(0, len-1) (an empty line parks the cursor at column 0 โ the one
place it sits on nothing). Insert mode relaxes this โ the cursor may sit
at len, "after everything" โ which is why a (append) at the end of a
line can insert where normal mode won't stand. Every motion ends by
re-imposing the clamp.
The core motions, and the details that make them feel right:
hlโ left/right, clamped to the line. No line wrapping: authentic vilat line end just stops. (vim'swhichwrapoption exists precisely because people argue about this.)jkโ down/up, and here's the classic subtlety: moving from a long line to a short one must clamp the column to the short line's end. vim actually remembers the "goal column" so j-j through a short line pops back out to the original column; we make the honest simplification of clamping without memory (the final challenge keeps it too, and upgrading is a great post-course exercise).0โ column zero, unconditionally.^โ the first non-blank (indentation-aware "start of line"; on an all-blank line there's nothing non-blank, so it clamps to the last column). The difference matters in real code:0on" return x;"reaches column 0,^reaches ther.$โ the last character. With a count,2$means "end of the next line" (count minus one rows down, then end) โ a real vi refinement the tests cover.Gโ with no count, the last line; with a count, that line (1-based:42G= line 42, clamped to the file).ggโ the same with the default flipped: no count means line one. (Real vi moves to the first non-blank afterG; we keep the column, clamped โ modern-editor behavior, and one less moving part.)- Counts:
5jisjfive times;0,^,$treat counts their own way (0and^ignore them). A count of 0 in our API means "no count given" โ vi cannot even express a count of zero, since0is itself a motion; our implementation honors that piece of grammar trivia directly in the representation.
Word motions: the fiddliest fifty lines in vi
w, b, e โ forward word, back word, end of word โ are where "word"
needs a definition, and vi's is precise. Characters divide into three
classes: word characters (letters, digits, underscore โ C
identifier characters, not a coincidence), punctuation (every other
non-blank), and whitespace. A word is a maximal run of a single
non-blank class. In foo_bar+=42:
foo_bar += 42 three words: word-class, punct-class, word-class
So w from f lands on +; w again lands on 4. Punctuation runs
are words of their own class โ that's what makes w useful in code,
hopping ->, ::, += as single units. (vi also has W/B/E โ
WORD motions, whitespace-delimited only, one class instead of two โ an
easy extension once small-word logic works.)
The rules that complete the spec:
w: skip the rest of the current word (if on one), then skip blanks, land on the first character of the next word.e: move at least one character, skip blanks, land on the last character of the current/next word.b: mirror ofe, backward: land on the first character of the current/previous word.- Lines end, words end. A word never spans a line break: the break acts as whitespace between the last word of one line and the first of the next. All three motions cross lines freely.
- Empty lines are words โ for
wandb, which stop on them;eskips them entirely. (Try it in vim:wthrough a paragraph break pauses on the blank line,esails past.) This asymmetry is authentic, ancient, and pinned by the tests. - At the edges: a motion that cannot move (e.g.
wat the last word's last character... actually anywhere in the file's final stretch) returns its input position โ the caller sees "no movement" rather than an error.
Implementation advice: model the line break as a virtual whitespace
character sitting one column past each line's last character (except
the final line). The starter's next_pos/prev_pos helpers implement
exactly that iteration; with them, each motion is a short loop over
character classes, and all the line-crossing edge cases dissolve into
"the slot is whitespace". Getting these three functions right is the
single most re-used work in the course: the operator lesson and the
final challenge both build directly on them.
โบ The Motion Engine
15 ptsImplement apply_motion(lines, p, motion, count) for the line motions:
h l j k 0 ^ $ G g (where 'g' denotes gg), with the semantics and
clamping described above. count == 0 means no count given. The
returned position always satisfies the normal-mode clamp; the input
position is always valid.
Log in to submit a solution and earn points.
โบ Word Motions
20 ptsImplement word_forward (w), word_back (b), and word_end (e)
per the class rules above โ one application each (counts are the
caller's loop). The starter provides the character classifier and the
virtual-newline position iterators; your job is the three class-run
loops. Inputs are valid normal-mode positions; outputs must be too.
Log in to submit a solution and earn points.