rope

moon add myfreess/rope@0.1.1
Download zip
Author
Version
0.1.1
License
Apache-2.0
Last updated
9 months ago
Downloads
18
README

#MoonBit Rope Library

A high-performance UTF-16 text rope implementation for MoonBit, based on the Rust Ropey library.

#Features

  • Fast Operations: All operations are O(log N) or better
  • UTF-16 Native: Designed for MoonBit's UTF-16 string representation
  • Unicode Safe: All operations work with character indices, not UTF-16 code units
  • Memory Efficient: Tree-based structure with copy-on-write semantics
  • Line-Aware: Built-in support for line-based operations

#Basic Usage

#Creating a Rope

test "basic_usage" {
// Create an empty rope
let rope = Rope::new()
inspect(rope.rope_is_empty(), content="true")

// Create a rope from a string
let rope = Rope::from_string("Hello, World!")
inspect(rope.len_chars(), content="13")
inspect(rope.rope_to_string(), content="Hello, World!")
}

#Text Information

test "text_info" {
let rope = Rope::from_string("Hello\nWorld\n!")

// Get various length measurements
inspect(rope.len_chars(), content="13") // Number of characters
inspect(rope.len_utf16_cu(), content="13") // Number of UTF-16 code units
inspect(rope.len_lines(), content="3") // Number of lines
}

#Character Operations

test "character_ops" {
let rope = Rope::from_string("Hello, 世界!")

// Get character at index (returns character code)
inspect(rope.rope_char_at(0), content="72") // 'H'
inspect(rope.rope_char_at(7), content="19990") // '世'

// Convert between character and UTF-16 indices
inspect(rope.char_to_utf16_cu(7), content="7")
inspect(rope.utf16_cu_to_char(7), content="7")
}

#Text Modification

test "modification" {
let rope = Rope::from_string("Hello World")

// Insert text at any position
let rope2 = rope.insert(5, ", Beautiful")
inspect(rope2.rope_to_string(), content="Hello, Beautiful World")

// Remove text ranges
let rope3 = rope2.remove(5, 17) // Remove ", Beautiful"
inspect(rope3.rope_to_string(), content="HelloWorld")

// Note: Original rope is unchanged (immutable)
inspect(rope.rope_to_string(), content="Hello World")
}

#Line Operations

test "line_ops" {
let rope = Rope::from_string("Line 1\nLine 2\nLine 3")

// Get line count
inspect(rope.len_lines(), content="3")

// Convert between character and line indices
inspect(rope.line_to_char(1), content="7") // Start of line 1
inspect(rope.char_to_line(7), content="1") // Character 7 is on line 1

// Get individual lines
inspect(rope.line(0), content="Line 1\n")
inspect(rope.line(1), content="Line 2\n")
inspect(rope.line(2), content="Line 3") // Last line without newline
}

#Rope Operations

test "rope_ops" {
let rope1 = Rope::from_string("Hello")
let rope2 = Rope::from_string(" World")

// Append ropes
let combined = rope1.rope_append(rope2)
inspect(combined.rope_to_string(), content="Hello World")

// Split rope at position
let (left, right) = combined.split_at(5)
inspect(left.rope_to_string(), content="Hello")
inspect(right.rope_to_string(), content=" World")

// Create slices
let slice = combined.slice(0, 5)
inspect(slice.rope_to_string(), content="Hello")
}

#Advanced Usage

#Error Handling

test "error_handling" {
let rope = Rope::from_string("Hello")

// Safe character access
match rope.try_char_at(0) {
Ok(char_code) => inspect(char_code, content="72") // 'H'
Err(err) => abort("Unexpected error")
}

// Out of bounds access returns error
match rope.try_char_at(10) {
Ok(_) => abort("Should have failed")
Err(err) => inspect(err.error_to_string(), content="Character index out of bounds: char index 10, Rope char length 5")
}
}

#Working with Unicode

