// lesson: operators
Operators โ the Verbs
Now the sentence. In vi, d alone does nothing โ it waits. The next
motion tells it what to act on: dw deletes to the next word, d$ to
the end of the line, dG to the end of the file, d2j this line and
two below. One verb, every noun you know, and every noun you learn
later multiplies through the grammar for free. This is the design idea
that made vi immortal โ commands aren't memorized as monoliths;
they're composed โ and the reason our motions were pure functions:
d needs to ask "where would w go?" without moving anything.
Three verbs share all the machinery: d (delete), c (change:
delete, then enter insert mode in the hole), y (yank: copy,
change nothing). Plus the doubled forms โ dd, cc, yy โ meaning
"this whole line": vi's convention for "the operator applied to itself"
(a verb with no noun acts on lines).
Ranges: charwise vs linewise, exclusive vs inclusive
Resolving operator + motion produces a range, and vi's ranges
come in two natures the range type must capture:
- Charwise: from one character position to another.
dw,d$,dh. The subtle bit is vi's exclusive/inclusive split, straight from the POSIX vi spec: motions that go to a place (w,b,0,h,l) are exclusive โ the character at the destination survives; motions that go onto a thing (e,$โ the end of a word, the last character) are inclusive โ the destination character dies too.dwfromfinfoo barleavesbar(thebsurvives);deleavesbar(theodies). We normalize both into one canonical form: start inclusive, end exclusive โ inclusive motions just add one to the end. Half-open ranges, same as STL iterators, same reasons: length isend - start, empty isstart == end, no ยฑ1 guesswork downstream. - Linewise: whole lines, regardless of columns.
dd, and any operator with an up/down motion (djdeletes two full lines โ yours and the one below;dG,dgglikewise whole-line spans). vi's rule: vertical motions make operators linewise. In the range this is a flag plus a row interval; columns are meaningless (we zero them).
Counts multiply through the grammar: 2dw and d2w and 2d3w are all
legal; the counts multiply (2d3w = delete six words) โ the
resolver takes the product and applies the motion that many times.
For G/gg the count is a line number instead (grammar quirk,
faithfully copied: d3G deletes from here through line 3).
An operator whose range turns out empty โ dh at column 0, d$ on
an empty line โ does nothing at all: std::optional<Range> again, and
the editor treats nullopt as "beep".
One vi special case is too load-bearing to skip: dw on the last word
of a line. Plain grammar says "delete to the next word's start" โ on
the next line! โ which would eat the line break. vi's actual rule
(vim documents it as: an exclusive motion ending in column 1 retreats
to the end of the previous line): when the w target lands on a later
row, the range's end retreats to the end of the row just before the
target. dw on the last word deletes to end of line; the newline
survives. A second edge: w with the buffer running out mid-word
(there is no next word start) โ then the operator extends to the end
of the line, so dw on the file's final word still deletes it. Both
are in the challenge spec and tests.
Applying the range
The second half is mechanical but detail-rich: given a resolved range,
extract the doomed text (that's the yank โ vi always yanks what it
deletes, which is why p after dd is "move line"), remove it for
d/c, splice the seam for cross-line charwise ranges, and place the
cursor per the verb: d clamps to normal mode's rule on the resulting
line; c leaves the cursor at the hole, unclamped โ insert mode is
about to begin, where sitting at len is legal; y doesn't edit at
all, cursor to the range start (yank-moves-to-start is why yG seems
to "jump": authentic). Linewise c (cc) has its own vi flavor: the
lines are deleted but an empty line is left open at the spot, cursor on
it โ change means "make me type the replacement".
Deleting every line linewise leaves {""} โ the buffer invariant from
the buffer lesson, honored by every code path that can empty the file.
โบ Resolve Operator + Motion
20 ptsImplement resolve(lines, cur, op, motion, count) โ
std::optional<Range>:
motion == op(dd/cc/yy): linewise, rowscur.rowthroughcur.row + reps - 1, clamped to the file.j/k: linewise,cur.rowthroughcur.row ยฑ reps, clamped, normalized sostart.row <= end.row.G: linewise,cur.rowthrough linecount(1-based; no count = last line), normalized.g(gg): same with no-count default line 1.- Charwise, exclusive (
w,b,0,h): apply the motionrepstimes (helpers provided); range between origin and destination, normalized to start < end, end exclusive.wfixups, in order: (a) target on a later row โ retreat: end ={target.row - 1, len(target.row - 1)}(the column-1 retreat rule from the lesson); (b) target == cur, or target is not at a word start (the buffer ran out mid-word: its previous character exists, isn't blank, and has the same class) โ extend: end ={cur.row, len(cur.row)}. lcomputes its own end:{cur.row, min(cur.col + reps, len)}โ operator motions may stand one past the end of the line, which is exactly whydldeletes the character under the cursor.- Charwise, inclusive (
e,$): destination character is included โ add one to the end column.$takes the count-minus-one-rows-down rule from the motion engine; on an empty target line โnullopt. Fore, a target that could not move still takes the character under the cursor (deat the very end of the buffer eats it). - Empty range โ
std::nullopt(charwise; a linewise range of one row is never empty). Linewise ranges: zero the cols. count == 0= no count; the caller already multiplied2d3wintocount = 6.
The starter provides working word_forward/word_back/word_end and
apply_motion (your previous two challenges โ here given, so this
challenge is purely about range algebra).
Log in to submit a solution and earn points.
โบ Apply the Operator
15 ptsImplement apply_op(lines, op, r) โ execute a resolved Range and
return the EditResult:
yanked: the extracted text, one vector entry per range row โ linewise: full copies of each row; charwise same-row: the one segment; charwise multi-row: first row's tail, middle rows whole, last row's head (text beforeend.col).'y': buffer untouched; cursor = range start (for linewise:{start.row, 0}).'d'charwise: delete[start, end); a multi-row range splices first-head + last-tail into one line. Cursor:start, with the col clamped to the resulting line's normal-mode max.'d'linewise: remove the rows;{""}if that empties the buffer. Cursor:{min(start.row, new_last_row), 0}.'c'charwise: likedbut the cursor col is not clamped (insert mode follows), andenter_insertis true.'c'linewise: the rows collapse to one empty line atstart.row; cursor there,enter_inserttrue.linewisein the result mirrors the range (your editor uses it to decide howpwill paste later).
Ranges arrive well-formed (from resolve): non-empty, normalized,
in-bounds.
Log in to submit a solution and earn points.