piece-tree 0.1.4

Purely functional (immutable) implementation of Piece Tree, inspired by fredbuf
Documentation

Piece Tree

Purely functional (immutable) implementation of Piece Tree, inspired by fredbuf.

Usage

You can access full documentation at: https://docs.rs/piece-tree.

Basic editing

fn main() {
    use piece_tree::PieceTree;

    let mut tree = PieceTree::new();

    // Insert text at a byte offset
    tree.insert(0, "Hello, World!");
    assert_eq!(tree.to_string(), "Hello, World!");

    // Insert in the middle
    tree.insert(7, "beautiful ");
    assert_eq!(tree.to_string(), "Hello, beautiful World!");

    // Remove a range by byte offset and length
    tree.remove(7..17);
    assert_eq!(tree.to_string(), "Hello, World!");
}

Coordinate queries

All coordinate functions work in O(log n) time.

fn main() {
    use piece_tree::PieceTree;

    let mut tree = PieceTree::new();
    tree.insert(0, "Hello\n🌐world\r\nПривет");

    // Char index <-> byte offset
    assert_eq!(tree.char_to_byte(6),  6);   // '🌐' starts     at byte 6
    assert_eq!(tree.char_to_byte(7),  10);  // 'w' after 🌐 is at byte 10
    assert_eq!(tree.byte_to_char(10), 7);

    // Line number -> byte offset of line start (0-indexed)
    assert_eq!(tree.line_to_byte(0),  0);
    assert_eq!(tree.line_to_byte(1),  6);
    assert_eq!(tree.line_to_byte(2),  17);

    // Byte offset -> (line, col) where col is a char count
    assert_eq!(tree.byte_to_line_col(0),  (0, 0));
    assert_eq!(tree.byte_to_line_col(10), (1, 1)); // 'w', line 1, col 1
    assert_eq!(tree.byte_to_line_col(17), (2, 0)); // 'П', line 2, col 0
}

Undo and redo

Each call to insert or remove automatically pushes an undo entry. try_undo and try_redo return the cursor offset that was saved with the entry.

fn main() {
    use piece_tree::PieceTree;

    let mut tree = PieceTree::new();
    tree.insert(0, "Hello");
    tree.insert(5, ", World!");
    assert_eq!(tree.to_string(), "Hello, World!");

    tree.try_undo(0);
    assert_eq!(tree.to_string(), "Hello");

    tree.try_redo(0);
    assert_eq!(tree.to_string(), "Hello, World!");
}

Use begin_undo_group and end_undo_group to batch multiple mutations into a single undo step. For manual control, use the *_no_commit variants to make silent changes, followed by commit_head call to record a checkpoint.

fn main() {
    use piece_tree::PieceTree;

    let mut tree = PieceTree::new();
    tree.insert(0, "Hello");

    tree.begin_undo_group(5);

    // Type ", World!" character by character without flooding undo history
    tree.insert(5, ",");
    tree.insert(6, " ");
    tree.insert(7, "World!");

    tree.end_undo_group();

    tree.try_undo(0);
    assert_eq!(tree.to_string(), "Hello");
}

Tree Snapshotting

fn main() {
    use piece_tree::PieceTree;

    let mut tree = PieceTree::new();
    let mut cursor = 0;

    // Basic typing and taking a snapshot
    tree.insert(cursor, "Hello ");   cursor += 6;
    tree.insert(cursor, "World!");   cursor += 6;
    let snap = tree.take_snapshot(cursor); // Saves "Hello World!" state

    // Batch mutations into a single undo group
    tree.begin_undo_group(cursor);
    tree.insert_no_commit(cursor, " Everything"); cursor += 11;
    tree.insert_no_commit(cursor, " is great!");  cursor += 10;
    tree.end_undo_group();
    assert_eq!(tree.to_string(), "Hello World! Everything is great!");

    // Undo and Redo the transaction group
    cursor = tree.try_undo(cursor).unwrap();
    assert_eq!(tree.to_string(), "Hello World!");

    cursor = tree.try_redo(cursor).unwrap();
    assert_eq!(tree.to_string(), "Hello World! Everything is great!");

    // Revert to snapshot (and show that the jump itself can be undone)
    cursor = tree.snap_to(snap, cursor);
    assert_eq!(tree.to_string(), "Hello World!");

    cursor = tree.try_undo(cursor).unwrap();
    assert_eq!(tree.to_string(), "Hello World! Everything is great!");
}

Vendoring

To keep piece-tree highly optimized and tailored for its specific use case, portions of the following third-party crates have been integrated directly into the source code:

  • cranelift-entity (Apache-2.0 with LLVM Exception).
  • smallvec (MIT).
  • bytecount (MIT).

This copy-pasted code remains under its original respective licenses. Full attribution notices are maintained at the top of the relevant source files, and the complete license texts can be found in THIRD-PARTY-LICENSES.md.

If you prefer to use the upstream, non-vendored versions of these crates via Cargo, you can enable the dont_vendor feature flag.

Benchmarks vs ropey's Rope

While these structures address different use cases, this crate is a purely functional Red-Black piece tree offering O(1) snapshotting and slicing, whereas ropey is a mutable B-tree optimized for cache-local, in-place editing, we can still compare their performance profiles.

