Open-Source Wikis

/

Helix

/

Features

/

Multiple selections

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:

  1. The insert_char command builds a Transaction with N inserts (one per range).
  2. It applies the transaction and updates each range with the right Assoc::AfterWord semantics.
  3. Auto-pair logic, completion triggers, and snippet expansion all run once but consult each range — see commands::insert::insert_char in commands.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 = true the 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_cursor semantics — fixed once, fixed everywhere.
  • Undo is per-transaction. N parallel edits collapse into one undoable unit, matching user intent.

See also

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Multiple selections – Helix wiki | Factory