// 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 n bytes at pos: offsets after the insertion point shift right by n. Convention for the boundary: an offset exactly at pos also 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 before pos stay; offsets past the end shift left by n; offsets inside the erased range have lost their home โ€” they clamp to pos.

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 pts

Implement the selection value type and the offset-adjustment functions.

  • empty(), begin(), end(): the normalized read-only view.
  • contains(o): is byte o highlighted? 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 >= pos shift right by n.
  • adjust_for_erase(off, pos, n): before pos unchanged; >= pos + n shifts left; inside the range clamps to pos.
  • adjust_selection_*: apply to both ends independently.

Log in to submit a solution and earn points.

โ€บ Double-Click Words

15 pts

Implement the word machinery: the three-class byte classifier, word_at, and the word-granularity drag.

  • classify(b): Word for ASCII letters, digits, '_', and any byte โ‰ฅ 0x80; Space for ' ', '\t', '\n'; Punct for everything else.
  • word_at(text, o): the maximal same-class run around position o. For o inside the text, use the class of text[o] (the character to the right of the caret position); for o == 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 of word_at(press) and word_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.