helix-editor/helix
Rope
Rope is the persistent text buffer Helix uses for every open file. It is re-exported from the ropey crate via helix-core/src/lib.rs.
Why a rope
A naive editor uses a contiguous Vec<u8> or String for the buffer. Inserting in the middle of a 100 MB file becomes O(n). Helix targets editing of "anything that comes up when coding, within reason — 200 MB XML, megabyte-of-minified-JS-on-one-line" (see vision). A rope makes:
- Inserts and deletes O(log n)
- Clones O(1) (copy-on-write through internal
Arcs) - Slicing into
RopeSliceO(log n) and lifetime-free
Cheap clones in particular drive the rest of the architecture: History snapshots the rope before every commit, and a DiffHandle keeps a rope clone around as the diff base. None of this would be feasible with a contiguous buffer.
Key types
| Type | Purpose |
|---|---|
Rope |
The persistent rope. Owned. Cheap clone. |
RopeSlice<'a> |
A read-only view into a rope. Carries a lifetime. |
RopeBuilder |
Used to build a rope from chunks (e.g. when reading from disk in 8 KiB blocks). |
Chunks<'a> |
Iterator over the rope's underlying string chunks. |
All of these are public re-exports.
Char-gap indexing
Indices into a rope are char gaps, not byte offsets and not char offsets:
- A buffer with N chars has positions
0..=N. - Position 0 is before the first char.
- Position N is after the last char.
This matches how Range::anchor and Range::head work in Selection. The helix-core::position module converts between char positions and visual (row, col) positions, and between char positions and byte positions when needed (e.g. for LSP UTF-8 encoding).
RopeSliceExt
helix-stdx adds an extension trait RopeSliceExt (helix-stdx/src/rope.rs) for things ropey doesn't ship:
regex_iter_at(start, regex)— run a regex over a rope slice without allocating aString.byte_range_at_char(idx),char_range_at_byte(idx),lines_at_byte(idx)— byte/char/line conversions.- Fast skipping of contiguous matching/non-matching runs.
These are used heavily by tree-sitter integration (which thinks in bytes), regex search, and the line-ending detector.
Reading and writing
Document I/O happens through the standard Read/Write traits:
RopeReader(helix-core/src/rope_reader.rs) implementsReadover aRopeSlice. Used to feed a rope into tools that need byte streams (like a formatter's stdin).- Loading a file goes through
RopeBuilderchunked byBUF_SIZE = 8192bytes (helix-view/src/document.rs) so we never hold the whole file twice.
Tendril
Helix uses Tendril (helix-core/src/lib.rs) for short text fragments — the String analog with a small-string optimization. Tendril is SmartString<LazyCompact>. Most Operation::Insert(Tendril) payloads fit inline without an allocation.
How edits modify a rope
Document::apply does not mutate the rope in place — it produces a new one and stores it. Specifically:
let new_text = old_text.clone(); // O(1) — Arc bump
new_text.apply(&change_set); // structural updates onlyThe previous rope is kept by History so undo is free.
Performance notes
- Walk the rope by
chunks()rather thanchars()when you need to scan large ranges; chunk iteration avoids per-char overhead. - Building incrementally? Use
RopeBuilderand finalize with.finish(). RopeSlice::is_empty()is O(1); avoid.len_chars() == 0.Rope::write_tostreams to aWriteimpl directly — used byDocument::saveto avoid a string materialization.
Ownership patterns in the codebase
- A
Documentowns oneRope. Selections, syntax trees, and diff handles reference it. Transaction::apply(&mut Rope)mutates in place via internalArc::make_mutcalls — but the rope is treated as a value type by callers.- Snapshots (history commits, diff base, save points) are
Ropeclones — cheap.
See also
- primitives/transaction — how edits are constructed.
- primitives/document — where the rope lives.
ropeydocumentation.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.