gpui_component/input/
rope_ext.rs

1use std::ops::Range;
2
3use ropey::{LineType, Rope, RopeSlice};
4use sum_tree::Bias;
5use tree_sitter::Point;
6
7use crate::input::Position;
8
9/// An iterator over the lines of a `Rope`.
10pub struct RopeLines<'a> {
11    rope: &'a Rope,
12    row: usize,
13    end_row: usize,
14}
15
16impl<'a> RopeLines<'a> {
17    /// Create a new `RopeLines` iterator.
18    pub fn new(rope: &'a Rope) -> Self {
19        let end_row = rope.lines_len();
20        Self {
21            row: 0,
22            end_row,
23            rope,
24        }
25    }
26}
27impl<'a> Iterator for RopeLines<'a> {
28    type Item = RopeSlice<'a>;
29
30    #[inline]
31    fn next(&mut self) -> Option<Self::Item> {
32        if self.row >= self.end_row {
33            return None;
34        }
35
36        let line = self.rope.slice_line(self.row);
37        self.row += 1;
38        Some(line)
39    }
40
41    #[inline]
42    fn nth(&mut self, n: usize) -> Option<Self::Item> {
43        self.row = self.row.saturating_add(n);
44        self.next()
45    }
46
47    #[inline]
48    fn size_hint(&self) -> (usize, Option<usize>) {
49        let len = self.end_row - self.row;
50        (len, Some(len))
51    }
52}
53
54impl std::iter::ExactSizeIterator for RopeLines<'_> {}
55impl std::iter::FusedIterator for RopeLines<'_> {}
56
57/// An extension trait for [`Rope`] to provide additional utility methods.
58pub trait RopeExt {
59    /// Start offset of the line at the given row (0-based) index.
60    ///
61    /// # Example
62    ///
63    /// ```
64    /// use gpui_component::{Rope, RopeExt};
65    ///
66    /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
67    /// assert_eq!(rope.line_start_offset(0), 0);
68    /// assert_eq!(rope.line_start_offset(1), 6);
69    /// ```
70    fn line_start_offset(&self, row: usize) -> usize;
71
72    /// Line the end offset (including `\n`) of the line at the given row (0-based) index.
73    ///
74    /// Return the end of the rope if the row is out of bounds.
75    ///
76    /// ```
77    /// use gpui_component::{Rope, RopeExt};
78    /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
79    /// assert_eq!(rope.line_end_offset(0), 5); // "Hello\n"
80    /// assert_eq!(rope.line_end_offset(1), 12); // "World\r\n"
81    /// ```
82    fn line_end_offset(&self, row: usize) -> usize;
83
84    /// Return a line slice at the given row (0-based) index. including `\r` if present, but not `\n`.
85    ///
86    /// ```
87    /// use gpui_component::{Rope, RopeExt};
88    /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
89    /// assert_eq!(rope.slice_line(0).to_string(), "Hello");
90    /// assert_eq!(rope.slice_line(1).to_string(), "World\r");
91    /// assert_eq!(rope.slice_line(2).to_string(), "This is a test 中文");
92    /// assert_eq!(rope.slice_line(6).to_string(), ""); // out of bounds
93    /// ```
94    fn slice_line(&self, row: usize) -> RopeSlice<'_>;
95
96    /// Return a slice of rows in the given range (0-based, end exclusive).
97    ///
98    /// If the range is out of bounds, it will be clamped to the valid range.
99    ///
100    /// ```
101    /// use gpui_component::{Rope, RopeExt};
102    /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
103    /// assert_eq!(rope.slice_lines(0..2).to_string(), "Hello\nWorld\r");
104    /// assert_eq!(rope.slice_lines(1..3).to_string(), "World\r\nThis is a test 中文");
105    /// assert_eq!(rope.slice_lines(2..5).to_string(), "This is a test 中文\nRope");
106    /// assert_eq!(rope.slice_lines(3..10).to_string(), "Rope");
107    /// assert_eq!(rope.slice_lines(5..10).to_string(), ""); // out of bounds
108    /// ```
109    fn slice_lines(&self, rows_range: Range<usize>) -> RopeSlice<'_>;
110
111    /// Return an iterator over all lines in the rope.
112    ///
113    /// Each line slice includes `\r` if present, but not `\n`.
114    ///
115    /// ```
116    /// use gpui_component::{Rope, RopeExt};
117    /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
118    /// let lines: Vec<_> = rope.iter_lines().map(|r| r.to_string()).collect();
119    /// assert_eq!(lines, vec!["Hello", "World\r", "This is a test 中文", "Rope"]);
120    /// ```
121    fn iter_lines(&self) -> RopeLines<'_>;
122
123    /// Return the number of lines in the rope.
124    ///
125    /// ```
126    /// use gpui_component::{Rope, RopeExt};
127    /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
128    /// assert_eq!(rope.lines_len(), 4);
129    /// ```
130    fn lines_len(&self) -> usize;
131
132    /// Return the length of the row (0-based) in characters, including `\r` if present, but not `\n`.
133    ///
134    /// If the row is out of bounds, return 0.
135    ///
136    /// ```
137    /// use gpui_component::{Rope, RopeExt};
138    /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
139    /// assert_eq!(rope.line_len(0), 5); // "Hello"
140    /// assert_eq!(rope.line_len(1), 6); // "World\r"
141    /// assert_eq!(rope.line_len(2), 21); // "This is a test 中文"
142    /// assert_eq!(rope.line_len(4), 0); // out of bounds
143    /// ```
144    fn line_len(&self, row: usize) -> usize;
145
146    /// Replace the text in the given byte range with new text.
147    ///
148    /// # Panics
149    ///
150    /// - If the range is not on char boundary.
151    /// - If the range is out of bounds.
152    ///
153    /// ```
154    /// use gpui_component::{Rope, RopeExt};
155    /// let mut rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
156    /// rope.replace(6..11, "Universe");
157    /// assert_eq!(rope.to_string(), "Hello\nUniverse\r\nThis is a test 中文\nRope");
158    /// ```
159    fn replace(&mut self, range: Range<usize>, new_text: &str);
160
161    /// Get char at the given offset (byte).
162    ///
163    /// - If the offset is in the middle of a multi-byte character will panic.
164    /// - If the offset is out of bounds, return None.
165    fn char_at(&self, offset: usize) -> Option<char>;
166
167    /// Get the byte offset from the given line, column [`Position`] (0-based).
168    ///
169    /// The column is in characters.
170    fn position_to_offset(&self, line_col: &Position) -> usize;
171
172    /// Get the line, column [`Position`] (0-based) from the given byte offset.
173    ///
174    /// The column is in characters.
175    fn offset_to_position(&self, offset: usize) -> Position;
176
177    /// Get point (row, column) from the given byte offset.
178    ///
179    /// The column is in bytes.
180    fn offset_to_point(&self, offset: usize) -> Point;
181
182    /// Get byte offset from the given point (row, column).
183    ///
184    /// The column is 0-based in bytes.
185    fn point_to_offset(&self, point: Point) -> usize;
186
187    /// Get the word byte range at the given byte offset (0-based).
188    fn word_range(&self, offset: usize) -> Option<Range<usize>>;
189
190    /// Get word at the given byte offset (0-based).
191    fn word_at(&self, offset: usize) -> String;
192
193    /// Convert offset in UTF-16 to byte offset (0-based).
194    ///
195    /// Runs in O(log N) time.
196    fn offset_utf16_to_offset(&self, offset_utf16: usize) -> usize;
197
198    /// Convert byte offset (0-based) to offset in UTF-16.
199    ///
200    /// Runs in O(log N) time.
201    fn offset_to_offset_utf16(&self, offset: usize) -> usize;
202
203    /// Get a clipped offset (avoid in a char boundary).
204    ///
205    /// - If Bias::Left and inside the char boundary, return the ix - 1;
206    /// - If Bias::Right and in inside char boundary, return the ix + 1;
207    /// - Otherwise return the ix.
208    ///
209    /// ```
210    /// use gpui_component::{Rope, RopeExt};
211    /// use sum_tree::Bias;
212    ///
213    /// let rope = Rope::from("Hello 中文🎉 test\nRope");
214    /// assert_eq!(rope.clip_offset(5, Bias::Left), 5);
215    /// // Inside multi-byte character '中' (3 bytes)
216    /// assert_eq!(rope.clip_offset(7, Bias::Left), 6);
217    /// assert_eq!(rope.clip_offset(7, Bias::Right), 9);
218    /// ```
219    fn clip_offset(&self, offset: usize, bias: Bias) -> usize;
220
221    /// Convert offset in characters to byte offset (0-based).
222    ///
223    /// Run in O(n) time.
224    ///
225    /// # Example
226    ///
227    /// ```
228    /// use gpui_component::{Rope, RopeExt};
229    /// let rope = Rope::from("a 中文🎉 test\nRope");
230    /// assert_eq!(rope.char_index_to_offset(0), 0);
231    /// assert_eq!(rope.char_index_to_offset(1), 1);
232    /// assert_eq!(rope.char_index_to_offset(3), "a 中".len());
233    /// assert_eq!(rope.char_index_to_offset(5), "a 中文🎉".len());
234    /// ```
235    fn char_index_to_offset(&self, char_index: usize) -> usize;
236
237    /// Convert byte offset (0-based) to offset in characters.
238    ///
239    /// Run in O(n) time.
240    ///
241    /// # Example
242    ///
243    /// ```
244    /// use gpui_component::{Rope, RopeExt};
245    /// let rope = Rope::from("a 中文🎉 test\nRope");
246    /// assert_eq!(rope.offset_to_char_index(0), 0);
247    /// assert_eq!(rope.offset_to_char_index(1), 1);
248    /// assert_eq!(rope.offset_to_char_index(3), 3);
249    /// assert_eq!(rope.offset_to_char_index(4), 3);
250    /// ```
251    fn offset_to_char_index(&self, offset: usize) -> usize;
252}
253
254impl RopeExt for Rope {
255    fn slice_line(&self, row: usize) -> RopeSlice<'_> {
256        let total_lines = self.lines_len();
257        if row >= total_lines {
258            return self.slice(0..0);
259        }
260
261        let line = self.line(row, LineType::LF);
262        if line.len() > 0 {
263            let line_end = line.len() - 1;
264            if line.is_char_boundary(line_end) && line.char(line_end) == '\n' {
265                return line.slice(..line_end);
266            }
267        }
268
269        line
270    }
271
272    fn slice_lines(&self, rows_range: Range<usize>) -> RopeSlice<'_> {
273        let start = self.line_start_offset(rows_range.start);
274        let end = self.line_end_offset(rows_range.end.saturating_sub(1));
275        self.slice(start..end)
276    }
277
278    fn iter_lines(&self) -> RopeLines<'_> {
279        RopeLines::new(&self)
280    }
281
282    fn line_len(&self, row: usize) -> usize {
283        self.slice_line(row).len()
284    }
285
286    fn line_start_offset(&self, row: usize) -> usize {
287        self.point_to_offset(Point::new(row, 0))
288    }
289
290    fn offset_to_point(&self, offset: usize) -> Point {
291        let offset = self.clip_offset(offset, Bias::Left);
292        let row = self.byte_to_line_idx(offset, LineType::LF);
293        let line_start = self.line_to_byte_idx(row, LineType::LF);
294        let column = offset.saturating_sub(line_start);
295        Point::new(row, column)
296    }
297
298    fn point_to_offset(&self, point: Point) -> usize {
299        if point.row >= self.lines_len() {
300            return self.len();
301        }
302
303        let line_start = self.line_to_byte_idx(point.row, LineType::LF);
304        line_start + point.column
305    }
306
307    fn position_to_offset(&self, pos: &Position) -> usize {
308        let line = self.slice_line(pos.line as usize);
309        self.line_start_offset(pos.line as usize)
310            + line
311                .chars()
312                .take(pos.character as usize)
313                .map(|c| c.len_utf8())
314                .sum::<usize>()
315    }
316
317    fn offset_to_position(&self, offset: usize) -> Position {
318        let point = self.offset_to_point(offset);
319        let line = self.slice_line(point.row);
320        let offset = line.utf16_to_byte_idx(line.byte_to_utf16_idx(point.column));
321        let character = line.slice(..offset).chars().count();
322        Position::new(point.row as u32, character as u32)
323    }
324
325    fn line_end_offset(&self, row: usize) -> usize {
326        if row > self.lines_len() {
327            return self.len();
328        }
329
330        self.line_start_offset(row) + self.line_len(row)
331    }
332
333    fn lines_len(&self) -> usize {
334        self.len_lines(LineType::LF)
335    }
336
337    fn char_at(&self, offset: usize) -> Option<char> {
338        if offset > self.len() {
339            return None;
340        }
341
342        self.get_char(offset).ok()
343    }
344
345    fn word_range(&self, offset: usize) -> Option<Range<usize>> {
346        if offset >= self.len() {
347            return None;
348        }
349
350        let mut left = String::new();
351        let offset = self.clip_offset(offset, Bias::Left);
352        for c in self.chars_at(offset).reversed() {
353            if c.is_alphanumeric() || c == '_' {
354                left.insert(0, c);
355            } else {
356                break;
357            }
358        }
359        let start = offset.saturating_sub(left.len());
360
361        let right = self
362            .chars_at(offset)
363            .take_while(|c| c.is_alphanumeric() || *c == '_')
364            .collect::<String>();
365
366        let end = offset + right.len();
367
368        if start == end {
369            None
370        } else {
371            Some(start..end)
372        }
373    }
374
375    fn word_at(&self, offset: usize) -> String {
376        if let Some(range) = self.word_range(offset) {
377            self.slice(range).to_string()
378        } else {
379            String::new()
380        }
381    }
382
383    #[inline]
384    fn offset_utf16_to_offset(&self, offset_utf16: usize) -> usize {
385        if offset_utf16 > self.len_utf16() {
386            return self.len();
387        }
388
389        self.utf16_to_byte_idx(offset_utf16)
390    }
391
392    #[inline]
393    fn offset_to_offset_utf16(&self, offset: usize) -> usize {
394        if offset > self.len() {
395            return self.len_utf16();
396        }
397
398        self.byte_to_utf16_idx(offset)
399    }
400
401    fn replace(&mut self, range: Range<usize>, new_text: &str) {
402        let range =
403            self.clip_offset(range.start, Bias::Left)..self.clip_offset(range.end, Bias::Right);
404        self.remove(range.clone());
405        self.insert(range.start, new_text);
406    }
407
408    fn clip_offset(&self, offset: usize, bias: Bias) -> usize {
409        if offset > self.len() {
410            return self.len();
411        }
412
413        if self.is_char_boundary(offset) {
414            return offset;
415        }
416
417        if bias == Bias::Left {
418            self.floor_char_boundary(offset)
419        } else {
420            self.ceil_char_boundary(offset)
421        }
422    }
423
424    fn char_index_to_offset(&self, char_offset: usize) -> usize {
425        self.chars().take(char_offset).map(|c| c.len_utf8()).sum()
426    }
427
428    fn offset_to_char_index(&self, offset: usize) -> usize {
429        let offset = self.clip_offset(offset, Bias::Right);
430        self.slice(..offset).chars().count()
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use ropey::Rope;
437    use sum_tree::Bias;
438    use tree_sitter::Point;
439
440    use crate::{input::Position, RopeExt};
441
442    #[test]
443    fn test_slice_line() {
444        let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
445        assert_eq!(rope.slice_line(0).to_string(), "Hello");
446        assert_eq!(rope.slice_line(1).to_string(), "World\r");
447        assert_eq!(rope.slice_line(2).to_string(), "This is a test 中文");
448        assert_eq!(rope.slice_line(3).to_string(), "Rope");
449
450        // over bounds
451        assert_eq!(rope.slice_line(6).to_string(), "");
452
453        // only have \r end
454        let rope = Rope::from("Hello\r");
455        assert_eq!(rope.slice_line(0).to_string(), "Hello\r");
456        assert_eq!(rope.slice_line(1).to_string(), "");
457    }
458
459    #[test]
460    fn test_lines_len() {
461        let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
462        assert_eq!(rope.lines_len(), 4);
463        let rope = Rope::from("");
464        assert_eq!(rope.lines_len(), 1);
465        let rope = Rope::from("Single line");
466        assert_eq!(rope.lines_len(), 1);
467
468        // only have \r end
469        let rope = Rope::from("Hello\r");
470        assert_eq!(rope.lines_len(), 1);
471    }
472
473    #[test]
474    fn test_lines() {
475        let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope\r");
476        let lines: Vec<_> = rope.iter_lines().map(|r| r.to_string()).collect();
477        assert_eq!(
478            lines,
479            vec!["Hello", "World\r", "This is a test 中文", "Rope\r"]
480        );
481    }
482
483    #[test]
484    fn test_eq() {
485        let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
486        assert!(rope.eq(&Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope")));
487        assert!(!rope.eq(&Rope::from("Hello\nWorld")));
488
489        let rope1 = rope.clone();
490        assert!(rope.eq(&rope1));
491    }
492
493    #[test]
494    fn test_iter_lines() {
495        let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
496        let lines: Vec<_> = rope
497            .iter_lines()
498            .skip(1)
499            .take(2)
500            .map(|r| r.to_string())
501            .collect();
502        assert_eq!(lines, vec!["World\r", "This is a test 中文"]);
503    }
504
505    #[test]
506    fn test_line_start_end_offset() {
507        let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
508        assert_eq!(rope.line_start_offset(0), 0);
509        assert_eq!(rope.line_end_offset(0), 5);
510
511        assert_eq!(rope.line_start_offset(1), 6);
512        assert_eq!(rope.line_end_offset(1), 12);
513
514        assert_eq!(rope.line_start_offset(2), 13);
515        assert_eq!(rope.line_end_offset(2), 34);
516
517        assert_eq!(rope.line_start_offset(3), 35);
518        assert_eq!(rope.line_end_offset(3), 39);
519
520        assert_eq!(rope.line_start_offset(4), 39);
521        assert_eq!(rope.line_end_offset(4), 39);
522    }
523
524    #[test]
525    fn test_line_column() {
526        let rope = Rope::from("a 中文🎉 test\nRope");
527        assert_eq!(rope.position_to_offset(&Position::new(0, 3)), "a 中".len());
528        assert_eq!(
529            rope.position_to_offset(&Position::new(0, 5)),
530            "a 中文🎉".len()
531        );
532        assert_eq!(
533            rope.position_to_offset(&Position::new(1, 1)),
534            "a 中文🎉 test\nR".len()
535        );
536
537        assert_eq!(
538            rope.offset_to_position("a 中文🎉 test\nR".len()),
539            Position::new(1, 1)
540        );
541        assert_eq!(
542            rope.offset_to_position("a 中文🎉".len()),
543            Position::new(0, 5)
544        );
545    }
546
547    #[test]
548    fn test_offset_to_point() {
549        let rope = Rope::from("a 中文🎉 test\nRope");
550        assert_eq!(rope.offset_to_point(0), Point::new(0, 0));
551        assert_eq!(rope.offset_to_point(1), Point::new(0, 1));
552        assert_eq!(rope.offset_to_point("a 中".len()), Point::new(0, 5));
553        assert_eq!(rope.offset_to_point("a 中文🎉".len()), Point::new(0, 12));
554        assert_eq!(
555            rope.offset_to_point("a 中文🎉 test\nR".len()),
556            Point::new(1, 1)
557        );
558    }
559
560    #[test]
561    fn test_point_to_offset() {
562        let rope = Rope::from("a 中文🎉 test\nRope");
563        assert_eq!(rope.point_to_offset(Point::new(0, 0)), 0);
564        assert_eq!(rope.point_to_offset(Point::new(0, 1)), 1);
565        assert_eq!(rope.point_to_offset(Point::new(0, 5)), "a 中".len());
566        assert_eq!(rope.point_to_offset(Point::new(0, 12)), "a 中文🎉".len());
567        assert_eq!(
568            rope.point_to_offset(Point::new(1, 1)),
569            "a 中文🎉 test\nR".len()
570        );
571    }
572
573    #[test]
574    fn test_char_at() {
575        let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文🎉\nRope");
576        assert_eq!(rope.char_at(0), Some('H'));
577        assert_eq!(rope.char_at(5), Some('\n'));
578        assert_eq!(rope.char_at(13), Some('T'));
579        assert_eq!(rope.char_at(28), Some('中'));
580        assert_eq!(rope.char_at(34), Some('🎉'));
581        assert_eq!(rope.char_at(38), Some('\n'));
582        assert_eq!(rope.char_at(50), None);
583    }
584
585    #[test]
586    fn test_word_at() {
587        let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文 世界\nRope");
588        assert_eq!(rope.word_at(0), "Hello");
589        assert_eq!(rope.word_range(0), Some(0..5));
590        assert_eq!(rope.word_at(8), "World");
591        assert_eq!(rope.word_range(8), Some(6..11));
592        assert_eq!(rope.word_at(12), "");
593        assert_eq!(rope.word_range(12), None);
594        assert_eq!(rope.word_at(13), "This");
595        assert_eq!(rope.word_range(13), Some(13..17));
596        assert_eq!(rope.word_at(31), "中文");
597        assert_eq!(rope.word_range(31), Some(28..34));
598        assert_eq!(rope.word_at(38), "世界");
599        assert_eq!(rope.word_range(38), Some(35..41));
600        assert_eq!(rope.word_at(44), "Rope");
601        assert_eq!(rope.word_range(44), Some(42..46));
602        assert_eq!(rope.word_at(45), "Rope");
603    }
604
605    #[test]
606    fn test_offset_utf16_conversion() {
607        let rope = Rope::from("hello 中文🎉 test\nRope");
608        assert_eq!(rope.offset_to_offset_utf16("hello".len()), 5);
609        assert_eq!(rope.offset_to_offset_utf16("hello 中".len()), 7);
610        assert_eq!(rope.offset_to_offset_utf16("hello 中文".len()), 8);
611        assert_eq!(rope.offset_to_offset_utf16("hello 中文🎉".len()), 10);
612        assert_eq!(rope.offset_to_offset_utf16(100), 20);
613
614        assert_eq!(rope.offset_utf16_to_offset(5), "hello".len());
615        assert_eq!(rope.offset_utf16_to_offset(7), "hello 中".len());
616        assert_eq!(rope.offset_utf16_to_offset(8), "hello 中文".len());
617        assert_eq!(rope.offset_utf16_to_offset(10), "hello 中文🎉".len());
618        assert_eq!(rope.offset_utf16_to_offset(100), rope.len());
619    }
620
621    #[test]
622    fn test_replace() {
623        let mut rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
624        rope.replace(6..11, "Universe");
625        assert_eq!(
626            rope.to_string(),
627            "Hello\nUniverse\r\nThis is a test 中文\nRope"
628        );
629
630        rope.replace(0..5, "Hi");
631        assert_eq!(
632            rope.to_string(),
633            "Hi\nUniverse\r\nThis is a test 中文\nRope"
634        );
635
636        rope.replace(rope.len() - 4..rope.len(), "String");
637        assert_eq!(
638            rope.to_string(),
639            "Hi\nUniverse\r\nThis is a test 中文\nString"
640        );
641
642        // Test for not on a char boundary
643        let mut rope = Rope::from("中文");
644        rope.replace(0..1, "New");
645        // autocorrect-disable
646        assert_eq!(rope.to_string(), "New文");
647        let mut rope = Rope::from("中文");
648        rope.replace(0..2, "New");
649        assert_eq!(rope.to_string(), "New文");
650        let mut rope = Rope::from("中文");
651        rope.replace(0..3, "New");
652        assert_eq!(rope.to_string(), "New文");
653        // autocorrect-enable
654        let mut rope = Rope::from("中文");
655        rope.replace(1..4, "New");
656        assert_eq!(rope.to_string(), "New");
657    }
658
659    #[test]
660    fn test_clip_offset() {
661        let rope = Rope::from("Hello 中文🎉 test\nRope");
662        // Inside multi-byte character '中' (3 bytes)
663        assert_eq!(rope.clip_offset(5, Bias::Left), 5);
664        assert_eq!(rope.clip_offset(7, Bias::Left), 6);
665        assert_eq!(rope.clip_offset(7, Bias::Right), 9);
666        assert_eq!(rope.clip_offset(9, Bias::Left), 9);
667
668        // Inside multi-byte character '🎉' (4 bytes)
669        assert_eq!(rope.clip_offset(13, Bias::Left), 12);
670        assert_eq!(rope.clip_offset(13, Bias::Right), 16);
671        assert_eq!(rope.clip_offset(16, Bias::Left), 16);
672
673        // At character boundary
674        assert_eq!(rope.clip_offset(5, Bias::Left), 5);
675        assert_eq!(rope.clip_offset(5, Bias::Right), 5);
676
677        // Out of bounds
678        assert_eq!(rope.clip_offset(26, Bias::Left), 26);
679        assert_eq!(rope.clip_offset(100, Bias::Left), 26);
680    }
681
682    #[test]
683    fn test_char_index_to_offset() {
684        let rope = Rope::from("a 中文🎉 test\nRope");
685        assert_eq!(rope.char_index_to_offset(0), 0);
686        assert_eq!(rope.char_index_to_offset(1), 1);
687        assert_eq!(rope.char_index_to_offset(3), "a 中".len());
688        assert_eq!(rope.char_index_to_offset(5), "a 中文🎉".len());
689        assert_eq!(rope.char_index_to_offset(6), "a 中文🎉 ".len());
690
691        assert_eq!(rope.offset_to_char_index(0), 0);
692        assert_eq!(rope.offset_to_char_index(1), 1);
693        assert_eq!(rope.offset_to_char_index(3), 3);
694        assert_eq!(rope.offset_to_char_index(4), 3);
695        assert_eq!(rope.offset_to_char_index(5), 3);
696        assert_eq!(rope.offset_to_char_index(6), 4);
697        assert_eq!(rope.offset_to_char_index(10), 5);
698    }
699}