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::input::{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::input::{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::input::{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::input::{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::input::{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::input::{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::input::{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::input::{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::input::{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
222impl RopeExt for Rope {
223    fn slice_line(&self, row: usize) -> RopeSlice<'_> {
224        let total_lines = self.lines_len();
225        if row >= total_lines {
226            return self.slice(0..0);
227        }
228
229        let line = self.line(row, LineType::LF);
230        if line.len() > 0 {
231            let line_end = line.len() - 1;
232            if line.is_char_boundary(line_end) && line.char(line_end) == '\n' {
233                return line.slice(..line_end);
234            }
235        }
236
237        line
238    }
239
240    fn slice_lines(&self, rows_range: Range<usize>) -> RopeSlice<'_> {
241        let start = self.line_start_offset(rows_range.start);
242        let end = self.line_end_offset(rows_range.end.saturating_sub(1));
243        self.slice(start..end)
244    }
245
246    fn iter_lines(&self) -> RopeLines<'_> {
247        RopeLines::new(&self)
248    }
249
250    fn line_len(&self, row: usize) -> usize {
251        self.slice_line(row).len()
252    }
253
254    fn line_start_offset(&self, row: usize) -> usize {
255        self.point_to_offset(Point::new(row, 0))
256    }
257
258    fn offset_to_point(&self, offset: usize) -> Point {
259        let offset = self.clip_offset(offset, Bias::Left);
260        let row = self.byte_to_line_idx(offset, LineType::LF);
261        let line_start = self.line_to_byte_idx(row, LineType::LF);
262        let column = offset.saturating_sub(line_start);
263        Point::new(row, column)
264    }
265
266    fn point_to_offset(&self, point: Point) -> usize {
267        if point.row >= self.lines_len() {
268            return self.len();
269        }
270
271        let line_start = self.line_to_byte_idx(point.row, LineType::LF);
272        line_start + point.column
273    }
274
275    fn position_to_offset(&self, pos: &Position) -> usize {
276        let line = self.slice_line(pos.line as usize);
277        self.line_start_offset(pos.line as usize)
278            + line
279                .chars()
280                .take(pos.character as usize)
281                .map(|c| c.len_utf8())
282                .sum::<usize>()
283    }
284
285    fn offset_to_position(&self, offset: usize) -> Position {
286        let point = self.offset_to_point(offset);
287        let line = self.slice_line(point.row);
288        let offset = line.utf16_to_byte_idx(line.byte_to_utf16_idx(point.column));
289        let character = line.slice(..offset).chars().count();
290        Position::new(point.row as u32, character as u32)
291    }
292
293    fn line_end_offset(&self, row: usize) -> usize {
294        if row > self.lines_len() {
295            return self.len();
296        }
297
298        self.line_start_offset(row) + self.line_len(row)
299    }
300
301    fn lines_len(&self) -> usize {
302        self.len_lines(LineType::LF)
303    }
304
305    fn char_at(&self, offset: usize) -> Option<char> {
306        if offset > self.len() {
307            return None;
308        }
309
310        self.get_char(offset).ok()
311    }
312
313    fn word_range(&self, offset: usize) -> Option<Range<usize>> {
314        if offset >= self.len() {
315            return None;
316        }
317
318        let mut left = String::new();
319        let offset = self.clip_offset(offset, Bias::Left);
320        for c in self.chars_at(offset).reversed() {
321            if c.is_alphanumeric() || c == '_' {
322                left.insert(0, c);
323            } else {
324                break;
325            }
326        }
327        let start = offset.saturating_sub(left.len());
328
329        let right = self
330            .chars_at(offset)
331            .take_while(|c| c.is_alphanumeric() || *c == '_')
332            .collect::<String>();
333
334        let end = offset + right.len();
335
336        if start == end {
337            None
338        } else {
339            Some(start..end)
340        }
341    }
342
343    fn word_at(&self, offset: usize) -> String {
344        if let Some(range) = self.word_range(offset) {
345            self.slice(range).to_string()
346        } else {
347            String::new()
348        }
349    }
350
351    #[inline]
352    fn offset_utf16_to_offset(&self, offset_utf16: usize) -> usize {
353        if offset_utf16 > self.len_utf16() {
354            return self.len();
355        }
356
357        self.utf16_to_byte_idx(offset_utf16)
358    }
359
360    #[inline]
361    fn offset_to_offset_utf16(&self, offset: usize) -> usize {
362        if offset > self.len() {
363            return self.len_utf16();
364        }
365
366        self.byte_to_utf16_idx(offset)
367    }
368
369    fn replace(&mut self, range: Range<usize>, new_text: &str) {
370        let range =
371            self.clip_offset(range.start, Bias::Left)..self.clip_offset(range.end, Bias::Right);
372        self.remove(range.clone());
373        self.insert(range.start, new_text);
374    }
375
376    fn clip_offset(&self, offset: usize, bias: Bias) -> usize {
377        if offset > self.len() {
378            return self.len();
379        }
380
381        if self.is_char_boundary(offset) {
382            return offset;
383        }
384
385        if bias == Bias::Left {
386            self.floor_char_boundary(offset)
387        } else {
388            self.ceil_char_boundary(offset)
389        }
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use ropey::Rope;
396    use sum_tree::Bias;
397    use tree_sitter::Point;
398
399    use crate::input::{Position, RopeExt};
400
401    #[test]
402    fn test_slice_line() {
403        let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
404        assert_eq!(rope.slice_line(0).to_string(), "Hello");
405        assert_eq!(rope.slice_line(1).to_string(), "World\r");
406        assert_eq!(rope.slice_line(2).to_string(), "This is a test 中文");
407        assert_eq!(rope.slice_line(3).to_string(), "Rope");
408
409        // over bounds
410        assert_eq!(rope.slice_line(6).to_string(), "");
411
412        // only have \r end
413        let rope = Rope::from("Hello\r");
414        assert_eq!(rope.slice_line(0).to_string(), "Hello\r");
415        assert_eq!(rope.slice_line(1).to_string(), "");
416    }
417
418    #[test]
419    fn test_lines_len() {
420        let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
421        assert_eq!(rope.lines_len(), 4);
422        let rope = Rope::from("");
423        assert_eq!(rope.lines_len(), 1);
424        let rope = Rope::from("Single line");
425        assert_eq!(rope.lines_len(), 1);
426
427        // only have \r end
428        let rope = Rope::from("Hello\r");
429        assert_eq!(rope.lines_len(), 1);
430    }
431
432    #[test]
433    fn test_lines() {
434        let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope\r");
435        let lines: Vec<_> = rope.iter_lines().map(|r| r.to_string()).collect();
436        assert_eq!(
437            lines,
438            vec!["Hello", "World\r", "This is a test 中文", "Rope\r"]
439        );
440    }
441
442    #[test]
443    fn test_eq() {
444        let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
445        assert!(rope.eq(&Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope")));
446        assert!(!rope.eq(&Rope::from("Hello\nWorld")));
447
448        let rope1 = rope.clone();
449        assert!(rope.eq(&rope1));
450    }
451
452    #[test]
453    fn test_iter_lines() {
454        let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
455        let lines: Vec<_> = rope
456            .iter_lines()
457            .skip(1)
458            .take(2)
459            .map(|r| r.to_string())
460            .collect();
461        assert_eq!(lines, vec!["World\r", "This is a test 中文"]);
462    }
463
464    #[test]
465    fn test_line_start_end_offset() {
466        let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
467        assert_eq!(rope.line_start_offset(0), 0);
468        assert_eq!(rope.line_end_offset(0), 5);
469
470        assert_eq!(rope.line_start_offset(1), 6);
471        assert_eq!(rope.line_end_offset(1), 12);
472
473        assert_eq!(rope.line_start_offset(2), 13);
474        assert_eq!(rope.line_end_offset(2), 34);
475
476        assert_eq!(rope.line_start_offset(3), 35);
477        assert_eq!(rope.line_end_offset(3), 39);
478
479        assert_eq!(rope.line_start_offset(4), 39);
480        assert_eq!(rope.line_end_offset(4), 39);
481    }
482
483    #[test]
484    fn test_line_column() {
485        let rope = Rope::from("a 中文🎉 test\nRope");
486        assert_eq!(rope.position_to_offset(&Position::new(0, 3)), "a 中".len());
487        assert_eq!(
488            rope.position_to_offset(&Position::new(0, 5)),
489            "a 中文🎉".len()
490        );
491        assert_eq!(
492            rope.position_to_offset(&Position::new(1, 1)),
493            "a 中文🎉 test\nR".len()
494        );
495
496        assert_eq!(
497            rope.offset_to_position("a 中文🎉 test\nR".len()),
498            Position::new(1, 1)
499        );
500        assert_eq!(
501            rope.offset_to_position("a 中文🎉".len()),
502            Position::new(0, 5)
503        );
504    }
505
506    #[test]
507    fn test_offset_to_point() {
508        let rope = Rope::from("a 中文🎉 test\nRope");
509        assert_eq!(rope.offset_to_point(0), Point::new(0, 0));
510        assert_eq!(rope.offset_to_point(1), Point::new(0, 1));
511        assert_eq!(rope.offset_to_point("a 中".len()), Point::new(0, 5));
512        assert_eq!(rope.offset_to_point("a 中文🎉".len()), Point::new(0, 12));
513        assert_eq!(
514            rope.offset_to_point("a 中文🎉 test\nR".len()),
515            Point::new(1, 1)
516        );
517    }
518
519    #[test]
520    fn test_point_to_offset() {
521        let rope = Rope::from("a 中文🎉 test\nRope");
522        assert_eq!(rope.point_to_offset(Point::new(0, 0)), 0);
523        assert_eq!(rope.point_to_offset(Point::new(0, 1)), 1);
524        assert_eq!(rope.point_to_offset(Point::new(0, 5)), "a 中".len());
525        assert_eq!(rope.point_to_offset(Point::new(0, 12)), "a 中文🎉".len());
526        assert_eq!(
527            rope.point_to_offset(Point::new(1, 1)),
528            "a 中文🎉 test\nR".len()
529        );
530    }
531
532    #[test]
533    fn test_char_at() {
534        let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文🎉\nRope");
535        assert_eq!(rope.char_at(0), Some('H'));
536        assert_eq!(rope.char_at(5), Some('\n'));
537        assert_eq!(rope.char_at(13), Some('T'));
538        assert_eq!(rope.char_at(28), Some('中'));
539        assert_eq!(rope.char_at(34), Some('🎉'));
540        assert_eq!(rope.char_at(38), Some('\n'));
541        assert_eq!(rope.char_at(50), None);
542    }
543
544    #[test]
545    fn test_word_at() {
546        let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文 世界\nRope");
547        assert_eq!(rope.word_at(0), "Hello");
548        assert_eq!(rope.word_range(0), Some(0..5));
549        assert_eq!(rope.word_at(8), "World");
550        assert_eq!(rope.word_range(8), Some(6..11));
551        assert_eq!(rope.word_at(12), "");
552        assert_eq!(rope.word_range(12), None);
553        assert_eq!(rope.word_at(13), "This");
554        assert_eq!(rope.word_range(13), Some(13..17));
555        assert_eq!(rope.word_at(31), "中文");
556        assert_eq!(rope.word_range(31), Some(28..34));
557        assert_eq!(rope.word_at(38), "世界");
558        assert_eq!(rope.word_range(38), Some(35..41));
559        assert_eq!(rope.word_at(44), "Rope");
560        assert_eq!(rope.word_range(44), Some(42..46));
561        assert_eq!(rope.word_at(45), "Rope");
562    }
563
564    #[test]
565    fn test_offset_utf16_conversion() {
566        let rope = Rope::from("hello 中文🎉 test\nRope");
567        assert_eq!(rope.offset_to_offset_utf16("hello".len()), 5);
568        assert_eq!(rope.offset_to_offset_utf16("hello 中".len()), 7);
569        assert_eq!(rope.offset_to_offset_utf16("hello 中文".len()), 8);
570        assert_eq!(rope.offset_to_offset_utf16("hello 中文🎉".len()), 10);
571        assert_eq!(rope.offset_to_offset_utf16(100), 20);
572
573        assert_eq!(rope.offset_utf16_to_offset(5), "hello".len());
574        assert_eq!(rope.offset_utf16_to_offset(7), "hello 中".len());
575        assert_eq!(rope.offset_utf16_to_offset(8), "hello 中文".len());
576        assert_eq!(rope.offset_utf16_to_offset(10), "hello 中文🎉".len());
577        assert_eq!(rope.offset_utf16_to_offset(100), rope.len());
578    }
579
580    #[test]
581    fn test_replace() {
582        let mut rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
583        rope.replace(6..11, "Universe");
584        assert_eq!(
585            rope.to_string(),
586            "Hello\nUniverse\r\nThis is a test 中文\nRope"
587        );
588
589        rope.replace(0..5, "Hi");
590        assert_eq!(
591            rope.to_string(),
592            "Hi\nUniverse\r\nThis is a test 中文\nRope"
593        );
594
595        rope.replace(rope.len() - 4..rope.len(), "String");
596        assert_eq!(
597            rope.to_string(),
598            "Hi\nUniverse\r\nThis is a test 中文\nString"
599        );
600
601        // Test for not on a char boundary
602        let mut rope = Rope::from("中文");
603        rope.replace(0..1, "New");
604        assert_eq!(rope.to_string(), "New文");
605        let mut rope = Rope::from("中文");
606        rope.replace(0..2, "New");
607        assert_eq!(rope.to_string(), "New文");
608        let mut rope = Rope::from("中文");
609        rope.replace(0..3, "New");
610        assert_eq!(rope.to_string(), "New文");
611        let mut rope = Rope::from("中文");
612        rope.replace(1..4, "New");
613        assert_eq!(rope.to_string(), "New");
614    }
615
616    #[test]
617    fn test_clip_offset() {
618        let rope = Rope::from("Hello 中文🎉 test\nRope");
619        // Inside multi-byte character '中' (3 bytes)
620        assert_eq!(rope.clip_offset(5, Bias::Left), 5);
621        assert_eq!(rope.clip_offset(7, Bias::Left), 6);
622        assert_eq!(rope.clip_offset(7, Bias::Right), 9);
623        assert_eq!(rope.clip_offset(9, Bias::Left), 9);
624
625        // Inside multi-byte character '🎉' (4 bytes)
626        assert_eq!(rope.clip_offset(13, Bias::Left), 12);
627        assert_eq!(rope.clip_offset(13, Bias::Right), 16);
628        assert_eq!(rope.clip_offset(16, Bias::Left), 16);
629
630        // At character boundary
631        assert_eq!(rope.clip_offset(5, Bias::Left), 5);
632        assert_eq!(rope.clip_offset(5, Bias::Right), 5);
633
634        // Out of bounds
635        assert_eq!(rope.clip_offset(26, Bias::Left), 26);
636        assert_eq!(rope.clip_offset(100, Bias::Left), 26);
637    }
638}