//|
///|
/// 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.
pub struct Rope {
root : Node
}
///|
/// Create a new empty Rope
pub fn Rope::new() -> Rope {
{ root: Node::new() }
}
///|
/// Create a Rope from a string
pub fn Rope::from_string(text : String) -> Rope {
if text.length() <= MAX_BYTES {
{ root: Node::new_leaf(text) }
} else {
// For large strings, split them across multiple nodes
let mid = text.length() / 2
let left = text.substring(start=0, end=mid)
let right = text.substring(start=mid, end=text.length())
let children = NodeChildren::new()
children.push(TextInfo::from_string(left), Node::new_leaf(left))
children.push(TextInfo::from_string(right), Node::new_leaf(right))
{ root: Node::new_internal(children) }
}
}
///|
/// Get the total number of UTF-16 code units in the rope
pub fn Rope::len_utf16_cu(self : Rope) -> Int {
self.root.utf16_cu_count().to_int()
}
///|
/// Get the total number of Unicode characters in the rope
pub fn Rope::len_chars(self : Rope) -> Int {
self.root.char_count().to_int()
}
///|
/// Get the total number of lines in the rope
pub fn Rope::len_lines(self : Rope) -> Int {
(self.root.line_break_count() + 1UL).to_int()
}
///|
/// Check if the rope is empty
pub fn Rope::rope_is_empty(self : Rope) -> Bool {
self.len_chars() == 0
}
///|
/// Get the character at the given character index (returns character code)
pub fn Rope::rope_char_at(self : Rope, char_idx : Int) -> Int {
match self.try_char_at(char_idx) {
Ok(c) => c
Err(err) => abort(err.error_to_string())
}
}
///|
/// Safe version of char_at that returns a Result
pub fn Rope::try_char_at(self : Rope, char_idx : Int) -> RopeResult[Int] {
match check_char_index(char_idx, self.len_chars()) {
Ok(_) => ()
Err(e) => return Err(e)
}
let (chunk_text, acc_info) = self.root.get_chunk_at_char(char_idx.to_uint64())
let chunk_char_idx = char_idx - acc_info.char_count_int()
Ok(char_at(chunk_text, chunk_char_idx))
}
///|
/// Convert character index to UTF-16 code unit index
pub fn Rope::char_to_utf16_cu(self : Rope, char_idx : Int) -> Int {
if char_idx > self.len_chars() {
abort("Character index out of bounds")
}
if char_idx == self.len_chars() {
return self.len_utf16_cu()
}
let (chunk_text, acc_info) = self.root.get_chunk_at_char(char_idx.to_uint64())
let chunk_char_idx = char_idx - acc_info.char_count_int()
let chunk_utf16_idx = char_to_utf16_cu_idx(chunk_text, chunk_char_idx)
acc_info.utf16_cu_count_int() + chunk_utf16_idx
}
///|
/// Convert UTF-16 code unit index to character index
pub fn Rope::utf16_cu_to_char(self : Rope, utf16_cu_idx : Int) -> Int {
if utf16_cu_idx > self.len_utf16_cu() {
abort("UTF-16 code unit index out of bounds")
}
if utf16_cu_idx == self.len_utf16_cu() {
return self.len_chars()
}
let (chunk_text, acc_info) = self.root.get_chunk_at_utf16_cu(
utf16_cu_idx.to_uint64(),
)
let chunk_utf16_idx = utf16_cu_idx - acc_info.utf16_cu_count_int()
let chunk_char_idx = utf16_cu_to_char_idx(chunk_text, chunk_utf16_idx)
acc_info.char_count_int() + chunk_char_idx
}
///|
/// Convert character index to line index
pub fn Rope::char_to_line(self : Rope, char_idx : Int) -> Int {
if char_idx > self.len_chars() {
abort("Character index out of bounds")
}
let (chunk_text, acc_info) = self.root.get_chunk_at_char(char_idx.to_uint64())
let chunk_char_idx = char_idx - acc_info.char_count_int()
let chunk_line_idx = char_to_line_idx(chunk_text, chunk_char_idx)
acc_info.line_break_count_int() + chunk_line_idx
}
///|
/// Convert line index to character index
pub fn Rope::line_to_char(self : Rope, line_idx : Int) -> Int {
if line_idx > self.len_lines() {
abort("Line index out of bounds")
}
if line_idx == self.len_lines() {
return self.len_chars()
}
let (chunk_text, acc_info) = self.root.get_chunk_at_line_break(
line_idx.to_uint64(),
)
let chunk_line_idx = line_idx - acc_info.line_break_count_int()
let chunk_char_idx = line_to_char_idx(chunk_text, chunk_line_idx)
acc_info.char_count_int() + chunk_char_idx
}
///|
/// Insert text at the given character index
pub fn Rope::insert(self : Rope, char_idx : Int, text : String) -> Rope {
if char_idx > self.len_chars() {
abort("Character index out of bounds")
}
{ root: self.root.insert_text(char_idx.to_uint64(), text) }
}
///|
/// Get a slice of the rope from start_char to end_char (exclusive)
pub fn Rope::slice(self : Rope, start_char : Int, end_char : Int) -> Rope {
if start_char > end_char {
abort("Invalid range: start > end")
}
if end_char > self.len_chars() {
abort("End index out of bounds")
}
// This is a simplified implementation
// In practice, this would return a RopeSlice that shares data with the original
let text = self.rope_to_string()
let start_utf16 = char_to_utf16_cu_idx(text, start_char)
let end_utf16 = char_to_utf16_cu_idx(text, end_char)
let slice_text = text.substring(start=start_utf16, end=end_utf16)
Rope::from_string(slice_text)
}
///|
/// Convert the entire rope to a string
pub fn Rope::rope_to_string(self : Rope) -> String {
self.root.node_to_string()
}
///|
/// Get a line by its index
pub fn Rope::line(self : Rope, line_idx : Int) -> String {
if line_idx >= self.len_lines() {
abort("Line index out of bounds")
}
let start_char = self.line_to_char(line_idx)
let end_char = if line_idx == self.len_lines() - 1 {
self.len_chars()
} else {
self.line_to_char(line_idx + 1)
}
self.slice(start_char, end_char).rope_to_string()
}
///|
/// Append another rope to this one
pub fn Rope::rope_append(self : Rope, other : Rope) -> Rope {
if self.rope_is_empty() {
other
} else if other.rope_is_empty() {
self
} else {
{ root: self.root.node_append(other.root) }
}
}
///|
/// Split the rope at the given character index
/// Returns a tuple of (left_part, right_part)
pub fn Rope::split_at(self : Rope, char_idx : Int) -> (Rope, Rope) {
if char_idx > self.len_chars() {
abort("Character index out of bounds")
}
if char_idx == 0 {
(Rope::new(), self)
} else if char_idx == self.len_chars() {
(self, Rope::new())
} else {
let left = self.slice(0, char_idx)
let right = self.slice(char_idx, self.len_chars())
(left, right)
}
}
///|
/// Remove text in the given character range
pub fn Rope::remove(self : Rope, start_char : Int, end_char : Int) -> Rope {
if start_char > end_char {
abort("Invalid range: start > end")
}
if end_char > self.len_chars() {
abort("End index out of bounds")
}
if start_char == 0 && end_char == self.len_chars() {
return Rope::new()
}
let left = if start_char > 0 {
self.slice(0, start_char)
} else {
Rope::new()
}
let right = if end_char < self.len_chars() {
self.slice(end_char, self.len_chars())
} else {
Rope::new()
}
left.rope_append(right)
}