// lesson: modal-editing-modes

Modal Editing โ€” Modes

Now the vi heart of the course. Everything so far โ€” raw input, painted frames, a buffer โ€” could become a notepad: keys insert themselves, arrows move. vi's founding idea is stranger and, once learned, unshakeable: the same key means different things depending on mode. In normal mode the keyboard is a command console โ€” x deletes, w hops a word, nothing inserts. In insert mode it's a typewriter. In command-line mode (after :) you're typing an instruction to be executed on Enter.

The lineage explains the shape. Bill Joy grew vi (1976) out of the line editor ex, itself descended from ed โ€” on printing terminals, where "editing" was a command language (ed still answers your typos with a lone ?). When video terminals arrived, Joy put a live viewport on top of ex; the command-language soul stayed. The hardware left fingerprints too: Joy's terminal was a Lear Siegler ADM-3A, whose keyboard had arrows printed on H, J, K, L โ€” that's why those keys move the cursor โ€” and whose Esc key sat where Tab does today, an easy pinky reach. Modern keyboards moved Esc; the mode-switch key stayed, and generations of vi users remap Caps Lock to chase it. :wq, hjkl, the ~ โ€” none of it is arbitrary; all of it is 1976 preserved in muscle memory.

Why does modality survive? Because it turns editing into a composable language. Next lesson: motions as nouns. The one after: operators as verbs, and d w โ€” "delete a word" โ€” as a sentence. None of that grammar works if every printable key is busy meaning itself, which is exactly what insert mode is for: a mode where keys mean themselves, entered deliberately, left with Esc.

The mode machine

Mechanically, modes are a small state machine that sits in front of everything you've built: each decoded Key is dispatched first on the current mode. The transitions:

  • Normal โ†’ Insert: i (insert here), I (insert at first non-blank), a (append after cursor), A (append at end of line), o (open a line below), O (open above). Six doors into the same mode, differing only in where they put the cursor first โ€” the cursor moves are the editor core's job (final challenge); the transition is the machine's.
  • Insert โ†’ Normal: Esc. The only door out. (vi's deep bet: you spend most of your time in normal mode, so leaving insert must be one keystroke, always the same one.)
  • Normal โ†’ Command: : opens the command line at the bottom of the screen. Keys now build up a string, shown as you type. Enter executes it; Esc abandons it; Backspace erases โ€” and backspacing past the start cancels back to normal mode, a small authentic vi behavior the tests check.

std::visit earns its keep here: dispatching a Key variant means handling each alternative, and the overloaded-lambdas idiom is the standard C++ pattern for it:

template <class... Ts> struct overloaded : Ts... { using Ts::operator()...; };

std::visit(overloaded{
    [&](char c)        { /* printable key  */ },
    [&](CtrlKey k)     { /* control chord  */ },
    [&](SpecialKey k)  { /* Esc, Enter ... */ },
}, key);

Forget one alternative and it doesn't compile โ€” the sum-type payoff: the compiler enforces that every kind of key has a decided meaning in every mode. An if/else chain over holds_alternative compiles fine with a case missing; the visit does not.

The machine below returns the executed command string and leaves interpreting it to a parser โ€” the second challenge, where std::variant appears on the output side: a parsed command is one-of-several shapes, exactly what a variant is for.

โ€บ The Mode Machine

15 pts

Implement ModeMachine, dispatching keys per the transitions above.

  • mode() starts as Mode::Normal.
  • feed(key) processes one key and returns std::optional<std::string>: engaged exactly when a command line was submitted (Enter in command mode), carrying its text (without the :). All other feeds return std::nullopt.
  • Normal: i I a A o O โ†’ Insert. : โ†’ Command with empty pending text. Every other key: stays normal (your editor core will interpret them; the machine ignores them).
  • Insert: SpecialKey::Escape โ†’ Normal; everything else stays.
  • Command: printable char โ†’ append to the pending text. SpecialKey::Enter โ†’ submit: return the text, clear it, back to Normal. SpecialKey::Escape โ†’ abandon: clear, back to Normal, return nullopt. SpecialKey::Backspace โ†’ erase the last pending char; if the pending text is already empty, cancel back to Normal. Other keys: ignored.
  • pending() returns the command line being typed (empty outside command mode) โ€” your render loop draws it in the message line.

Log in to submit a solution and earn points.

โ€บ Parse the Command Line

10 pts

The mode machine hands you "wq" or "w notes.txt" or "42". Now interpret it. The output is a textbook sum type: a command is one of write / quit / write-and-quit / go-to-line, each carrying different data โ€” std::variant on the return side, wrapped in std::optional because the input might be gibberish.

Implement parse_ex(input) (input arrives without the leading :):

  • "w" โ†’ WriteCmd{""} (write to the current file). "w <name>" โ†’ WriteCmd{name} โ€” name is everything after the first space (may itself contain spaces; filenames are like that).
  • "q" โ†’ QuitCmd{false}; "q!" โ†’ QuitCmd{true} (force: discard changes).
  • "wq" or "x" โ†’ WriteQuitCmd{}.
  • A string of digits โ†’ GotoCmd{n} with n โ‰ฅ 1 (:42 jumps to line 42; :0 is invalid in our dialect โ€” reject it).
  • Anything else โ€” empty string, unknown words, "w" with trailing junk like "wfoo", digits with a suffix โ€” โ†’ std::nullopt; the editor shows "not an editor command".

Log in to submit a solution and earn points.