test "unicode_handling" {
let rope = Rope::from_string("Hello, 世界! 🌍")

// All operations work with logical characters, not UTF-16 code units
inspect(rope.len_chars(), content="12")

// Character-based slicing works correctly with Unicode
let slice = rope.slice(7, 9) // Extract "世界"
inspect(slice.rope_to_string(), content="世界")

// Line operations handle Unicode correctly
let multiline = Rope::from_string("English\n中文\nEmoji🎉")
inspect(multiline.len_lines(), content="3")
inspect(multiline.line(1), content="中文\n")
}

#Performance Considerations

test "performance_tips" {
// Large text handling - rope structure scales well
let large_text = "This is a very long text that would be expensive to manipulate with regular strings...\n".repeat(1000)
let rope = Rope::from_string(large_text)

// Insertions and deletions are O(log N)
let modified = rope.insert(100, "INSERTED TEXT")

// Slicing is also O(log N) and shares data where possible
let slice = rope.slice(0, 1000)

// Multiple operations can be chained efficiently
let result = rope
.insert(50, "FIRST")
.insert(100, "SECOND")
.remove(200, 300)
.slice(0, 500)

inspect(result.len_chars() > 0, content="true")
}

#String Utilities

The library also provides direct string manipulation utilities:

test "string_utils" {
// Character counting (handles surrogate pairs correctly)
inspect(count_chars("Hello 世界"), content="8")

// Line break counting (handles CRLF correctly)
inspect(count_line_breaks("Line1\r\nLine2\nLine3"), content="2")

// Character/UTF-16 index conversion
let text = "Hello"
inspect(char_to_utf16_cu_idx(text, 3), content="3")
inspect(utf16_cu_to_char_idx(text, 3), content="3")

// Character/line index conversion
let lines = "Line1\nLine2\nLine3"
inspect(char_to_line_idx(lines, 6), content="1")
inspect(line_to_char_idx(lines, 1), content="6")
}

#Implementation Details

#UTF-16 Handling

Unlike the original Ropey library which works with UTF-8, this implementation is designed for MoonBit's UTF-16 strings:

  • All character indices refer to logical Unicode characters, not UTF-16 code units
  • Surrogate pairs are handled correctly and counted as single characters
  • Performance is optimized for UTF-16 operations

#Tree Structure

The rope uses a balanced tree structure where:
  • Leaf nodes contain actual text (up to ~1KB each)
  • Internal nodes contain metadata and child references
  • All operations maintain tree balance automatically
  • Text info (character count, line count, etc.) is cached in each node

#Line Break Handling

The library recognizes several line break patterns:
  • \n (LF) - Unix style
  • \r\n (CRLF) - Windows style (counted as single line break)
  • \r (CR) - Classic Mac style

#Memory Efficiency

  • Copy-on-write semantics mean that cloning ropes is O(1)
  • Slicing shares data with the original rope where possible
  • Tree rebalancing happens automatically to maintain performance

#API Reference

#Rope Creation

  • Rope::new() - Create empty rope
  • Rope::from_string(text: String) - Create rope from string

#Information

  • len_chars() - Character count
  • len_utf16_cu() - UTF-16 code unit count
  • len_lines() - Line count
  • rope_is_empty() - Check if empty

#Character Access

  • rope_char_at(index: Int) - Get character code at index
  • try_char_at(index: Int) - Safe character access

#Index Conversion

  • char_to_utf16_cu(char_idx: Int) - Convert character to UTF-16 index
  • utf16_cu_to_char(utf16_idx: Int) - Convert UTF-16 to character index
  • char_to_line(char_idx: Int) - Convert character to line index
  • line_to_char(line_idx: Int) - Convert line to character index

#Text Operations

  • insert(index: Int, text: String) - Insert text at position
  • remove(start: Int, end: Int) - Remove text range
  • slice(start: Int, end: Int) - Create slice
  • rope_append(other: Rope) - Append another rope
  • split_at(index: Int) - Split rope at position
  • line(index: Int) - Get line text

#Conversion

  • rope_to_string() - Convert entire rope to string

This implementation provides a solid foundation for efficient text manipulation in MoonBit applications, especially those dealing with large documents or requiring frequent text modifications.

#
RopeResult

type RopeResult[T] = Result[T, RopeError]

