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_read_clipboard: bool,
476        allow_write_clipboard: bool,
477    ) -> TextEvent {
478        let mut event = TextEvent::empty();
479
480        let selection = self.get_selection();
481        let skip_arrows_movement = !modifiers.contains(Modifiers::SHIFT) && selection.is_some();
482
483        match key {
484            Key::Named(NamedKey::Shift) => {}
485            Key::Named(NamedKey::Control) => {}
486            Key::Named(NamedKey::Alt) => {}
487            Key::Named(NamedKey::Escape) => {
488                self.clear_selection();
489            }
490            Key::Named(NamedKey::ArrowDown) => {
491                if modifiers.contains(Modifiers::SHIFT) {
492                    self.selection_mut().set_as_range();
493                } else {
494                    self.selection_mut().set_as_cursor();
495                }
496
497                if !skip_arrows_movement && self.cursor_down() {
498                    event.insert(TextEvent::CURSOR_CHANGED);
499                }
500            }
501            Key::Named(NamedKey::ArrowLeft) => {
502                if modifiers.contains(Modifiers::SHIFT) {
503                    self.selection_mut().set_as_range();
504                } else {
505                    self.selection_mut().set_as_cursor();
506                }
507
508                let word_jump = if cfg!(target_os = "macos") {
509                    modifiers.contains(Modifiers::ALT)
510                } else {
511                    modifiers.contains(Modifiers::CONTROL)
512                };
513
514                let moved = !skip_arrows_movement
515                    && if word_jump {
516                        self.cursor_word_left()
517                    } else {
518                        self.cursor_left()
519                    };
520
521                if moved {
522                    event.insert(TextEvent::CURSOR_CHANGED);
523                }
524            }
525            Key::Named(NamedKey::ArrowRight) => {
526                if modifiers.contains(Modifiers::SHIFT) {
527                    self.selection_mut().set_as_range();
528                } else {
529                    self.selection_mut().set_as_cursor();
530                }
531
532                let word_jump = if cfg!(target_os = "macos") {
533                    modifiers.contains(Modifiers::ALT)
534                } else {
535                    modifiers.contains(Modifiers::CONTROL)
536                };
537
538                let moved = !skip_arrows_movement
539                    && if word_jump {
540                        self.cursor_word_right()
541                    } else {
542                        self.cursor_right()
543                    };
544
545                if moved {
546                    event.insert(TextEvent::CURSOR_CHANGED);
547                }
548            }
549            Key::Named(NamedKey::ArrowUp) => {
550                if modifiers.contains(Modifiers::SHIFT) {
551                    self.selection_mut().set_as_range();
552                } else {
553                    self.selection_mut().set_as_cursor();
554                }
555
556                if !skip_arrows_movement && self.cursor_up() {
557                    event.insert(TextEvent::CURSOR_CHANGED);
558                }
559            }
560            Key::Named(NamedKey::Backspace) if allow_changes => {
561                let cursor_pos = self.cursor_pos();
562                let selection = self.get_selection_range();
563
564                if let Some((start, end)) = selection {
565                    self.remove(start..end);
566                    self.move_cursor_to(start);
567                    event.insert(TextEvent::TEXT_CHANGED);
568                } else if cursor_pos > 0 {
569                    // Remove the character to the left if there is any
570                    let removed_text_len = self.remove(cursor_pos - 1..cursor_pos);
571                    self.move_cursor_to(cursor_pos - removed_text_len);
572                    event.insert(TextEvent::TEXT_CHANGED);
573                }
574            }
575            Key::Named(NamedKey::Delete) if allow_changes => {
576                let cursor_pos = self.cursor_pos();
577                let selection = self.get_selection_range();
578
579                if let Some((start, end)) = selection {
580                    self.remove(start..end);
581                    self.move_cursor_to(start);
582                    event.insert(TextEvent::TEXT_CHANGED);
583                } else if cursor_pos < self.len_utf16_cu() {
584                    // Remove the character to the right if there is any
585                    self.remove(cursor_pos..cursor_pos + 1);
586                    event.insert(TextEvent::TEXT_CHANGED);
587                }
588            }
589            Key::Named(NamedKey::Enter) if allow_changes => {
590                // Breaks the line
591                let cursor_pos = self.cursor_pos();
592                self.insert_char('\n', cursor_pos);
593                self.cursor_right();
594
595                event.insert(TextEvent::TEXT_CHANGED);
596            }
597            Key::Named(NamedKey::Tab) if allow_tabs && allow_changes => {
598                // Inserts a tab
599                let text = " ".repeat(self.get_indentation().into());
600                let cursor_pos = self.cursor_pos();
601                self.insert(&text, cursor_pos);
602                self.move_cursor_to(cursor_pos + text.chars().count());
603
604                event.insert(TextEvent::TEXT_CHANGED);
605            }
606            Key::Character(character) => {
607                let meta_or_ctrl = modifiers.contains(Modifiers::ctrl_or_meta());
608
609                match character.as_str() {
610                    " " if allow_changes => {
611                        let selection = self.get_selection_range();
612                        if let Some((start, end)) = selection {
613                            self.remove(start..end);
614                            self.move_cursor_to(start);
615                            event.insert(TextEvent::TEXT_CHANGED);
616                        }
617
618                        // Simply adds an space
619                        let cursor_pos = self.cursor_pos();
620                        self.insert_char(' ', cursor_pos);
621                        self.cursor_right();
622
623                        event.insert(TextEvent::TEXT_CHANGED);
624                    }
625
626                    // Select all text
627                    "a" if meta_or_ctrl => {
628                        let len = self.len_utf16_cu();
629                        self.set_selection((0, len));
630                    }
631
632                    // Copy selected text
633                    "c" if meta_or_ctrl && allow_write_clipboard => {
634                        let selected = self.get_selected_text();
635                        if let Some(selected) = selected {
636                            Clipboard::set(selected).ok();
637                        }
638                    }
639
640                    // Cut selected text
641                    "x" if meta_or_ctrl && allow_changes && allow_write_clipboard => {
642                        let selection = self.get_selection_range();
643                        if let Some((start, end)) = selection {
644                            let text = self.get_selected_text().unwrap();
645                            self.remove(start..end);
646                            Clipboard::set(text).ok();
647                            self.move_cursor_to(start);
648                            event.insert(TextEvent::TEXT_CHANGED);
649                        }
650                    }
651
652                    // Paste copied text
653                    "v" if meta_or_ctrl && allow_changes && allow_read_clipboard => {
654                        if let Ok(copied_text) = Clipboard::get() {
655                            let selection = self.get_selection_range();
656                            if let Some((start, end)) = selection {
657                                self.remove(start..end);
658                                self.move_cursor_to(start);
659                            }
660                            let cursor_pos = self.cursor_pos();
661                            self.insert(&copied_text, cursor_pos);
662                            let last_idx = copied_text.encode_utf16().count() + cursor_pos;
663                            self.move_cursor_to(last_idx);
664                            event.insert(TextEvent::TEXT_CHANGED);
665                        }
666                    }
667
668                    // Undo last change
669                    "z" if meta_or_ctrl && allow_changes => {
670                        let undo_result = self.undo();
671
672                        if let Some(selection) = undo_result {
673                            *self.selection_mut() = selection;
674                            event.insert(TextEvent::TEXT_CHANGED);
675                            event.insert(TextEvent::SELECTION_CHANGED);
676                        }
677                    }
678
679                    // Redo last change
680                    "y" if meta_or_ctrl && allow_changes => {
681                        let redo_result = self.redo();
682
683                        if let Some(selection) = redo_result {
684                            *self.selection_mut() = selection;
685                            event.insert(TextEvent::TEXT_CHANGED);
686                            event.insert(TextEvent::SELECTION_CHANGED);
687                        }
688                    }
689
690                    _ if allow_changes => {
691                        // Remove selected text
692                        let selection = self.get_selection_range();
693                        if let Some((start, end)) = selection {
694                            self.remove(start..end);
695                            self.move_cursor_to(start);
696                            event.insert(TextEvent::TEXT_CHANGED);
697                        }
698
699                        if let Ok(ch) = character.parse::<char>() {
700                            // Inserts a character
701                            let cursor_pos = self.cursor_pos();
702                            let inserted_text_len = self.insert_char(ch, cursor_pos);
703                            self.move_cursor_to(cursor_pos + inserted_text_len);
704                            event.insert(TextEvent::TEXT_CHANGED);
705                        } else {
706                            // Inserts a text
707                            let cursor_pos = self.cursor_pos();
708                            let inserted_text_len = self.insert(character, cursor_pos);
709                            self.move_cursor_to(cursor_pos + inserted_text_len);
710                            event.insert(TextEvent::TEXT_CHANGED);
711                        }
712                    }
713                    _ => {}
714                }
715            }
716            _ => {}
717        }
718
719        if event.contains(TextEvent::TEXT_CHANGED) && !event.contains(TextEvent::SELECTION_CHANGED)
720        {
721            self.clear_selection();
722        }
723
724        if self.get_selection() != selection {
725            event.insert(TextEvent::SELECTION_CHANGED);
726        }
727
728        event
729    }
730
731    fn get_selected_text(&self) -> Option<String>;
732
733    fn undo(&mut self) -> Option<TextSelection>;
734
735    fn redo(&mut self) -> Option<TextSelection>;
736
737    fn editor_history(&mut self) -> &mut EditorHistory;
738
739    fn get_selection_range(&self) -> Option<(usize, usize)>;
740
741    fn get_indentation(&self) -> u8;
742
743    fn find_word_boundaries(&self, pos: usize) -> (usize, usize) {
744        let pos_char = self.utf16_cu_to_char(pos);
745        let len_chars = self.len_chars();
746
747        if len_chars == 0 {
748            return (pos, pos);
749        }
750
751        // Get the line containing the cursor
752        let line_idx = self.char_to_line(pos_char);
753        let line_char = self.line_to_char(line_idx);
754        let line = self.line(line_idx).unwrap();
755
756        let line_str: std::borrow::Cow<str> = line.text;
757        let pos_in_line = pos_char - line_char;
758
759        // Find word boundaries within the line
760        let mut char_offset = 0;
761        for word in line_str.split_word_bounds() {
762            let word_char_len = word.chars().count();
763            let word_start = char_offset;
764            let word_end = char_offset + word_char_len;
765
766            if pos_in_line >= word_start && pos_in_line < word_end {
767                let start_char = line_char + word_start;
768                let end_char = line_char + word_end;
769                return (
770                    self.char_to_utf16_cu(start_char),
771                    self.char_to_utf16_cu(end_char),
772                );
773            }
774
775            char_offset = word_end;
776        }
777
778        (pos, pos)
779    }
780}