// lesson: selection-and-the-mouse
Selection and the Mouse
Selection looks like a range. It isn't โ not quite. Drag from offset 20 leftward to offset 5, and the selected range is [5, 20), but the editor must remember more: if you now press Shift+Right, the selection shrinks at the left edge (to [6, 20)) rather than growing at the right. The selection has a fixed end and a moving end.
So the model every serious editor uses is an anchor and a focus (browsers use exactly these names in the DOM Selection API; you'll also see "mark and point" in Emacs lineage):
- the anchor is where selection started โ it never moves during a gesture;
- the focus is the caret โ every extension gesture (drag, Shift+click, Shift+arrow) moves only the focus.
The pair is allowed to be "backwards" (focus < anchor after a leftward
drag) and must not be normalized in place โ normalizing destroys the
information of which end moves next. Instead, expose normalization as a
read-only view: begin()/end() return the sorted pair for consumers
(rendering the highlight, deleting the range), while anchor/focus
keep gesture state. A collapsed selection (anchor == focus) is the
caret; there is no separate caret object. One state, two readings โ
this little discipline eliminates a whole class of "caret and selection
disagree" bugs.
The paint rule follows mechanically: a byte at offset o is highlighted
iff begin() <= o < end() โ half-open, like every range in this course,
so adjacent selections tile without overlap and an empty selection
highlights nothing.
Edits move selections
Here's the wrinkle that separates toy editors from real ones: positions are only meaningful against a particular revision of the text. When text changes โ by your own typing, or in lesson 14 by an undo โ every stored offset (both selection ends, bookmark positions, the scroll anchor) must be adjusted through the edit:
- Insert of
nbytes atpos: offsets after the insertion point shift right byn. Convention for the boundary: an offset exactly atposalso shifts โ type at the caret and the caret rides ahead of the inserted text. (This is the natural typing behavior; sophisticated systems make the boundary policy explicit per marker โ the "left/right gravity" you'll meet in any editor-internals discussion.) - Erase of
[pos, pos+n): offsets beforeposstay; offsets past the end shift left byn; offsets inside the erased range have lost their home โ they clamp topos.
Apply the adjustment to anchor and focus independently and the selection survives edits made elsewhere in the document โ exactly what you expect when an undo happens while you have text selected.
Clicks: one, two, three
Mouse gestures map onto the anchor/focus model with counted clicks โ these conventions are so old (they shipped with the original Macintosh) that users' hands know them:
- Single click: collapse โ anchor = focus = hit position. Drag moves focus, character by character.
- Double click: select the word under the cursor. What's a word?
The pragmatic editor answer (matching vi's
w, roughly): a maximal run of word characters (letters, digits, underscore), or a maximal run of other punctuation, or a run of whitespace โ double-clicking a gap selects the gap. Classify each byte into one of those three classes; the word at a position is the maximal same-class run around it. At a boundary between runs, the convention is to look at the character to the right of the position (and at end-of-text, the one to the left). - Triple click: the whole line โ no challenge needed once you have
lesson 8's
line_span.
Double-click-and-drag selects by whole words: the selection is always the union of the word under the press and the word under the pointer, with anchor and focus arranged so the moving end tracks the mouse โ drag right and focus is the right edge; drag left and focus is the left edge. (Word-drag granularity is also why the anchor is remembered as the whole anchor word, not a point โ release inside the original word and it stays fully selected.)
ASCII classes suffice here; the classifier takes an unsigned char and
bytes โฅ 0x80 (UTF-8 continuation or lead bytes) count as word bytes, so
multi-byte letters clump together with their neighbors rather than
splitting words โ crude but surprisingly serviceable, and the honest
version (Unicode word segmentation, UAX #29) plugs into the same two
functions later.
โบ Anchor and Focus
12 ptsImplement the selection value type and the offset-adjustment functions.
empty(),begin(),end(): the normalized read-only view.contains(o): is byteohighlighted? Half-open; empty selections contain nothing.extend_to(sel, o): the Shift+click / drag primitive โ move focus only.adjust_for_insert(off, pos, n): offsets>= posshift right byn.adjust_for_erase(off, pos, n): beforeposunchanged;>= pos + nshifts left; inside the range clamps topos.adjust_selection_*: apply to both ends independently.
Log in to submit a solution and earn points.
โบ Double-Click Words
15 ptsImplement the word machinery: the three-class byte classifier, word_at,
and the word-granularity drag.
classify(b):Wordfor ASCII letters, digits,'_', and any byte โฅ 0x80;Spacefor' ','\t','\n';Punctfor everything else.word_at(text, o): the maximal same-class run around positiono. Foroinside the text, use the class oftext[o](the character to the right of the caret position); foro == text.size()use the run ending at the text's end. Empty text:{0, 0}.drag_words(text, press, current): the double-click-drag selection โ the union ofword_at(press)andword_at(current), ends arranged so focus tracks the mouse: dragging at-or-past the press point puts the focus at the union's right edge; dragging before it, at the left edge.
Log in to submit a solution and earn points.