Result type for Rope operations

#
Node

pub enum Node {
Leaf(String)
Internal(NodeChildren)
}

Node in the Rope tree structure Can be either a leaf node (containing text) or internal node (containing children)

#
Node::char_count

fn Node::char_count(self : Node) -> UInt64

Get the number of characters in this node

#
Node::child_count

fn Node::child_count(self : Node) -> Int

Get the number of children (0 for leaf nodes)

#
Node::depth

fn Node::depth(self : Node) -> Int

Get the depth of this node in the tree

#
Node::get_chunk_at_char

fn Node::get_chunk_at_char(self : Node, char_idx : UInt64) -> (String, TextInfo)

Get chunk at character index Returns (text_chunk, accumulated_info_before_chunk)

#
Node::get_chunk_at_line_break

fn Node::get_chunk_at_line_break(self : Node, line_idx : UInt64) -> (String, TextInfo)

Get chunk at line break index

#
Node::get_chunk_at_utf16_cu

fn Node::get_chunk_at_utf16_cu(self : Node, utf16_idx : UInt64) -> (String, TextInfo)

Get chunk at UTF-16 code unit index

#
Node::insert_text

fn Node::insert_text(self : Node, char_idx : UInt64, text : String) -> Node

Insert text at the given character index

#
Node::is_internal

fn Node::is_internal(self : Node) -> Bool

Check if this is an internal node

#
Node::is_leaf

fn Node::is_leaf(self : Node) -> Bool

Check if this is a leaf node

#
Node::line_break_count

fn Node::line_break_count(self : Node) -> UInt64

Get the number of line breaks in this node

#
Node::new

fn Node::new() -> Node

Create a new empty leaf node

#
Node::new_empty

fn Node::new_empty() -> Node

Create a new empty node (for placeholder purposes)

#
Node::new_internal

fn Node::new_internal(children : NodeChildren) -> Node

Create a new internal node with children

#
Node::new_leaf

fn Node::new_leaf(text : String) -> Node

Create a new leaf node with text

#
Node::node_append

fn Node::node_append(self : Node, other : Node) -> Node

Append another node to this one

#
Node::node_to_string

fn Node::node_to_string(self : Node) -> String

Convert node to string representation

#
Node::split

fn Node::split(self : Node, char_idx : UInt64) -> Node

Split this node at the given character index Returns the right part of the split

#
Node::text_info

fn Node::text_info(self : Node) -> TextInfo

Get the text info for this node

#
Node::utf16_cu_count

fn Node::utf16_cu_count(self : Node) -> UInt64

Get the number of UTF-16 code units in this node

#
NodeChildren

pub struct NodeChildren {
children : Array[(TextInfo, Node)]
}

Container for child nodes in internal nodes Each child is stored with its accumulated text information

#
NodeChildren::children_insert

fn NodeChildren::children_insert(self : NodeChildren, index : Int, info : TextInfo, node : Node) -> Unit

Insert child at index

#
NodeChildren::children_is_empty

fn NodeChildren::children_is_empty(self : NodeChildren) -> Bool

Check if container is empty

#
NodeChildren::children_remove

fn NodeChildren::children_remove(self : NodeChildren, index : Int) -> (TextInfo, Node)

Remove and return child at index

#
NodeChildren::find_child_at_char

fn NodeChildren::find_child_at_char(self : NodeChildren, char_idx : UInt64) -> (Int, TextInfo)

Find the child that contains the given character index Returns (child_index, accumulated_info_before_child)

#
NodeChildren::find_child_at_line_break

fn NodeChildren::find_child_at_line_break(self : NodeChildren, line_idx : UInt64) -> (Int, TextInfo)

Find the child that contains the given line break index

#
NodeChildren::find_child_at_utf16_cu

fn NodeChildren::find_child_at_utf16_cu(self : NodeChildren, utf16_idx : UInt64) -> (Int, TextInfo)

Find the child that contains the given UTF-16 code unit index

#
NodeChildren::get

fn NodeChildren::get(self : NodeChildren, index : Int) -> (TextInfo, Node)

Get child at index

