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