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 UTF-16 range of the grapheme cluster containing the given position.
176    fn grapheme_cluster_at(&self, pos_utf16: usize) -> Range<usize> {
177        let line_idx = self.char_to_line(self.utf16_cu_to_char(pos_utf16));
178        let line_start = self.char_to_utf16_cu(self.line_to_char(line_idx));
179        let Some(line) = self.line(line_idx) else {
180            return pos_utf16..pos_utf16;
181        };
182
183        let mut cluster = line_start..line_start;
184        for grapheme in line.text.graphemes(true) {
185            cluster = cluster.end..cluster.end + grapheme.encode_utf16().count();
186            if cluster.end > pos_utf16 {
187                return cluster;
188            }
189        }
190        pos_utf16..pos_utf16
191    }
192
193    /// Get the cursor row
194    fn cursor_row(&self) -> usize {
195        let pos = self.cursor_pos();
196        let pos_utf8 = self.utf16_cu_to_char(pos);
197        self.char_to_line(pos_utf8)
198    }
199
200    /// Get the cursor column
201    fn cursor_col(&self) -> usize {
202        let pos = self.cursor_pos();
203        let pos_utf8 = self.utf16_cu_to_char(pos);
204        let line = self.char_to_line(pos_utf8);
205        let line_char_utf8 = self.line_to_char(line);
206        let line_char = self.char_to_utf16_cu(line_char_utf8);
207        pos - line_char
208    }
209
210    /// Move the cursor to `row`, keeping `col` when possible and snapping to a
211    /// grapheme cluster boundary.
212    fn move_cursor_to_row(&mut self, row: usize, col: usize) {
213        let Some(line) = self.line(row) else { return };
214        let row_start = self.char_to_utf16_cu(self.line_to_char(row));
215        let col = col.min(line.utf16_len().saturating_sub(1));
216        let pos = self.grapheme_cluster_at(row_start + col).start;
217        self.selection_mut().move_to(pos);
218    }
219
220    /// Move the cursor 1 line down
221    fn cursor_down(&mut self) -> bool {
222        let old_row = self.cursor_row();
223        let old_col = self.cursor_col();
224
225        match old_row.cmp(&(self.len_lines() - 1)) {
226            Ordering::Less => {
227                self.move_cursor_to_row(old_row + 1, old_col);
228
229                true
230            }
231            Ordering::Equal => {
232                let end = self.len_utf16_cu();
233                self.selection_mut().move_to(end);
234
235                true
236            }
237            Ordering::Greater => false,
238        }
239    }
240
241    /// Move the cursor 1 line up
242    fn cursor_up(&mut self) -> bool {
243        let pos = self.cursor_pos();
244        let old_row = self.cursor_row();
245        let old_col = self.cursor_col();
246
247        if pos > 0 {
248            if old_row == 0 {
249                self.selection_mut().move_to(0);
250            } else {
251                self.move_cursor_to_row(old_row - 1, old_col);
252            }
253
254            true
255        } else {
256            false
257        }
258    }
259
260    /// Move the cursor 1 grapheme cluster to the right
261    fn cursor_right(&mut self) -> bool {
262        if self.cursor_pos() < self.len_utf16_cu() {
263            let to = self.grapheme_cluster_at(self.selection().end()).end;
264            self.selection_mut().move_to(to);
265
266            true
267        } else {
268            false
269        }
270    }
271
272    /// Move the cursor 1 grapheme cluster to the left
273    fn cursor_left(&mut self) -> bool {
274        if self.cursor_pos() > 0 {
275            let to = self.grapheme_cluster_at(self.selection().end() - 1).start;
276            self.selection_mut().move_to(to);
277
278            true
279        } else {
280            false
281        }
282    }
283
284    /// Move the cursor to the end of the next word.
285    fn cursor_word_right(&mut self) -> bool {
286        let pos = self.cursor_pos();
287        let len = self.len_utf16_cu();
288        if pos >= len {
289            return false;
290        }
291
292        // Walk forward line by line starting at the cursor.
293        let start_char = self.utf16_cu_to_char(pos);
294        let initial_line = self.char_to_line(start_char);
295        let initial_offset = start_char - self.line_to_char(initial_line);
296
297        for line_idx in initial_line..self.len_lines() {
298            let Some(line) = self.line(line_idx) else {
299                continue;
300            };
301            let line_char_offset = self.line_to_char(line_idx);
302            let from = if line_idx == initial_line {
303                initial_offset
304            } else {
305                0
306            };
307
308            // Stop at the end of the first non-whitespace segment past the cursor.
309            let mut char_offset = 0;
310            for word in line.text.split_word_bounds() {
311                char_offset += word.chars().count();
312                if char_offset > from && !word.chars().all(char::is_whitespace) {
313                    let new_pos = self.char_to_utf16_cu(line_char_offset + char_offset);
314                    self.selection_mut().move_to(new_pos);
315                    return true;
316                }
317            }
318        }
319
320        // Trailing whitespace only, snap to text end.
321        self.selection_mut().move_to(len);
322        true
323    }
324
325    /// Move the cursor to the start of the previous word.
326    fn cursor_word_left(&mut self) -> bool {
327        let pos = self.cursor_pos();
328        if pos == 0 {
329            return false;
330        }
331
332        // Walk backward line by line starting at the cursor.
333        let start_char = self.utf16_cu_to_char(pos);
334        let initial_line = self.char_to_line(start_char);
335        let initial_offset = start_char - self.line_to_char(initial_line);
336
337        for line_idx in (0..=initial_line).rev() {
338            let Some(line) = self.line(line_idx) else {
339                continue;
340            };
341            let line_char_offset = self.line_to_char(line_idx);
342            let to = if line_idx == initial_line {
343                initial_offset
344            } else {
345                line.text.chars().count()
346            };
347
348            // Track the latest non-whitespace segment that starts before the cursor.
349            let mut char_offset = 0;
350            let mut last_word_start = None;
351            for word in line.text.split_word_bounds() {
352                if char_offset >= to {
353                    break;
354                }
355                if !word.chars().all(char::is_whitespace) {
356                    last_word_start = Some(char_offset);
357                }
358                char_offset += word.chars().count();
359            }
360
361            // Found one on this line, jump to its start.
362            if let Some(start) = last_word_start {
363                let new_pos = self.char_to_utf16_cu(line_char_offset + start);
364                self.selection_mut().move_to(new_pos);
365                return true;
366            }
367        }
368
369        // Leading whitespace only, snap to text start.
370        self.selection_mut().move_to(0);
371        true
372    }
373
374    /// Get the cursor position
375    fn cursor_pos(&self) -> usize {
376        self.selection().pos()
377    }
378
379    /// Move the cursor position
380    fn move_cursor_to(&mut self, pos: usize) {
381        self.selection_mut().move_to(pos);
382    }
383
384    // Check if has any selection at all
385    fn has_any_selection(&self) -> bool;
386
387    // Return the selected text
388    fn get_selection(&self) -> Option<(usize, usize)>;
389
390    // Return the visible selected text for the given editor line
391    fn get_visible_selection(&self, editor_line: EditorLine) -> Option<(usize, usize)> {
392        let (selected_from, selected_to) = match self.selection() {
393            TextSelection::Cursor(_) => return None,
394            TextSelection::Range { from, to } => (*from, *to),
395        };
396
397        match editor_line {
398            EditorLine::Paragraph(line_index) => {
399                let selected_from_row = self.char_to_line(self.utf16_cu_to_char(selected_from));
400                let selected_to_row = self.char_to_line(self.utf16_cu_to_char(selected_to));
401
402                let editor_row_idx = self.char_to_utf16_cu(self.line_to_char(line_index));
403                let selected_from_row_idx =
404                    self.char_to_utf16_cu(self.line_to_char(selected_from_row));
405                let selected_to_row_idx = self.char_to_utf16_cu(self.line_to_char(selected_to_row));
406
407                let selected_from_col_idx = selected_from - selected_from_row_idx;
408                let selected_to_col_idx = selected_to - selected_to_row_idx;
409
410                // Between starting line and endling line
411                if (line_index > selected_from_row && line_index < selected_to_row)
412                    || (line_index < selected_from_row && line_index > selected_to_row)
413                {
414                    let len = self.line(line_index).unwrap().utf16_len();
415                    return Some((0, len));
416                }
417
418                match selected_from_row.cmp(&selected_to_row) {
419                    // Selection direction is from bottom -> top
420                    Ordering::Greater => {
421                        if selected_from_row == line_index {
422                            // Starting line
423                            Some((0, selected_from_col_idx))
424                        } else if selected_to_row == line_index {
425                            // Ending line
426                            let len = self.line(selected_to_row).unwrap().utf16_len();
427                            Some((selected_to_col_idx, len))
428                        } else {
429                            None
430                        }
431                    }
432                    // Selection direction is from top -> bottom
433                    Ordering::Less => {
434                        if selected_from_row == line_index {
435                            // Starting line
436                            let len = self.line(selected_from_row).unwrap().utf16_len();
437                            Some((selected_from_col_idx, len))
438                        } else if selected_to_row == line_index {
439                            // Ending line
440                            Some((0, selected_to_col_idx))
441                        } else {
442                            None
443                        }
444                    }
445                    Ordering::Equal if selected_from_row == line_index => {
446                        // Starting and endline line are the same
447                        Some((selected_from - editor_row_idx, selected_to - editor_row_idx))
448                    }
449                    _ => None,
450                }
451            }
452            EditorLine::SingleParagraph => Some((selected_from, selected_to)),
453        }
454    }
455
456    // Remove the selection
457    fn clear_selection(&mut self);
458
459    // Select some text
460    fn set_selection(&mut self, selected: (usize, usize));
461
462    // Measure a new text selection
463
464    fn measure_selection(&self, to: usize, line_index: EditorLine) -> TextSelection {
465        let mut selection = self.selection().clone();
466
467        match line_index {
468            EditorLine::Paragraph(line_index) => {
469                let row_char = self.line_to_char(line_index);
470                let pos = self.char_to_utf16_cu(row_char) + to;
471                selection.move_to(pos);
472            }
473            EditorLine::SingleParagraph => {
474                selection.move_to(to);
475            }
476        }
477
478        selection
479    }
480
481    // Process a Keyboard event
482    fn process_key(
483        &mut self,
484        key: &Key,
485        modifiers: &Modifiers,
486        allow_tabs: bool,
487        allow_changes: bool,
488        allow_read_clipboard: bool,
489        allow_write_clipboard: bool,
490    ) -> TextEvent {
491        let mut event = TextEvent::empty();
492
493        let selection = self.get_selection();
494        let skip_arrows_movement = !modifiers.contains(Modifiers::SHIFT) && selection.is_some();
495
496        match key {
497            Key::Named(NamedKey::Shift) => {}
498            Key::Named(NamedKey::Control) => {}
499            Key::Named(NamedKey::Alt) => {}
500            Key::Named(NamedKey::Escape) => {
501                self.clear_selection();
502            }
503            Key::Named(NamedKey::ArrowDown) => {
504                if modifiers.contains(Modifiers::SHIFT) {
505                    self.selection_mut().set_as_range();
506                } else {
507                    self.selection_mut().set_as_cursor();
508                }
509
510                if !skip_arrows_movement && self.cursor_down() {
511                    event.insert(TextEvent::CURSOR_CHANGED);
512                }
513            }
514            Key::Named(NamedKey::ArrowLeft) => {
515                if modifiers.contains(Modifiers::SHIFT) {
516                    self.selection_mut().set_as_range();
517                } else {
518                    self.selection_mut().set_as_cursor();
519                }
520
521                let word_jump = if cfg!(target_os = "macos") {
522                    modifiers.contains(Modifiers::ALT)
523                } else {
524                    modifiers.contains(Modifiers::CONTROL)
525                };
526
527                let moved = !skip_arrows_movement
528                    && if word_jump {
529                        self.cursor_word_left()
530                    } else {
531                        self.cursor_left()
532                    };
533
534                if moved {
535                    event.insert(TextEvent::CURSOR_CHANGED);
536                }
537            }
538            Key::Named(NamedKey::ArrowRight) => {
539                if modifiers.contains(Modifiers::SHIFT) {
540                    self.selection_mut().set_as_range();
541                } else {
542                    self.selection_mut().set_as_cursor();
543                }
544
545                let word_jump = if cfg!(target_os = "macos") {
546                    modifiers.contains(Modifiers::ALT)
547                } else {
548                    modifiers.contains(Modifiers::CONTROL)
549                };
550
551                let moved = !skip_arrows_movement
552                    && if word_jump {
553                        self.cursor_word_right()
554                    } else {
555                        self.cursor_right()
556                    };
557
558                if moved {
559                    event.insert(TextEvent::CURSOR_CHANGED);
560                }
561            }
562            Key::Named(NamedKey::ArrowUp) => {
563                if modifiers.contains(Modifiers::SHIFT) {
564                    self.selection_mut().set_as_range();
565                } else {
566                    self.selection_mut().set_as_cursor();
567                }
568
569                if !skip_arrows_movement && self.cursor_up() {
570                    event.insert(TextEvent::CURSOR_CHANGED);
571                }
572            }
573            Key::Named(NamedKey::Backspace) 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 > 0 {
582                    let remove_from = self.grapheme_cluster_at(cursor_pos - 1).start;
583                    self.remove(remove_from..cursor_pos);
584                    self.move_cursor_to(remove_from);
585                    event.insert(TextEvent::TEXT_CHANGED);
586                }
587            }
588            Key::Named(NamedKey::Delete) if allow_changes => {
589                let cursor_pos = self.cursor_pos();
590                let selection = self.get_selection_range();
591
592                if let Some((start, end)) = selection {
593                    self.remove(start..end);
594                    self.move_cursor_to(start);
595                    event.insert(TextEvent::TEXT_CHANGED);
596                } else if cursor_pos < self.len_utf16_cu() {
597                    let remove_to = self.grapheme_cluster_at(cursor_pos).end;
598                    self.remove(cursor_pos..remove_to);
599                    event.insert(TextEvent::TEXT_CHANGED);
600                }
601            }
602            Key::Named(NamedKey::Enter) if allow_changes => {
603                // Breaks the line
604                let cursor_pos = self.cursor_pos();
605                self.insert_char('\n', cursor_pos);
606                self.cursor_right();
607
608                event.insert(TextEvent::TEXT_CHANGED);
609            }
610            Key::Named(NamedKey::Tab) if allow_tabs && allow_changes => {
611                // Inserts a tab
612                let text = " ".repeat(self.get_indentation().into());
613                let cursor_pos = self.cursor_pos();
614                self.insert(&text, cursor_pos);
615                self.move_cursor_to(cursor_pos + text.chars().count());
616
617                event.insert(TextEvent::TEXT_CHANGED);
618            }
619            Key::Character(character) => {
620                let meta_or_ctrl = modifiers.contains(Modifiers::ctrl_or_meta());
621
622                match character.as_str() {
623                    " " if allow_changes => {
624                        let selection = self.get_selection_range();
625                        if let Some((start, end)) = selection {
626                            self.remove(start..end);
627                            self.move_cursor_to(start);
628                            event.insert(TextEvent::TEXT_CHANGED);
629                        }
630
631                        // Simply adds an space
632                        let cursor_pos = self.cursor_pos();
633                        self.insert_char(' ', cursor_pos);
634                        self.cursor_right();
635
636                        event.insert(TextEvent::TEXT_CHANGED);
637                    }
638
639                    // Select all text
640                    "a" if meta_or_ctrl => {
641                        let len = self.len_utf16_cu();
642                        self.set_selection((0, len));
643                    }
644
645                    // Copy selected text
646                    "c" if meta_or_ctrl && allow_write_clipboard => {
647                        let selected = self.get_selected_text();
648                        if let Some(selected) = selected {
649                            Clipboard::set(selected).ok();
650                        }
651                    }
652
653                    // Cut selected text
654                    "x" if meta_or_ctrl && allow_changes && allow_write_clipboard => {
655                        let selection = self.get_selection_range();
656                        if let Some((start, end)) = selection {
657                            let text = self.get_selected_text().unwrap();
658                            self.remove(start..end);
659                            Clipboard::set(text).ok();
660                            self.move_cursor_to(start);
661                            event.insert(TextEvent::TEXT_CHANGED);
662                        }
663                    }
664
665                    // Paste copied text
666                    "v" if meta_or_ctrl && allow_changes && allow_read_clipboard => {
667                        if let Ok(copied_text) = Clipboard::get() {
668                            let selection = self.get_selection_range();
669                            if let Some((start, end)) = selection {
670                                self.remove(start..end);
671                                self.move_cursor_to(start);
672                            }
673                            let cursor_pos = self.cursor_pos();
674                            self.insert(&copied_text, cursor_pos);
675                            let last_idx = copied_text.encode_utf16().count() + cursor_pos;
676                            self.move_cursor_to(last_idx);
677                            event.insert(TextEvent::TEXT_CHANGED);
678                        }
679                    }
680
681                    // Undo last change
682                    "z" if meta_or_ctrl && allow_changes => {
683                        let undo_result = self.undo();
684
685                        if let Some(selection) = undo_result {
686                            *self.selection_mut() = selection;
687                            event.insert(TextEvent::TEXT_CHANGED);
688                            event.insert(TextEvent::SELECTION_CHANGED);
689                        }
690                    }
691
692                    // Redo last change
693                    "y" if meta_or_ctrl && allow_changes => {
694                        let redo_result = self.redo();
695
696                        if let Some(selection) = redo_result {
697                            *self.selection_mut() = selection;
698                            event.insert(TextEvent::TEXT_CHANGED);
699                            event.insert(TextEvent::SELECTION_CHANGED);
700                        }
701                    }
702
703                    _ if allow_changes => {
704                        // Remove selected text
705                        let selection = self.get_selection_range();
706                        if let Some((start, end)) = selection {
707                            self.remove(start..end);
708                            self.move_cursor_to(start);
709                            event.insert(TextEvent::TEXT_CHANGED);
710                        }
711
712                        if let Ok(ch) = character.parse::<char>() {
713                            // Inserts a character
714                            let cursor_pos = self.cursor_pos();
715                            let inserted_text_len = self.insert_char(ch, cursor_pos);
716                            self.move_cursor_to(cursor_pos + inserted_text_len);
717                            event.insert(TextEvent::TEXT_CHANGED);
718                        } else {
719                            // Inserts a text
720                            let cursor_pos = self.cursor_pos();
721                            let inserted_text_len = self.insert(character, cursor_pos);
722                            self.move_cursor_to(cursor_pos + inserted_text_len);
723                            event.insert(TextEvent::TEXT_CHANGED);
724                        }
725                    }
726                    _ => {}
727                }
728            }
729            _ => {}
730        }
731
732        if event.contains(TextEvent::TEXT_CHANGED) && !event.contains(TextEvent::SELECTION_CHANGED)
733        {
734            self.clear_selection();
735        }
736
737        if self.get_selection() != selection {
738            event.insert(TextEvent::SELECTION_CHANGED);
739        }
740
741        event
742    }
743
744    fn get_selected_text(&self) -> Option<String>;
745
746    fn undo(&mut self) -> Option<TextSelection>;
747
748    fn redo(&mut self) -> Option<TextSelection>;
749
750    fn editor_history(&mut self) -> &mut EditorHistory;
751
752    fn get_selection_range(&self) -> Option<(usize, usize)>;
753
754    fn get_indentation(&self) -> u8;
755
756    fn find_word_boundaries(&self, pos: usize) -> (usize, usize) {
757        let pos_char = self.utf16_cu_to_char(pos);
758        let len_chars = self.len_chars();
759
760        if len_chars == 0 {
761            return (pos, pos);
762        }
763
764        // Get the line containing the cursor
765        let line_idx = self.char_to_line(pos_char);
766        let line_char = self.line_to_char(line_idx);
767        let line = self.line(line_idx).unwrap();
768
769        let line_str: std::borrow::Cow<str> = line.text;
770        let pos_in_line = pos_char - line_char;
771
772        // Find word boundaries within the line
773        let mut char_offset = 0;
774        for word in line_str.split_word_bounds() {
775            let word_char_len = word.chars().count();
776            let word_start = char_offset;
777            let word_end = char_offset + word_char_len;
778
779            if pos_in_line >= word_start && pos_in_line < word_end {
780                let start_char = line_char + word_start;
781                let end_char = line_char + word_end;
782                return (
783                    self.char_to_utf16_cu(start_char),
784                    self.char_to_utf16_cu(end_char),
785                );
786            }
787
788            char_offset = word_end;
789        }
790
791        (pos, pos)
792    }
793}