โ˜… Final Challenge

โ€บ The Headless Editor

75 pts

Everything converges. You have a buffer, a mode machine, motions, operators, and an undo design; now assemble them into an Editor โ€” the object that, in your real program, sits between decode_input and build_frame:

read() -> decode_input() -> editor.feed(key)  ...  render(editor) -> write()

Here it runs headless: tests construct an Editor, feed it a script of keystrokes, and assert on the three things that define an editor's observable state โ€” buffer content, cursor position, mode. If this class is correct, wrapping it in the terminal glue from Lessons 1โ€“4 produces a working vi. (That's the course's endgame, and it's yours to run: RawMode guard, read/decode/feed/render loop, done.)

Keys arrive as single chars via feed(char) (feed(string_view) just loops): 0x1B is Esc, '\r' or '\n' is Enter, 0x7F or 0x08 is Backspace, 0x12 is Ctrl-R. Everything else is a literal key.

Normal mode โ€” the command grammar, built from your earlier pieces (all provided in the starter):

  • Counts: digits 1โ€“9 accumulate a count; 0 is a digit only if a count has started (otherwise it's the motion). A count before and after an operator multiply (2d3w = 6 words). For G/gg the combined count is a line number. Esc clears any pending count/operator; so does an unrecognized key.
  • Motions (cursor moves, count-aware): h l j k 0 ^ $ w b e G and gg (a g waits for a second g; g + anything else aborts cleanly โ€” that second key is discarded, the same as an unrecognized key clearing a pending count, not executed as a command of its own). Semantics exactly as in your motion challenges โ€” including the no-goal-column j/k clamp.
  • Operators: d and c, with any motion above (via resolve + apply_op, provided), plus doubled dd/cc. A nullopt range (e.g. dh at column 0) is a no-op โ€” and must not create an undo entry. c ends in insert mode. (Our dialect: cw follows the regular grammar, unlike real vi's cw-acts-like-ce special case โ€” a documented divergence, and a good upgrade exercise later.)
  • x: delete count characters at the cursor (clamped to the line end); no-op on an empty line; cursor re-clamped after.
  • Insert doors: i (here), I (first non-blank), a (after cursor โ€” col may become len, legal in insert mode), A (end of line), o / O (open a line below / above, cursor on it).
  • u undo, Ctrl-R redo.

Insert mode: printable ASCII (0x20โ€“0x7E) inserts at the cursor; Enter splits the line; Backspace deletes left, joining lines at column 0 (no-op at the very start of the buffer); Esc returns to normal mode, moving the cursor one left (not below 0) and re-imposing the normal-mode clamp. Other bytes are ignored.

Undo โ€” snapshot-based ({lines, cursor}), per the design lesson: simple, correct, and honest about its memory cost at this file size. The unit rules:

  • Every successful x, d, c, o, O pushes one snapshot (taken before the change) โ€” one command, one unit.
  • An insert session (door key through Esc) is one unit: snapshot the state at the door press, but commit it lazily โ€” only when the first actual edit of the session happens. A session with no edits (i then Esc) must leave no trace in the history. For c, o, O the snapshot pushed by the command itself covers the whole following session (they already changed the buffer).
  • u restores the top snapshot โ€” buffer and cursor (the cursor returns to where the change began; test-pinned). Ctrl-R re-applies. Any new undoable change clears the redo stack. u with an empty history is a quiet no-op.

Work through the dispatch methodically โ€” normal mode is a switch over one char, with four little pieces of pending state (count, operator count, operator, the g flag) threaded through it. That state is the mode machine's normal-mode half, now for real.

Log in to submit a solution and earn points.