Agent skill
ropey
Efficient UTF-8 text rope data structure for editors
Install this agent skill to your Project
npx add-skill https://github.com/johnlindquist/script-kit-next/tree/main/.opencode/skill/ropey
SKILL.md
ropey
Ropey is a UTF-8 text rope library for Rust, optimized for efficient text manipulation in editors. A rope is a tree-based data structure that stores text as a sequence of smaller chunks, enabling O(log N) insertions and deletions at arbitrary positions - far more efficient than String's O(N) for large documents.
Why Ropes?
Traditional strings require shifting all characters after an edit point, making insertions/deletions O(N). Ropes split text into chunks organized in a tree, so edits only affect local nodes:
- Insert/Delete: O(M + log N) where M is the edited text length
- Index Conversion: O(log N)
- Length Queries: O(1)
- Clone: O(1) with copy-on-write semantics
This makes ropey ideal for text editors handling documents of any size.
Key Types
Rope
The main ownable text container. Cloning is O(1) via data sharing.
use ropey::Rope;
let mut rope = Rope::from_str("Hello world!");
rope.insert(6, "beautiful ");
rope.remove(0..6);
assert_eq!(rope, "beautiful world!");
RopeSlice<'a>
An immutable view into part of a Rope. Behaves like &str but for ropes.
let rope = Rope::from_str("Hello\nWorld\n");
let slice = rope.slice(0..5); // "Hello"
let line = rope.line(1); // "World\n" as RopeSlice
RopeBuilder
Efficient incremental rope construction for streaming/large files.
use ropey::RopeBuilder;
let mut builder = RopeBuilder::new();
builder.append("Hello ");
builder.append("world!");
let rope = builder.finish();
Iterators (ropey::iter)
Bytes- iterate over raw bytesChars- iterate over Unicode charsLines- iterate over lines (includes trailing newline)Chunks- iterate over internal&strchunks (for low-level access)
All iterators support bidirectional traversal via next() and prev().
Common Operations
Creating Ropes
// From string
let rope = Rope::from_str("content");
// From file
let rope = Rope::from_reader(BufReader::new(File::open("file.txt")?))?;
// Empty
let rope = Rope::new();
Editing
let mut rope = Rope::from_str("Hello world!");
// Insert at char index
rope.insert(6, "beautiful ");
// Insert single char
rope.insert_char(0, '!');
// Remove range (char indices)
rope.remove(0..6);
// Append another rope
rope.append(other_rope);
// Split at position
let right = rope.split_off(5);
Querying
let rope = Rope::from_str("Hello\nWorld\n");
// Lengths
rope.len_bytes(); // Total bytes
rope.len_chars(); // Total chars
rope.len_lines(); // Total lines (3 for above)
// Access by index
rope.byte(0); // First byte
rope.char(0); // First char: 'H'
rope.line(1); // Second line as RopeSlice: "World\n"
Slicing
// By char range
let slice = rope.slice(0..5);
let slice = rope.slice(5..);
let slice = rope.slice(..5);
// By byte range (must align with char boundaries!)
let slice = rope.byte_slice(0..5);
Writing to File
rope.write_to(BufWriter::new(File::create("output.txt")?))?;
Index Conversions
Critical: Ropey uses three distinct indexing systems. Mixing them causes bugs.
| Method | From | To |
|---|---|---|
byte_to_char(byte_idx) |
byte | char |
byte_to_line(byte_idx) |
byte | line |
char_to_byte(char_idx) |
char | byte |
char_to_line(char_idx) |
char | line |
line_to_byte(line_idx) |
line | byte (start) |
line_to_char(line_idx) |
line | char (start) |
let rope = Rope::from_str("Hello\nWorld\n");
// Get char index where line 1 starts
let start = rope.line_to_char(1); // 6
// Get line containing char index 8
let line = rope.char_to_line(8); // 1
// Convert byte to char (handles multi-byte UTF-8)
let char_idx = rope.byte_to_char(7);
UTF-16 Support (for JS/LSP interop)
rope.len_utf16_cu(); // Total UTF-16 code units
rope.char_to_utf16_cu(idx); // char -> UTF-16 index
rope.utf16_cu_to_char(idx); // UTF-16 -> char index
Indexing Caveats
1. Char vs Byte Indices
Ropey's primary indexing is by char (Unicode scalar value), not byte. This matters for non-ASCII text:
let rope = Rope::from_str("cafe"); // 5 bytes, 4 chars
rope.char(3); // '' (1 char, 2 bytes)
rope.len_chars(); // 4
rope.len_bytes(); // 5
2. Line Counting
- Lines are zero-indexed
len_lines()counts logical lines (text after last newline = extra line)- Line iterators include the trailing newline in each line
let rope = Rope::from_str("a\nb"); // 3 lines: "a\n", "b", ""
let rope = Rope::from_str("a\nb\n"); // 3 lines: "a\n", "b\n", ""
3. One-Past-End Indexing
Many methods accept one-past-the-end indices:
let rope = Rope::from_str("abc"); // len_chars() = 3
rope.byte_to_char(3); // Valid, returns 3
rope.char(3); // PANICS! char() is strict
4. CRLF Handling
CRLF (\r\n) is treated as a single line break. By default, ropey recognizes:
\n(LF)\r\n(CRLF)
With unicode_lines feature (default): also \r, \x0B, \x0C, \u{0085}, \u{2028}, \u{2029}.
Low-Level Chunk Access
For performance-critical code, access raw chunks directly:
// Get chunk containing char index
let (chunk, chunk_byte_idx, chunk_char_idx, chunk_line_idx) =
rope.chunk_at_char(char_idx);
// Iterate chunks
for chunk in rope.chunks() {
// Process &str directly
}
// Use str_utils for operations on chunks
use ropey::str_utils::byte_to_char_idx;
let local_char = byte_to_char_idx(chunk, byte_offset_in_chunk);
Usage in script-kit-gpui
In script-kit-gpui, ropey is used indirectly through gpui-component's Input component for the editor buffer. The crate handles:
- Editor buffer storage with efficient editing
- Char/byte offset conversion (critical for cursor positioning)
- Line-based operations (go to line, line numbers)
The editor.rs file shows patterns for converting between char and byte offsets when dealing with gpui-component's selection API.
Anti-patterns
Using byte indices with char methods
// WRONG - byte index used with char method
let byte_pos = some_api_returning_bytes();
rope.char(byte_pos); // BUG if byte_pos is a byte index!
// CORRECT
let char_pos = rope.byte_to_char(byte_pos);
rope.char(char_pos);
Assuming 1:1 char:byte mapping
// WRONG - assumes ASCII
let byte_idx = char_idx;
// CORRECT
let byte_idx = rope.char_to_byte(char_idx);
Forgetting line semantics
// WRONG - off by one on last line
let line_count = rope.to_string().split('\n').count();
// CORRECT
let line_count = rope.len_lines();
Mutating while iterating
// WRONG - can't mutate during iteration
for line in rope.lines() {
rope.insert(0, "prefix"); // BUG!
}
// CORRECT - collect first
let line_count = rope.len_lines();
for i in (0..line_count).rev() {
let start = rope.line_to_char(i);
rope.insert(start, "prefix");
}
Ignoring panics on out-of-bounds
// PANICS if idx >= len_chars()
rope.char(idx);
// SAFE - returns Option
rope.get_char(idx);
// SAFE - returns Result
rope.try_char_to_byte(idx);
Performance Tips
- Clone freely - O(1) due to structural sharing
- Use
chunks()for bulk processing - avoid per-char overhead - Prefer
RopeSliceoverto_string()- zero-copy views - Use
RopeBuilderfor construction - more efficient than repeated inserts - Batch edits at end - avoid shifting for sequential appends
Feature Flags
simd(default): SIMD acceleration for counting/searchingunicode_lines(default): Full Unicode line break supportcr_lines: Treat standalone\ras line break
Reference
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
Generate Component Documentation
Based on existing docs styles and specific API implementations, and referencing same name stories, generate comprehensive documentation for the new component.
Generate Component Story
Generate a comprehensive story for a new component for as example.
new-component
How to write a new component of GPUI Component.
troubleshooting
Diagnose and fix common Script Kit issues. Use when the user reports bugs, crashes, missing features, or unexpected behavior in Script Kit GPUI.
script-authoring
Create and manage TypeScript scripts for Script Kit. Use when the user wants to write a new script, edit an existing script, or understand Script Kit's SDK and metadata system.
agents
Create mdflow-backed agent files for Script Kit. Use when the user wants to create AI agents, configure agent backends (Claude, Gemini, Codex), or manage agent metadata.
Didn't find tool you were looking for?