#
NodeChildren::get_info

fn NodeChildren::get_info(self : NodeChildren, index : Int) -> TextInfo

Get text info at index

#
NodeChildren::get_node

fn NodeChildren::get_node(self : NodeChildren, index : Int) -> Node

Get node at index

#
NodeChildren::length

fn NodeChildren::length(self : NodeChildren) -> Int

Get the number of children

#
NodeChildren::new

Create a new empty NodeChildren container

#
NodeChildren::push

fn NodeChildren::push(self : NodeChildren, info : TextInfo, node : Node) -> Unit

Add a child node with its text info

#
NodeChildren::set

fn NodeChildren::set(self : NodeChildren, index : Int, info : TextInfo, node : Node) -> Unit

Set child at index

#
NodeChildren::split_off

fn NodeChildren::split_off(self : NodeChildren, at : Int) -> NodeChildren

Split children at index, returning the right part

#
NodeChildren::total_info

fn NodeChildren::total_info(self : NodeChildren) -> TextInfo

Combine all text info from children

#
NodeChildren::with_capacity

fn NodeChildren::with_capacity(capacity : Int) -> NodeChildren

Create NodeChildren with initial capacity

#
Rope

pub struct Rope {
root : Node
}

A UTF-16 text rope for efficient string operations

Rope is a tree-based data structure optimized for efficient insertion, deletion, and concatenation of large strings. All operations work with Unicode character indices rather than UTF-16 code unit indices.

#
Rope::char_to_line

fn Rope::char_to_line(self : Rope, char_idx : Int) -> Int

Convert character index to line index

#
Rope::char_to_utf16_cu

fn Rope::char_to_utf16_cu(self : Rope, char_idx : Int) -> Int

Convert character index to UTF-16 code unit index

#
Rope::from_string

fn Rope::from_string(text : String) -> Rope

Create a Rope from a string

#
Rope::insert

fn Rope::insert(self : Rope, char_idx : Int, text : String) -> Rope

Insert text at the given character index

#
Rope::len_chars

fn Rope::len_chars(self : Rope) -> Int

Get the total number of Unicode characters in the rope

#
Rope::len_lines

fn Rope::len_lines(self : Rope) -> Int

Get the total number of lines in the rope

#
Rope::len_utf16_cu

fn Rope::len_utf16_cu(self : Rope) -> Int

Get the total number of UTF-16 code units in the rope

#
Rope::line

fn Rope::line(self : Rope, line_idx : Int) -> String

Get a line by its index

#
Rope::line_to_char

fn Rope::line_to_char(self : Rope, line_idx : Int) -> Int

Convert line index to character index

#
Rope::new

fn Rope::new() -> Rope

Create a new empty Rope

#
Rope::remove

fn Rope::remove(self : Rope, start_char : Int, end_char : Int) -> Rope

Remove text in the given character range

#
Rope::rope_append

fn Rope::rope_append(self : Rope, other : Rope) -> Rope

Append another rope to this one

#
Rope::rope_char_at

fn Rope::rope_char_at(self : Rope, char_idx : Int) -> Int

Get the character at the given character index (returns character code)

#
Rope::rope_is_empty

fn Rope::rope_is_empty(self : Rope) -> Bool

Check if the rope is empty

#
Rope::rope_to_string

fn Rope::rope_to_string(self : Rope) -> String

Convert the entire rope to a string

#
Rope::slice

fn Rope::slice(self : Rope, start_char : Int, end_char : Int) -> Rope

Get a slice of the rope from start_char to end_char (exclusive)

#
Rope::split_at

fn Rope::split_at(self : Rope, char_idx : Int) -> (Rope, Rope)

Split the rope at the given character index Returns a tuple of (left_part, right_part)

#
Rope::try_char_at

fn Rope::try_char_at(self : Rope, char_idx : Int) -> Result[Int, RopeError]

Safe version of char_at that returns a Result

#
Rope::utf16_cu_to_char

fn Rope::utf16_cu_to_char(self : Rope, utf16_cu_idx : Int) -> Int

Convert UTF-16 code unit index to character index

