ADR 0019: Selection model — a per-cursor anchor, not a separate range type¶
Status¶
Accepted
Context¶
Every prior phase gave EditorViewport point cursors only (m_cursors,
ADR 0012) — no selection/anchor concept at all, and no
mouseMoveEvent/mouseReleaseEvent handling. Clipboard, find/replace,
and typing-over-a-selection (this pass's next phases) all need a real
selection to operate on; building it once, correctly, avoids three
half-selection-aware features later.
Decision¶
- New state, index-aligned with
m_cursors:QVector<size_t> m_selectionAnchors, same size asm_cursorsalways.m_selectionAnchors[i] == m_cursors[i]means cursorihas no active selection; otherwise the range is[min, max)of the pair. This reuses the exact patternm_renderedCaretPosalready established (ADR 0015) rather than inventing a separateSelectionstruct or a parallel single-selection field that would need its own multi-cursor-awareness. - Single-cursor primitives take an index, not a reference. Every
insertTextAt/deleteBackwardAt/deleteForwardAt/moveCursor*Atchanged fromsize_t &cursortoint i, since each now needs to reach bothm_cursors[i]andm_selectionAnchors[i]. No external callers exist (all are private, called only from their batch-loop counterpart in this file), so the signature change is self-contained. extendparameter on every move op (moveCursorLeft/Right/ Vertically/Home/End, both batch and*Atforms), set fromevent->modifiers() & Qt::ShiftModifierinkeyPressEvent:extend == true(Shift held): step the head via the existing movement logic, unconditionally, leaving the anchor untouched.extend == falsewith an active selection: collapse instead of stepping — jump straight to the near edge (Left/Up/Home →selectionMinAt) or far edge (Right/Down/End →selectionMaxAt), matching standard editor convention, then clear the selection (anchor = new cursor).extend == falsewith no active selection: unchanged from before this phase — step normally, anchor follows to stay collapsed.- Vertical movement's single-cursor sticky-column special case
(ADR 0012) got the same three-way split; a collapsing jump resets
m_desiredColumnto-1since the landed column isn't one the sticky-column logic was tracking. - Escape clears the selection, not just multi-cursor state:
collapseToOneCursornow also syncsm_selectionAnchors[0]when already down to one cursor (previously a no-op in that case). - Mouse: plain click sets a fresh cursor+anchor at the point
(unchanged otherwise);
Shift+click extends the last cursor's selection to the click point, anchor untouched — deliberately only the last cursor, since combining Shift+click with multi-cursor (Alt+click,Ctrl+D) isn't a scoped interaction for v1. NewmouseMoveEventoverride: while the left button is held (checked viaevent->buttons()) and exactly one cursor is active, updates that cursor's head to the live point under the pointer every move — Qt's implicit press-grab already routes move events to this widget regardless ofsetMouseTracking, so no extra grab is needed.mouseReleaseEventis deliberately not overridden: there's nothing left to do at release — the selection persists exactly as the drag left it, and Qt needs no explicit ungrab from us. - Typing/Backspace/Delete over a selection: each edit primitive
checks
hasSelectionAt(i)first. Insert: delete the selection, then insert at the collapse point (both recorded in the same undo group — no plumbing changes needed, this pass sits entirely on top of Phase 10/ADR 0018'sbegin_group/record_*/end_group). Backspace/ Delete: the selection-delete is the whole operation, no extra byte removed beyond it. - Undo/redo restores point cursors only: the undo stack only ever
snapshotted plain offsets (ADR 0018), so
applyUndoResultnow also resets every anchor to match its cursor — an undo/redo always lands with no active selection, same as typing over one collapses it. - Rendering: one new pass in
paintEvent, inside the same translate/clip block as the text-drawing loop and running before it (so glyphs stay crisp on top of the translucent fill, not underneath it). Per cursor with an active selection, per visual line the selection spans,xForColumngives the same pixelxthe text renderer would use for that column — reusing the exact run-segmented measurement from ADR 0013 rather than an approximatecolumn * charWidth. A line that isn't the selection's last line gets its rect widened by half a char to visually hint the selected newline continues. New config keyselection = #45403866(background- tinted, ~40% alpha), added toase_config_create_default(soase_config_get_coloralways succeeds, same guaranteebackground/texthave) and to the starter-file template.
Consequences¶
Verified live end-to-end against the running ase_gui, driven with
xdotool under the real X session and captured with import:
Shift+Right repeatedly extends a selection and a plain Right collapses
it to the far edge (confirmed via cursor position in successive
screenshots); typing a character while a selection is active replaces
exactly that range — checked by save-to-file round trip ("hello
world" + select "o worl" + type X → saved file reads "hellXd")
— and one Ctrl+Z restores the original text as a single action
(same-session round trip). Click-drag selection was verified two ways:
cursor tracking the drag endpoint (screenshot), and — because the
overlay's ~40% alpha over a near-black background is visually subtle
in a downscaled screenshot — a pixel-level check confirmed the
expected alpha-blended color ((52,50,46), matching #45403866 over
#282828) filling exactly the selected columns. The low-contrast
overlay is as designed (background-tinted, not a separate hue, per the
project's "one font color" aesthetic pillar — same reasoning ADR 0007
used for syntax highlighting), not a rendering bug; worth revisiting
only if reported as hard to see in practice.
Not handled, deliberately, as v1 scope: Shift+click and drag-selection
both operate on a single cursor only (no defined behavior combined
with Alt+click/Ctrl+D multi-cursor); overlapping multi-cursor
selections during a batch edit aren't specially guarded (same
implicit-non-overlap assumption every existing multi-cursor op
already carries, per ADR 0012). Clipboard (Phase 12) and find/replace
(Phase 13) build directly on hasSelectionAt/selectionMinAt/
selectionMaxAt/deleteSelectionAt added here.