Skip to main content

gpui_base/input/base/
rope_ext.rs

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