nano-rope: B-tree text rope with flat chunks, multi-unit indexing and custom measures

[ library, mit, text ] [ Propose Tags ] [ Report a vulnerability ]

A persistent text rope for editors, language servers and parsers.

  • UTF-8 chunks of at most 512 bytes keep local edits small, even in documents with very long lines.

  • A B-tree with up to 16 children per node shares unchanged text between versions, making snapshots and undo inexpensive.

  • Consecutive insertions can use a bounded keystroke buffer to reduce tree updates.

  • Cached byte, code point, UTF-16 and newline counts support logarithmic-time indexing and conversion between units.

  • Chunk scans use SSE2 or AVX2 on supported x86-64 systems, with portable C elsewhere. Build with -f -simd for Haskell-only scans.

  • Chunk views and buffered UTF-8 output avoid flattening the document.

  • Custom monoidal measures support application-specific summaries and prefix searches.


[Skip to Readme]

Flags

Manual Flags

NameDescriptionDefault
simd

Enable C chunk scans, with runtime-selected SSE2 or AVX2 on supported x86-64 systems and portable C elsewhere. Disable for Haskell-only scans that process up to 8 bytes at a time.

Enabled
compare-text-rope

Run the benchmarks on text-rope as well, for comparison.

Disabled
compare-yi-rope

Run the benchmarks on yi-rope as well, for comparison.

Disabled
compare-core-text

Run the benchmarks on core-text as well, for comparison.

Disabled

Use -f <flag> to enable a flag, or -f -<flag> to disable that flag. More info

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.1.0.0
Change log CHANGELOG.md
Dependencies base (>=4.17 && <4.23), deepseq (>=1.4 && <1.6), primitive (>=0.9 && <0.10), text (>=2.0 && <2.2) [details]
Tested with ghc ==9.14.1
License MIT
Author goolord
Maintainer zacharyachurchill@gmail.com
Uploaded by goolord at 2026-09-20T17:06:08Z
Category Text
Distributions
Downloads 2 total (2 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user
Build status unknown [no reports yet]

Readme for nano-rope-0.1.0.0

[back to package description]

nano-rope

A persistent UTF-8 rope for Haskell. Index and edit by bytes, Unicode code points, UTF-16 units, or lines in logarithmic time. Versions share unchanged text for undo and snapshots.

Uses a B-tree of chunks up to 512 bytes, buffered consecutive insertions, zero-copy chunk reads, and optional custom monoidal summaries.

Quick start

Requires GHC 9.4+. Add nano-rope and text to your Cabal build-depends.

{-# LANGUAGE OverloadedStrings #-}

import Data.Text.NanoRope (Rope, Unit (..), Position (..))
import qualified Data.Text.NanoRope as Rope

document :: Rope
document = Rope.fromText "let x = \"😀\"\nlet y = x\n"

edited = Rope.replace Lines 1 2 "let z = x\n" document
-- document remains valid

line = Rope.getLine 1 edited                          -- "let z = x"
size = Rope.length Utf16 document                     -- 23
byte = Rope.convert Utf16 Bytes 11 document           -- 13
pos  = Rope.offsetToPosition Bytes Utf16 17 document   -- Position 1 2

Coordinates

Unit Meaning
Bytes UTF-8 bytes
Chars Unicode code points, not graphemes or display columns
Utf16 UTF-16 code units; LSP's default position encoding
Lines Line starts; length Lines counts \n characters
  • Offsets are zero-based and clamped. Offsets inside a code point round down.
  • Ranges are half-open; both endpoints refer to the original rope. If j <= i, slicing is empty, deletion does nothing, and replacement inserts at i.
  • Position is a zero-based line and column, with the column unit supplied separately. Columns clamp before \n or \r\n; lines past the document clamp to its end. Negative lines and columns clamp to zero. Only \n starts a new line.
  • getLine strips the terminator and returns empty text for invalid indices. lineCount includes the final, possibly empty line. lines omits that empty trailing line and returns [] for an empty rope.

For multiple coordinates, reuse metricsAtPosition, or metricsAtLineAndPosition for both line-start and position metrics. Subtract their bytes, chars, or utf16Units fields to get columns.

Reading and writing

  • chunkAt returns a zero-copy view of the remaining chunk at an offset.
  • foldlChunks' / foldrChunks traverse chunks; toChunks / toLazyText share their buffers. toText copies unless the rope fits in one chunk.
  • hPutUtf8 / writeFileUtf8 stream UTF-8, preserving bytes and line endings regardless of handle encoding or newline translation.

Custom measures

Data.Text.NanoRope.Measured provides Rope m with cached monoidal summaries. Implement Measure.measureChunk, then use measure to read the summary or splitWhere to search by it. Measures must obey:

measureChunk (a <> b) == measureChunk a <> measureChunk b
measureChunk mempty   == mempty

Search predicates must stay true once true for a growing prefix. Pairs and triples of measures are supported. measured / unmeasured in the plain module rebuild annotations while sharing text buffers.

Performance

For n document bytes and k inserted or returned bytes:

Operation Cost
null, length, metrics, lineCount; settled measure O(1)
Tree edits, slices, lookups, coordinate conversions, searches O(log n)
insert, replace, getLine, sliceText O(log n + k)
Construction, flattening, chunk traversal, UTF-8 output O(n)

Bounds assume constant-time measure combination and search predicates, and linear-time chunk measurement. Consecutive insertions can use a bounded buffer; tree reads flush it, while length and metrics do not.

Scans use SSE2/AVX2 on supported x86-64 systems, portable C elsewhere, and Haskell for short scans. Build with -f -simd for Haskell-only scans.

Benchmarks

GHC 9.14.1, about 4 MB / 100,000 lines of generated source. A fresh rope retains 4.55 MB for 4.03 MB of text; after 10,000 random inserts, 4.60 MB. Retaining older versions uses additional memory. Construction and flattening usually copy the text.

Timings, allocation, and live heap: nano-rope, text-rope, yi-rope, core-text

Raw results · Benchmark source · Language-server workloads (edits, completion, tokens, positions). Results vary with hardware, compiler, and document shape.

Development

cabal test
cabal test -f -simd
cabal haddock
cabal bench

Regenerate the comparison chart and CSV:

cabal bench -f compare-text-rope -f compare-yi-rope -f compare-core-text --benchmark-options="--chart bench/results.svg"

Add --redraw inside --benchmark-options to reuse CSV timings and allocations; live heap is measured again.