floem_editor_core/buffer/
rope_text.rs

1use std::{borrow::Cow, ops::Range};
2
3use lapce_xi_rope::{interval::IntervalBounds, rope::ChunkIter, Cursor, Rope};
4
5use crate::{mode::Mode, paragraph::ParagraphCursor, word::WordCursor};
6
7pub trait RopeText {
8    fn text(&self) -> &Rope;
9
10    fn len(&self) -> usize {
11        self.text().len()
12    }
13
14    fn is_empty(&self) -> bool {
15        self.len() == 0
16    }
17
18    /// The last line of the held rope
19    fn last_line(&self) -> usize {
20        self.line_of_offset(self.len())
21    }
22
23    /// Get the offset into the rope of the start of the given line.
24    /// If the line it out of bounds, then the last offset (the len) is returned.
25    fn offset_of_line(&self, line: usize) -> usize {
26        let last_line = self.last_line();
27        let line = line.min(last_line + 1);
28        self.text().offset_of_line(line)
29    }
30
31    fn offset_line_end(&self, offset: usize, caret: bool) -> usize {
32        let line = self.line_of_offset(offset);
33        self.line_end_offset(line, caret)
34    }
35
36    fn line_of_offset(&self, offset: usize) -> usize {
37        let offset = offset.min(self.len());
38        let offset = self
39            .text()
40            .at_or_prev_codepoint_boundary(offset)
41            .unwrap_or(offset);
42
43        self.text().line_of_offset(offset)
44    }
45
46    fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
47        let offset = offset.min(self.len());
48        let line = self.line_of_offset(offset);
49        let line_start = self.offset_of_line(line);
50        if offset == line_start {
51            return (line, 0);
52        }
53
54        let col = offset - line_start;
55        (line, col)
56    }
57
58    /// Get the offset for a specific line and column.
59    ///
60    /// This should be preferred over simply adding the column to the line offset, because it
61    /// validates better and avoids returning newlines.
62    /// ```rust
63    /// # use floem_editor_core::xi_rope::Rope;
64    /// # use floem_editor_core::buffer::rope_text::{RopeText, RopeTextRef};
65    /// let text = Rope::from("hello\nworld");
66    /// let text = RopeTextRef::new(&text);
67    /// assert_eq!(text.offset_of_line_col(0, 0), 0);  // "h"
68    /// assert_eq!(text.offset_of_line_col(0, 4), 4);  // "o"
69    /// assert_eq!(text.offset_of_line_col(0, 5), 5);  // "\n"
70    /// assert_eq!(text.offset_of_line_col(0, 6), 5);  // "\n", avoids newline
71    /// assert_eq!(text.offset_of_line_col(1, 0), 6);  // "w"
72    /// assert_eq!(text.offset_of_line_col(1, 4), 10); // "d"
73    /// let text = Rope::from("hello\r\nworld");
74    /// let text = RopeTextRef::new(&text);
75    /// assert_eq!(text.offset_of_line_col(0, 0), 0);  // "h"
76    /// assert_eq!(text.offset_of_line_col(0, 4), 4);  // "o"
77    /// assert_eq!(text.offset_of_line_col(0, 5), 5);  // "\r"
78    /// assert_eq!(text.offset_of_line_col(0, 6), 5);  // "\r", avoids being in the middle
79    /// assert_eq!(text.offset_of_line_col(1, 0), 7);  // "w"
80    /// assert_eq!(text.offset_of_line_col(1, 4), 11); // "d"
81    /// ````
82    fn offset_of_line_col(&self, line: usize, col: usize) -> usize {
83        let mut pos = 0;
84        let mut offset = self.offset_of_line(line);
85        let text = self.slice_to_cow(offset..self.offset_of_line(line + 1));
86        let mut iter = text.chars().peekable();
87        while let Some(c) = iter.next() {
88            // Stop at the end of the line
89            if c == '\n' || (c == '\r' && iter.peek() == Some(&'\n')) {
90                return offset;
91            }
92
93            let char_len = c.len_utf8();
94            if pos + char_len > col {
95                return offset;
96            }
97            pos += char_len;
98            offset += char_len;
99        }
100        offset
101    }
102
103    fn line_end_col(&self, line: usize, caret: bool) -> usize {
104        let line_start = self.offset_of_line(line);
105        let offset = self.line_end_offset(line, caret);
106        offset - line_start
107    }
108
109    /// Get the offset of the end of the line. The caret decides whether it is after the last
110    /// character, or before it.
111    /// If the line is out of bounds, then the last offset (the len) is returned.
112    ///
113    /// ```rust
114    /// # use floem_editor_core::xi_rope::Rope;
115    /// # use floem_editor_core::buffer::rope_text::{RopeText, RopeTextRef};
116    /// let text = Rope::from("hello\nworld");
117    /// let text = RopeTextRef::new(&text);
118    /// assert_eq!(text.line_end_offset(0, false), 4);  // "hell|o"
119    /// assert_eq!(text.line_end_offset(0, true), 5);   // "hello|"
120    /// assert_eq!(text.line_end_offset(1, false), 10); // "worl|d"
121    /// assert_eq!(text.line_end_offset(1, true), 11);  // "world|"
122    /// // Out of bounds
123    /// assert_eq!(text.line_end_offset(2, false), 11); // "world|"
124    /// ```
125    fn line_end_offset(&self, line: usize, caret: bool) -> usize {
126        let mut offset = self.offset_of_line(line + 1);
127        let mut line_content: &str = &self.line_content(line);
128        if line_content.ends_with("\r\n") {
129            offset -= 2;
130            line_content = &line_content[..line_content.len() - 2];
131        } else if line_content.ends_with('\n') {
132            offset -= 1;
133            line_content = &line_content[..line_content.len() - 1];
134        }
135        if !caret && !line_content.is_empty() {
136            offset = self.prev_grapheme_offset(offset, 1, 0);
137        }
138        offset
139    }
140
141    /// Returns the content of the given line.
142    /// Includes the line ending if it exists. (-> the last line won't have a line ending)
143    /// Lines past the end of the document will return an empty string.
144    fn line_content(&self, line: usize) -> Cow<'_, str> {
145        self.text()
146            .slice_to_cow(self.offset_of_line(line)..self.offset_of_line(line + 1))
147    }
148
149    /// Get the offset of the previous grapheme cluster.
150    fn prev_grapheme_offset(&self, offset: usize, count: usize, limit: usize) -> usize {
151        let offset = offset.min(self.len());
152        let mut cursor = Cursor::new(self.text(), offset);
153        let mut new_offset = offset;
154        for _i in 0..count {
155            if let Some(prev_offset) = cursor.prev_grapheme() {
156                if prev_offset < limit {
157                    return new_offset;
158                }
159                new_offset = prev_offset;
160                cursor.set(prev_offset);
161            } else {
162                return new_offset;
163            }
164        }
165        new_offset
166    }
167
168    fn next_grapheme_offset(&self, offset: usize, count: usize, limit: usize) -> usize {
169        let offset = if offset > self.len() {
170            self.len()
171        } else {
172            offset
173        };
174        let mut cursor = Cursor::new(self.text(), offset);
175        let mut new_offset = offset;
176        for _i in 0..count {
177            if let Some(next_offset) = cursor.next_grapheme() {
178                if next_offset > limit {
179                    return new_offset;
180                }
181                new_offset = next_offset;
182                cursor.set(next_offset);
183            } else {
184                return new_offset;
185            }
186        }
187        new_offset
188    }
189
190    fn prev_code_boundary(&self, offset: usize) -> usize {
191        WordCursor::new(self.text(), offset).prev_code_boundary()
192    }
193
194    fn next_code_boundary(&self, offset: usize) -> usize {
195        WordCursor::new(self.text(), offset).next_code_boundary()
196    }
197
198    /// Return the previous and end boundaries of the word under cursor.
199    fn select_word(&self, offset: usize) -> (usize, usize) {
200        WordCursor::new(self.text(), offset).select_word()
201    }
202
203    /// Returns the offset of the first non-blank character on the given line.
204    /// If the line is one past the last line, then the offset at the end of the rope is returned.
205    /// If the line is further past that, then it defaults to the last line.
206    fn first_non_blank_character_on_line(&self, line: usize) -> usize {
207        let last_line = self.last_line();
208        let line = if line > last_line + 1 {
209            last_line
210        } else {
211            line
212        };
213        let line_start_offset = self.text().offset_of_line(line);
214        WordCursor::new(self.text(), line_start_offset).next_non_blank_char()
215    }
216
217    fn indent_on_line(&self, line: usize) -> String {
218        let line_start_offset = self.text().offset_of_line(line);
219        let word_boundary = WordCursor::new(self.text(), line_start_offset).next_non_blank_char();
220        let indent = self.text().slice_to_cow(line_start_offset..word_boundary);
221        indent.to_string()
222    }
223
224    /// Get the content of the rope as a [`Cow`] string, for 'nice' ranges (small, and at the right
225    /// offsets) this will be a reference to the rope's data. Otherwise, it allocates a new string.
226    /// You should be somewhat wary of requesting large parts of the rope, as it will allocate
227    /// a new string since it isn't contiguous in memory for large chunks.
228    fn slice_to_cow(&self, range: Range<usize>) -> Cow<'_, str> {
229        self.text()
230            .slice_to_cow(range.start.min(self.len())..range.end.min(self.len()))
231    }
232
233    // TODO(minor): Once you can have an `impl Trait` return type in a trait, this could use that.
234    /// Iterate over `(utf8_offset, char)` values in the given range.
235    #[allow(clippy::type_complexity)]
236    /// This uses `iter_chunks` and so does not allocate, compared to [`Self::slice_to_cow`] which can
237    fn char_indices_iter<'a, T: IntervalBounds>(
238        &'a self,
239        range: T,
240    ) -> CharIndicesJoin<
241        std::str::CharIndices<'a>,
242        std::iter::Map<ChunkIter<'a>, fn(&str) -> std::str::CharIndices<'_>>,
243    > {
244        let iter: ChunkIter<'a> = self.text().iter_chunks(range);
245        let iter: std::iter::Map<ChunkIter<'a>, fn(&str) -> std::str::CharIndices<'_>> =
246            iter.map(str::char_indices);
247        CharIndicesJoin::new(iter)
248    }
249
250    /// The number of lines in the file
251    fn num_lines(&self) -> usize {
252        self.last_line() + 1
253    }
254
255    /// The length of the given line
256    fn line_len(&self, line: usize) -> usize {
257        self.offset_of_line(line + 1) - self.offset_of_line(line)
258    }
259
260    /// Returns `true` if the given line contains no non-whitespace characters.
261    fn is_line_whitespace(&self, line: usize) -> bool {
262        let line_start_offset = self.text().offset_of_line(line);
263        let mut word_cursor = WordCursor::new(self.text(), line_start_offset);
264
265        word_cursor.next_non_blank_char();
266        let c = word_cursor.inner.next_codepoint();
267
268        match c {
269            None | Some('\n') => true,
270            Some('\r') => {
271                let c = word_cursor.inner.next_codepoint();
272                c.is_some_and(|c| c == '\n')
273            }
274            _ => false,
275        }
276    }
277
278    fn move_left(&self, offset: usize, mode: Mode, count: usize) -> usize {
279        let min_offset = if mode == Mode::Insert {
280            0
281        } else {
282            let line = self.line_of_offset(offset);
283            self.offset_of_line(line)
284        };
285
286        self.prev_grapheme_offset(offset, count, min_offset)
287    }
288
289    fn move_right(&self, offset: usize, mode: Mode, count: usize) -> usize {
290        let max_offset = if mode == Mode::Insert {
291            self.len()
292        } else {
293            self.offset_line_end(offset, mode != Mode::Normal)
294        };
295
296        self.next_grapheme_offset(offset, count, max_offset)
297    }
298
299    fn find_nth_paragraph<F>(&self, offset: usize, mut count: usize, mut find_next: F) -> usize
300    where
301        F: FnMut(&mut ParagraphCursor) -> Option<usize>,
302    {
303        let mut cursor = ParagraphCursor::new(self.text(), offset);
304        let mut new_offset = offset;
305        while count != 0 {
306            // FIXME: wait for if-let-chain
307            if let Some(offset) = find_next(&mut cursor) {
308                new_offset = offset;
309            } else {
310                break;
311            }
312            count -= 1;
313        }
314        new_offset
315    }
316
317    fn move_n_paragraphs_forward(&self, offset: usize, count: usize) -> usize {
318        self.find_nth_paragraph(offset, count, |cursor| cursor.next_boundary())
319    }
320
321    fn move_n_paragraphs_backward(&self, offset: usize, count: usize) -> usize {
322        self.find_nth_paragraph(offset, count, |cursor| cursor.prev_boundary())
323    }
324
325    /// Find the nth (`count`) word starting at `offset` in either direction
326    /// depending on `find_next`.
327    ///
328    /// A `WordCursor` is created and given to the `find_next` function for the
329    /// search.  The `find_next` function should return None when there is no
330    /// more word found.  Despite the name, `find_next` can search in either
331    /// direction.
332    fn find_nth_word<F>(&self, offset: usize, mut count: usize, mut find_next: F) -> usize
333    where
334        F: FnMut(&mut WordCursor) -> Option<usize>,
335    {
336        let mut cursor = WordCursor::new(self.text(), offset);
337        let mut new_offset = offset;
338        while count != 0 {
339            // FIXME: wait for if-let-chain
340            if let Some(offset) = find_next(&mut cursor) {
341                new_offset = offset;
342            } else {
343                break;
344            }
345            count -= 1;
346        }
347        new_offset
348    }
349
350    fn move_n_words_forward(&self, offset: usize, count: usize) -> usize {
351        self.find_nth_word(offset, count, |cursor| cursor.next_boundary())
352    }
353
354    fn move_n_wordends_forward(&self, offset: usize, count: usize, inserting: bool) -> usize {
355        let mut new_offset = self.find_nth_word(offset, count, |cursor| cursor.end_boundary());
356        if !inserting && new_offset != self.len() {
357            new_offset = self.prev_grapheme_offset(new_offset, 1, 0);
358        }
359        new_offset
360    }
361
362    fn move_n_words_backward(&self, offset: usize, count: usize, mode: Mode) -> usize {
363        self.find_nth_word(offset, count, |cursor| cursor.prev_boundary(mode))
364    }
365
366    fn move_word_backward_deletion(&self, offset: usize) -> usize {
367        self.find_nth_word(offset, 1, |cursor| cursor.prev_deletion_boundary())
368    }
369}
370
371#[derive(Clone)]
372pub struct RopeTextVal {
373    pub text: Rope,
374}
375impl RopeTextVal {
376    pub fn new(text: Rope) -> Self {
377        Self { text }
378    }
379}
380impl RopeText for RopeTextVal {
381    fn text(&self) -> &Rope {
382        &self.text
383    }
384}
385impl From<Rope> for RopeTextVal {
386    fn from(text: Rope) -> Self {
387        Self::new(text)
388    }
389}
390#[derive(Copy, Clone)]
391pub struct RopeTextRef<'a> {
392    pub text: &'a Rope,
393}
394impl<'a> RopeTextRef<'a> {
395    pub fn new(text: &'a Rope) -> Self {
396        Self { text }
397    }
398}
399impl RopeText for RopeTextRef<'_> {
400    fn text(&self) -> &Rope {
401        self.text
402    }
403}
404impl<'a> From<&'a Rope> for RopeTextRef<'a> {
405    fn from(text: &'a Rope) -> Self {
406        Self::new(text)
407    }
408}
409
410impl RopeText for Rope {
411    fn text(&self) -> &Rope {
412        self
413    }
414}
415
416/// Joins an iterator of iterators over char indices `(usize, char)` into one
417/// as if they were from a single long string
418/// Assumes the iterators end after the first `None` value
419#[derive(Clone)]
420pub struct CharIndicesJoin<I: Iterator<Item = (usize, char)>, O: Iterator<Item = I>> {
421    /// Our iterator of iterators
422    main_iter: O,
423    /// Our current working iterator of indices
424    current_indices: Option<I>,
425    /// The amount we should shift future offsets
426    current_base: usize,
427    /// The latest base, since we don't know when the `current_indices` iterator will end
428    latest_base: usize,
429}
430
431impl<I: Iterator<Item = (usize, char)>, O: Iterator<Item = I>> CharIndicesJoin<I, O> {
432    pub fn new(main_iter: O) -> CharIndicesJoin<I, O> {
433        CharIndicesJoin {
434            main_iter,
435            current_indices: None,
436            current_base: 0,
437            latest_base: 0,
438        }
439    }
440}
441
442impl<I: Iterator<Item = (usize, char)>, O: Iterator<Item = I>> Iterator for CharIndicesJoin<I, O> {
443    type Item = (usize, char);
444
445    fn next(&mut self) -> Option<Self::Item> {
446        if let Some(current) = &mut self.current_indices {
447            if let Some((next_offset, next_ch)) = current.next() {
448                // Shift by the current base offset, which is the accumulated offset from previous
449                // iterators, which makes so the offset produced looks like it is from one long str
450                let next_offset = self.current_base + next_offset;
451                // Store the latest base offset, because we don't know when the current iterator
452                // will end (though technically the str iterator impl does)
453                self.latest_base = next_offset + next_ch.len_utf8();
454                return Some((next_offset, next_ch));
455            }
456        }
457
458        // Otherwise, if we didn't return something above, then we get a next iterator
459        if let Some(next_current) = self.main_iter.next() {
460            // Update our current working iterator
461            self.current_indices = Some(next_current);
462            // Update the current base offset with the previous iterators latest offset base
463            // This is what we are shifting by
464            self.current_base = self.latest_base;
465
466            // Get the next item without new current iterator
467            // As long as main_iter and the iterators it produces aren't infinite then this
468            // recursion won't be infinite either
469            // and even the non-recursion version would be infinite if those were infinite
470            self.next()
471        } else {
472            // We didn't get anything from the main iter, so we're completely done.
473            None
474        }
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use lapce_xi_rope::Rope;
481
482    use super::RopeText;
483    use crate::buffer::rope_text::RopeTextVal;
484
485    #[test]
486    fn test_line_content() {
487        let text = Rope::from("");
488        let text = RopeTextVal::new(text);
489
490        assert_eq!(text.line_content(0), "");
491        assert_eq!(text.line_content(1), "");
492        assert_eq!(text.line_content(2), "");
493
494        let text = Rope::from("abc\ndef\nghi");
495        let text = RopeTextVal::new(text);
496
497        assert_eq!(text.line_content(0), "abc\n");
498        assert_eq!(text.line_content(1), "def\n");
499        assert_eq!(text.line_content(2), "ghi");
500        assert_eq!(text.line_content(3), "");
501        assert_eq!(text.line_content(4), "");
502        assert_eq!(text.line_content(5), "");
503
504        let text = Rope::from("abc\r\ndef\r\nghi");
505        let text = RopeTextVal::new(text);
506
507        assert_eq!(text.line_content(0), "abc\r\n");
508        assert_eq!(text.line_content(1), "def\r\n");
509        assert_eq!(text.line_content(2), "ghi");
510        assert_eq!(text.line_content(3), "");
511        assert_eq!(text.line_content(4), "");
512        assert_eq!(text.line_content(5), "");
513    }
514
515    #[test]
516    fn test_offset_of_line() {
517        let text = Rope::from("");
518        let text = RopeTextVal::new(text);
519
520        assert_eq!(text.offset_of_line(0), 0);
521        assert_eq!(text.offset_of_line(1), 0);
522        assert_eq!(text.offset_of_line(2), 0);
523
524        let text = Rope::from("abc\ndef\nghi");
525        let text = RopeTextVal::new(text);
526
527        assert_eq!(text.offset_of_line(0), 0);
528        assert_eq!(text.offset_of_line(1), 4);
529        assert_eq!(text.offset_of_line(2), 8);
530        assert_eq!(text.offset_of_line(3), text.len()); // 11
531        assert_eq!(text.offset_of_line(4), text.len());
532        assert_eq!(text.offset_of_line(5), text.len());
533
534        let text = Rope::from("abc\r\ndef\r\nghi");
535        let text = RopeTextVal::new(text);
536
537        assert_eq!(text.offset_of_line(0), 0);
538        assert_eq!(text.offset_of_line(1), 5);
539        assert_eq!(text.offset_of_line(2), 10);
540        assert_eq!(text.offset_of_line(3), text.len()); // 13
541        assert_eq!(text.offset_of_line(4), text.len());
542        assert_eq!(text.offset_of_line(5), text.len());
543    }
544
545    #[test]
546    fn test_offset_of_line_col() {
547        let text = Rope::from("abc\ndef\nghi");
548        let text = RopeTextVal::new(text);
549
550        assert_eq!(text.offset_of_line_col(0, 0), 0);
551        assert_eq!(text.offset_of_line_col(0, 1), 1);
552        assert_eq!(text.offset_of_line_col(0, 2), 2);
553        assert_eq!(text.offset_of_line_col(0, 3), 3);
554        assert_eq!(text.offset_of_line_col(0, 4), 3);
555        assert_eq!(text.offset_of_line_col(1, 0), 4);
556
557        let text = Rope::from("abc\r\ndef\r\nghi");
558        let text = RopeTextVal::new(text);
559
560        assert_eq!(text.offset_of_line_col(0, 0), 0);
561        assert_eq!(text.offset_of_line_col(0, 1), 1);
562        assert_eq!(text.offset_of_line_col(0, 2), 2);
563        assert_eq!(text.offset_of_line_col(0, 3), 3);
564        assert_eq!(text.offset_of_line_col(0, 4), 3);
565        assert_eq!(text.offset_of_line_col(1, 0), 5);
566    }
567
568    #[test]
569    fn test_line_end_offset() {
570        let text = Rope::from("");
571        let text = RopeTextVal::new(text);
572
573        assert_eq!(text.line_end_offset(0, false), 0);
574        assert_eq!(text.line_end_offset(0, true), 0);
575        assert_eq!(text.line_end_offset(1, false), 0);
576        assert_eq!(text.line_end_offset(1, true), 0);
577        assert_eq!(text.line_end_offset(2, false), 0);
578        assert_eq!(text.line_end_offset(2, true), 0);
579
580        let text = Rope::from("abc\ndef\nghi");
581        let text = RopeTextVal::new(text);
582
583        assert_eq!(text.line_end_offset(0, false), 2);
584        assert_eq!(text.line_end_offset(0, true), 3);
585        assert_eq!(text.line_end_offset(1, false), 6);
586        assert_eq!(text.line_end_offset(1, true), 7);
587        assert_eq!(text.line_end_offset(2, false), 10);
588        assert_eq!(text.line_end_offset(2, true), text.len());
589        assert_eq!(text.line_end_offset(3, false), text.len());
590        assert_eq!(text.line_end_offset(3, true), text.len());
591        assert_eq!(text.line_end_offset(4, false), text.len());
592        assert_eq!(text.line_end_offset(4, true), text.len());
593    }
594
595    #[test]
596    fn test_prev_grapheme_offset() {
597        let text = Rope::from("");
598        let text = RopeTextVal::new(text);
599
600        assert_eq!(text.prev_grapheme_offset(0, 0, 0), 0);
601        assert_eq!(text.prev_grapheme_offset(0, 1, 0), 0);
602        assert_eq!(text.prev_grapheme_offset(0, 1, 1), 0);
603
604        let text = Rope::from("abc def ghi");
605        let text = RopeTextVal::new(text);
606
607        assert_eq!(text.prev_grapheme_offset(0, 0, 0), 0);
608        assert_eq!(text.prev_grapheme_offset(0, 1, 0), 0);
609        assert_eq!(text.prev_grapheme_offset(0, 1, 1), 0);
610        assert_eq!(text.prev_grapheme_offset(2, 1, 0), 1);
611        assert_eq!(text.prev_grapheme_offset(2, 1, 1), 1);
612    }
613
614    #[test]
615    fn test_first_non_blank_character_on_line() {
616        let text = Rope::from("");
617        let text = RopeTextVal::new(text);
618
619        assert_eq!(text.first_non_blank_character_on_line(0), 0);
620        assert_eq!(text.first_non_blank_character_on_line(1), 0);
621        assert_eq!(text.first_non_blank_character_on_line(2), 0);
622
623        let text = Rope::from("abc\ndef\nghi");
624        let text = RopeTextVal::new(text);
625
626        assert_eq!(text.first_non_blank_character_on_line(0), 0);
627        assert_eq!(text.first_non_blank_character_on_line(1), 4);
628        assert_eq!(text.first_non_blank_character_on_line(2), 8);
629        assert_eq!(text.first_non_blank_character_on_line(3), 11);
630        assert_eq!(text.first_non_blank_character_on_line(4), 8);
631        assert_eq!(text.first_non_blank_character_on_line(5), 8);
632
633        let text = Rope::from("abc\r\ndef\r\nghi");
634        let text = RopeTextVal::new(text);
635
636        assert_eq!(text.first_non_blank_character_on_line(0), 0);
637        assert_eq!(text.first_non_blank_character_on_line(1), 5);
638        assert_eq!(text.first_non_blank_character_on_line(2), 10);
639        assert_eq!(text.first_non_blank_character_on_line(3), 13);
640        assert_eq!(text.first_non_blank_character_on_line(4), 10);
641        assert_eq!(text.first_non_blank_character_on_line(5), 10);
642    }
643
644    #[test]
645    fn test_is_line_whitespace() {
646        let text = Rope::from("");
647        let text = RopeTextVal::new(text);
648
649        assert!(text.is_line_whitespace(0));
650
651        let text = Rope::from("\n  \t\r\t \t  \n");
652        let text = RopeTextVal::new(text);
653
654        assert!(text.is_line_whitespace(0));
655        assert!(!text.is_line_whitespace(1));
656        assert!(text.is_line_whitespace(2));
657
658        let text = Rope::from("qwerty\n\tf\t\r\n00");
659        let text = RopeTextVal::new(text);
660
661        assert!(!text.is_line_whitespace(0));
662        assert!(!text.is_line_whitespace(1));
663        assert!(!text.is_line_whitespace(2));
664
665        let text = Rope::from("  \r#\n\t                   \r\n)\t\t\t\t\t\t\t\t");
666        let text = RopeTextVal::new(text);
667
668        assert!(!text.is_line_whitespace(0));
669        assert!(text.is_line_whitespace(1));
670        assert!(!text.is_line_whitespace(2));
671
672        let text = Rope::from("   \r\n  \r");
673        let text = RopeTextVal::new(text);
674
675        assert!(text.is_line_whitespace(0));
676        assert!(!text.is_line_whitespace(1));
677    }
678}