#
RopeError

pub enum RopeError {
CharIndexOutOfBounds(Int, Int)
Utf16IndexOutOfBounds(Int, Int)
LineIndexOutOfBounds(Int, Int)
CharRangeInvalid(Int, Int)
CharRangeOutOfBounds(Int, Int, Int)
InvalidOperation(String)
}

Error types that can occur during Rope operations

#
RopeError::error_to_string

fn RopeError::error_to_string(self : RopeError) -> String

Convert RopeError to String for display

#
TextInfo

pub struct TextInfo {
utf16_cu : UInt64
chars : UInt64
line_breaks : UInt64
}

Text metadata information, similar to Ropey's TextInfo Stores cumulative counts for efficient tree operations
impl Eq for TextInfo

#
TextInfo::char_count_int

fn TextInfo::char_count_int(self : TextInfo) -> Int

Convert character count to Int

#
TextInfo::from_string

fn TextInfo::from_string(text : String) -> TextInfo

Create TextInfo from a string

#
TextInfo::is_empty

fn TextInfo::is_empty(self : TextInfo) -> Bool

Check if this TextInfo represents empty text

#
TextInfo::line_break_count_int

fn TextInfo::line_break_count_int(self : TextInfo) -> Int

Convert line break count to Int

#
TextInfo::new

fn TextInfo::new() -> TextInfo

Create a new empty TextInfo

#
TextInfo::op_add

fn TextInfo::op_add(self : TextInfo, other : TextInfo) -> TextInfo

Add two TextInfo instances

#
TextInfo::op_sub

fn TextInfo::op_sub(self : TextInfo, other : TextInfo) -> TextInfo

Subtract two TextInfo instances

#
TextInfo::utf16_cu_count_int

fn TextInfo::utf16_cu_count_int(self : TextInfo) -> Int

Convert UTF-16 code unit count to Int

#
char_at

fn char_at(text : String, char_idx : Int) -> Int

Get character at character index (not UTF-16 index)

#
char_to_line_idx

fn char_to_line_idx(text : String, char_idx : Int) -> Int

Convert character index to line index

#
char_to_utf16_cu_idx

fn char_to_utf16_cu_idx(text : String, char_idx : Int) -> Int

Convert character index to UTF-16 code unit index

#
check_char_index

fn check_char_index(char_idx : Int, rope_len : Int) -> Result[Unit, RopeError]

Check if a character index is valid

#
check_char_index_for_insert

fn check_char_index_for_insert(char_idx : Int, rope_len : Int) -> Result[Unit, RopeError]

Check if a character index is valid for insertion (can be equal to length)

#
check_char_range

fn check_char_range(start : Int, end : Int, rope_len : Int) -> Result[Unit, RopeError]

Check if a character range is valid

#
check_line_index

fn check_line_index(line_idx : Int, rope_line_count : Int) -> Result[Unit, RopeError]

Check if a line index is valid

#
check_utf16_index

fn check_utf16_index(utf16_idx : Int, rope_utf16_len : Int) -> Result[Unit, RopeError]

Check if a UTF-16 index is valid

#
count_chars

fn count_chars(text : String) -> Int

Count the number of Unicode characters in a string In MoonBit, this is simpler since String already stores UTF-16

#
count_line_breaks

fn count_line_breaks(text : String) -> Int

Count the number of line breaks in a string

#
ends_with_line_break

fn ends_with_line_break(text : String) -> Bool

Check if string ends with a line break

#
is_line_break

fn is_line_break(ch_code : Int) -> Bool

Check if a character is a line break

#
last_line_start_char_idx

fn last_line_start_char_idx(text : String) -> Int

Find the start of the last line in the text

#
line_length_from_char

fn line_length_from_char(text : String, start_char_idx : Int) -> Int

Get the length of a line starting at the given character index

#
line_to_char_idx

fn line_to_char_idx(text : String, line_idx : Int) -> Int

Convert line index to character index

#
utf16_cu_to_char_idx

fn utf16_cu_to_char_idx(text : String, utf16_idx : Int) -> Int

Convert UTF-16 code unit index to character index