Skip to main content

tui_lipan/text/
editor.rs

1//! Multi-line text editor with selection support.
2
3use crate::clipboard::ImageContent;
4use crate::core::event::KeyEvent;
5use crate::text::buffer::TextBufferCore;
6use crate::text::edit::TextEditKind;
7use crate::utils::text::clamp_cursor;
8use crate::widgets::{TextAreaEvent, TextAreaSentinel};
9use std::ops::{Deref, DerefMut};
10
11const SENTINEL_SNAPSHOT_LIMIT: usize = 1000;
12
13/// A multi-line text editor with selection support.
14///
15/// The cursor is stored as a byte index into the UTF-8 string and is always kept on a
16/// character boundary. Selection is represented by an optional anchor position.
17///
18/// All shared text-buffer operations (cursor movement, selection, word
19/// boundaries, undo/redo, etc.) are provided via [`Deref`]/[`DerefMut`] to the
20/// inner `TextBufferCore`.
21#[derive(Clone, Debug)]
22pub struct TextEditor {
23    pub(crate) core: TextBufferCore,
24    pending_external_paste: Option<PendingExternalPaste>,
25    image_snapshots: Vec<ImageSnapshot>,
26    sentinel_snapshots: Vec<SentinelSnapshot>,
27    /// Sticky visual column used for wrap-aware vertical navigation.
28    /// Cleared whenever the cursor moves due to anything other than
29    /// visual up/down (including external cursor sync, edits, horizontal
30    /// movement, clicks). Set by the textarea key handler before performing
31    /// visual navigation.
32    pub(crate) visual_nav_col: Option<usize>,
33}
34
35#[derive(Clone, Debug)]
36struct PendingExternalPaste {
37    text_before: String,
38    cursor_before: usize,
39    anchor_before: Option<usize>,
40}
41
42#[derive(Clone, Debug)]
43struct ImageSnapshot {
44    text: String,
45    images: Vec<ImageContent>,
46}
47
48#[derive(Clone, Debug)]
49struct SentinelSnapshot {
50    text: String,
51    sentinels: Vec<TextAreaSentinel>,
52}
53
54impl Default for TextEditor {
55    fn default() -> Self {
56        Self::new("")
57    }
58}
59
60impl PartialEq for TextEditor {
61    fn eq(&self, other: &Self) -> bool {
62        self.core == other.core
63    }
64}
65
66impl Eq for TextEditor {}
67
68impl Deref for TextEditor {
69    type Target = TextBufferCore;
70
71    fn deref(&self) -> &Self::Target {
72        &self.core
73    }
74}
75
76impl DerefMut for TextEditor {
77    fn deref_mut(&mut self) -> &mut Self::Target {
78        &mut self.core
79    }
80}
81
82impl TextEditor {
83    /// Create a new editor with the cursor at the start.
84    pub fn new(text: impl Into<String>) -> Self {
85        let text = text.into();
86        Self {
87            core: TextBufferCore {
88                text,
89                cursor: 0,
90                anchor: None,
91                history: TextBufferCore::fresh_history(),
92                last_edit: None,
93            },
94            pending_external_paste: None,
95            image_snapshots: Vec::new(),
96            sentinel_snapshots: Vec::new(),
97            visual_nav_col: None,
98        }
99    }
100
101    /// Sticky visual column used for wrap-aware vertical navigation.
102    pub fn visual_nav_col(&self) -> Option<usize> {
103        self.visual_nav_col
104    }
105
106    /// Set or clear the sticky visual column for wrap-aware vertical navigation.
107    pub fn set_visual_nav_col(&mut self, col: Option<usize>) {
108        self.visual_nav_col = col;
109    }
110
111    /// Reconcile the editor with externally-supplied text/cursor/anchor.
112    ///
113    /// Clears the sticky visual column when the externally-supplied cursor
114    /// differs from our last known position, so a subsequent up/down arrow
115    /// recomputes the column instead of using a stale one.
116    pub(crate) fn sync_from(&mut self, text: &str, cursor: usize, anchor: Option<usize>) {
117        let prev_cursor = self.core.cursor;
118        let text_changed = self.core.text != text;
119        let pending_external_paste = self.pending_external_paste.take();
120        if text_changed
121            && let Some(pending) = pending_external_paste
122            && self.core.text == pending.text_before
123        {
124            self.core.sync_external_edit_from(
125                text,
126                pending.cursor_before,
127                pending.anchor_before,
128                cursor,
129                anchor,
130            );
131        } else {
132            self.core.sync_from(text, cursor, anchor);
133        }
134        if text_changed || self.core.cursor != prev_cursor {
135            self.visual_nav_col = None;
136        }
137    }
138
139    pub(crate) fn expect_external_paste_change(&mut self) {
140        self.core.normalize_cursor_and_anchor();
141        self.pending_external_paste = Some(PendingExternalPaste {
142            text_before: self.core.text().to_owned(),
143            cursor_before: self.core.cursor(),
144            anchor_before: self.core.anchor(),
145        });
146    }
147
148    pub(crate) fn remember_text_area_sentinels(
149        &mut self,
150        text: &str,
151        sentinels: &[TextAreaSentinel],
152    ) {
153        if sentinels.is_empty() {
154            return;
155        }
156
157        if let Some(pos) = self
158            .sentinel_snapshots
159            .iter()
160            .position(|snapshot| snapshot.text == text)
161        {
162            self.sentinel_snapshots.remove(pos);
163        }
164        self.sentinel_snapshots.push(SentinelSnapshot {
165            text: text.to_owned(),
166            sentinels: sentinels.to_vec(),
167        });
168        if self.sentinel_snapshots.len() > SENTINEL_SNAPSHOT_LIMIT {
169            self.sentinel_snapshots.remove(0);
170        }
171    }
172
173    pub(crate) fn remembered_text_area_sentinels(
174        &self,
175        text: &str,
176    ) -> Option<Vec<TextAreaSentinel>> {
177        self.sentinel_snapshots
178            .iter()
179            .rev()
180            .find(|snapshot| snapshot.text == text)
181            .map(|snapshot| snapshot.sentinels.clone())
182    }
183
184    pub(crate) fn remember_text_area_images(&mut self, text: &str, images: &[ImageContent]) {
185        if images.is_empty() {
186            return;
187        }
188
189        if let Some(pos) = self
190            .image_snapshots
191            .iter()
192            .position(|snapshot| snapshot.text == text)
193        {
194            self.image_snapshots.remove(pos);
195        }
196        self.image_snapshots.push(ImageSnapshot {
197            text: text.to_owned(),
198            images: images.to_vec(),
199        });
200        if self.image_snapshots.len() > SENTINEL_SNAPSHOT_LIMIT {
201            self.image_snapshots.remove(0);
202        }
203    }
204
205    pub(crate) fn remembered_text_area_images(&self, text: &str) -> Option<Vec<ImageContent>> {
206        self.image_snapshots
207            .iter()
208            .rev()
209            .find(|snapshot| snapshot.text == text)
210            .map(|snapshot| snapshot.images.clone())
211    }
212
213    pub(crate) fn insert_text(&mut self, text: &str) -> bool {
214        if text.is_empty() {
215            return false;
216        }
217        let (start, end) = self
218            .core
219            .selection()
220            .unwrap_or((self.core.cursor, self.core.cursor));
221        self.core
222            .replace_range(start, end, text, TextEditKind::Replace)
223    }
224
225    /// Clear all text while preserving undo/redo history.
226    pub fn clear(&mut self) -> bool {
227        let changed = self.core.clear_preserving_history();
228        if changed {
229            self.visual_nav_col = None;
230        }
231        changed
232    }
233
234    /// Apply a [`TextAreaEvent`] to this state bundle.
235    pub fn apply(&mut self, ev: &TextAreaEvent) {
236        self.core.text = ev.value.to_string();
237        self.core.cursor = clamp_cursor(&self.core.text, ev.cursor);
238        self.core.anchor = ev
239            .anchor
240            .map(|anchor| clamp_cursor(&self.core.text, anchor));
241    }
242
243    /// Handle a key event.
244    ///
245    /// Returns `true` if the state changed (text, cursor, or selection).
246    pub fn handle_key(&mut self, key: KeyEvent) -> bool {
247        self.handle_key_with(key, crate::app::input::keymap::default_keymap())
248    }
249
250    pub(crate) fn handle_key_with(
251        &mut self,
252        key: KeyEvent,
253        keymap: &crate::app::input::keymap::Keymap,
254    ) -> bool {
255        use crate::app::input::keymap::Action;
256
257        match keymap.resolve_action(key) {
258            Action::MoveLeft => self.core.move_left(),
259            Action::MoveRight => self.core.move_right(),
260            Action::MoveUp => self.move_up(),
261            Action::MoveDown => self.move_down(),
262            Action::MoveWordLeft => self.core.move_word_left(),
263            Action::MoveWordRight => self.core.move_word_right(),
264            Action::MoveHome => self.move_home(),
265            Action::MoveEnd => self.move_end(),
266
267            Action::SelectLeft => self.core.select_left(),
268            Action::SelectRight => self.core.select_right(),
269            Action::SelectUp => self.select_up(),
270            Action::SelectDown => self.select_down(),
271            Action::SelectWordLeft => self.core.select_word_left(),
272            Action::SelectWordRight => self.core.select_word_right(),
273            Action::SelectHome => self.select_home(),
274            Action::SelectEnd => self.select_end(),
275            Action::SelectAll => self.core.select_all(),
276
277            Action::Backspace => self.core.backspace(),
278            Action::Clear => self.clear(),
279            Action::Delete => self.core.delete(),
280            Action::DeleteWordLeft => self.core.delete_word_left(),
281            Action::DeleteWordRight => self.core.delete_word_right(),
282
283            Action::Copy
284            | Action::Cut
285            | Action::Paste
286            | Action::PasteFromSelection
287            | Action::CopyImage
288            | Action::PasteImage
289            | Action::ToggleDevTools
290            | Action::Quit => false,
291            Action::Undo => self.core.undo(),
292            Action::Redo => self.core.redo(),
293
294            Action::InsertChar(c) => self.core.insert_char(c),
295            Action::InsertNewline => self.core.insert_char('\n'),
296
297            Action::DismissOverlay | Action::FocusNext | Action::FocusPrev | Action::None => false,
298        }
299    }
300
301    /// Insert a string at the cursor, replacing any selection.
302    pub fn insert_str(&mut self, s: &str) -> bool {
303        let (start, end, kind) = if let Some((start, end)) = self.core.selection() {
304            (start, end, TextEditKind::Replace)
305        } else {
306            (self.core.cursor, self.core.cursor, TextEditKind::Insert)
307        };
308        self.core.replace_range(start, end, s, kind)
309    }
310
311    // ── Line-aware home/end ──────────────────────────────────────────────
312
313    /// Move to the start of the current line, clearing selection.
314    pub fn move_home(&mut self) -> bool {
315        self.core.anchor = None;
316        self.core.normalize_cursor_and_anchor();
317        let start = self.core.text[..self.core.cursor]
318            .rfind('\n')
319            .map(|i| i + 1)
320            .unwrap_or(0);
321        if self.core.cursor == start {
322            return false;
323        }
324        self.core.cursor = start;
325        true
326    }
327
328    /// Move to the end of the current line, clearing selection.
329    pub fn move_end(&mut self) -> bool {
330        self.core.anchor = None;
331        self.core.normalize_cursor_and_anchor();
332        let end = self.core.text[self.core.cursor..]
333            .find('\n')
334            .map(|i| self.core.cursor + i)
335            .unwrap_or(self.core.text.len());
336        if self.core.cursor == end {
337            return false;
338        }
339        self.core.cursor = end;
340        true
341    }
342
343    /// Extend selection to the start of the current line.
344    pub fn select_home(&mut self) -> bool {
345        self.core.normalize_cursor_and_anchor();
346        let start = self.core.text[..self.core.cursor]
347            .rfind('\n')
348            .map(|i| i + 1)
349            .unwrap_or(0);
350        if self.core.cursor == start {
351            return false;
352        }
353        if self.core.anchor.is_none() {
354            self.core.anchor = Some(self.core.cursor);
355        }
356        self.core.cursor = start;
357        self.core.collapse_empty_selection();
358        true
359    }
360
361    /// Extend selection to the end of the current line.
362    pub fn select_end(&mut self) -> bool {
363        self.core.normalize_cursor_and_anchor();
364        let end = self.core.text[self.core.cursor..]
365            .find('\n')
366            .map(|i| self.core.cursor + i)
367            .unwrap_or(self.core.text.len());
368        if self.core.cursor == end {
369            return false;
370        }
371        if self.core.anchor.is_none() {
372            self.core.anchor = Some(self.core.cursor);
373        }
374        self.core.cursor = end;
375        self.core.collapse_empty_selection();
376        true
377    }
378
379    // ── Vertical movement ────────────────────────────────────────────────
380
381    /// Move up one line, clearing selection.
382    pub fn move_up(&mut self) -> bool {
383        self.core.anchor = None;
384        self.move_up_internal()
385    }
386
387    /// Move down one line, clearing selection.
388    pub fn move_down(&mut self) -> bool {
389        self.core.anchor = None;
390        self.move_down_internal()
391    }
392
393    /// Extend selection up one line.
394    pub fn select_up(&mut self) -> bool {
395        if self.core.anchor.is_none() {
396            self.core.anchor = Some(self.core.cursor);
397        }
398        let moved = self.move_up_internal();
399        self.core.collapse_empty_selection();
400        moved
401    }
402
403    /// Extend selection down one line.
404    pub fn select_down(&mut self) -> bool {
405        if self.core.anchor.is_none() {
406            self.core.anchor = Some(self.core.cursor);
407        }
408        let moved = self.move_down_internal();
409        self.core.collapse_empty_selection();
410        moved
411    }
412
413    fn move_up_internal(&mut self) -> bool {
414        self.core.normalize_cursor_and_anchor();
415        let line_start = self.core.text[..self.core.cursor]
416            .rfind('\n')
417            .map(|i| i + 1)
418            .unwrap_or(0);
419        if line_start == 0 {
420            if self.core.cursor == 0 {
421                return false;
422            }
423            self.core.cursor = 0;
424            return true;
425        }
426
427        let col = self.core.text[line_start..self.core.cursor].chars().count();
428
429        let prev_line_end = line_start - 1;
430        let prev_line_start = self.core.text[..prev_line_end]
431            .rfind('\n')
432            .map(|i| i + 1)
433            .unwrap_or(0);
434
435        let prev_line = &self.core.text[prev_line_start..prev_line_end];
436        let new_col = col.min(prev_line.chars().count());
437
438        let offset: usize = prev_line.chars().take(new_col).map(|c| c.len_utf8()).sum();
439        self.core.cursor = prev_line_start + offset;
440        true
441    }
442
443    fn move_down_internal(&mut self) -> bool {
444        self.core.normalize_cursor_and_anchor();
445        let len = self.core.text.len();
446        if self.core.cursor >= len {
447            return false;
448        }
449
450        let line_end = self.core.text[self.core.cursor..]
451            .find('\n')
452            .map(|i| self.core.cursor + i);
453
454        match line_end {
455            Some(end_idx) => {
456                let line_start = self.core.text[..self.core.cursor]
457                    .rfind('\n')
458                    .map(|i| i + 1)
459                    .unwrap_or(0);
460                let col = self.core.text[line_start..self.core.cursor].chars().count();
461
462                let next_line_start = end_idx + 1;
463                if next_line_start >= len {
464                    self.core.cursor = len;
465                    return true;
466                }
467
468                let next_line_end = self.core.text[next_line_start..]
469                    .find('\n')
470                    .map(|i| next_line_start + i)
471                    .unwrap_or(len);
472
473                let next_line = &self.core.text[next_line_start..next_line_end];
474                let new_col = col.min(next_line.chars().count());
475
476                let offset: usize = next_line.chars().take(new_col).map(|c| c.len_utf8()).sum();
477                self.core.cursor = next_line_start + offset;
478                true
479            }
480            None => {
481                if self.core.cursor == len {
482                    return false;
483                }
484                self.core.cursor = len;
485                true
486            }
487        }
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494    use crate::core::event::{KeyCode, KeyEvent, KeyMods};
495    use crate::widgets::TextAreaEvent;
496
497    fn key(code: KeyCode) -> KeyEvent {
498        KeyEvent {
499            code,
500            mods: KeyMods::default(),
501        }
502    }
503
504    fn ctrl(code: KeyCode) -> KeyEvent {
505        KeyEvent {
506            code,
507            mods: KeyMods {
508                ctrl: true,
509                ..KeyMods::default()
510            },
511        }
512    }
513
514    #[test]
515    fn undo_redo_multiline() {
516        let mut editor = TextEditor::new("");
517        assert!(editor.handle_key(key(KeyCode::Char('a'))));
518        assert!(editor.handle_key(key(KeyCode::Enter)));
519        assert!(editor.handle_key(key(KeyCode::Char('b'))));
520        assert_eq!(editor.text(), "a\nb");
521
522        assert!(editor.handle_key(ctrl(KeyCode::Char('z'))));
523        assert_eq!(editor.text(), "a\n");
524
525        assert!(editor.handle_key(ctrl(KeyCode::Char('y'))));
526        assert_eq!(editor.text(), "a\nb");
527    }
528
529    #[test]
530    fn undo_redo_word_merges() {
531        let mut editor = TextEditor::new("");
532        assert!(editor.handle_key(key(KeyCode::Char('a'))));
533        assert!(editor.handle_key(key(KeyCode::Char('b'))));
534        assert!(editor.handle_key(key(KeyCode::Char('c'))));
535        assert_eq!(editor.text(), "abc");
536
537        assert!(editor.handle_key(ctrl(KeyCode::Char('z'))));
538        assert_eq!(editor.text(), "");
539
540        assert!(editor.handle_key(ctrl(KeyCode::Char('y'))));
541        assert_eq!(editor.text(), "abc");
542    }
543
544    #[test]
545    fn undo_merges_trailing_space() {
546        let mut editor = TextEditor::new("");
547        assert!(editor.handle_key(key(KeyCode::Char('a'))));
548        assert!(editor.handle_key(key(KeyCode::Char('b'))));
549        assert!(editor.handle_key(key(KeyCode::Char(' '))));
550        assert_eq!(editor.text(), "ab ");
551
552        assert!(editor.handle_key(ctrl(KeyCode::Char('z'))));
553        assert_eq!(editor.text(), "");
554
555        assert!(editor.handle_key(ctrl(KeyCode::Char('y'))));
556        assert_eq!(editor.text(), "ab ");
557    }
558
559    #[test]
560    fn move_down_and_up_through_lines() {
561        let mut editor = TextEditor::new("abc\ndef\nghi");
562        // cursor starts at 0
563        assert_eq!(editor.cursor(), 0);
564
565        // move_down twice -> line 2 (start of "ghi")
566        assert!(editor.move_down());
567        assert!(editor.move_down());
568
569        // move_up twice -> back to line 0, col 0
570        assert!(editor.move_up());
571        assert!(editor.move_up());
572        assert_eq!(editor.cursor(), 0);
573    }
574
575    #[test]
576    fn move_down_clamps_column_to_shorter_line() {
577        // "abcde" (len 5), "ab" (len 2), "abcde" (len 5)
578        let mut editor = TextEditor::new("abcde\nab\nabcde");
579        // Place cursor at end of first line (byte 5)
580        editor.set_cursor(5);
581        assert_eq!(editor.cursor(), 5);
582
583        // move_down -> line "ab", col should clamp to 2 -> byte 6 + 2 = 8
584        assert!(editor.move_down());
585        assert_eq!(editor.cursor(), 8); // byte index of end of "ab"
586
587        // move_down again -> line "abcde", col should restore to 2 (clamped) -> byte 9 + 2 = 11
588        assert!(editor.move_down());
589        assert_eq!(editor.cursor(), 11);
590    }
591
592    #[test]
593    fn move_up_on_first_line_moves_to_line_start() {
594        let mut editor = TextEditor::new("abc\ndef");
595        editor.set_cursor(2); // middle of first line
596        assert!(editor.move_up());
597        assert_eq!(editor.cursor(), 0);
598        assert!(!editor.move_up());
599    }
600
601    #[test]
602    fn move_down_on_last_line_moves_to_line_end() {
603        let mut editor = TextEditor::new("abc\ndef");
604        // Place cursor on second line
605        editor.set_cursor(5); // middle of "def"
606        assert!(editor.move_down());
607        assert_eq!(editor.cursor(), 7);
608        assert!(!editor.move_down());
609    }
610
611    #[test]
612    fn select_up_down_at_boundaries_select_line_edge() {
613        let mut editor = TextEditor::new("abc\ndef");
614        editor.set_cursor(2);
615        assert!(editor.select_up());
616        assert_eq!(editor.cursor(), 0);
617        assert_eq!(editor.selection(), Some((0, 2)));
618
619        editor.set_cursor(5);
620        assert!(editor.select_down());
621        assert_eq!(editor.cursor(), 7);
622        assert_eq!(editor.selection(), Some((5, 7)));
623    }
624
625    #[test]
626    fn move_home_goes_to_line_start() {
627        let mut editor = TextEditor::new("abc\ndef");
628        // Place cursor in middle of "def" (byte 5 = 'd','e' -> byte 5 is 'e')
629        editor.set_cursor(5);
630        assert!(editor.move_home());
631        // Start of "def" is byte 4
632        assert_eq!(editor.cursor(), 4);
633    }
634
635    #[test]
636    fn move_end_goes_to_line_end() {
637        let mut editor = TextEditor::new("abc\ndef");
638        // Place cursor at start of "def" (byte 4)
639        editor.set_cursor(4);
640        assert!(editor.move_end());
641        // End of "def" is byte 7
642        assert_eq!(editor.cursor(), 7);
643    }
644
645    #[test]
646    fn backspace_at_line_start_joins_lines() {
647        let mut editor = TextEditor::new("abc\ndef");
648        // Place cursor at start of "def" (byte 4)
649        editor.set_cursor(4);
650        assert!(editor.backspace());
651        assert_eq!(editor.text(), "abcdef");
652        assert_eq!(editor.cursor(), 3); // cursor at join point
653    }
654
655    #[test]
656    fn delete_at_line_end_joins_lines() {
657        let mut editor = TextEditor::new("abc\ndef");
658        // Place cursor at end of "abc" (byte 3, which is the '\n')
659        editor.set_cursor(3);
660        assert!(editor.delete());
661        assert_eq!(editor.text(), "abcdef");
662        assert_eq!(editor.cursor(), 3); // cursor stays
663    }
664
665    #[test]
666    fn insert_newline_splits_line() {
667        let mut editor = TextEditor::new("abcdef");
668        editor.set_cursor(3);
669        assert!(editor.insert_char('\n'));
670        assert_eq!(editor.text(), "abc\ndef");
671        assert_eq!(editor.cursor(), 4); // cursor at start of new line
672    }
673
674    #[test]
675    fn select_down_creates_vertical_selection() {
676        let mut editor = TextEditor::new("abc\ndef");
677        // cursor at byte 1 (after 'a')
678        editor.set_cursor(1);
679        assert!(editor.select_down());
680        // anchor should be original position
681        assert_eq!(editor.anchor(), Some(1));
682        // cursor should be at col 1 on line "def" -> byte 4 + 1 = 5
683        assert_eq!(editor.cursor(), 5);
684    }
685
686    #[test]
687    fn empty_lines_traversal() {
688        let mut editor = TextEditor::new("a\n\nb");
689        // "a" at byte 0, '\n' at byte 1, empty line '\n' at byte 2, "b" at byte 3
690        assert_eq!(editor.cursor(), 0);
691
692        // move_down -> empty line, cursor at byte 2 (start of empty line)
693        assert!(editor.move_down());
694        assert_eq!(editor.cursor(), 2);
695
696        // move_down -> line "b", cursor at byte 3
697        assert!(editor.move_down());
698        assert_eq!(editor.cursor(), 3);
699
700        // move_up back to empty line
701        assert!(editor.move_up());
702        assert_eq!(editor.cursor(), 2);
703
704        // move_up back to "a"
705        assert!(editor.move_up());
706        assert_eq!(editor.cursor(), 0);
707    }
708
709    #[test]
710    fn toggle_devtools_action_is_non_editing() {
711        let mut editor = TextEditor::new("hello");
712        let changed = editor.handle_key(KeyEvent {
713            code: KeyCode::F(12),
714            mods: KeyMods::default(),
715        });
716
717        assert!(!changed);
718        assert_eq!(editor.text(), "hello");
719        assert_eq!(editor.cursor(), 0);
720    }
721
722    #[test]
723    fn multi_line_selection_delete() {
724        let mut editor = TextEditor::new("abc\ndef\nghi");
725        // Select from byte 1 to byte 8 (across "bc\ndef\n")
726        editor.set_cursor(1);
727        editor.set_anchor(Some(8));
728        assert!(editor.delete_selection());
729        assert_eq!(editor.text(), "aghi");
730        assert_eq!(editor.cursor(), 1);
731        assert_eq!(editor.anchor(), None);
732    }
733
734    #[test]
735    fn apply_updates_editor_after_insert() {
736        let mut editor = TextEditor::new("abc");
737        TextAreaEvent {
738            value: "abc\ndef".into(),
739            cursor: 7,
740            anchor: None,
741        }
742        .apply_to(&mut editor);
743
744        assert_eq!(editor.text(), "abc\ndef");
745        assert_eq!(editor.cursor(), 7);
746        assert_eq!(editor.anchor(), None);
747    }
748
749    #[test]
750    fn apply_updates_editor_after_delete() {
751        let mut editor = TextEditor::new("abc\ndef");
752        TextAreaEvent {
753            value: "abcdef".into(),
754            cursor: 3,
755            anchor: None,
756        }
757        .apply_to(&mut editor);
758
759        assert_eq!(editor.text(), "abcdef");
760        assert_eq!(editor.cursor(), 3);
761        assert_eq!(editor.anchor(), None);
762    }
763
764    #[test]
765    fn apply_updates_cursor_navigation_without_text_change() {
766        let mut editor = TextEditor::new("abc\ndef");
767        TextAreaEvent {
768            value: "abc\ndef".into(),
769            cursor: 5,
770            anchor: None,
771        }
772        .apply_to(&mut editor);
773
774        assert_eq!(editor.text(), "abc\ndef");
775        assert_eq!(editor.cursor(), 5);
776        assert_eq!(editor.anchor(), None);
777    }
778
779    #[test]
780    fn apply_updates_selection_anchor() {
781        let mut editor = TextEditor::new("abc\ndef");
782        TextAreaEvent {
783            value: "abc\ndef".into(),
784            cursor: 6,
785            anchor: Some(1),
786        }
787        .apply_to(&mut editor);
788
789        assert_eq!(editor.selection(), Some((1, 6)));
790    }
791
792    #[test]
793    fn apply_clamps_cursor_and_anchor_inside_unicode_characters() {
794        let value = "części\newaluacyjnej";
795        let cursor_inside_s = 5;
796        assert!(!value.is_char_boundary(cursor_inside_s));
797
798        let mut editor = TextEditor::new("");
799        TextAreaEvent {
800            value: value.into(),
801            cursor: cursor_inside_s,
802            anchor: Some(cursor_inside_s),
803        }
804        .apply_to(&mut editor);
805
806        assert_eq!(editor.cursor(), 4);
807        assert_eq!(editor.anchor(), Some(4));
808        assert!(editor.text().is_char_boundary(editor.cursor()));
809    }
810
811    #[test]
812    fn line_navigation_tolerates_unicode_boundary_drift() {
813        let mut editor = TextEditor::new("abc\nczęści\nxyz");
814        editor.core.cursor = 9;
815        assert!(!editor.text().is_char_boundary(editor.core.cursor));
816
817        assert!(editor.move_end());
818        assert_eq!(editor.cursor(), "abc\nczęści".len());
819
820        editor.core.cursor = 9;
821        assert!(editor.move_home());
822        assert_eq!(editor.cursor(), "abc\n".len());
823
824        editor.core.cursor = 9;
825        assert!(editor.move_down());
826        assert!(editor.text().is_char_boundary(editor.cursor()));
827    }
828
829    #[test]
830    fn clear_is_undoable_and_redoable_with_cursor_and_anchor() {
831        let mut editor = TextEditor::new("abc\ndef");
832        editor.set_cursor(6);
833        editor.set_anchor(Some(1));
834        editor.set_visual_nav_col(Some(3));
835
836        assert!(editor.clear());
837        assert_eq!(editor.text(), "");
838        assert_eq!(editor.cursor(), 0);
839        assert_eq!(editor.anchor(), None);
840        assert_eq!(editor.visual_nav_col(), None);
841
842        assert!(editor.undo());
843        assert_eq!(editor.text(), "abc\ndef");
844        assert_eq!(editor.cursor(), 6);
845        assert_eq!(editor.anchor(), Some(1));
846
847        assert!(editor.redo());
848        assert_eq!(editor.text(), "");
849        assert_eq!(editor.cursor(), 0);
850        assert_eq!(editor.anchor(), None);
851    }
852
853    #[test]
854    fn clear_empty_text_only_normalizes_cursor_and_anchor() {
855        let mut editor = TextEditor::new("");
856        editor.set_cursor_keep_anchor(10);
857        editor.set_anchor(Some(10));
858
859        assert!(editor.clear());
860        assert_eq!(editor.cursor(), 0);
861        assert_eq!(editor.anchor(), None);
862        assert!(!editor.can_undo());
863        assert!(!editor.clear());
864    }
865}