Open-Source Wikis

/

Helix

/

Primitives

/

Rope

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 RopeSlice O(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 a String.
  • 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) implements Read over a RopeSlice. Used to feed a rope into tools that need byte streams (like a formatter's stdin).
  • Loading a file goes through RopeBuilder chunked by BUF_SIZE = 8192 bytes (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 only

The previous rope is kept by History so undo is free.

Performance notes

  • Walk the rope by chunks() rather than chars() when you need to scan large ranges; chunk iteration avoids per-char overhead.
  • Building incrementally? Use RopeBuilder and finalize with .finish().
  • RopeSlice::is_empty() is O(1); avoid .len_chars() == 0.
  • Rope::write_to streams to a Write impl directly — used by Document::save to avoid a string materialization.

Ownership patterns in the codebase

  • A Document owns one Rope. Selections, syntax trees, and diff handles reference it.
  • Transaction::apply(&mut Rope) mutates in place via internal Arc::make_mut calls — but the rope is treated as a value type by callers.
  • Snapshots (history commits, diff base, save points) are Rope clones — cheap.

See also

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

Rope – Helix wiki | Factory