โ Final Challenge
โบ The Headless Editor
75 ptsEverything 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โ9accumulate a count;0is a digit only if a count has started (otherwise it's the motion). A count before and after an operator multiply (2d3w= 6 words). ForG/ggthe 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 Gandgg(agwaits for a secondg;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-columnj/kclamp. - Operators:
dandc, with any motion above (viaresolve+apply_op, provided), plus doubleddd/cc. Anulloptrange (e.g.dhat column 0) is a no-op โ and must not create an undo entry.cends in insert mode. (Our dialect:cwfollows the regular grammar, unlike real vi'scw-acts-like-cespecial case โ a documented divergence, and a good upgrade exercise later.) x: deletecountcharacters 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 becomelen, legal in insert mode),A(end of line),o/O(open a line below / above, cursor on it). uundo, 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,Opushes 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 (
ithen Esc) must leave no trace in the history. Forc,o,Othe snapshot pushed by the command itself covers the whole following session (they already changed the buffer). urestores 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.uwith 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.