Open-Source Wikis

/

Helix

/

Primitives

/

Selection

helix-editor/helix

Selection

A Selection is a non-empty list of Ranges with one designated as primary. Together they're the editing target for nearly every command.

The implementation is in helix-core/src/selection.rs — a 1,459-line module that contains both the data structures and a large library of selection-aware combinators.

Range

pub struct Range {
    pub anchor: usize,           // fixed end (in char gaps)
    pub head: usize,             // moving end (in char gaps)
    pub old_visual_offset: Option<(u16, u16)>,  // sticky column for vertical moves
}
  • anchor and head are char gap indices (positions between chars, see primitives/rope).
  • A range is forward if anchor <= head, backward otherwise.
  • A range with anchor == head is a zero-width selection (a "cursor").

User-facing semantics: a block cursor sits one grapheme before head for forward ranges, at head for zero-width ranges. The helpers Range::cursor, Range::cursor_line, Range::put_cursor translate between range coordinates and "where is the user looking".

Common methods

Method Purpose
Range::new(anchor, head) Construct a range.
Range::point(pos) Zero-width range at pos.
Range::from(), to() Min and max of (anchor, head).
Range::is_empty() Anchor equals head.
Range::contains_range(other) Range subset test.
Range::overlaps(other) Including the shared edge of zero-width ranges.
Range::cursor(text) The grapheme-aware cursor position.
Range::put_cursor(text, pos, extend) Move the head, optionally extending.
Range::flip() Swap anchor and head.
Range::map(change_set) Translate through a ChangeSet.
Range::min_width_1(text), slice(text) Get the slice this range covers.

Selection

pub struct Selection {
    ranges: SmallVec<[Range; 1]>,
    primary_index: usize,
}

Always non-empty. Always has a primary. Ranges are stored in document order (sorted by from()); operations that produce new ranges call Selection::normalize to resort and merge overlapping ones.

Construction

Selection::single(anchor, head)
Selection::point(pos)
Selection::new(ranges, primary_index)        // sorts and validates

Combinators

  • Selection::transform(|range| -> Range) — apply a function to every range, returning a new selection.
  • Selection::map(change_set) — translate every range through a transaction's change set.
  • Selection::push(range) — append, marking the new range as primary.
  • Selection::merge_consecutive_ranges / merge_overlapping — used by normalize.
  • Selection::transform_iter — flatten ranges into multiple new ones (e.g. s regex select).

Primary semantics

  • Selection::primary() returns the primary Range.
  • Selection::primary_index() returns its position in the sorted vector.
  • Commands like keep_primary_selection (,) drop everything but the primary; remove_primary_selection (Alt-,) drops the primary; rotate_selections_* ((/)) shift the primary index.

Why ranges are built this way

A (from, to) pair would be simpler but loses the direction. (anchor, head) lets:

  • extend_* motions move only the head while pinning the anchor.
  • A user reverse a selection with Alt-; (flip_selections).
  • Vertical motions remember the original visual column via old_visual_offset so up-down-up returns to the start column.

Normalization rules

Selection::normalize (helix-core/src/selection.rs):

  1. Stable-sort ranges by from().
  2. Merge overlapping or directly-adjacent ranges (zero-width touching is allowed but not merged).
  3. Track the primary index across the sort/merge.

Most commands rely on this — they produce ranges via a closure and trust normalization to clean up.

Grapheme awareness

Range indices are char gaps, but visible cursor placement is grapheme-aware. The helpers in helix-core/src/graphemes.rs snap positions to grapheme boundaries:

  • ensure_grapheme_boundary_next(text, idx)
  • ensure_grapheme_boundary_prev(text, idx)
  • next_grapheme_boundary(text, idx)
  • prev_grapheme_boundary(text, idx)

Range::min_width_1 extends a zero-width range by one grapheme so the user always has something visibly selected.

Per-view selections

A Document stores selections keyed by ViewId:

selections: HashMap<ViewId, Selection>

Two views of the same buffer can have independent selections — moving the cursor in one view does not jump the cursor in the other. Selections are pruned when a view closes (helix-view/src/document.rs).

Mapping over edits

When a transaction is applied, every alive selection (this view's, other views' on the same doc, the savepoint stack) is updated by mapping through the ChangeSet:

let new_sel = old_sel.map(transaction.changes());

Assoc (helix-core/src/transaction.rs) controls how a position tracks edits at its boundary — should it stick to the left or to the right? AfterWord is used in insert mode to keep the cursor at the end of newly typed text.

See also

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

Selection – Helix wiki | Factory