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
}anchorandheadare char gap indices (positions between chars, see primitives/rope).- A range is forward if
anchor <= head, backward otherwise. - A range with
anchor == headis 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 validatesCombinators
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 bynormalize.Selection::transform_iter— flatten ranges into multiple new ones (e.g.sregex select).
Primary semantics
Selection::primary()returns the primaryRange.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_offsetso up-down-up returns to the start column.
Normalization rules
Selection::normalize (helix-core/src/selection.rs):
- Stable-sort ranges by
from(). - Merge overlapping or directly-adjacent ranges (zero-width touching is allowed but not merged).
- 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
- primitives/transaction —
change_by_selectionandSelection::map. - features/multiple-selections — how the multi-cursor UX uses these types.
- packages/helix-core — the rest of the core types.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.