saorsa_core/
text_buffer.rs1use ropey::Rope;
7use std::fmt;
8
9#[derive(Clone, Debug)]
15pub struct TextBuffer {
16 rope: Rope,
17}
18
19impl TextBuffer {
20 pub fn new() -> Self {
22 Self { rope: Rope::new() }
23 }
24
25 pub fn from_text(text: &str) -> Self {
27 Self {
28 rope: Rope::from_str(text),
29 }
30 }
31
32 pub fn line_count(&self) -> usize {
37 self.rope.len_lines()
38 }
39
40 pub fn line(&self, idx: usize) -> Option<String> {
44 if idx >= self.rope.len_lines() {
45 return None;
46 }
47 let line = self.rope.line(idx);
48 let text = line.to_string();
49 let trimmed = text.trim_end_matches('\n').trim_end_matches('\r');
51 Some(trimmed.to_string())
52 }
53
54 pub fn line_len(&self, idx: usize) -> Option<usize> {
58 self.line(idx).map(|l| l.chars().count())
59 }
60
61 pub fn total_chars(&self) -> usize {
63 self.rope.len_chars()
64 }
65
66 pub fn insert_char(&mut self, line: usize, col: usize, ch: char) {
71 if let Some(char_idx) = self.line_col_to_char(line, col) {
72 self.rope.insert_char(char_idx, ch);
73 }
74 }
75
76 pub fn insert_str(&mut self, line: usize, col: usize, text: &str) {
80 if let Some(char_idx) = self.line_col_to_char(line, col) {
81 self.rope.insert(char_idx, text);
82 }
83 }
84
85 pub fn delete_char(&mut self, line: usize, col: usize) {
90 if let Some(char_idx) = self.line_col_to_char(line, col)
91 && char_idx < self.rope.len_chars()
92 {
93 self.rope.remove(char_idx..char_idx + 1);
94 }
95 }
96
97 pub fn delete_range(
103 &mut self,
104 start_line: usize,
105 start_col: usize,
106 end_line: usize,
107 end_col: usize,
108 ) {
109 let start = self.line_col_to_char(start_line, start_col);
110 let end = self.line_col_to_char(end_line, end_col);
111 if let (Some(s), Some(e)) = (start, end)
112 && s < e
113 && e <= self.rope.len_chars()
114 {
115 self.rope.remove(s..e);
116 }
117 }
118
119 pub fn lines_range(&self, start: usize, end: usize) -> Vec<String> {
124 let total = self.rope.len_lines();
125 let start = start.min(total);
126 let end = end.min(total);
127 (start..end).filter_map(|i| self.line(i)).collect()
128 }
129
130 fn line_col_to_char(&self, line: usize, col: usize) -> Option<usize> {
135 if line >= self.rope.len_lines() {
136 return None;
137 }
138 let line_start = self.rope.line_to_char(line);
139 let line_rope = self.rope.line(line);
140 let line_char_len = line_rope.len_chars();
141 let clamped_col = col.min(line_char_len);
146 Some(line_start + clamped_col)
147 }
148}
149
150impl Default for TextBuffer {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156impl fmt::Display for TextBuffer {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 for chunk in self.rope.chunks() {
159 f.write_str(chunk)?;
160 }
161 Ok(())
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
172 fn empty_buffer() {
173 let buf = TextBuffer::new();
174 assert!(buf.line_count() == 1);
175 assert!(buf.total_chars() == 0);
176 assert!(buf.to_string().is_empty());
177 }
178
179 #[test]
180 fn from_str_single_line() {
181 let buf = TextBuffer::from_text("hello");
182 assert!(buf.line_count() == 1);
183 assert!(buf.total_chars() == 5);
184 match buf.line(0) {
185 Some(ref s) if s == "hello" => {}
186 _ => unreachable!("expected 'hello'"),
187 }
188 }
189
190 #[test]
191 fn from_str_multi_line() {
192 let buf = TextBuffer::from_text("one\ntwo\nthree");
193 assert!(buf.line_count() == 3);
194 match buf.line(0) {
195 Some(ref s) if s == "one" => {}
196 _ => unreachable!("expected 'one'"),
197 }
198 match buf.line(1) {
199 Some(ref s) if s == "two" => {}
200 _ => unreachable!("expected 'two'"),
201 }
202 match buf.line(2) {
203 Some(ref s) if s == "three" => {}
204 _ => unreachable!("expected 'three'"),
205 }
206 }
207
208 #[test]
211 fn line_out_of_bounds() {
212 let buf = TextBuffer::from_text("abc");
213 assert!(buf.line(1).is_none());
214 assert!(buf.line(100).is_none());
215 }
216
217 #[test]
218 fn line_len_returns_char_count() {
219 let buf = TextBuffer::from_text("hello\nhi");
220 match buf.line_len(0) {
221 Some(5) => {}
222 other => unreachable!("expected Some(5), got {other:?}"),
223 }
224 match buf.line_len(1) {
225 Some(2) => {}
226 other => unreachable!("expected Some(2), got {other:?}"),
227 }
228 assert!(buf.line_len(2).is_none());
229 }
230
231 #[test]
232 fn lines_range_subset() {
233 let buf = TextBuffer::from_text("a\nb\nc\nd");
234 let range = buf.lines_range(1, 3);
235 assert!(range.len() == 2);
236 assert!(range[0] == "b");
237 assert!(range[1] == "c");
238 }
239
240 #[test]
241 fn lines_range_out_of_bounds_clamped() {
242 let buf = TextBuffer::from_text("x\ny");
243 let range = buf.lines_range(0, 100);
244 assert!(range.len() == 2);
245 }
246
247 #[test]
250 fn insert_char_middle() {
251 let mut buf = TextBuffer::from_text("ac");
252 buf.insert_char(0, 1, 'b');
253 assert!(buf.to_string() == "abc");
254 }
255
256 #[test]
257 fn insert_newline_splits_line() {
258 let mut buf = TextBuffer::from_text("hello world");
259 buf.insert_char(0, 5, '\n');
260 assert!(buf.line_count() == 2);
261 match buf.line(0) {
262 Some(ref s) if s == "hello" => {}
263 other => unreachable!("expected 'hello', got {other:?}"),
264 }
265 match buf.line(1) {
266 Some(ref s) if s == " world" => {}
267 other => unreachable!("expected ' world', got {other:?}"),
268 }
269 }
270
271 #[test]
272 fn insert_str_with_newlines() {
273 let mut buf = TextBuffer::from_text("ac");
274 buf.insert_str(0, 1, "b\nd\ne");
275 assert!(buf.line_count() == 3);
277 match buf.line(0) {
278 Some(ref s) if s == "ab" => {}
279 other => unreachable!("expected 'ab', got {other:?}"),
280 }
281 }
282
283 #[test]
286 fn delete_char_middle() {
287 let mut buf = TextBuffer::from_text("abc");
288 buf.delete_char(0, 1);
289 assert!(buf.to_string() == "ac");
290 }
291
292 #[test]
293 fn delete_char_joins_lines() {
294 let mut buf = TextBuffer::from_text("ab\ncd");
295 buf.delete_char(0, 2);
297 assert!(buf.line_count() == 1);
298 assert!(buf.to_string() == "abcd");
299 }
300
301 #[test]
302 fn delete_range_within_line() {
303 let mut buf = TextBuffer::from_text("abcdef");
304 buf.delete_range(0, 1, 0, 4);
305 assert!(buf.to_string() == "aef");
306 }
307
308 #[test]
309 fn delete_range_across_lines() {
310 let mut buf = TextBuffer::from_text("hello\nworld\nfoo");
311 buf.delete_range(0, 3, 1, 3);
313 assert!(buf.to_string() == "helld\nfoo");
314 }
315
316 #[test]
319 fn empty_lines() {
320 let buf = TextBuffer::from_text("\n\n\n");
321 assert!(buf.line_count() == 4);
322 match buf.line(0) {
323 Some(ref s) if s.is_empty() => {}
324 other => unreachable!("expected empty string, got {other:?}"),
325 }
326 }
327
328 #[test]
329 fn unicode_content() {
330 let buf = TextBuffer::from_text("日本語\némoji 🎉");
331 assert!(buf.line_count() == 2);
332 match buf.line(0) {
333 Some(ref s) if s == "日本語" => {}
334 other => unreachable!("expected '日本語', got {other:?}"),
335 }
336 match buf.line_len(1) {
337 Some(7) => {}
338 other => unreachable!("expected Some(7), got {other:?}"),
339 }
340 }
341
342 #[test]
343 fn display_trait() {
344 let buf = TextBuffer::from_text("hello\nworld");
345 assert!(buf.to_string() == "hello\nworld");
346 }
347
348 #[test]
349 fn default_is_empty() {
350 let buf = TextBuffer::default();
351 assert!(buf.line_count() == 1);
352 assert!(buf.total_chars() == 0);
353 }
354}