Skip to main content

servo_base/
rope.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::iter::once;
6use std::ops::Range;
7
8use malloc_size_of_derive::MallocSizeOf;
9use rayon::iter::Either;
10use unicode_segmentation::UnicodeSegmentation;
11
12use crate::text::{AssumeUnder4GB, Utf8CodeUnits, Utf16CodeUnits, Utf32CodeUnits};
13
14fn contents_vec(contents: impl Into<String>) -> Vec<String> {
15    let mut contents: Vec<_> = contents
16        .into()
17        .split('\n')
18        .map(|line| format!("{line}\n"))
19        .collect();
20    // The last line should not have a newline.
21    if let Some(last_line) = contents.last_mut() {
22        last_line.truncate(last_line.len() - 1);
23    }
24    contents
25}
26
27/// Describes a unit of movement for [`Rope::move_by`].
28pub enum RopeMovement {
29    Character,
30    Grapheme,
31    Word,
32    Line,
33    LineStartOrEnd,
34    RopeStartOrEnd,
35}
36
37/// An implementation of a [rope data structure], composed of lines of
38/// owned strings. This is used to implement text controls in Servo.
39///
40/// [rope data structure]: https://en.wikipedia.org/wiki/Rope_(data_structure)
41#[derive(MallocSizeOf)]
42pub struct Rope {
43    /// The lines of the rope. Each line is an owned string that ends with a newline
44    /// (`\n`), apart from the last line which has no trailing newline.
45    lines: Vec<String>,
46}
47
48impl Rope {
49    pub fn new(contents: impl Into<String>) -> Self {
50        Self {
51            lines: contents_vec(contents),
52        }
53    }
54
55    pub fn contents(&self) -> String {
56        self.lines.join("")
57    }
58
59    pub fn first_index(&self) -> RopeIndex {
60        RopeIndex::new(0, 0)
61    }
62
63    pub fn last_index(&self) -> RopeIndex {
64        let line_index = self.lines.len() - 1;
65        RopeIndex::new(line_index, self.line(line_index).len())
66    }
67
68    /// Replace the given range of [`RopeIndex`]s with the given string. Returns the
69    /// [`RopeIndex`] of the end of the insertion.
70    pub fn replace_range(
71        &mut self,
72        mut range: Range<RopeIndex>,
73        string: impl Into<String>,
74    ) -> RopeIndex {
75        range.start = self.normalize_index(range.start);
76        range.end = self.normalize_index(range.end);
77        assert!(range.start <= range.end);
78
79        let start_index = range.start;
80        self.delete_range(range);
81
82        let mut new_contents = contents_vec(string);
83        let Some(first_line_of_new_contents) = new_contents.first() else {
84            return start_index;
85        };
86
87        if new_contents.len() == 1 {
88            self.line_for_index_mut(start_index)
89                .insert_str(start_index.code_point, first_line_of_new_contents);
90            return RopeIndex::new(
91                start_index.line,
92                start_index.code_point + first_line_of_new_contents.len(),
93            );
94        }
95
96        let start_line = self.line_for_index_mut(start_index);
97        let last_line = new_contents.last().expect("Should have at least one line");
98        let last_index = RopeIndex::new(
99            start_index.line + new_contents.len().saturating_sub(1),
100            last_line.len(),
101        );
102
103        let remaining_string = start_line.split_off(start_index.code_point);
104        start_line.push_str(first_line_of_new_contents);
105        new_contents
106            .last_mut()
107            .expect("Should have at least one line")
108            .push_str(&remaining_string);
109
110        let splice_index = start_index.line + 1;
111        self.lines
112            .splice(splice_index..splice_index, new_contents.into_iter().skip(1));
113        last_index
114    }
115
116    fn delete_range(&mut self, mut range: Range<RopeIndex>) {
117        range.start = self.normalize_index(range.start);
118        range.end = self.normalize_index(range.end);
119        assert!(range.start <= range.end);
120
121        if range.start.line == range.end.line {
122            self.line_for_index_mut(range.start)
123                .replace_range(range.start.code_point..range.end.code_point, "");
124            return;
125        }
126
127        // Remove the start line and any before the last line.
128        let removed_lines = self.lines.splice(range.start.line..range.end.line, []);
129        let first_line = removed_lines
130            .into_iter()
131            .nth(0)
132            .expect("Should have removed at least one line");
133
134        let first_line_prefix = &first_line[0..range.start.code_point];
135        let new_end_line = range.start.line;
136        self.lines[new_end_line].replace_range(0..range.end.code_point, first_line_prefix);
137    }
138
139    /// Create a [`RopeSlice`] for this [`Rope`] from `start` to `end`. If either of
140    /// these is `None`, then the slice will extend to the extent of the rope.
141    pub fn slice<'a>(&'a self, start: Option<RopeIndex>, end: Option<RopeIndex>) -> RopeSlice<'a> {
142        RopeSlice {
143            rope: self,
144            start: start.unwrap_or_default(),
145            end: end.unwrap_or_else(|| self.last_index()),
146        }
147    }
148
149    pub fn chars<'a>(&'a self) -> RopeChars<'a> {
150        self.slice(None, None).chars()
151    }
152
153    /// Return `true` if the [`Rope`] is empty or false otherwise. This will also
154    /// return `true` if the contents of the [`Rope`] are a single empty line.
155    pub fn is_empty(&self) -> bool {
156        self.lines.first().is_none_or(String::is_empty)
157    }
158
159    /// The total number of code units required to encode the content in utf16.
160    pub fn len_utf16(&self) -> Utf16CodeUnits {
161        // TODO: add some check that ropes stay under 4 GiB total?
162        self.lines
163            .iter()
164            .map(|line| Utf16CodeUnits::length_of(AssumeUnder4GB, line))
165            .sum()
166    }
167
168    fn line(&self, index: usize) -> &str {
169        &self.lines[index]
170    }
171
172    fn line_for_index(&self, index: RopeIndex) -> &String {
173        &self.lines[index.line]
174    }
175
176    fn line_for_index_mut(&mut self, index: RopeIndex) -> &mut String {
177        &mut self.lines[index.line]
178    }
179
180    fn last_index_in_line(&self, line: usize) -> RopeIndex {
181        if line >= self.lines.len() - 1 {
182            return self.last_index();
183        }
184        RopeIndex {
185            line,
186            code_point: self.line(line).len() - 1,
187        }
188    }
189
190    /// Return a [`RopeIndex`] which points to the start of the subsequent line.
191    /// If the given [`RopeIndex`] is already on the final line, this will return
192    /// the final index of the entire [`Rope`].
193    fn start_of_following_line(&self, index: RopeIndex) -> RopeIndex {
194        if index.line >= self.lines.len() - 1 {
195            return self.last_index();
196        }
197        RopeIndex::new(index.line + 1, 0)
198    }
199
200    /// Return a [`RopeIndex`] which points to the end of preceding line. If already
201    /// at the end of the first line, this will return the start index of the entire
202    /// [`Rope`].
203    fn end_of_preceding_line(&self, index: RopeIndex) -> RopeIndex {
204        if index.line == 0 {
205            return Default::default();
206        }
207        let line_index = index.line.saturating_sub(1);
208        RopeIndex::new(line_index, self.line(line_index).len())
209    }
210
211    pub fn move_by(&self, origin: RopeIndex, unit: RopeMovement, amount: isize) -> RopeIndex {
212        if amount == 0 {
213            return origin;
214        }
215
216        match unit {
217            RopeMovement::Character | RopeMovement::Grapheme | RopeMovement::Word => {
218                self.move_by_iterator(origin, unit, amount)
219            },
220            RopeMovement::Line => self.move_by_lines(origin, amount),
221            RopeMovement::LineStartOrEnd => {
222                if amount >= 0 {
223                    self.last_index_in_line(origin.line)
224                } else {
225                    RopeIndex::new(origin.line, 0)
226                }
227            },
228            RopeMovement::RopeStartOrEnd => {
229                if amount >= 0 {
230                    self.last_index()
231                } else {
232                    Default::default()
233                }
234            },
235        }
236    }
237
238    fn move_by_lines(&self, origin: RopeIndex, lines_to_move: isize) -> RopeIndex {
239        let new_line_index = (origin.line as isize) + lines_to_move;
240        if new_line_index < 0 {
241            return Default::default();
242        }
243        if new_line_index > (self.lines.len() - 1) as isize {
244            return self.last_index();
245        }
246
247        let new_line_index = new_line_index.unsigned_abs();
248        let char_count = self.line(origin.line)[0..origin.code_point].chars().count();
249        let new_code_point_index = self
250            .line(new_line_index)
251            .char_indices()
252            .take(char_count)
253            .last()
254            .map(|(byte_index, character)| byte_index + character.len_utf8())
255            .unwrap_or_default();
256        RopeIndex::new(new_line_index, new_code_point_index)
257            .min(self.last_index_in_line(new_line_index))
258    }
259
260    fn move_by_iterator(&self, origin: RopeIndex, unit: RopeMovement, amount: isize) -> RopeIndex {
261        assert_ne!(amount, 0);
262        let (boundary_value, slice) = if amount > 0 {
263            (self.last_index(), self.slice(Some(origin), None))
264        } else {
265            (RopeIndex::default(), self.slice(None, Some(origin)))
266        };
267
268        let iterator = match unit {
269            RopeMovement::Character => slice.char_indices(),
270            RopeMovement::Grapheme => slice.grapheme_indices(),
271            RopeMovement::Word => slice.word_indices(),
272            _ => unreachable!("Should not be called for other movement types"),
273        };
274        let iterator = if amount > 0 {
275            Either::Left(iterator)
276        } else {
277            Either::Right(iterator.rev())
278        };
279
280        let mut iterations = amount.unsigned_abs();
281        for mut index in iterator {
282            iterations = iterations.saturating_sub(1);
283            if iterations == 0 {
284                // Instead of returning offsets for the absolute end of a line, return the
285                // start offset for the next line.
286                if index.code_point >= self.line_for_index(index).len() {
287                    index = self.start_of_following_line(index);
288                }
289                return index;
290            }
291        }
292
293        boundary_value
294    }
295
296    /// Given a [`RopeIndex`], clamp it and ensure that it is on a character boundary,
297    /// meaning that its indices are all bound by the actual size of the line and the
298    /// number of lines in this [`Rope`].
299    pub fn normalize_index(&self, rope_index: RopeIndex) -> RopeIndex {
300        let last_line = self.lines.len().saturating_sub(1);
301        let line_index = rope_index.line.min(last_line);
302
303        // This may appear a bit odd as we are adding an index to the end of the line,
304        // but `RopeIndex` isn't just an offset to a UTF-8 code point, but also can
305        // serve as the end of an exclusive range so there is one more index at the end
306        // that is still valid.
307        //
308        // Lines other than the last line have a trailing newline. We do not want to allow
309        // an index past the trailing newline.
310        let line = self.line(line_index);
311        let line_length_utf8 = if line_index == last_line {
312            line.len()
313        } else {
314            line.len() - 1
315        };
316
317        let mut code_point = rope_index.code_point.min(line_length_utf8);
318        while code_point < line.len() && !line.is_char_boundary(code_point) {
319            code_point += 1;
320        }
321
322        RopeIndex::new(line_index, code_point)
323    }
324
325    /// Convert a [`RopeIndex`] into a byte offset from the start of the content.
326    pub fn index_to_utf8_offset(&self, rope_index: RopeIndex) -> Utf8CodeUnits {
327        let rope_index = self.normalize_index(rope_index);
328        let sum = self
329            .lines
330            .iter()
331            .take(rope_index.line)
332            .map(String::len)
333            .sum::<usize>() +
334            rope_index.code_point;
335        // TODO: add some check that ropes stay under 4 GiB total?
336        Utf8CodeUnits(sum as u32)
337    }
338
339    pub fn index_to_utf16_offset(&self, rope_index: RopeIndex) -> Utf16CodeUnits {
340        let rope_index = self.normalize_index(rope_index);
341        let final_line = self.line(rope_index.line);
342
343        // The offset might be past the end of the line due to being an exclusive offset.
344        let final_line_offset =
345            Utf16CodeUnits::length_of(AssumeUnder4GB, &final_line[..rope_index.code_point]);
346
347        // TODO: add some check that ropes stay under 4 GiB total?
348        self.lines[..rope_index.line]
349            .iter()
350            .map(|line| Utf16CodeUnits::length_of(AssumeUnder4GB, line))
351            .sum::<Utf16CodeUnits>() +
352            final_line_offset
353    }
354
355    /// Convert a [`RopeIndex`] into a character offset from the start of the content.
356    pub fn index_to_character_offset(&self, rope_index: RopeIndex) -> Utf32CodeUnits {
357        let rope_index = self.normalize_index(rope_index);
358
359        // The offset might be past the end of the line due to being an exclusive offset.
360        let final_line = self.line(rope_index.line);
361        // TODO: add some check that ropes stay under 4 GiB total?
362        let final_line_offset =
363            Utf32CodeUnits::length_of(AssumeUnder4GB, &final_line[..rope_index.code_point]);
364        self.lines
365            .iter()
366            .take(rope_index.line)
367            .map(|line| Utf32CodeUnits::length_of(AssumeUnder4GB, line))
368            .sum::<Utf32CodeUnits>() +
369            final_line_offset
370    }
371
372    /// Convert a byte offset from the start of the content into a [`RopeIndex`].
373    pub fn utf8_offset_to_rope_index(&self, utf8_offset: Utf8CodeUnits) -> RopeIndex {
374        let mut current_utf8_offset = usize::from(utf8_offset);
375        for (line_index, line) in self.lines.iter().enumerate() {
376            if current_utf8_offset == 0 || current_utf8_offset < line.len() {
377                return RopeIndex::new(line_index, current_utf8_offset);
378            }
379            current_utf8_offset -= line.len();
380        }
381        self.last_index()
382    }
383
384    pub fn utf16_offset_to_utf8_offset(&self, utf16_offset: Utf16CodeUnits) -> Utf8CodeUnits {
385        // TODO: add some check that ropes stay under 4 GiB total?
386        utf16_offset.to_utf8_code_units_in_iter(AssumeUnder4GB, &self.lines)
387    }
388
389    /// Find the boundaries of the word most relevant to the given [`RopeIndex`]. Word
390    /// returned in order or precedence:
391    ///
392    /// - If the index intersects the word or is the index directly preceding a word,
393    ///   the boundaries of that word are returned.
394    /// - The word preceding the cursor.
395    /// - If there is no word preceding the cursor, the start of the line to the end
396    ///   of the next word.
397    pub fn relevant_word_boundaries<'a>(&'a self, index: RopeIndex) -> RopeSlice<'a> {
398        let line = self.line_for_index(index);
399        let mut result_start = 0;
400        let mut result_end = None;
401        for (word_start, word) in line.unicode_word_indices() {
402            if word_start > index.code_point {
403                result_end = result_end.or_else(|| Some(word_start + word.len()));
404                break;
405            }
406            result_start = word_start;
407            result_end = Some(word_start + word.len());
408        }
409
410        let result_end = result_end.unwrap_or(result_start);
411        self.slice(
412            Some(RopeIndex::new(index.line, result_start)),
413            Some(RopeIndex::new(index.line, result_end)),
414        )
415    }
416
417    /// Return the boundaries of the line that contains the given [`RopeIndex`].
418    pub fn line_boundaries<'a>(&'a self, index: RopeIndex) -> RopeSlice<'a> {
419        self.slice(
420            Some(RopeIndex::new(index.line, 0)),
421            Some(self.last_index_in_line(index.line)),
422        )
423    }
424
425    fn character_at(&self, index: RopeIndex) -> Option<char> {
426        let line = self.line_for_index(index);
427        line[index.code_point..].chars().next()
428    }
429
430    fn character_before(&self, index: RopeIndex) -> Option<char> {
431        let line = self.line_for_index(index);
432        line[..index.code_point].chars().next_back()
433    }
434}
435
436/// An index into a [`Rope`] data structure. Used to efficiently identify a particular
437/// position in a [`Rope`]. As [`Rope`] always uses Rust strings interally, code point
438/// indices represented in a [`RopeIndex`] are assumed to be UTF-8 code points (one byte
439/// each).
440///
441/// Note that it is possible for a [`RopeIndex`] to point past the end of the last line,
442/// as it can be used in exclusive ranges. In lines other than the last line, it should
443/// always refer to offsets before the trailing newline.
444#[derive(Clone, Copy, Debug, Default, Eq, MallocSizeOf, PartialEq, PartialOrd, Ord)]
445pub struct RopeIndex {
446    /// The index of the line that this [`RopeIndex`] refers to.
447    pub line: usize,
448    /// The index of the code point on the [`RopeIndex`]'s line in UTF-8 code
449    /// points.
450    ///
451    /// Note: This is not a `Utf8CodeUnits` in order to avoid continually having
452    /// to unpack the inner value.
453    pub code_point: usize,
454}
455
456impl RopeIndex {
457    pub fn new(line: usize, code_point: usize) -> Self {
458        Self { line, code_point }
459    }
460}
461
462/// A slice of a [`Rope`]. This can be used to to iterate over a subset of characters of a
463/// [`Rope`] or to return the content of the [`RopeSlice`] as a `String`.
464pub struct RopeSlice<'a> {
465    /// The underlying [`Rope`] of this [`RopeSlice`]
466    rope: &'a Rope,
467    /// The inclusive `RopeIndex` of the start of this [`RopeSlice`].
468    pub start: RopeIndex,
469    /// The exclusive end `RopeIndex` of this [`RopeSlice`].
470    pub end: RopeIndex,
471}
472
473impl From<RopeSlice<'_>> for String {
474    fn from(slice: RopeSlice<'_>) -> Self {
475        if slice.start.line == slice.end.line {
476            slice.rope.line_for_index(slice.start)[slice.start.code_point..slice.end.code_point]
477                .into()
478        } else {
479            once(&slice.rope.line_for_index(slice.start)[slice.start.code_point..])
480                .chain(
481                    (slice.start.line + 1..slice.end.line)
482                        .map(|line_index| slice.rope.line(line_index)),
483                )
484                .chain(once(
485                    &slice.rope.line_for_index(slice.end)[..slice.end.code_point],
486                ))
487                .collect()
488        }
489    }
490}
491
492impl<'a> RopeSlice<'a> {
493    pub fn chars(self) -> RopeChars<'a> {
494        RopeChars {
495            movement_iterator: RopeMovementIterator {
496                slice: self,
497                end_of_forward_motion: |_, string| {
498                    let (offset, character) = string.char_indices().next()?;
499                    Some(offset + character.len_utf8())
500                },
501                start_of_backward_motion: |_, string: &str| {
502                    Some(string.char_indices().next_back()?.0)
503                },
504            },
505        }
506    }
507
508    fn char_indices(self) -> RopeMovementIterator<'a> {
509        RopeMovementIterator {
510            slice: self,
511            end_of_forward_motion: |_, string| {
512                let (offset, character) = string.char_indices().next()?;
513                Some(offset + character.len_utf8())
514            },
515            start_of_backward_motion: |_, string: &str| Some(string.char_indices().next_back()?.0),
516        }
517    }
518
519    fn grapheme_indices(self) -> RopeMovementIterator<'a> {
520        RopeMovementIterator {
521            slice: self,
522            end_of_forward_motion: |_, string| {
523                let (offset, grapheme) = string.grapheme_indices(true).next()?;
524                Some(offset + grapheme.len())
525            },
526            start_of_backward_motion: |_, string| {
527                Some(string.grapheme_indices(true).next_back()?.0)
528            },
529        }
530    }
531
532    fn word_indices(self) -> RopeMovementIterator<'a> {
533        RopeMovementIterator {
534            slice: self,
535            end_of_forward_motion: |_, string| {
536                let (offset, word) = string.unicode_word_indices().next()?;
537                Some(offset + word.len())
538            },
539            start_of_backward_motion: |_, string| {
540                Some(string.unicode_word_indices().next_back()?.0)
541            },
542        }
543    }
544
545    pub fn len_utf16(self) -> Utf16CodeUnits {
546        // TODO: add some check that ropes stay under 4 GiB total?
547        // TODO: iterate `&str` slices instead and use `Utf16CodeUnits::length_of`
548        self.chars().map(Utf16CodeUnits::length_of_char).sum()
549    }
550}
551
552/// A generic movement iterator for a [`Rope`]. This can move in both directions. Note
553/// than when moving forward and backward, the indices returned for each unit are
554/// different. When moving forward, the end of the unit of movement is returned and when
555/// moving backward the start of the unit of movement is returned. This matches the
556/// expected behavior when interactively moving through editable text.
557struct RopeMovementIterator<'a> {
558    slice: RopeSlice<'a>,
559    end_of_forward_motion: fn(&RopeSlice, &'a str) -> Option<usize>,
560    start_of_backward_motion: fn(&RopeSlice, &'a str) -> Option<usize>,
561}
562
563impl Iterator for RopeMovementIterator<'_> {
564    type Item = RopeIndex;
565
566    fn next(&mut self) -> Option<Self::Item> {
567        // If the two indices have crossed over, iteration is done.
568        if self.slice.start >= self.slice.end {
569            return None;
570        }
571
572        assert!(self.slice.start.line < self.slice.rope.lines.len());
573        let line = self.slice.rope.line_for_index(self.slice.start);
574
575        if self.slice.start.code_point < line.len() + 1 &&
576            let Some(end_offset) =
577                (self.end_of_forward_motion)(&self.slice, &line[self.slice.start.code_point..])
578        {
579            self.slice.start.code_point += end_offset;
580            return Some(self.slice.start);
581        }
582
583        // Advance the line as we are at the end of the line.
584        self.slice.start = self.slice.rope.start_of_following_line(self.slice.start);
585        self.next()
586    }
587}
588
589impl DoubleEndedIterator for RopeMovementIterator<'_> {
590    fn next_back(&mut self) -> Option<Self::Item> {
591        // If the two indices have crossed over, iteration is done.
592        if self.slice.end <= self.slice.start {
593            return None;
594        }
595
596        let line = self.slice.rope.line_for_index(self.slice.end);
597        if self.slice.end.code_point > 0 &&
598            let Some(new_start_index) =
599                (self.start_of_backward_motion)(&self.slice, &line[..self.slice.end.code_point])
600        {
601            self.slice.end.code_point = new_start_index;
602            return Some(self.slice.end);
603        }
604
605        // Decrease the line index as we are at the start of the line.
606        self.slice.end = self.slice.rope.end_of_preceding_line(self.slice.end);
607        self.next_back()
608    }
609}
610
611/// A `Chars`-like iterator for [`Rope`].
612pub struct RopeChars<'a> {
613    movement_iterator: RopeMovementIterator<'a>,
614}
615
616impl Iterator for RopeChars<'_> {
617    type Item = char;
618    fn next(&mut self) -> Option<Self::Item> {
619        self.movement_iterator
620            .next()
621            .and_then(|index| self.movement_iterator.slice.rope.character_before(index))
622    }
623}
624
625impl DoubleEndedIterator for RopeChars<'_> {
626    fn next_back(&mut self) -> Option<Self::Item> {
627        self.movement_iterator
628            .next_back()
629            .and_then(|index| self.movement_iterator.slice.rope.character_at(index))
630    }
631}
632
633#[test]
634fn test_rope_index_conversion_to_utf8_offset() {
635    let rope = Rope::new("A\nBB\nCCC\nDDDD");
636    assert_eq!(
637        rope.index_to_utf8_offset(RopeIndex::new(0, 0)),
638        Utf8CodeUnits(0),
639    );
640    assert_eq!(
641        rope.index_to_utf8_offset(RopeIndex::new(0, 1)),
642        Utf8CodeUnits(1),
643    );
644    assert_eq!(
645        rope.index_to_utf8_offset(RopeIndex::new(0, 10)),
646        Utf8CodeUnits(1),
647        "RopeIndex with offset past the end of the line should return final offset in line",
648    );
649    assert_eq!(
650        rope.index_to_utf8_offset(RopeIndex::new(1, 0)),
651        Utf8CodeUnits(2),
652    );
653    assert_eq!(
654        rope.index_to_utf8_offset(RopeIndex::new(1, 2)),
655        Utf8CodeUnits(4),
656    );
657
658    assert_eq!(
659        rope.index_to_utf8_offset(RopeIndex::new(3, 0)),
660        Utf8CodeUnits(9),
661    );
662    assert_eq!(
663        rope.index_to_utf8_offset(RopeIndex::new(3, 3)),
664        Utf8CodeUnits(12),
665    );
666    assert_eq!(
667        rope.index_to_utf8_offset(RopeIndex::new(3, 4)),
668        Utf8CodeUnits(13),
669        "There should be no newline at the end of the TextInput",
670    );
671    assert_eq!(
672        rope.index_to_utf8_offset(RopeIndex::new(3, 40)),
673        Utf8CodeUnits(13),
674        "There should be no newline at the end of the TextInput",
675    );
676}
677
678#[test]
679fn test_rope_index_conversion_to_utf16_offset() {
680    let rope = Rope::new("A\nBB\nCCC\n家家");
681    assert_eq!(
682        rope.index_to_utf16_offset(RopeIndex::new(0, 0)),
683        Utf16CodeUnits(0),
684    );
685    assert_eq!(
686        rope.index_to_utf16_offset(RopeIndex::new(0, 1)),
687        Utf16CodeUnits(1),
688    );
689    assert_eq!(
690        rope.index_to_utf16_offset(RopeIndex::new(0, 10)),
691        Utf16CodeUnits(1),
692        "RopeIndex with offset past the end of the line should return final offset in line",
693    );
694    assert_eq!(
695        rope.index_to_utf16_offset(RopeIndex::new(3, 0)),
696        Utf16CodeUnits(9),
697    );
698
699    assert_eq!(
700        rope.index_to_utf16_offset(RopeIndex::new(3, 3)),
701        Utf16CodeUnits(10),
702        "3 code unit UTF-8 encodede character"
703    );
704    assert_eq!(
705        rope.index_to_utf16_offset(RopeIndex::new(3, 6)),
706        Utf16CodeUnits(11),
707    );
708    assert_eq!(
709        rope.index_to_utf16_offset(RopeIndex::new(3, 20)),
710        Utf16CodeUnits(11),
711    );
712}
713
714#[test]
715fn test_utf16_offset_to_utf8_offset() {
716    let rope = Rope::new("A\nBB\nCCC\n家家");
717    assert_eq!(
718        rope.utf16_offset_to_utf8_offset(Utf16CodeUnits(0)),
719        Utf8CodeUnits(0),
720    );
721    assert_eq!(
722        rope.utf16_offset_to_utf8_offset(Utf16CodeUnits(1)),
723        Utf8CodeUnits(1),
724    );
725    assert_eq!(
726        rope.utf16_offset_to_utf8_offset(Utf16CodeUnits(2)),
727        Utf8CodeUnits(2),
728        "Offset past the end of the line",
729    );
730    assert_eq!(
731        rope.utf16_offset_to_utf8_offset(Utf16CodeUnits(9)),
732        Utf8CodeUnits(9),
733    );
734
735    assert_eq!(
736        rope.utf16_offset_to_utf8_offset(Utf16CodeUnits(10)),
737        Utf8CodeUnits(12),
738        "3 code unit UTF-8 encodede character"
739    );
740    assert_eq!(
741        rope.utf16_offset_to_utf8_offset(Utf16CodeUnits(11)),
742        Utf8CodeUnits(15),
743    );
744    assert_eq!(
745        rope.utf16_offset_to_utf8_offset(Utf16CodeUnits(300)),
746        Utf8CodeUnits(15),
747    );
748}
749
750#[test]
751fn test_rope_delete_slice() {
752    let mut rope = Rope::new("ABC\nDEF\n");
753    rope.delete_range(RopeIndex::new(0, 1)..RopeIndex::new(0, 2));
754    assert_eq!(rope.contents(), "AC\nDEF\n");
755
756    // Trying to delete beyond the last index of the line should note remove any trailing
757    // newlines from the rope.
758    let mut rope = Rope::new("ABC\nDEF\n");
759    rope.delete_range(RopeIndex::new(0, 3)..RopeIndex::new(0, 4));
760    assert_eq!(rope.lines, ["ABC\n", "DEF\n", ""]);
761
762    let mut rope = Rope::new("ABC\nDEF\n");
763    rope.delete_range(RopeIndex::new(0, 0)..RopeIndex::new(0, 4));
764    assert_eq!(rope.lines, ["\n", "DEF\n", ""]);
765
766    let mut rope = Rope::new("A\nBB\nCCC");
767    rope.delete_range(RopeIndex::new(0, 2)..RopeIndex::new(1, 0));
768    assert_eq!(rope.lines, ["ABB\n", "CCC"]);
769}
770
771#[test]
772fn test_rope_replace_slice() {
773    let mut rope = Rope::new("AAA\nBBB\nCCC");
774    rope.replace_range(RopeIndex::new(0, 1)..RopeIndex::new(0, 2), "x");
775    assert_eq!(rope.contents(), "AxA\nBBB\nCCC",);
776
777    let mut rope = Rope::new("A\nBB\nCCC");
778    rope.replace_range(RopeIndex::new(0, 2)..RopeIndex::new(1, 0), "D");
779    assert_eq!(rope.lines, ["ADBB\n", "CCC"]);
780
781    let mut rope = Rope::new("AAA\nBBB\nCCC\nDDD");
782    rope.replace_range(RopeIndex::new(0, 2)..RopeIndex::new(2, 1), "x");
783    assert_eq!(rope.lines, ["AAxCC\n", "DDD"]);
784}
785
786#[test]
787fn test_rope_relevant_word() {
788    let rope = Rope::new("AAA    BBB   CCC");
789    let boundaries = rope.relevant_word_boundaries(RopeIndex::new(0, 0));
790    assert_eq!(boundaries.start, RopeIndex::new(0, 0));
791    assert_eq!(boundaries.end, RopeIndex::new(0, 3));
792
793    // Choose previous word if starting on whitespace.
794    let boundaries = rope.relevant_word_boundaries(RopeIndex::new(0, 4));
795    assert_eq!(boundaries.start, RopeIndex::new(0, 0));
796    assert_eq!(boundaries.end, RopeIndex::new(0, 3));
797
798    // Choose next word if starting at word start.
799    let boundaries = rope.relevant_word_boundaries(RopeIndex::new(0, 7));
800    assert_eq!(boundaries.start, RopeIndex::new(0, 7));
801    assert_eq!(boundaries.end, RopeIndex::new(0, 10));
802
803    // Choose word if starting at in middle.
804    let boundaries = rope.relevant_word_boundaries(RopeIndex::new(0, 8));
805    assert_eq!(boundaries.start, RopeIndex::new(0, 7));
806    assert_eq!(boundaries.end, RopeIndex::new(0, 10));
807
808    // Choose start of line to end of first word if in whitespace at start of line.
809    let rope = Rope::new("         AAA    BBB   CCC");
810    let boundaries = rope.relevant_word_boundaries(RopeIndex::new(0, 3));
811    assert_eq!(boundaries.start, RopeIndex::new(0, 0));
812    assert_eq!(boundaries.end, RopeIndex::new(0, 12));
813
814    // Works properly if line is empty.
815    let rope = Rope::new("");
816    let boundaries = rope.relevant_word_boundaries(RopeIndex::new(0, 0));
817    assert_eq!(boundaries.start, RopeIndex::new(0, 0));
818    assert_eq!(boundaries.end, RopeIndex::new(0, 0));
819}
820
821#[test]
822fn test_rope_index_intersects_character() {
823    let rope = Rope::new("񉡚");
824    let rope_index = RopeIndex::new(0, 1);
825    assert_eq!(rope.normalize_index(rope_index), RopeIndex::new(0, 4));
826    assert_eq!(rope.index_to_utf16_offset(rope_index), Utf16CodeUnits(2));
827    assert_eq!(rope.index_to_utf8_offset(rope_index), Utf8CodeUnits(4));
828
829    let rope = Rope::new("abc\ndef");
830    assert_eq!(
831        rope.normalize_index(RopeIndex::new(0, 100)),
832        RopeIndex::new(0, 3),
833        "Normalizing index past end of line should just clamp to line length."
834    );
835    assert_eq!(
836        rope.normalize_index(RopeIndex::new(1, 100)),
837        RopeIndex::new(1, 3),
838        "Normalizing index past end of line should just clamp to line length."
839    );
840}