Skip to main content

freya_edit/
text_editor.rs

1use std::{
2    borrow::Cow,
3    cmp::Ordering,
4    fmt::Display,
5    ops::Range,
6};
7
8use freya_clipboard::clipboard::Clipboard;
9use keyboard_types::{
10    Key,
11    Modifiers,
12    NamedKey,
13};
14use unicode_segmentation::UnicodeSegmentation;
15
16use crate::editor_history::EditorHistory;
17
18#[derive(PartialEq, Clone, Debug, Copy, Hash)]
19pub enum EditorLine {
20    /// Only one `paragraph` element exists in the whole editor.
21    SingleParagraph,
22    /// There are multiple `paragraph` elements in the editor, one per line.
23    Paragraph(usize),
24}
25
26/// Holds the position of a cursor in a text
27#[derive(Clone, PartialEq, Debug)]
28pub enum TextSelection {
29    Cursor(usize),
30    Range { from: usize, to: usize },
31}
32
33impl TextSelection {
34    /// Create a new [TextSelection::Cursor]
35    pub fn new_cursor(pos: usize) -> Self {
36        Self::Cursor(pos)
37    }
38
39    /// Create a new [TextSelection::Range]
40    pub fn new_range((from, to): (usize, usize)) -> Self {
41        Self::Range { from, to }
42    }
43
44    /// Get the position
45    pub fn pos(&self) -> usize {
46        self.end()
47    }
48
49    /// Set the selection as a cursor
50    pub fn set_as_cursor(&mut self) {
51        *self = Self::Cursor(self.end())
52    }
53
54    /// Set the selection as a range
55    pub fn set_as_range(&mut self) {
56        *self = Self::Range {
57            from: self.start(),
58            to: self.end(),
59        }
60    }
61
62    /// Get the start of the cursor position.
63    pub fn start(&self) -> usize {
64        match self {
65            Self::Cursor(pos) => *pos,
66            Self::Range { from, .. } => *from,
67        }
68    }
69
70    /// Get the end of the cursor position.
71    pub fn end(&self) -> usize {
72        match self {
73            Self::Cursor(pos) => *pos,
74            Self::Range { to, .. } => *to,
75        }
76    }
77
78    /// Move the end position of the cursor.
79    pub fn move_to(&mut self, position: usize) {
80        match self {
81            Self::Cursor(pos) => *pos = position,
82            Self::Range { to, .. } => {
83                *to = position;
84            }
85        }
86    }
87
88    pub fn is_range(&self) -> bool {
89        matches!(self, Self::Range { .. })
90    }
91}
92
93/// A text line from a [TextEditor]
94#[derive(Clone)]
95pub struct Line<'a> {
96    pub text: Cow<'a, str>,
97    pub utf16_len: usize,
98}
99
100impl Line<'_> {
101    /// Get the length of the line
102    pub fn utf16_len(&self) -> usize {
103        self.utf16_len
104    }
105}
106
107impl Display for Line<'_> {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        f.write_str(&self.text)
110    }
111}
112
113bitflags::bitflags! {
114    /// Events for [TextEditor]
115    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
116    pub struct TextEvent: u8 {
117         /// Cursor position has been moved
118        const CURSOR_CHANGED = 0x01;
119        /// Text has changed
120        const TEXT_CHANGED = 0x02;
121        /// Selected text has changed
122        const SELECTION_CHANGED = 0x04;
123    }
124}
125
126/// Common trait for editable texts
127pub trait TextEditor {
128    type LinesIterator<'a>: Iterator<Item = Line<'a>>
129    where
130        Self: 'a;
131
132    fn set(&mut self, text: &str);
133
134    /// Iterator over all the lines in the text.
135    fn lines(&self) -> Self::LinesIterator<'_>;
136
137    /// Insert a character in the text in the given position.
138    fn insert_char(&mut self, char: char, char_idx: usize) -> usize;
139
140    /// Insert a string in the text in the given position.
141    fn insert(&mut self, text: &str, char_idx: usize) -> usize;
142
143    /// Remove a part of the text.
144    fn remove(&mut self, range: Range<usize>) -> usize;
145
146    /// Get line from the given char
147    fn char_to_line(&self, char_idx: usize) -> usize;
148
149    /// Get the first char from the given line
150    fn line_to_char(&self, line_idx: usize) -> usize;
151
152    fn utf16_cu_to_char(&self, utf16_cu_idx: usize) -> usize;
153
154    fn char_to_utf16_cu(&self, idx: usize) -> usize;
155
156    /// Get a line from the text
157    fn line(&self, line_idx: usize) -> Option<Line<'_>>;
158
159    /// Total of lines
160    fn len_lines(&self) -> usize;
161
162    /// Total of chars
163    fn len_chars(&self) -> usize;
164
165    /// Total of utf16 code units
166    fn len_utf16_cu(&self) -> usize;
167
168    /// Get a readable text selection
169    fn selection(&self) -> &TextSelection;
170
171    /// Get a mutable reference to text selection
172    fn selection_mut(&mut self) -> &mut TextSelection;
173
174    /// Get the cursor row
175    fn cursor_row(&self) -> usize {
176        let pos = self.cursor_pos();
177        let pos_utf8 = self.utf16_cu_to_char(pos);
178        self.char_to_line(pos_utf8)
179    }
180
181    /// Get the cursor column
182    fn cursor_col(&self) -> usize {
183        let pos = self.cursor_pos();
184        let pos_utf8 = self.utf16_cu_to_char(pos);
185        let line = self.char_to_line(pos_utf8);
186        let line_char_utf8 = self.line_to_char(line);
187        let line_char = self.char_to_utf16_cu(line_char_utf8);
188        pos - line_char
189    }
190
191    /// Move the cursor 1 line down
192    fn cursor_down(&mut self) -> bool {
193        let old_row = self.cursor_row();
194        let old_col = self.cursor_col();
195
196        match old_row.cmp(&(self.len_lines() - 1)) {
197            Ordering::Less => {
198                // One line below
199                let new_row = old_row + 1;
200                let new_row_char = self.char_to_utf16_cu(self.line_to_char(new_row));
201                let new_row_len = self.line(new_row).unwrap().utf16_len();
202                let new_col = old_col.min(new_row_len.saturating_sub(1));
203                self.selection_mut().move_to(new_row_char + new_col);
204
205                true
206            }
207            Ordering::Equal => {
208                let end = self.len_utf16_cu();
209                // Reached max
210                self.selection_mut().move_to(end);
211
212                true
213            }
214            Ordering::Greater => {
215                // Can't go further
216
217                false
218            }
219        }
220    }
221
222    /// Move the cursor 1 line up
223    fn cursor_up(&mut self) -> bool {
224        let pos = self.cursor_pos();
225        let old_row = self.cursor_row();
226        let old_col = self.cursor_col();
227
228        if pos > 0 {
229            // Reached max
230            if old_row == 0 {
231                self.selection_mut().move_to(0);
232            } else {
233                let new_row = old_row - 1;
234                let new_row_char = self.char_to_utf16_cu(self.line_to_char(new_row));
235                let new_row_len = self.line(new_row).unwrap().utf16_len();
236                let new_col = old_col.min(new_row_len.saturating_sub(1));
237                self.selection_mut().move_to(new_row_char + new_col);
238            }
239
240            true
241        } else {
242            false
243        }
244    }
245
246    /// Move the cursor 1 char to the right
247    fn cursor_right(&mut self) -> bool {
248        if self.cursor_pos() < self.len_utf16_cu() {
249            let to = self.selection().end() + 1;
250            self.selection_mut().move_to(to);
251
252            true
253        } else {
254            false
255        }
256    }
257
258    /// Move the cursor 1 char to the left
259    fn cursor_left(&mut self) -> bool {
260        if self.cursor_pos() > 0 {
261            let to = self.selection().end() - 1;
262            self.selection_mut().move_to(to);
263
264            true
265        } else {
266            false
267        }
268    }
269
270    /// Move the cursor to the end of the next word.
271    fn cursor_word_right(&mut self) -> bool {
272        let pos = self.cursor_pos();
273        let len = self.len_utf16_cu();
274        if pos >= len {
275            return false;
276        }
277
278        // Walk forward line by line starting at the cursor.
279        let start_char = self.utf16_cu_to_char(pos);
280        let initial_line = self.char_to_line(start_char);
281        let initial_offset = start_char - self.line_to_char(initial_line);
282
283        for line_idx in initial_line..self.len_lines() {
284            let Some(line) = self.line(line_idx) else {
285                continue;
286            };
287            let line_char_offset = self.line_to_char(line_idx);
288            let from = if line_idx == initial_line {
289                initial_offset
290            } else {
291                0
292            };
293
294            // Stop at the end of the first non-whitespace segment past the cursor.
295            let mut char_offset = 0;
296            for word in line.text.split_word_bounds() {
297                char_offset += word.chars().count();
298                if char_offset > from && !word.chars().all(char::is_whitespace) {
299                    let new_pos = self.char_to_utf16_cu(line_char_offset + char_offset);
300                    self.selection_mut().move_to(new_pos);
301                    return true;
302                }
303            }
304        }
305
306        // Trailing whitespace only, snap to text end.
307        self.selection_mut().move_to(len);
308        true
309    }
310
311    /// Move the cursor to the start of the previous word.
312    fn cursor_word_left(&mut self) -> bool {
313        let pos = self.cursor_pos();
314        if pos == 0 {
315            return false;
316        }
317
318        // Walk backward line by line starting at the cursor.
319        let start_char = self.utf16_cu_to_char(pos);
320        let initial_line = self.char_to_line(start_char);
321        let initial_offset = start_char - self.line_to_char(initial_line);
322
323        for line_idx in (0..=initial_line).rev() {
324            let Some(line) = self.line(line_idx) else {
325                continue;
326            };
327            let line_char_offset = self.line_to_char(line_idx);
328            let to = if line_idx == initial_line {
329                initial_offset
330            } else {
331                line.text.chars().count()
332            };
333
334            // Track the latest non-whitespace segment that starts before the cursor.
335            let mut char_offset = 0;
336            let mut last_word_start = None;
337            for word in line.text.split_word_bounds() {
338                if char_offset >= to {
339                    break;
340                }
341                if !word.chars().all(char::is_whitespace) {
342                    last_word_start = Some(char_offset);
343                }
344                char_offset += word.chars().count();
345            }
346
347            // Found one on this line, jump to its start.
348            if let Some(start) = last_word_start {
349                let new_pos = self.char_to_utf16_cu(line_char_offset + start);
350                self.selection_mut().move_to(new_pos);
351                return true;
352            }
353        }
354
355        // Leading whitespace only, snap to text start.
356        self.selection_mut().move_to(0);
357        true
358    }
359
360    /// Get the cursor position
361    fn cursor_pos(&self) -> usize {
362        self.selection().pos()
363    }
364
365    /// Move the cursor position
366    fn move_cursor_to(&mut self, pos: usize) {
367        self.selection_mut().move_to(pos);
368    }
369
370    // Check if has any selection at all
371    fn has_any_selection(&self) -> bool;
372
373    // Return the selected text
374    fn get_selection(&self) -> Option<(usize, usize)>;
375
376    // Return the visible selected text for the given editor line
377    fn get_visible_selection(&self, editor_line: EditorLine) -> Option<(usize, usize)> {
378        let (selected_from, selected_to) = match self.selection() {
379            TextSelection::Cursor(_) => return None,
380            TextSelection::Range { from, to } => (*from, *to),
381        };
382
383        match editor_line {
384            EditorLine::Paragraph(line_index) => {
385                let selected_from_row = self.char_to_line(self.utf16_cu_to_char(selected_from));
386                let selected_to_row = self.char_to_line(self.utf16_cu_to_char(selected_to));
387
388                let editor_row_idx = self.char_to_utf16_cu(self.line_to_char(line_index));
389                let selected_from_row_idx =
390                    self.char_to_utf16_cu(self.line_to_char(selected_from_row));
391                let selected_to_row_idx = self.char_to_utf16_cu(self.line_to_char(selected_to_row));
392
393                let selected_from_col_idx = selected_from - selected_from_row_idx;
394                let selected_to_col_idx = selected_to - selected_to_row_idx;
395
396                // Between starting line and endling line
397                if (line_index > selected_from_row && line_index < selected_to_row)
398                    || (line_index < selected_from_row && line_index > selected_to_row)
399                {
400                    let len = self.line(line_index).unwrap().utf16_len();
401                    return Some((0, len));
402                }
403
404                match selected_from_row.cmp(&selected_to_row) {
405                    // Selection direction is from bottom -> top
406                    Ordering::Greater => {
407                        if selected_from_row == line_index {
408                            // Starting line
409                            Some((0, selected_from_col_idx))
410                        } else if selected_to_row == line_index {
411                            // Ending line
412                            let len = self.line(selected_to_row).unwrap().utf16_len();
413                            Some((selected_to_col_idx, len))
414                        } else {
415                            None
416                        }
417                    }
418                    // Selection direction is from top -> bottom
419                    Ordering::Less => {
420                        if selected_from_row == line_index {
421                            // Starting line
422                            let len = self.line(selected_from_row).unwrap().utf16_len();
423                            Some((selected_from_col_idx, len))
424                        } else if selected_to_row == line_index {
425                            // Ending line
426                            Some((0, selected_to_col_idx))
427                        } else {
428                            None
429                        }
430                    }
431                    Ordering::Equal if selected_from_row == line_index => {
432                        // Starting and endline line are the same
433                        Some((selected_from - editor_row_idx, selected_to - editor_row_idx))
434                    }
435                    _ => None,
436                }
437            }
438            EditorLine::SingleParagraph => Some((selected_from, selected_to)),
439        }
440    }
441
442    // Remove the selection
443    fn clear_selection(&mut self);
444
445    // Select some text
446    fn set_selection(&mut self, selected: (usize, usize));
447
448    // Measure a new text selection
449
450    fn measure_selection(&self, to: usize, line_index: EditorLine) -> TextSelection {
451        let mut selection = self.selection().clone();
452
453        match line_index {
454            EditorLine::Paragraph(line_index) => {
455                let row_char = self.line_to_char(line_index);
456                let pos = self.char_to_utf16_cu(row_char) + to;
457                selection.move_to(pos);
458            }
459            EditorLine::SingleParagraph => {
460                selection.move_to(to);
461            }
462        }
463
464        selection
465    }
466
467    // Process a Keyboard event
468    fn process_key(
469        &mut self,
470        key: &Key,
471        modifiers: &Modifiers,
472        allow_tabs: bool,
473        allow_changes: bool,
474        allow_clipboard: bool,
475    ) -> TextEvent {
476        let mut event = TextEvent::empty();
477
478        let selection = self.get_selection();
479        let skip_arrows_movement = !modifiers.contains(Modifiers::SHIFT) && selection.is_some();
480
481        match key {
482            Key::Named(NamedKey::Shift) => {}
483            Key::Named(NamedKey::Control) => {}
484            Key::Named(NamedKey::Alt) => {}
485            Key::Named(NamedKey::Escape) => {
486                self.clear_selection();
487            }
488            Key::Named(NamedKey::ArrowDown) => {
489                if modifiers.contains(Modifiers::SHIFT) {
490                    self.selection_mut().set_as_range();
491                } else {
492                    self.selection_mut().set_as_cursor();
493                }
494
495                if !skip_arrows_movement && self.cursor_down() {
496                    event.insert(TextEvent::CURSOR_CHANGED);
497                }
498            }
499            Key::Named(NamedKey::ArrowLeft) => {
500                if modifiers.contains(Modifiers::SHIFT) {
501                    self.selection_mut().set_as_range();
502                } else {
503                    self.selection_mut().set_as_cursor();
504                }
505
506                let word_jump = if cfg!(target_os = "macos") {
507                    modifiers.contains(Modifiers::ALT)
508                } else {
509                    modifiers.contains(Modifiers::CONTROL)
510                };
511
512                let moved = !skip_arrows_movement
513                    && if word_jump {
514                        self.cursor_word_left()
515                    } else {
516                        self.cursor_left()
517                    };
518
519                if moved {
520                    event.insert(TextEvent::CURSOR_CHANGED);
521                }
522            }
523            Key::Named(NamedKey::ArrowRight) => {
524                if modifiers.contains(Modifiers::SHIFT) {
525                    self.selection_mut().set_as_range();
526                } else {
527                    self.selection_mut().set_as_cursor();
528                }
529
530                let word_jump = if cfg!(target_os = "macos") {
531                    modifiers.contains(Modifiers::ALT)
532                } else {
533                    modifiers.contains(Modifiers::CONTROL)
534                };
535
536                let moved = !skip_arrows_movement
537                    && if word_jump {
538                        self.cursor_word_right()
539                    } else {
540                        self.cursor_right()
541                    };
542
543                if moved {
544                    event.insert(TextEvent::CURSOR_CHANGED);
545                }
546            }
547            Key::Named(NamedKey::ArrowUp) => {
548                if modifiers.contains(Modifiers::SHIFT) {
549                    self.selection_mut().set_as_range();
550                } else {
551                    self.selection_mut().set_as_cursor();
552                }
553
554                if !skip_arrows_movement && self.cursor_up() {
555                    event.insert(TextEvent::CURSOR_CHANGED);
556                }
557            }
558            Key::Named(NamedKey::Backspace) if allow_changes => {
559                let cursor_pos = self.cursor_pos();
560                let selection = self.get_selection_range();
561
562                if let Some((start, end)) = selection {
563                    self.remove(start..end);
564                    self.move_cursor_to(start);
565                    event.insert(TextEvent::TEXT_CHANGED);
566                } else if cursor_pos > 0 {
567                    // Remove the character to the left if there is any
568                    let removed_text_len = self.remove(cursor_pos - 1..cursor_pos);
569                    self.move_cursor_to(cursor_pos - removed_text_len);
570                    event.insert(TextEvent::TEXT_CHANGED);
571                }
572            }
573            Key::Named(NamedKey::Delete) if allow_changes => {
574                let cursor_pos = self.cursor_pos();
575                let selection = self.get_selection_range();
576
577                if let Some((start, end)) = selection {
578                    self.remove(start..end);
579                    self.move_cursor_to(start);
580                    event.insert(TextEvent::TEXT_CHANGED);
581                } else if cursor_pos < self.len_utf16_cu() {
582                    // Remove the character to the right if there is any
583                    self.remove(cursor_pos..cursor_pos + 1);
584                    event.insert(TextEvent::TEXT_CHANGED);
585                }
586            }
587            Key::Named(NamedKey::Enter) if allow_changes => {
588                // Breaks the line
589                let cursor_pos = self.cursor_pos();
590                self.insert_char('\n', cursor_pos);
591                self.cursor_right();
592
593                event.insert(TextEvent::TEXT_CHANGED);
594            }
595            Key::Named(NamedKey::Tab) if allow_tabs && allow_changes => {
596                // Inserts a tab
597                let text = " ".repeat(self.get_indentation().into());
598                let cursor_pos = self.cursor_pos();
599                self.insert(&text, cursor_pos);
600                self.move_cursor_to(cursor_pos + text.chars().count());
601
602                event.insert(TextEvent::TEXT_CHANGED);
603            }
604            Key::Character(character) => {
605                let meta_or_ctrl = if cfg!(target_os = "macos") {
606                    modifiers.meta()
607                } else {
608                    modifiers.ctrl()
609                };
610
611                match character.as_str() {
612                    " " if allow_changes => {
613                        let selection = self.get_selection_range();
614                        if let Some((start, end)) = selection {
615                            self.remove(start..end);
616                            self.move_cursor_to(start);
617                            event.insert(TextEvent::TEXT_CHANGED);
618                        }
619
620                        // Simply adds an space
621                        let cursor_pos = self.cursor_pos();
622                        self.insert_char(' ', cursor_pos);
623                        self.cursor_right();
624
625                        event.insert(TextEvent::TEXT_CHANGED);
626                    }
627
628                    // Select all text
629                    "a" if meta_or_ctrl => {
630                        let len = self.len_utf16_cu();
631                        self.set_selection((0, len));
632                    }
633
634                    // Copy selected text
635                    "c" if meta_or_ctrl && allow_clipboard => {
636                        let selected = self.get_selected_text();
637                        if let Some(selected) = selected {
638                            Clipboard::set(selected).ok();
639                        }
640                    }
641
642                    // Cut selected text
643                    "x" if meta_or_ctrl && allow_changes && allow_clipboard => {
644                        let selection = self.get_selection_range();
645                        if let Some((start, end)) = selection {
646                            let text = self.get_selected_text().unwrap();
647                            self.remove(start..end);
648                            Clipboard::set(text).ok();
649                            self.move_cursor_to(start);
650                            event.insert(TextEvent::TEXT_CHANGED);
651                        }
652                    }
653
654                    // Paste copied text
655                    "v" if meta_or_ctrl && allow_changes && allow_clipboard => {
656                        if let Ok(copied_text) = Clipboard::get() {
657                            let selection = self.get_selection_range();
658                            if let Some((start, end)) = selection {
659                                self.remove(start..end);
660                                self.move_cursor_to(start);
661                            }
662                            let cursor_pos = self.cursor_pos();
663                            self.insert(&copied_text, cursor_pos);
664                            let last_idx = copied_text.encode_utf16().count() + cursor_pos;
665                            self.move_cursor_to(last_idx);
666                            event.insert(TextEvent::TEXT_CHANGED);
667                        }
668                    }
669
670                    // Undo last change
671                    "z" if meta_or_ctrl && allow_changes => {
672                        let undo_result = self.undo();
673
674                        if let Some(selection) = undo_result {
675                            *self.selection_mut() = selection;
676                            event.insert(TextEvent::TEXT_CHANGED);
677                            event.insert(TextEvent::SELECTION_CHANGED);
678                        }
679                    }
680
681                    // Redo last change
682                    "y" if meta_or_ctrl && allow_changes => {
683                        let redo_result = self.redo();
684
685                        if let Some(selection) = redo_result {
686                            *self.selection_mut() = selection;
687                            event.insert(TextEvent::TEXT_CHANGED);
688                            event.insert(TextEvent::SELECTION_CHANGED);
689                        }
690                    }
691
692                    _ if allow_changes => {
693                        // Remove selected text
694                        let selection = self.get_selection_range();
695                        if let Some((start, end)) = selection {
696                            self.remove(start..end);
697                            self.move_cursor_to(start);
698                            event.insert(TextEvent::TEXT_CHANGED);
699                        }
700
701                        if let Ok(ch) = character.parse::<char>() {
702                            // Inserts a character
703                            let cursor_pos = self.cursor_pos();
704                            let inserted_text_len = self.insert_char(ch, cursor_pos);
705                            self.move_cursor_to(cursor_pos + inserted_text_len);
706                            event.insert(TextEvent::TEXT_CHANGED);
707                        } else {
708                            // Inserts a text
709                            let cursor_pos = self.cursor_pos();
710                            let inserted_text_len = self.insert(character, cursor_pos);
711                            self.move_cursor_to(cursor_pos + inserted_text_len);
712                            event.insert(TextEvent::TEXT_CHANGED);
713                        }
714                    }
715                    _ => {}
716                }
717            }
718            _ => {}
719        }
720
721        if event.contains(TextEvent::TEXT_CHANGED) && !event.contains(TextEvent::SELECTION_CHANGED)
722        {
723            self.clear_selection();
724        }
725
726        if self.get_selection() != selection {
727            event.insert(TextEvent::SELECTION_CHANGED);
728        }
729
730        event
731    }
732
733    fn get_selected_text(&self) -> Option<String>;
734
735    fn undo(&mut self) -> Option<TextSelection>;
736
737    fn redo(&mut self) -> Option<TextSelection>;
738
739    fn editor_history(&mut self) -> &mut EditorHistory;
740
741    fn get_selection_range(&self) -> Option<(usize, usize)>;
742
743    fn get_indentation(&self) -> u8;
744
745    fn find_word_boundaries(&self, pos: usize) -> (usize, usize) {
746        let pos_char = self.utf16_cu_to_char(pos);
747        let len_chars = self.len_chars();
748
749        if len_chars == 0 {
750            return (pos, pos);
751        }
752
753        // Get the line containing the cursor
754        let line_idx = self.char_to_line(pos_char);
755        let line_char = self.line_to_char(line_idx);
756        let line = self.line(line_idx).unwrap();
757
758        let line_str: std::borrow::Cow<str> = line.text;
759        let pos_in_line = pos_char - line_char;
760
761        // Find word boundaries within the line
762        let mut char_offset = 0;
763        for word in line_str.split_word_bounds() {
764            let word_char_len = word.chars().count();
765            let word_start = char_offset;
766            let word_end = char_offset + word_char_len;
767
768            if pos_in_line >= word_start && pos_in_line < word_end {
769                let start_char = line_char + word_start;
770                let end_char = line_char + word_end;
771                return (
772                    self.char_to_utf16_cu(start_char),
773                    self.char_to_utf16_cu(end_char),
774                );
775            }
776
777            char_offset = word_end;
778        }
779
780        (pos, pos)
781    }
782}