TL;DR: Slicing is 100-300x faster, and line-based access (including byte/line conversions) is roughly 10-45x faster. However, insertions at the start/middle are 6-10x slower: peaking at ~2-5µs for 1.5MB inserts, while insertions at the end are faster and removes remain roughly equivalent.

Benchmark Group e2-piece-tree ropey's Rope
from_str
from_str/large 1886.9 ± 1.33 µs 1002.6 ± 412.22 µs
from_str/linefeeds 36.2 ± 9.60 µs 6.0 ± 1.39 µs
from_str/medium 264.7 ± 0.24 µs 139.7 ± 10.71 µs
from_str/small 2.6 ± 0.01 µs 1186.1 ± 8.36 ns
get
get/byte 7.0 ± 0.00 ns 70.9 ± 2.38 ns
get/char 122.3 ± 0.67 ns 144.9 ± 3.77 ns
get/chunk_at_byte 4.9 ± 0.00 ns 62.7 ± 6.42 ns
get/chunk_at_byte_slice 7.6 ± 0.01 ns 70.4 ± 0.27 ns
get/chunk_at_char 117.6 ± 0.41 ns 67.8 ± 0.83 ns
get/chunk_at_char_slice 316.1 ± 0.96 ns 70.2 ± 4.07 ns
get/chunk_at_line_break 5.8 ± 0.01 ns 66.8 ± 4.98 ns
get/chunk_at_line_break_slice 119.3 ± 4.18 ns 71.9 ± 4.20 ns
get/line 14.9 ± 0.01 ns 453.4 ± 35.59 ns
index_convert
index_convert/byte_to_char 81.2 ± 0.34 ns 74.2 ± 0.68 ns
index_convert/byte_to_line 123.5 ± 21.21 ns 83.8 ± 11.82 ns
index_convert/char_to_byte 113.8 ± 0.75 ns 126.3 ± 6.31 ns
index_convert/char_to_line 249.6 ± 1.19 ns 153.1 ± 17.01 ns
index_convert/line_to_byte 6.6 ± 0.02 ns 292.7 ± 13.54 ns
index_convert/line_to_char 94.5 ± 0.56 ns 294.2 ± 1.77 ns
insert
insert_after_clone 537.7 ± 5.19 ns 1562.7 ± 100.31 ns
insert_char/start 1112.7 ± 121.29 ns 112.3 ± 3.74 ns
insert_char/middle 622.4 ± 52.70 ns 155.4 ± 4.91 ns
insert_char/end 48.1 ± 2.82 ns 162.9 ± 2.23 ns
insert_char/random 1944.6 ± 378.40 ns 332.4 ± 74.61 ns
insert_small/start 1143.2 ± 99.71 ns 95.6 ± 16.34 ns
insert_small/middle 650.0 ± 49.08 ns 155.9 ± 10.94 ns
insert_small/end 49.2 ± 3.95 ns 166.5 ± 5.60 ns
insert_small/random 1930.7 ± 369.78 ns 333.8 ± 55.31 ns
insert_medium/start 1195.3 ± 106.69 ns 186.0 ± 5.83 ns
insert_medium/middle 1324.6 ± 100.89 ns 250.3 ± 2.97 ns
insert_medium/end 68.9 ± 11.38 ns 243.0 ± 3.71 ns
insert_medium/random 2.0 ± 0.38 µs 395.3 ± 73.67 ns
insert_large/start 3.9 ± 0.55 µs 1773.7 ± 474.86 ns
insert_large/middle 4.2 ± 0.53 µs 2.3 ± 0.13 µs
insert_large/end 2.3 ± 0.13 µs 2.3 ± 0.38 µs
insert_large/random 4.7 ± 0.83 µs 2.7 ± 0.40 µs
remove
remove_initial_after_clone 744.5 ± 1.90 ns 862.4 ± 107.87 ns
remove_small/start 230.0 ± 18.89 ns 145.8 ± 5.28 ns
remove_small/middle 293.9 ± 18.56 ns 180.0 ± 7.63 ns
remove_small/end 229.6 ± 13.70 ns 181.9 ± 19.80 ns
remove_small/random 2.9 ± 0.59 µs 276.8 ± 4.09 ns
remove_medium/start 296.0 ± 10.74 ns 242.6 ± 15.83 ns
remove_medium/middle 560.0 ± 30.23 ns 347.3 ± 15.83 ns
remove_medium/end 306.6 ± 13.19 ns 299.2 ± 44.31 ns
remove_medium/random 3.1 ± 0.61 µs 372.8 ± 14.77 ns
remove_large/start 1912.1 ± 510.75 ns 2.1 ± 0.31 µs
remove_large/middle 2.2 ± 0.54 µs 2.6 ± 0.32 µs
remove_large/end 1852.8 ± 524.83 ns 1930.4 ± 302.20 ns
remove_large/random 4.2 ± 0.90 µs 2.6 ± 0.45 µs
slice
slice/slice 2.6 ± 0.00 ns 866.6 ± 6.02 ns
slice/slice_small 1.6 ± 0.38 ns 245.6 ± 24.54 ns
slice/slice_whole_slice 0.0 ± 0.02 ns 0.3 ± 0.01 ns
slice/slice_from_small_* 2.6 ± 0.00 ns 424.8 ± 56.02 ns
slice/slice_whole_* 0.1 ± 0.02 ns 29.8 ± 7.83 ns