helix-editor/helix
Multiple selections
Multi-cursor editing is built into Helix from the bottom up. Every command operates on a Selection — a non-empty list of Ranges — and most commands work transparently with one cursor or many.
Mental model
In Helix, a "cursor" is a Range whose head is one grapheme past the visual position. The set of all such ranges is a Selection (helix-core/src/selection.rs). One range is the "primary" — it's the one drawn in the cursor color and the one most commands display info about.
graph LR
Doc[Document] -->|per ViewId| Selection
Selection -->|primary index| Primary[Primary Range]
Selection -->|ranges| R1[Range 1]
Selection -->|ranges| R2[Range 2]
Selection -->|ranges| Rn[…]Creating multiple selections
| Action | Default key | Implementation |
|---|---|---|
s — select regex matches in selection |
normal mode s |
select_regex in commands.rs |
S — split selection on regex |
S |
split_selection |
Alt-s — split into one range per line |
Alt-s |
split_selection_on_newline |
C — copy primary onto next line |
C |
copy_selection_on_next_line |
Alt-C — copy primary onto previous line |
Alt-C |
copy_selection_on_prev_line |
J — join lines (also collapses cursors) |
J |
join_selections |
, — collapse to primary |
, |
keep_primary_selection |
Alt-, — remove primary |
Alt-, |
remove_primary_selection |
( / ) — rotate primary backward / forward |
(, ) |
rotate_selections_backward/forward |
How edits flow
Most commands use Transaction::change_by_selection (helix-core/src/transaction.rs):
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
let from = range.from();
let to = range.to();
let replacement = transform(doc.text().slice(from..to));
(from, to, Some(replacement))
});
doc.apply(&transaction, view.id);Internally the closure runs once per range. The resulting transaction is one ChangeSet with all the edits coalesced; positions in the new selection are computed by mapping over the change set with the right Assoc. This guarantees that all edits land atomically and that the multi-cursor selection moves consistently.
Movements compose the same way through Selection::transform:
let new_selection = selection.transform(|range| {
movement::move_horizontally(text, range, dir, count, behaviour, &text_fmt)
});Selection::normalize deduplicates and merges overlapping ranges so multi-cursor input never produces redundant ranges.
Insert mode with N cursors
In Insert mode, every typed character is replicated at every cursor. The implementation:
- The
insert_charcommand builds aTransactionwith N inserts (one per range). - It applies the transaction and updates each range with the right
Assoc::AfterWordsemantics. - Auto-pair logic, completion triggers, and snippet expansion all run once but consult each range — see
commands::insert::insert_charincommands.rs.
Selection-aware paste is similar: paste_after/paste_before (commands.rs) uses one register entry per cursor when the registers were yanked from a multi-selection, and falls back to inserting the same content at each cursor otherwise.
Visual feedback
Multi-cursor selections are rendered by EditorView in helix-term/src/ui/editor.rs:
- The primary range uses theme keys
ui.cursor.primary/ui.selection.primary. - Non-primary ranges use
ui.cursor/ui.selection. - When
editor.color_modes = truethe cursor color tracks the active mode (ui.cursor.normal,ui.cursor.insert,ui.cursor.select).
Jump labels
The Alt-Tab/gw jump-label feature (helix-term/src/ui/text_decorations) places virtual labels next to visible word starts and lets the user jump (or extend the selection) by typing the label. The label alphabet is configurable (editor.jump_label_alphabet).
Why this design
- Edits are atomic. A multi-range transaction either applies fully or not at all. There's no partial state to roll back.
- Composability. The same code path serves single-cursor and multi-cursor edits. Bugs are usually in
Range::cursor/put_cursorsemantics — fixed once, fixed everywhere. - Undo is per-transaction. N parallel edits collapse into one undoable unit, matching user intent.
See also
- primitives/selection — the
Range/Selectiontypes and helpers. - primitives/transaction —
change_by_selection, mapping, composition. - features/modal-editing — how counts, registers, and modes interact with multi-selection.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.