JumpRope Rust Library

repository·master·Indexed 41 days ago

https://github.com/josephg/jumprope-rs

A high-performance rope data structure implemented in Rust, built on top of Skiplists and optimized for efficient large-string editing and processing of editing traces. Version 1.1.2 provides O(log n) in-place edits via insert, remove, and replace. It includes JumpRopeBuf for buffered editing to improve performance, a WebAssembly API for JavaScript environments, and optional wchar conversion for UTF-16 compatibility.

Tokens
6.4K
Snippets
29
Records
36
Agent score
99%

What's inside JumpRope

  1. Optimize edits using JumpRopeBuf

    master

    For high-performance editing, use JumpRopeBuf. This wrapper provides a buffered API that attempts to merge consecutive editing runs (like multiple character insertions) before flushing them to the underlying JumpRope object. This can make editing up to 10x faster.

    If a specific method you need is missing from JumpRopeBuf, you can call rope.borrow() or rope.as_mut() to flush pending changes and access the underlying JumpRope instance.

  2. Install JumpRope via Cargo

    master

    To use JumpRope in your Rust project, add it to your Cargo.toml file.

    Note: JumpRope is optimized for large strings (e.g., source code files). For very small strings (less than 100 bytes), standard Rust String or small-string-optimized libraries like SmartString are recommended.

    jumprope = "1.0.0"
  3. Read content from a JumpRope

    master

    There are several ways to read data back from a rope depending on your performance and allocation needs:

    1. Convert to String: Use rope.to_string(). This is convenient but requires full allocations.
    2. Iterate Characters: Use rope.chars() to iterate over Unicode characters.
    3. Iterate Substrings (Fastest): Use rope.substrings() to get an iterator over &str chunks contained in the document. This is the most efficient method for reading content.
    4. Slice Substrings: Use rope.slice_substrings(range) to read all content within a specific range (e.g., 10..20).
    fn main() {
        let rope = JumpRope::from("xxxGreetings!xxx");
    
        let string = rope.slice_substrings(3..13).collect::<String>();
        assert_eq!(string, "Greetings!");
    }
  4. Run rope implementation benchmarks

    master

    To execute the performance benchmarks comparing various rope implementations (including Jumprope, librope, ropey, xi-rope, and an-rope), navigate to the rope_benches directory and use the cargo run command with the --bench flag in release mode. The results are generated as a Criterion report accessible via HTML.

    cargo run --release -- --bench
  5. Use JumpRopeBuf for optimized buffered edits

    master

    If you are performing many small, sequential edits (inserts or deletes) to a rope, use JumpRopeBuf instead of JumpRope. JumpRopeBuf buffers adjacent edits and applies them to the underlying rope all at once when the rope is read or flushed. This can improve performance by approximately 10x for sequential operations by amortizing the cost of skip list updates.

    Note: This feature is experimental and must be enabled via the buffered feature flag in your Cargo.toml.

    Caveats:

    • JumpRopeBuf uses a RefCell internally. It does not expose a &JumpRope directly.
    • It is Send but not Sync due to the internal RefCell.
    // Ensure the 'buffered' feature is enabled in Cargo.toml
    use jumprope::JumpRopeBuf;
    
    let mut buf = JumpRopeBuf::new();
    buf.insert(0, "Hello ");
    buf.insert(6, "World");
    
    // The edits are buffered and not yet applied to the underlying JumpRope.
    // Accessing the rope will flush the buffer.
    let rope = buf.into_inner();
    assert_eq!(rope.len_chars(), 11);
  6. Enable Wchar conversion for UTF-16 compatibility

    master

    If you need to interface with languages like JavaScript, Java, or C# that use 2-byte UTF-16 offsets (wchars), you can enable the wchar_conversion feature. This allows $O(\log n)$ conversion between Unicode character offsets and wchar offsets.

    Warning: This feature adds extra bookkeeping that slows down JumpRope by approximately 15%.

    jumprope = { version = "1.0.0", features = ["wchar_conversion"] }
  7. How JumpRope cursors work

    master

    JumpRope uses Cursors to express the location where an edit (like an insertion) will occur. Because JumpRope is implemented as a skip list, moving or editing content requires updating the 'skip' counts in the pointers of previous nodes.

    There are two types of cursors:

    1. ReadCursor: Used for read-only access at a specific character position.
    2. MutCursor: Used for performing mutations (like insertions). A MutCursor maintains a path of SkipEntry objects from the root down to the target location, allowing the rope to efficiently update the skip-list structure when the content changes.

    To perform an edit, you typically:

    1. Create a MutCursor at a specific character position using mut_cursor_at_char(char_pos, stick_end).
    2. Perform operations via the cursor (though the specific high-level mutation API is partially internal in this segment, the pattern involves using the cursor to locate the insertion point).
  8. How JumpRope handles string lengths and indexing

    master

    JumpRope uses Unicode character positions (the number of Unicode scalar values) for all internal operations, indexing, and length reporting. This differs from byte length or grapheme cluster counts.

    When performing operations like remove(start..end), the indices refer to the character positions. For example, in the string "🐻‍❄️" (which contains 4 Unicode characters), removing indices 1..4 will leave only the first character.

    Key length methods:

    • len_bytes(): Returns the number of UTF-8 bytes.
    • len_chars(): Returns the number of Unicode characters (the standard for JumpRope indexing).

    Example with multi-byte characters:

    let mut rope = JumpRope::from("🐻‍❄️");
    // The string contains 4 unicode characters
    rope.remove(1..4); 
    assert_eq!(rope, "🐻");
    # use jumprope::JumpRope;
    let mut rope = JumpRope::from("🐻‍❄️");
    rope.remove(1..4); // Remove characters from index 1 to 4
    assert_eq!(rope, "🐻");
  9. Iterate over individual characters in a JumpRope

    master

    Use the chars() method to get an iterator over the individual char elements in the rope. Note that this is generally less efficient than using substrings() because it processes the rope character by character rather than in chunks.

    use jumprope::JumpRope;
    
    let rope = JumpRope::from("oh hai");
    let collected: String = rope.chars().collect();
    assert_eq!(collected, "oh hai");
  10. Iterate over substrings with Unicode character lengths

    master

    If you need to know the Unicode character count for each chunk (to avoid recalculating it), use substrings_with_len(). This returns an iterator yielding pairs of (&str, usize), where the usize is the number of Unicode characters in that specific substring.

    Stability Warning: Like substrings(), the chunking logic is an internal detail and subject to change.

    use jumprope::JumpRope;
    
    let rope = JumpRope::from("oh hai");
    for (s, char_len) in rope.substrings_with_len() {
        assert_eq!(s.chars().count(), char_len);
    }
  11. Initialize a new JumpRope

    master

    You can create a new, empty JumpRope using several methods depending on your requirements for determinism and entropy:

    • JumpRope::new(): Creates an empty rope. In release mode, this uses entropy. In debug/test mode, it uses a fixed seed for determinism.
    • JumpRope::new_from_entropy(): Creates an empty rope seeded from a high-entropy source.
    • JumpRope::new_from_seed(seed: u64): Creates an empty rope using a specific u64 seed. This is useful for making benchmarks or tests entirely deterministic.
    use jumprope::JumpRope;
    
    // Deterministic for testing
    let rope = JumpRope::new_from_seed(123);
    
    // Random for production
    let rope = JumpRope::new_from_entropy();