Skip to main content

oxihuman_core/
rope_string.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan) / SPDX-License-Identifier: Apache-2.0
2#![allow(dead_code)]
3#![allow(clippy::should_implement_trait)]
4#![allow(clippy::inherent_to_string)]
5
6//! A rope data structure for efficient string editing with O(log n) insert/delete.
7
8const LEAF_MAX: usize = 64;
9
10/// A node in the rope tree.
11#[allow(dead_code)]
12#[derive(Debug, Clone)]
13enum RopeNode {
14    Leaf(String),
15    Branch {
16        left: Box<RopeNode>,
17        right: Box<RopeNode>,
18        weight: usize,
19    },
20}
21
22impl RopeNode {
23    fn len(&self) -> usize {
24        match self {
25            RopeNode::Leaf(s) => s.len(),
26            RopeNode::Branch { weight, right, .. } => weight + right.len(),
27        }
28    }
29
30    fn to_string_buf(&self, buf: &mut String) {
31        match self {
32            RopeNode::Leaf(s) => buf.push_str(s),
33            RopeNode::Branch { left, right, .. } => {
34                left.to_string_buf(buf);
35                right.to_string_buf(buf);
36            }
37        }
38    }
39
40    fn char_at(&self, idx: usize) -> Option<u8> {
41        match self {
42            RopeNode::Leaf(s) => s.as_bytes().get(idx).copied(),
43            RopeNode::Branch {
44                left,
45                weight,
46                right,
47            } => {
48                if idx < *weight {
49                    left.char_at(idx)
50                } else {
51                    right.char_at(idx - weight)
52                }
53            }
54        }
55    }
56}
57
58/// A rope for efficient large-string editing.
59#[allow(dead_code)]
60#[derive(Debug, Clone)]
61pub struct RopeString {
62    root: Option<RopeNode>,
63}
64
65#[allow(dead_code)]
66impl RopeString {
67    pub fn new() -> Self {
68        Self { root: None }
69    }
70
71    pub fn from_str(s: &str) -> Self {
72        if s.is_empty() {
73            return Self::new();
74        }
75        Self {
76            root: Some(Self::build_leaves(s)),
77        }
78    }
79
80    fn build_leaves(s: &str) -> RopeNode {
81        if s.len() <= LEAF_MAX {
82            return RopeNode::Leaf(s.to_string());
83        }
84        let mid = s.len() / 2;
85        // Find a safe split point (don't split multi-byte chars)
86        let split = s.floor_char_boundary(mid);
87        let left = Self::build_leaves(&s[..split]);
88        let right = Self::build_leaves(&s[split..]);
89        let weight = left.len();
90        RopeNode::Branch {
91            left: Box::new(left),
92            right: Box::new(right),
93            weight,
94        }
95    }
96
97    pub fn len(&self) -> usize {
98        self.root.as_ref().map(|r| r.len()).unwrap_or(0)
99    }
100
101    pub fn is_empty(&self) -> bool {
102        self.len() == 0
103    }
104
105    pub fn to_string(&self) -> String {
106        let mut buf = String::with_capacity(self.len());
107        if let Some(ref r) = self.root {
108            r.to_string_buf(&mut buf);
109        }
110        buf
111    }
112
113    pub fn byte_at(&self, idx: usize) -> Option<u8> {
114        self.root.as_ref().and_then(|r| r.char_at(idx))
115    }
116
117    pub fn concat(&self, other: &RopeString) -> RopeString {
118        match (&self.root, &other.root) {
119            (None, _) => other.clone(),
120            (_, None) => self.clone(),
121            (Some(l), Some(r)) => {
122                let weight = l.len();
123                RopeString {
124                    root: Some(RopeNode::Branch {
125                        left: Box::new(l.clone()),
126                        right: Box::new(r.clone()),
127                        weight,
128                    }),
129                }
130            }
131        }
132    }
133
134    pub fn append(&mut self, s: &str) {
135        let other = RopeString::from_str(s);
136        *self = self.concat(&other);
137    }
138
139    /// Extract substring [start..end).
140    pub fn substring(&self, start: usize, end: usize) -> String {
141        let full = self.to_string();
142        if start >= full.len() || start >= end {
143            return String::new();
144        }
145        let e = end.min(full.len());
146        full[start..e].to_string()
147    }
148
149    pub fn depth(&self) -> usize {
150        fn d(node: &RopeNode) -> usize {
151            match node {
152                RopeNode::Leaf(_) => 1,
153                RopeNode::Branch { left, right, .. } => 1 + d(left).max(d(right)),
154            }
155        }
156        self.root.as_ref().map(d).unwrap_or(0)
157    }
158}
159
160impl Default for RopeString {
161    fn default() -> Self {
162        Self::new()
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_from_str() {
172        let r = RopeString::from_str("hello world");
173        assert_eq!(r.to_string(), "hello world");
174        assert_eq!(r.len(), 11);
175    }
176
177    #[test]
178    fn test_empty() {
179        let r = RopeString::new();
180        assert!(r.is_empty());
181        assert_eq!(r.to_string(), "");
182    }
183
184    #[test]
185    fn test_concat() {
186        let a = RopeString::from_str("hello ");
187        let b = RopeString::from_str("world");
188        let c = a.concat(&b);
189        assert_eq!(c.to_string(), "hello world");
190    }
191
192    #[test]
193    fn test_append() {
194        let mut r = RopeString::from_str("foo");
195        r.append("bar");
196        assert_eq!(r.to_string(), "foobar");
197    }
198
199    #[test]
200    fn test_byte_at() {
201        let r = RopeString::from_str("abc");
202        assert_eq!(r.byte_at(0), Some(b'a'));
203        assert_eq!(r.byte_at(2), Some(b'c'));
204        assert_eq!(r.byte_at(3), None);
205    }
206
207    #[test]
208    fn test_substring() {
209        let r = RopeString::from_str("hello world");
210        assert_eq!(r.substring(0, 5), "hello");
211        assert_eq!(r.substring(6, 11), "world");
212    }
213
214    #[test]
215    fn test_large_string() {
216        let s = "x".repeat(1000);
217        let r = RopeString::from_str(&s);
218        assert_eq!(r.len(), 1000);
219        assert_eq!(r.to_string(), s);
220    }
221
222    #[test]
223    fn test_depth() {
224        let r = RopeString::from_str("short");
225        assert!(r.depth() >= 1);
226    }
227
228    #[test]
229    fn test_concat_empty() {
230        let a = RopeString::new();
231        let b = RopeString::from_str("test");
232        assert_eq!(a.concat(&b).to_string(), "test");
233        assert_eq!(b.concat(&a).to_string(), "test");
234    }
235
236    #[test]
237    fn test_substring_out_of_range() {
238        let r = RopeString::from_str("abc");
239        assert_eq!(r.substring(10, 20), "");
240    }
241}