Skip to main content

ftui_text/
editor.rs

1#![forbid(unsafe_code)]
2
3//! Core text editing operations on top of Rope + CursorNavigator.
4//!
5//! [`Editor`] combines a [`Rope`] with a [`CursorPosition`] and provides
6//! the standard editing operations (insert, delete, cursor movement) that
7//! power TextArea and other editing widgets.
8//!
9//! # Example
10//! ```
11//! use ftui_text::editor::Editor;
12//!
13//! let mut ed = Editor::new();
14//! ed.insert_text("hello");
15//! ed.insert_char(' ');
16//! ed.insert_text("world");
17//! assert_eq!(ed.text(), "hello world");
18//!
19//! // Move cursor and delete
20//! ed.move_left();
21//! ed.move_left();
22//! ed.move_left();
23//! ed.move_left();
24//! ed.move_left();
25//! ed.delete_backward(); // deletes the space
26//! assert_eq!(ed.text(), "helloworld");
27//! ```
28
29use crate::cursor::{CursorNavigator, CursorPosition};
30use crate::rope::Rope;
31
32/// A single edit operation for undo/redo.
33#[derive(Debug, Clone)]
34enum EditOp {
35    Insert { byte_offset: usize, text: String },
36    Delete { byte_offset: usize, text: String },
37}
38
39impl EditOp {
40    fn inverse(&self) -> Self {
41        match self {
42            Self::Insert { byte_offset, text } => Self::Delete {
43                byte_offset: *byte_offset,
44                text: text.clone(),
45            },
46            Self::Delete { byte_offset, text } => Self::Insert {
47                byte_offset: *byte_offset,
48                text: text.clone(),
49            },
50        }
51    }
52}
53
54/// Selection defined by anchor (fixed) and head (moving with cursor).
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct Selection {
57    /// The fixed end of the selection.
58    pub anchor: CursorPosition,
59    /// The moving end (same as cursor).
60    pub head: CursorPosition,
61}
62
63impl Selection {
64    /// Byte range of the selection (start, end) where start <= end.
65    #[must_use]
66    pub fn byte_range(&self, nav: &CursorNavigator<'_>) -> (usize, usize) {
67        let a = nav.to_byte_index(self.anchor);
68        let b = nav.to_byte_index(self.head);
69        if a <= b { (a, b) } else { (b, a) }
70    }
71
72    /// Whether the selection is empty (anchor == head).
73    #[must_use]
74    pub fn is_empty(&self) -> bool {
75        self.anchor == self.head
76    }
77}
78
79/// Core text editor combining Rope storage with cursor management.
80///
81/// Provides insert/delete/move operations with grapheme-aware cursor
82/// handling, undo/redo, and selection support.
83/// Cursor is always kept in valid bounds.
84#[derive(Debug, Clone)]
85pub struct Editor {
86    /// The text buffer.
87    rope: Rope,
88    /// Current cursor position.
89    cursor: CursorPosition,
90    /// Active selection (None when no selection).
91    selection: Option<Selection>,
92    /// Undo stack: (operation, cursor-before).
93    undo_stack: Vec<(EditOp, CursorPosition)>,
94    /// Redo stack: (operation, cursor-before).
95    redo_stack: Vec<(EditOp, CursorPosition)>,
96    /// Maximum undo history depth.
97    max_history: usize,
98    /// Current size of undo history in bytes.
99    current_undo_size: usize,
100    /// Maximum size of undo history in bytes (default 10MB).
101    max_undo_size: usize,
102}
103
104impl Default for Editor {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110impl Editor {
111    /// Create an empty editor.
112    #[must_use]
113    pub fn new() -> Self {
114        Self {
115            rope: Rope::new(),
116            cursor: CursorPosition::default(),
117            selection: None,
118            undo_stack: Vec::new(),
119            redo_stack: Vec::new(),
120            max_history: 1000,
121            current_undo_size: 0,
122            max_undo_size: 10 * 1024 * 1024, // 10MB default
123        }
124    }
125
126    /// Create an editor with initial text. Cursor starts at the end.
127    #[must_use]
128    pub fn with_text(text: &str) -> Self {
129        let rope = Rope::from_text(text);
130        let nav = CursorNavigator::new(&rope);
131        let cursor = nav.document_end();
132        Self {
133            rope,
134            cursor,
135            selection: None,
136            undo_stack: Vec::new(),
137            redo_stack: Vec::new(),
138            max_history: 1000,
139            current_undo_size: 0,
140            max_undo_size: 10 * 1024 * 1024,
141        }
142    }
143
144    /// Set the maximum undo history depth.
145    pub fn set_max_history(&mut self, max: usize) {
146        self.max_history = max;
147    }
148
149    /// Set the maximum undo history size in bytes.
150    pub fn set_max_undo_size(&mut self, bytes: usize) {
151        self.max_undo_size = bytes;
152        // Prune if now over limit
153        while self.current_undo_size > self.max_undo_size && !self.undo_stack.is_empty() {
154            let (op, _) = self.undo_stack.remove(0);
155            self.current_undo_size -= op.byte_len();
156        }
157    }
158
159    /// Get the full text content as a string.
160    #[must_use]
161    pub fn text(&self) -> String {
162        self.rope.to_string()
163    }
164
165    /// Get a reference to the underlying rope.
166    #[must_use]
167    pub fn rope(&self) -> &Rope {
168        &self.rope
169    }
170
171    /// Get the current cursor position.
172    #[must_use]
173    pub fn cursor(&self) -> CursorPosition {
174        self.cursor
175    }
176
177    /// Set cursor position (will be clamped to valid bounds). Clears selection.
178    pub fn set_cursor(&mut self, pos: CursorPosition) {
179        let nav = CursorNavigator::new(&self.rope);
180        self.cursor = nav.clamp(pos);
181        self.selection = None;
182    }
183
184    /// Current selection, if any.
185    #[must_use]
186    pub fn selection(&self) -> Option<Selection> {
187        self.selection
188    }
189
190    /// Whether undo is available.
191    #[must_use]
192    pub fn can_undo(&self) -> bool {
193        !self.undo_stack.is_empty()
194    }
195
196    /// Whether redo is available.
197    #[must_use]
198    pub fn can_redo(&self) -> bool {
199        !self.redo_stack.is_empty()
200    }
201
202    /// Check if the editor is empty.
203    #[must_use]
204    pub fn is_empty(&self) -> bool {
205        self.rope.is_empty()
206    }
207
208    /// Number of lines in the buffer.
209    #[must_use]
210    pub fn line_count(&self) -> usize {
211        self.rope.len_lines()
212    }
213
214    /// Get the text of a specific line (without trailing newline).
215    #[must_use]
216    pub fn line_text(&self, line: usize) -> Option<String> {
217        self.rope.line(line).map(|cow| {
218            let s = cow.as_ref();
219            s.trim_end_matches('\n').trim_end_matches('\r').to_string()
220        })
221    }
222
223    // ====================================================================
224    // Insert operations
225    // ====================================================================
226
227    /// Insert a single character at the cursor position.
228    pub fn insert_char(&mut self, ch: char) {
229        let mut buf = [0u8; 4];
230        let s = ch.encode_utf8(&mut buf);
231        self.insert_text(s);
232    }
233
234    /// Insert text at the cursor position. Deletes selection first if active.
235    ///
236    /// Control characters (except newline and tab) are stripped to prevent
237    /// terminal corruption.
238    pub fn insert_text(&mut self, text: &str) {
239        if text.is_empty() {
240            return;
241        }
242
243        // Sanitize input: allow \n and \t, strip other control chars
244        let sanitized: String = text
245            .chars()
246            .filter(|&c| !c.is_control() || c == '\n' || c == '\t')
247            .collect();
248
249        if sanitized.is_empty() {
250            return;
251        }
252
253        self.delete_selection_inner();
254        let nav = CursorNavigator::new(&self.rope);
255        let byte_idx = nav.to_byte_index(self.cursor);
256        let char_idx = self.rope.byte_to_char(byte_idx);
257
258        self.push_undo(EditOp::Insert {
259            byte_offset: byte_idx,
260            text: sanitized.clone(),
261        });
262
263        self.rope.insert(char_idx, &sanitized);
264
265        // Move cursor to end of inserted text
266        let new_byte_idx = byte_idx + sanitized.len();
267        let nav = CursorNavigator::new(&self.rope);
268        self.cursor = nav.from_byte_index(new_byte_idx);
269    }
270
271    /// Insert a newline at the cursor position.
272    pub fn insert_newline(&mut self) {
273        self.insert_text("\n");
274    }
275
276    // ====================================================================
277    // Delete operations
278    // ====================================================================
279
280    /// Delete the character before the cursor (backspace). Deletes selection if active.
281    ///
282    /// Returns `true` if a character was deleted.
283    pub fn delete_backward(&mut self) -> bool {
284        if self.delete_selection_inner() {
285            return true;
286        }
287        let nav = CursorNavigator::new(&self.rope);
288        let old_pos = self.cursor;
289        let new_pos = nav.move_left(old_pos);
290
291        if new_pos == old_pos {
292            return false; // At beginning, nothing to delete
293        }
294
295        let start_byte = nav.to_byte_index(new_pos);
296        let end_byte = nav.to_byte_index(old_pos);
297        let start_char = self.rope.byte_to_char(start_byte);
298        let end_char = self.rope.byte_to_char(end_byte);
299        let deleted = self.rope.slice(start_char..end_char).into_owned();
300
301        self.push_undo(EditOp::Delete {
302            byte_offset: start_byte,
303            text: deleted,
304        });
305
306        self.rope.remove(start_char..end_char);
307
308        let nav = CursorNavigator::new(&self.rope);
309        self.cursor = nav.from_byte_index(start_byte);
310        true
311    }
312
313    /// Delete the character after the cursor (delete key). Deletes selection if active.
314    ///
315    /// Returns `true` if a character was deleted.
316    pub fn delete_forward(&mut self) -> bool {
317        if self.delete_selection_inner() {
318            return true;
319        }
320        let nav = CursorNavigator::new(&self.rope);
321        let old_pos = self.cursor;
322        let next_pos = nav.move_right(old_pos);
323
324        if next_pos == old_pos {
325            return false; // At end, nothing to delete
326        }
327
328        let start_byte = nav.to_byte_index(old_pos);
329        let end_byte = nav.to_byte_index(next_pos);
330        let start_char = self.rope.byte_to_char(start_byte);
331        let end_char = self.rope.byte_to_char(end_byte);
332        let deleted = self.rope.slice(start_char..end_char).into_owned();
333
334        self.push_undo(EditOp::Delete {
335            byte_offset: start_byte,
336            text: deleted,
337        });
338
339        self.rope.remove(start_char..end_char);
340
341        // Cursor stays at same position, just re-clamp
342        let nav = CursorNavigator::new(&self.rope);
343        self.cursor = nav.clamp(self.cursor);
344        true
345    }
346
347    /// Delete the word before the cursor (Ctrl+Backspace).
348    ///
349    /// Returns `true` if any text was deleted.
350    pub fn delete_word_backward(&mut self) -> bool {
351        if self.delete_selection_inner() {
352            return true;
353        }
354        let nav = CursorNavigator::new(&self.rope);
355        let old_pos = self.cursor;
356        let word_start = nav.move_word_left(old_pos);
357
358        if word_start == old_pos {
359            return false;
360        }
361
362        let start_byte = nav.to_byte_index(word_start);
363        let end_byte = nav.to_byte_index(old_pos);
364        let start_char = self.rope.byte_to_char(start_byte);
365        let end_char = self.rope.byte_to_char(end_byte);
366        let deleted = self.rope.slice(start_char..end_char).into_owned();
367
368        self.push_undo(EditOp::Delete {
369            byte_offset: start_byte,
370            text: deleted,
371        });
372
373        self.rope.remove(start_char..end_char);
374
375        let nav = CursorNavigator::new(&self.rope);
376        self.cursor = nav.from_byte_index(start_byte);
377        true
378    }
379
380    /// Delete from cursor to end of line (Ctrl+K).
381    ///
382    /// Returns `true` if any text was deleted.
383    pub fn delete_to_end_of_line(&mut self) -> bool {
384        if self.delete_selection_inner() {
385            return true;
386        }
387        let nav = CursorNavigator::new(&self.rope);
388        let old_pos = self.cursor;
389        let line_end = nav.line_end(old_pos);
390
391        if line_end == old_pos {
392            // At end of line: delete the newline to join lines
393            return self.delete_forward();
394        }
395
396        let start_byte = nav.to_byte_index(old_pos);
397        let end_byte = nav.to_byte_index(line_end);
398        let start_char = self.rope.byte_to_char(start_byte);
399        let end_char = self.rope.byte_to_char(end_byte);
400        let deleted = self.rope.slice(start_char..end_char).into_owned();
401
402        self.push_undo(EditOp::Delete {
403            byte_offset: start_byte,
404            text: deleted,
405        });
406
407        self.rope.remove(start_char..end_char);
408
409        let nav = CursorNavigator::new(&self.rope);
410        self.cursor = nav.clamp(self.cursor);
411        true
412    }
413
414    // ====================================================================
415    // Undo / redo
416    // ====================================================================
417
418    /// Push an edit operation onto the undo stack.
419    fn push_undo(&mut self, op: EditOp) {
420        let op_len = op.byte_len();
421        self.undo_stack.push((op, self.cursor));
422        self.current_undo_size += op_len;
423
424        // Prune by count
425        if self.undo_stack.len() > self.max_history {
426            if let Some((removed_op, _)) = self.undo_stack.first() {
427                self.current_undo_size =
428                    self.current_undo_size.saturating_sub(removed_op.byte_len());
429            }
430            self.undo_stack.remove(0);
431        }
432
433        // Prune by size
434        while self.current_undo_size > self.max_undo_size && !self.undo_stack.is_empty() {
435            let (removed_op, _) = self.undo_stack.remove(0);
436            self.current_undo_size = self.current_undo_size.saturating_sub(removed_op.byte_len());
437        }
438
439        self.redo_stack.clear();
440    }
441
442    /// Undo the last edit operation.
443    pub fn undo(&mut self) -> bool {
444        let Some((op, cursor_before)) = self.undo_stack.pop() else {
445            return false;
446        };
447        self.current_undo_size = self.current_undo_size.saturating_sub(op.byte_len());
448        let inverse = op.inverse();
449        self.apply_op(&inverse);
450        self.redo_stack.push((inverse, self.cursor));
451        self.cursor = cursor_before;
452        self.selection = None;
453        true
454    }
455
456    /// Redo the last undone operation.
457    pub fn redo(&mut self) -> bool {
458        let Some((op, cursor_before)) = self.redo_stack.pop() else {
459            return false;
460        };
461        let inverse = op.inverse();
462        self.apply_op(&inverse);
463
464        let op_len = inverse.byte_len();
465        self.undo_stack.push((inverse, self.cursor));
466        self.current_undo_size += op_len;
467
468        // Ensure size limit after redo (edge case where redo grows stack)
469        while self.current_undo_size > self.max_undo_size && !self.undo_stack.is_empty() {
470            let (removed_op, _) = self.undo_stack.remove(0);
471            self.current_undo_size = self.current_undo_size.saturating_sub(removed_op.byte_len());
472        }
473
474        self.cursor = cursor_before;
475        self.selection = None;
476        // Move cursor to the correct position after redo
477        let nav = CursorNavigator::new(&self.rope);
478        self.cursor = nav.clamp(self.cursor);
479        true
480    }
481
482    /// Apply an edit operation directly to the rope.
483    fn apply_op(&mut self, op: &EditOp) {
484        match op {
485            EditOp::Insert { byte_offset, text } => {
486                let char_idx = self.rope.byte_to_char(*byte_offset);
487                self.rope.insert(char_idx, text);
488            }
489            EditOp::Delete { byte_offset, text } => {
490                let start_char = self.rope.byte_to_char(*byte_offset);
491                let end_char = self.rope.byte_to_char(*byte_offset + text.len());
492                self.rope.remove(start_char..end_char);
493            }
494        }
495    }
496
497    // ====================================================================
498    // Selection helpers
499    // ====================================================================
500
501    /// Delete the current selection if active. Returns true if something was deleted.
502    fn delete_selection_inner(&mut self) -> bool {
503        let Some(sel) = self.selection.take() else {
504            return false;
505        };
506        if sel.is_empty() {
507            return false;
508        }
509        let nav = CursorNavigator::new(&self.rope);
510        let (start_byte, end_byte) = sel.byte_range(&nav);
511        let start_char = self.rope.byte_to_char(start_byte);
512        let end_char = self.rope.byte_to_char(end_byte);
513        let deleted = self.rope.slice(start_char..end_char).into_owned();
514
515        self.push_undo(EditOp::Delete {
516            byte_offset: start_byte,
517            text: deleted,
518        });
519
520        self.rope.remove(start_char..end_char);
521        let nav = CursorNavigator::new(&self.rope);
522        self.cursor = nav.from_byte_index(start_byte);
523        true
524    }
525
526    // ====================================================================
527    // Cursor movement (clears selection)
528    // ====================================================================
529
530    /// Move cursor left by one grapheme.
531    pub fn move_left(&mut self) {
532        self.selection = None;
533        let nav = CursorNavigator::new(&self.rope);
534        self.cursor = nav.move_left(self.cursor);
535    }
536
537    /// Move cursor right by one grapheme.
538    pub fn move_right(&mut self) {
539        self.selection = None;
540        let nav = CursorNavigator::new(&self.rope);
541        self.cursor = nav.move_right(self.cursor);
542    }
543
544    /// Move cursor up one line.
545    pub fn move_up(&mut self) {
546        self.selection = None;
547        let nav = CursorNavigator::new(&self.rope);
548        self.cursor = nav.move_up(self.cursor);
549    }
550
551    /// Move cursor down one line.
552    pub fn move_down(&mut self) {
553        self.selection = None;
554        let nav = CursorNavigator::new(&self.rope);
555        self.cursor = nav.move_down(self.cursor);
556    }
557
558    /// Move cursor left by one word.
559    pub fn move_word_left(&mut self) {
560        self.selection = None;
561        let nav = CursorNavigator::new(&self.rope);
562        self.cursor = nav.move_word_left(self.cursor);
563    }
564
565    /// Move cursor right by one word.
566    pub fn move_word_right(&mut self) {
567        self.selection = None;
568        let nav = CursorNavigator::new(&self.rope);
569        self.cursor = nav.move_word_right(self.cursor);
570    }
571
572    /// Move cursor to start of line.
573    pub fn move_to_line_start(&mut self) {
574        self.selection = None;
575        let nav = CursorNavigator::new(&self.rope);
576        self.cursor = nav.line_start(self.cursor);
577    }
578
579    /// Move cursor to end of line.
580    pub fn move_to_line_end(&mut self) {
581        self.selection = None;
582        let nav = CursorNavigator::new(&self.rope);
583        self.cursor = nav.line_end(self.cursor);
584    }
585
586    /// Move cursor to start of document.
587    pub fn move_to_document_start(&mut self) {
588        self.selection = None;
589        let nav = CursorNavigator::new(&self.rope);
590        self.cursor = nav.document_start();
591    }
592
593    /// Move cursor to end of document.
594    pub fn move_to_document_end(&mut self) {
595        self.selection = None;
596        let nav = CursorNavigator::new(&self.rope);
597        self.cursor = nav.document_end();
598    }
599
600    // ====================================================================
601    // Selection extension
602    // ====================================================================
603
604    /// Extend selection left by one grapheme.
605    pub fn select_left(&mut self) {
606        self.extend_selection(|nav, pos| nav.move_left(pos));
607    }
608
609    /// Extend selection right by one grapheme.
610    pub fn select_right(&mut self) {
611        self.extend_selection(|nav, pos| nav.move_right(pos));
612    }
613
614    /// Extend selection up one line.
615    pub fn select_up(&mut self) {
616        self.extend_selection(|nav, pos| nav.move_up(pos));
617    }
618
619    /// Extend selection down one line.
620    pub fn select_down(&mut self) {
621        self.extend_selection(|nav, pos| nav.move_down(pos));
622    }
623
624    /// Extend selection left by one word.
625    pub fn select_word_left(&mut self) {
626        self.extend_selection(|nav, pos| nav.move_word_left(pos));
627    }
628
629    /// Extend selection right by one word.
630    pub fn select_word_right(&mut self) {
631        self.extend_selection(|nav, pos| nav.move_word_right(pos));
632    }
633
634    /// Select all text.
635    pub fn select_all(&mut self) {
636        let nav = CursorNavigator::new(&self.rope);
637        let start = nav.document_start();
638        let end = nav.document_end();
639        self.selection = Some(Selection {
640            anchor: start,
641            head: end,
642        });
643        self.cursor = end;
644    }
645
646    /// Clear current selection without moving cursor.
647    pub fn clear_selection(&mut self) {
648        self.selection = None;
649    }
650
651    /// Get selected text, if any non-empty selection exists.
652    #[must_use]
653    pub fn selected_text(&self) -> Option<String> {
654        let sel = self.selection?;
655        if sel.is_empty() {
656            return None;
657        }
658        let nav = CursorNavigator::new(&self.rope);
659        let (start, end) = sel.byte_range(&nav);
660        let start_char = self.rope.byte_to_char(start);
661        let end_char = self.rope.byte_to_char(end);
662        Some(self.rope.slice(start_char..end_char).into_owned())
663    }
664
665    fn extend_selection(
666        &mut self,
667        f: impl FnOnce(&CursorNavigator<'_>, CursorPosition) -> CursorPosition,
668    ) {
669        let anchor = match self.selection {
670            Some(sel) => sel.anchor,
671            None => self.cursor,
672        };
673        let nav = CursorNavigator::new(&self.rope);
674        let new_head = f(&nav, self.cursor);
675        self.cursor = new_head;
676        self.selection = Some(Selection {
677            anchor,
678            head: new_head,
679        });
680    }
681
682    // ====================================================================
683    // Content replacement
684    // ====================================================================
685
686    /// Replace all content and reset cursor to end. Clears undo history.
687    pub fn set_text(&mut self, text: &str) {
688        self.rope.replace(text);
689        let nav = CursorNavigator::new(&self.rope);
690        self.cursor = nav.document_end();
691        self.selection = None;
692        self.undo_stack.clear();
693        self.redo_stack.clear();
694        self.current_undo_size = 0;
695    }
696
697    /// Clear all content and reset cursor. Clears undo history.
698    pub fn clear(&mut self) {
699        self.rope.clear();
700        self.cursor = CursorPosition::default();
701        self.selection = None;
702        self.undo_stack.clear();
703        self.redo_stack.clear();
704        self.current_undo_size = 0;
705    }
706}
707
708impl EditOp {
709    fn byte_len(&self) -> usize {
710        match self {
711            Self::Insert { text, .. } => text.len(),
712            Self::Delete { text, .. } => text.len(),
713        }
714    }
715}
716
717#[cfg(test)]
718mod tests {
719    use super::*;
720
721    #[test]
722    fn new_editor_is_empty() {
723        let ed = Editor::new();
724        assert!(ed.is_empty());
725        assert_eq!(ed.text(), "");
726        assert_eq!(ed.cursor(), CursorPosition::default());
727    }
728
729    #[test]
730    fn with_text_cursor_at_end() {
731        let ed = Editor::with_text("hello");
732        assert_eq!(ed.text(), "hello");
733        assert_eq!(ed.cursor().line, 0);
734        assert_eq!(ed.cursor().grapheme, 5);
735    }
736
737    #[test]
738    fn insert_char_at_end() {
739        let mut ed = Editor::new();
740        ed.insert_char('a');
741        ed.insert_char('b');
742        ed.insert_char('c');
743        assert_eq!(ed.text(), "abc");
744        assert_eq!(ed.cursor().grapheme, 3);
745    }
746
747    #[test]
748    fn insert_text() {
749        let mut ed = Editor::new();
750        ed.insert_text("hello world");
751        assert_eq!(ed.text(), "hello world");
752    }
753
754    #[test]
755    fn insert_in_middle() {
756        let mut ed = Editor::with_text("helo");
757        // Move cursor to position 3 (after "hel")
758        ed.set_cursor(CursorPosition::new(0, 3, 3));
759        ed.insert_char('l');
760        assert_eq!(ed.text(), "hello");
761    }
762
763    #[test]
764    fn insert_newline() {
765        let mut ed = Editor::with_text("hello world");
766        // Move cursor after "hello"
767        ed.set_cursor(CursorPosition::new(0, 5, 5));
768        ed.insert_newline();
769        assert_eq!(ed.text(), "hello\n world");
770        assert_eq!(ed.cursor().line, 1);
771        assert_eq!(ed.line_count(), 2);
772    }
773
774    #[test]
775    fn delete_backward() {
776        let mut ed = Editor::with_text("hello");
777        assert!(ed.delete_backward());
778        assert_eq!(ed.text(), "hell");
779    }
780
781    #[test]
782    fn delete_backward_at_beginning() {
783        let mut ed = Editor::with_text("hello");
784        ed.set_cursor(CursorPosition::new(0, 0, 0));
785        assert!(!ed.delete_backward());
786        assert_eq!(ed.text(), "hello");
787    }
788
789    #[test]
790    fn delete_backward_joins_lines() {
791        let mut ed = Editor::with_text("hello\nworld");
792        // Cursor at start of "world"
793        ed.set_cursor(CursorPosition::new(1, 0, 0));
794        assert!(ed.delete_backward());
795        assert_eq!(ed.text(), "helloworld");
796        assert_eq!(ed.line_count(), 1);
797    }
798
799    #[test]
800    fn delete_forward() {
801        let mut ed = Editor::with_text("hello");
802        ed.set_cursor(CursorPosition::new(0, 0, 0));
803        assert!(ed.delete_forward());
804        assert_eq!(ed.text(), "ello");
805    }
806
807    #[test]
808    fn delete_forward_at_end() {
809        let mut ed = Editor::with_text("hello");
810        assert!(!ed.delete_forward());
811        assert_eq!(ed.text(), "hello");
812    }
813
814    #[test]
815    fn delete_forward_joins_lines() {
816        let mut ed = Editor::with_text("hello\nworld");
817        // Cursor at end of "hello"
818        ed.set_cursor(CursorPosition::new(0, 5, 5));
819        assert!(ed.delete_forward());
820        assert_eq!(ed.text(), "helloworld");
821    }
822
823    #[test]
824    fn move_left_right() {
825        let mut ed = Editor::with_text("abc");
826        assert_eq!(ed.cursor().grapheme, 3);
827
828        ed.move_left();
829        assert_eq!(ed.cursor().grapheme, 2);
830
831        ed.move_left();
832        assert_eq!(ed.cursor().grapheme, 1);
833
834        ed.move_right();
835        assert_eq!(ed.cursor().grapheme, 2);
836    }
837
838    #[test]
839    fn move_left_at_start_is_noop() {
840        let mut ed = Editor::with_text("abc");
841        ed.set_cursor(CursorPosition::new(0, 0, 0));
842        ed.move_left();
843        assert_eq!(ed.cursor().grapheme, 0);
844        assert_eq!(ed.cursor().line, 0);
845    }
846
847    #[test]
848    fn move_right_at_end_is_noop() {
849        let mut ed = Editor::with_text("abc");
850        ed.move_right();
851        assert_eq!(ed.cursor().grapheme, 3);
852    }
853
854    #[test]
855    fn move_up_down() {
856        let mut ed = Editor::with_text("line 1\nline 2\nline 3");
857        // Cursor at end of "line 3"
858        assert_eq!(ed.cursor().line, 2);
859
860        ed.move_up();
861        assert_eq!(ed.cursor().line, 1);
862
863        ed.move_up();
864        assert_eq!(ed.cursor().line, 0);
865
866        // At top, stays
867        ed.move_up();
868        assert_eq!(ed.cursor().line, 0);
869
870        ed.move_down();
871        assert_eq!(ed.cursor().line, 1);
872    }
873
874    #[test]
875    fn move_to_line_start_end() {
876        let mut ed = Editor::with_text("hello world");
877        ed.set_cursor(CursorPosition::new(0, 5, 5));
878
879        ed.move_to_line_start();
880        assert_eq!(ed.cursor().grapheme, 0);
881
882        ed.move_to_line_end();
883        assert_eq!(ed.cursor().grapheme, 11);
884    }
885
886    #[test]
887    fn move_to_document_start_end() {
888        let mut ed = Editor::with_text("line 1\nline 2\nline 3");
889
890        ed.move_to_document_start();
891        assert_eq!(ed.cursor().line, 0);
892        assert_eq!(ed.cursor().grapheme, 0);
893
894        ed.move_to_document_end();
895        assert_eq!(ed.cursor().line, 2);
896    }
897
898    #[test]
899    fn move_word_left_right() {
900        let mut ed = Editor::with_text("hello world foo");
901        // Cursor at end (grapheme 15)
902        let start = ed.cursor().grapheme;
903
904        ed.move_word_left();
905        let after_first = ed.cursor().grapheme;
906        assert!(after_first < start, "word_left should move cursor left");
907
908        ed.move_word_left();
909        let after_second = ed.cursor().grapheme;
910        assert!(
911            after_second < after_first,
912            "second word_left should move further left"
913        );
914
915        ed.move_word_right();
916        let after_right = ed.cursor().grapheme;
917        assert!(
918            after_right > after_second,
919            "word_right should move cursor right"
920        );
921    }
922
923    #[test]
924    fn delete_word_backward() {
925        let mut ed = Editor::with_text("hello world");
926        assert!(ed.delete_word_backward());
927        assert_eq!(ed.text(), "hello ");
928    }
929
930    #[test]
931    fn delete_to_end_of_line() {
932        let mut ed = Editor::with_text("hello world");
933        ed.set_cursor(CursorPosition::new(0, 5, 5));
934        assert!(ed.delete_to_end_of_line());
935        assert_eq!(ed.text(), "hello");
936    }
937
938    #[test]
939    fn delete_to_end_joins_when_at_line_end() {
940        let mut ed = Editor::with_text("hello\nworld");
941        ed.set_cursor(CursorPosition::new(0, 5, 5));
942        assert!(ed.delete_to_end_of_line());
943        assert_eq!(ed.text(), "helloworld");
944    }
945
946    #[test]
947    fn set_text_replaces_content() {
948        let mut ed = Editor::with_text("old");
949        ed.set_text("new content");
950        assert_eq!(ed.text(), "new content");
951    }
952
953    #[test]
954    fn clear_resets() {
955        let mut ed = Editor::with_text("hello");
956        ed.clear();
957        assert!(ed.is_empty());
958        assert_eq!(ed.cursor(), CursorPosition::default());
959    }
960
961    #[test]
962    fn line_text_works() {
963        let ed = Editor::with_text("line 0\nline 1\nline 2");
964        assert_eq!(ed.line_text(0), Some("line 0".to_string()));
965        assert_eq!(ed.line_text(1), Some("line 1".to_string()));
966        assert_eq!(ed.line_text(2), Some("line 2".to_string()));
967        assert_eq!(ed.line_text(3), None);
968    }
969
970    #[test]
971    fn cursor_stays_in_bounds_after_delete() {
972        let mut ed = Editor::with_text("a");
973        assert!(ed.delete_backward());
974        assert_eq!(ed.text(), "");
975        assert_eq!(ed.cursor(), CursorPosition::default());
976
977        // Further deletes are no-ops
978        assert!(!ed.delete_backward());
979        assert!(!ed.delete_forward());
980    }
981
982    #[test]
983    fn multiline_editing() {
984        let mut ed = Editor::new();
985        ed.insert_text("first");
986        ed.insert_newline();
987        ed.insert_text("second");
988        ed.insert_newline();
989        ed.insert_text("third");
990
991        assert_eq!(ed.text(), "first\nsecond\nthird");
992        assert_eq!(ed.line_count(), 3);
993        assert_eq!(ed.cursor().line, 2);
994
995        // Move up and insert at start of middle line
996        ed.move_up();
997        ed.move_to_line_start();
998        ed.insert_text(">> ");
999        assert_eq!(ed.line_text(1), Some(">> second".to_string()));
1000    }
1001
1002    // ================================================================
1003    // Undo / Redo tests
1004    // ================================================================
1005
1006    #[test]
1007    fn undo_insert() {
1008        let mut ed = Editor::new();
1009        ed.insert_text("hello");
1010        assert!(ed.can_undo());
1011        assert!(ed.undo());
1012        assert_eq!(ed.text(), "");
1013    }
1014
1015    #[test]
1016    fn undo_delete() {
1017        let mut ed = Editor::with_text("hello");
1018        ed.delete_backward();
1019        assert_eq!(ed.text(), "hell");
1020        assert!(ed.undo());
1021        assert_eq!(ed.text(), "hello");
1022    }
1023
1024    #[test]
1025    fn redo_after_undo() {
1026        let mut ed = Editor::new();
1027        ed.insert_text("abc");
1028        ed.undo();
1029        assert_eq!(ed.text(), "");
1030        assert!(ed.can_redo());
1031        assert!(ed.redo());
1032        assert_eq!(ed.text(), "abc");
1033    }
1034
1035    #[test]
1036    fn redo_cleared_on_new_edit() {
1037        let mut ed = Editor::new();
1038        ed.insert_text("abc");
1039        ed.undo();
1040        ed.insert_text("xyz");
1041        assert!(!ed.can_redo());
1042    }
1043
1044    #[test]
1045    fn multiple_undo_redo() {
1046        let mut ed = Editor::new();
1047        ed.insert_text("a");
1048        ed.insert_text("b");
1049        ed.insert_text("c");
1050        assert_eq!(ed.text(), "abc");
1051
1052        ed.undo();
1053        assert_eq!(ed.text(), "ab");
1054        ed.undo();
1055        assert_eq!(ed.text(), "a");
1056        ed.undo();
1057        assert_eq!(ed.text(), "");
1058
1059        ed.redo();
1060        assert_eq!(ed.text(), "a");
1061        ed.redo();
1062        assert_eq!(ed.text(), "ab");
1063    }
1064
1065    #[test]
1066    fn undo_restores_cursor() {
1067        let mut ed = Editor::new();
1068        let before = ed.cursor();
1069        ed.insert_text("x");
1070        ed.undo();
1071        assert_eq!(ed.cursor(), before);
1072    }
1073
1074    #[test]
1075    fn max_history_respected() {
1076        let mut ed = Editor::new();
1077        ed.set_max_history(3);
1078        for c in ['a', 'b', 'c', 'd', 'e'] {
1079            ed.insert_text(&c.to_string());
1080        }
1081        assert!(ed.undo());
1082        assert!(ed.undo());
1083        assert!(ed.undo());
1084        assert!(!ed.undo());
1085        assert_eq!(ed.text(), "ab");
1086    }
1087
1088    #[test]
1089    fn set_text_clears_undo() {
1090        let mut ed = Editor::new();
1091        ed.insert_text("abc");
1092        ed.set_text("new");
1093        assert!(!ed.can_undo());
1094        assert!(!ed.can_redo());
1095    }
1096
1097    #[test]
1098    fn clear_clears_undo() {
1099        let mut ed = Editor::new();
1100        ed.insert_text("abc");
1101        ed.clear();
1102        assert!(!ed.can_undo());
1103    }
1104
1105    // ================================================================
1106    // Selection tests
1107    // ================================================================
1108
1109    #[test]
1110    fn select_right_creates_selection() {
1111        let mut ed = Editor::with_text("hello");
1112        ed.set_cursor(CursorPosition::new(0, 0, 0));
1113        ed.select_right();
1114        ed.select_right();
1115        ed.select_right();
1116        let sel = ed.selection().unwrap();
1117        assert_eq!(sel.anchor, CursorPosition::new(0, 0, 0));
1118        assert_eq!(sel.head.grapheme, 3);
1119        assert_eq!(ed.selected_text(), Some("hel".to_string()));
1120    }
1121
1122    #[test]
1123    fn select_all_selects_everything() {
1124        let mut ed = Editor::with_text("abc\ndef");
1125        ed.select_all();
1126        assert_eq!(ed.selected_text(), Some("abc\ndef".to_string()));
1127    }
1128
1129    #[test]
1130    fn insert_replaces_selection() {
1131        let mut ed = Editor::with_text("hello world");
1132        ed.set_cursor(CursorPosition::new(0, 0, 0));
1133        for _ in 0..5 {
1134            ed.select_right();
1135        }
1136        ed.insert_text("goodbye");
1137        assert_eq!(ed.text(), "goodbye world");
1138        assert!(ed.selection().is_none());
1139    }
1140
1141    #[test]
1142    fn delete_backward_removes_selection() {
1143        let mut ed = Editor::with_text("hello world");
1144        ed.set_cursor(CursorPosition::new(0, 0, 0));
1145        for _ in 0..5 {
1146            ed.select_right();
1147        }
1148        ed.delete_backward();
1149        assert_eq!(ed.text(), " world");
1150    }
1151
1152    #[test]
1153    fn movement_clears_selection() {
1154        let mut ed = Editor::with_text("hello");
1155        ed.set_cursor(CursorPosition::new(0, 0, 0));
1156        ed.select_right();
1157        ed.select_right();
1158        assert!(ed.selection().is_some());
1159        ed.move_right();
1160        assert!(ed.selection().is_none());
1161    }
1162
1163    #[test]
1164    fn undo_selection_delete() {
1165        let mut ed = Editor::with_text("hello world");
1166        ed.set_cursor(CursorPosition::new(0, 0, 0));
1167        for _ in 0..5 {
1168            ed.select_right();
1169        }
1170        ed.delete_backward();
1171        assert_eq!(ed.text(), " world");
1172        ed.undo();
1173        assert_eq!(ed.text(), "hello world");
1174    }
1175
1176    // ================================================================
1177    // Edge case tests
1178    // ================================================================
1179
1180    #[test]
1181    fn insert_empty_text_is_noop() {
1182        let mut ed = Editor::with_text("hello");
1183        let before = ed.text();
1184        ed.insert_text("");
1185        assert_eq!(ed.text(), before);
1186        // No undo entry for empty insert
1187        assert!(!ed.can_undo());
1188    }
1189
1190    #[test]
1191    fn unicode_emoji_handling() {
1192        let mut ed = Editor::new();
1193        ed.insert_text("hello πŸŽ‰ world");
1194        assert_eq!(ed.text(), "hello πŸŽ‰ world");
1195        // Emoji counts as one grapheme
1196        ed.move_left(); // d
1197        ed.move_left(); // l
1198        ed.move_left(); // r
1199        ed.move_left(); // o
1200        ed.move_left(); // w
1201        ed.move_left(); // space
1202        ed.move_left(); // emoji (single grapheme move)
1203        ed.delete_backward(); // deletes space before emoji
1204        assert_eq!(ed.text(), "helloπŸŽ‰ world");
1205    }
1206
1207    #[test]
1208    fn unicode_combining_character() {
1209        let mut ed = Editor::new();
1210        // Γ© as e + combining acute accent (decomposed form)
1211        ed.insert_text("caf\u{0065}\u{0301}");
1212        // Text stays in decomposed form (e + combining accent)
1213        assert_eq!(ed.text(), "caf\u{0065}\u{0301}");
1214        // The combining sequence is one grapheme, so delete_backward removes both
1215        ed.delete_backward();
1216        assert_eq!(ed.text(), "caf");
1217    }
1218
1219    #[test]
1220    fn unicode_zwj_sequence() {
1221        let mut ed = Editor::new();
1222        // Woman astronaut: woman + ZWJ + rocket
1223        ed.insert_text("πŸ‘©\u{200D}πŸš€");
1224        let text = ed.text();
1225        assert!(text.contains("πŸ‘©"));
1226        // Move left should treat ZWJ sequence as one grapheme
1227        ed.move_left();
1228        // We're now before the ZWJ sequence
1229        ed.insert_char('x');
1230        assert!(ed.text().starts_with('x'));
1231    }
1232
1233    #[test]
1234    fn unicode_cjk_wide_chars() {
1235        let mut ed = Editor::new();
1236        ed.insert_text("δΈ–η•Œ");
1237        assert_eq!(ed.text(), "δΈ–η•Œ");
1238        ed.move_left();
1239        assert_eq!(ed.cursor().grapheme, 1);
1240        ed.move_left();
1241        assert_eq!(ed.cursor().grapheme, 0);
1242    }
1243
1244    #[test]
1245    fn crlf_handling() {
1246        let ed = Editor::with_text("hello\r\nworld");
1247        assert_eq!(ed.line_count(), 2);
1248        assert_eq!(ed.line_text(0), Some("hello".to_string()));
1249        assert_eq!(ed.line_text(1), Some("world".to_string()));
1250    }
1251
1252    #[test]
1253    fn mixed_newlines() {
1254        let ed = Editor::with_text("line1\nline2\r\nline3");
1255        assert_eq!(ed.line_count(), 3);
1256        assert_eq!(ed.line_text(0), Some("line1".to_string()));
1257        assert_eq!(ed.line_text(1), Some("line2".to_string()));
1258        assert_eq!(ed.line_text(2), Some("line3".to_string()));
1259    }
1260
1261    #[test]
1262    fn trailing_newline() {
1263        let ed = Editor::with_text("hello\n");
1264        assert_eq!(ed.line_count(), 2);
1265        assert_eq!(ed.line_text(0), Some("hello".to_string()));
1266        assert_eq!(ed.line_text(1), Some(String::new()));
1267    }
1268
1269    #[test]
1270    fn only_newlines() {
1271        let ed = Editor::with_text("\n\n\n");
1272        assert_eq!(ed.line_count(), 4);
1273        for i in 0..4 {
1274            assert_eq!(ed.line_text(i), Some(String::new()));
1275        }
1276    }
1277
1278    #[test]
1279    fn delete_word_backward_at_start_is_noop() {
1280        let mut ed = Editor::with_text("hello");
1281        ed.set_cursor(CursorPosition::new(0, 0, 0));
1282        assert!(!ed.delete_word_backward());
1283        assert_eq!(ed.text(), "hello");
1284    }
1285
1286    #[test]
1287    fn delete_word_backward_multiple_spaces() {
1288        let mut ed = Editor::with_text("hello    world");
1289        // Cursor at end
1290        assert!(ed.delete_word_backward());
1291        // Should delete "world"
1292        let remaining = ed.text();
1293        assert!(remaining.starts_with("hello"));
1294    }
1295
1296    #[test]
1297    fn delete_to_end_at_document_end() {
1298        let mut ed = Editor::with_text("hello");
1299        // Cursor already at end from with_text
1300        assert!(!ed.delete_to_end_of_line());
1301        assert_eq!(ed.text(), "hello");
1302    }
1303
1304    #[test]
1305    fn select_word_operations() {
1306        let mut ed = Editor::with_text("hello world");
1307        ed.set_cursor(CursorPosition::new(0, 0, 0));
1308        // move_word_right now skips the word and trailing whitespace
1309        ed.select_word_right();
1310        assert_eq!(ed.selected_text(), Some("hello ".to_string()));
1311        ed.clear_selection();
1312        ed.move_to_line_end();
1313        ed.select_word_left();
1314        assert_eq!(ed.selected_text(), Some("world".to_string()));
1315    }
1316
1317    #[test]
1318    fn select_up_down() {
1319        let mut ed = Editor::with_text("line1\nline2\nline3");
1320        ed.set_cursor(CursorPosition::new(1, 3, 3));
1321        ed.select_up();
1322        let sel = ed.selection().expect("should have selection");
1323        assert_eq!(sel.anchor.line, 1);
1324        assert_eq!(sel.head.line, 0);
1325        ed.select_down();
1326        ed.select_down();
1327        let sel = ed.selection().expect("should have selection");
1328        assert_eq!(sel.head.line, 2);
1329    }
1330
1331    #[test]
1332    fn selection_extending_preserves_anchor() {
1333        let mut ed = Editor::with_text("abcdef");
1334        ed.set_cursor(CursorPosition::new(0, 2, 2));
1335        ed.select_right();
1336        ed.select_right();
1337        ed.select_right();
1338        let sel = ed.selection().unwrap();
1339        assert_eq!(sel.anchor.grapheme, 2);
1340        assert_eq!(sel.head.grapheme, 5);
1341        // Now extend left
1342        ed.select_left();
1343        let sel = ed.selection().unwrap();
1344        assert_eq!(sel.anchor.grapheme, 2);
1345        assert_eq!(sel.head.grapheme, 4);
1346    }
1347
1348    #[test]
1349    fn empty_selection_returns_none() {
1350        let mut ed = Editor::with_text("hello");
1351        ed.set_cursor(CursorPosition::new(0, 2, 2));
1352        // Create selection with same anchor and head
1353        ed.select_right();
1354        ed.select_left();
1355        // Now anchor == head
1356        let sel = ed.selection().unwrap();
1357        assert!(sel.is_empty());
1358        assert_eq!(ed.selected_text(), None);
1359    }
1360
1361    #[test]
1362    fn cursor_clamp_after_set_text() {
1363        let mut ed = Editor::with_text("very long text here");
1364        ed.set_text("hi");
1365        // Cursor should be at end of "hi"
1366        assert_eq!(ed.cursor().line, 0);
1367        assert_eq!(ed.cursor().grapheme, 2);
1368    }
1369
1370    #[test]
1371    fn undo_redo_with_selection() {
1372        let mut ed = Editor::with_text("hello world");
1373        ed.set_cursor(CursorPosition::new(0, 6, 6));
1374        // Select "world"
1375        for _ in 0..5 {
1376            ed.select_right();
1377        }
1378        ed.insert_text("universe");
1379        assert_eq!(ed.text(), "hello universe");
1380        // insert_text with selection creates 2 undo entries: delete + insert
1381        // So we need 2 undos to fully restore
1382        ed.undo(); // undoes the insert
1383        assert_eq!(ed.text(), "hello ");
1384        ed.undo(); // undoes the selection delete
1385        assert_eq!(ed.text(), "hello world");
1386        // And 2 redos to restore
1387        ed.redo();
1388        assert_eq!(ed.text(), "hello ");
1389        ed.redo();
1390        assert_eq!(ed.text(), "hello universe");
1391    }
1392
1393    #[test]
1394    fn rapid_insert_delete_cycle() {
1395        let mut ed = Editor::new();
1396        for i in 0..100 {
1397            ed.insert_char(char::from_u32('a' as u32 + (i % 26)).unwrap());
1398            if i % 3 == 0 {
1399                ed.delete_backward();
1400            }
1401        }
1402        // Should not panic, cursor should be valid
1403        let cursor = ed.cursor();
1404        assert!(cursor.line == 0);
1405        assert!(cursor.grapheme <= ed.text().chars().count());
1406    }
1407
1408    #[test]
1409    fn multiline_select_all_and_replace() {
1410        let mut ed = Editor::with_text("line1\nline2\nline3");
1411        ed.select_all();
1412        ed.insert_text("replaced");
1413        assert_eq!(ed.text(), "replaced");
1414        assert_eq!(ed.line_count(), 1);
1415    }
1416
1417    #[test]
1418    fn delete_forward_with_selection() {
1419        let mut ed = Editor::with_text("hello world");
1420        ed.set_cursor(CursorPosition::new(0, 0, 0));
1421        for _ in 0..5 {
1422            ed.select_right();
1423        }
1424        // delete_forward with selection should delete selection, not char after
1425        ed.delete_forward();
1426        assert_eq!(ed.text(), " world");
1427    }
1428
1429    #[test]
1430    fn delete_word_backward_with_selection() {
1431        let mut ed = Editor::with_text("hello world");
1432        ed.set_cursor(CursorPosition::new(0, 6, 6));
1433        for _ in 0..5 {
1434            ed.select_right();
1435        }
1436        // Should delete selection, not word
1437        ed.delete_word_backward();
1438        assert_eq!(ed.text(), "hello ");
1439    }
1440
1441    #[test]
1442    fn default_impl() {
1443        let ed = Editor::default();
1444        assert!(ed.is_empty());
1445        assert_eq!(ed.cursor(), CursorPosition::default());
1446    }
1447
1448    #[test]
1449    fn line_text_out_of_bounds() {
1450        let ed = Editor::with_text("hello");
1451        assert_eq!(ed.line_text(0), Some("hello".to_string()));
1452        assert_eq!(ed.line_text(1), None);
1453        assert_eq!(ed.line_text(100), None);
1454    }
1455
1456    #[test]
1457    fn rope_accessor() {
1458        let ed = Editor::with_text("test");
1459        let rope = ed.rope();
1460        assert_eq!(rope.len_bytes(), 4);
1461    }
1462
1463    #[test]
1464    fn insert_text_sanitizes_controls() {
1465        let mut ed = Editor::new();
1466        // Insert text with ESC (\x1b) and BEL (\x07) mixed with safe chars
1467        ed.insert_text("hello\x1bworld\x07\n\t!");
1468        // Should contain "hello", "world", "\n", "\t", "!" but NO control chars
1469        assert_eq!(ed.text(), "helloworld\n\t!");
1470    }
1471
1472    #[test]
1473    fn cursor_position_after_multiline_insert() {
1474        let mut ed = Editor::new();
1475        ed.insert_text("hello\nworld\nfoo");
1476        assert_eq!(ed.cursor().line, 2);
1477        assert_eq!(ed.line_count(), 3);
1478    }
1479
1480    #[test]
1481    fn delete_backward_across_lines() {
1482        let mut ed = Editor::with_text("abc\ndef");
1483        ed.set_cursor(CursorPosition::new(1, 0, 0));
1484        ed.delete_backward();
1485        assert_eq!(ed.text(), "abcdef");
1486        assert_eq!(ed.cursor().line, 0);
1487        assert_eq!(ed.cursor().grapheme, 3);
1488    }
1489
1490    #[test]
1491    fn very_long_line() {
1492        let long_text: String = "a".repeat(10000);
1493        let mut ed = Editor::with_text(&long_text);
1494        assert_eq!(ed.text().len(), 10000);
1495        ed.move_to_line_start();
1496        assert_eq!(ed.cursor().grapheme, 0);
1497        ed.move_to_line_end();
1498        assert_eq!(ed.cursor().grapheme, 10000);
1499    }
1500
1501    #[test]
1502    fn many_lines() {
1503        let text: String = (0..1000)
1504            .map(|i| format!("line{i}"))
1505            .collect::<Vec<_>>()
1506            .join("\n");
1507        let ed = Editor::with_text(&text);
1508        assert_eq!(ed.line_count(), 1000);
1509        assert_eq!(ed.line_text(999), Some("line999".to_string()));
1510    }
1511
1512    #[test]
1513    fn selection_byte_range_order() {
1514        use crate::cursor::CursorNavigator;
1515
1516        let mut ed = Editor::with_text("hello world");
1517        // Select backwards (anchor after head)
1518        ed.set_cursor(CursorPosition::new(0, 8, 8));
1519        ed.select_left();
1520        ed.select_left();
1521        ed.select_left();
1522
1523        let sel = ed.selection().unwrap();
1524        let nav = CursorNavigator::new(ed.rope());
1525        let (start, end) = sel.byte_range(&nav);
1526        // byte_range should always have start <= end
1527        assert!(start <= end);
1528        assert_eq!(end - start, 3);
1529    }
1530}
1531
1532// ================================================================
1533// Property-based tests
1534// ================================================================
1535
1536#[cfg(test)]
1537mod proptests {
1538    use super::*;
1539    use proptest::prelude::*;
1540
1541    // Strategy for generating valid text content (ASCII + some unicode)
1542    fn text_strategy() -> impl Strategy<Value = String> {
1543        prop::string::string_regex("[a-zA-Z0-9 \n]{0,100}")
1544            .unwrap()
1545            .prop_filter("non-empty or empty", |_| true)
1546    }
1547
1548    // Strategy for text with unicode
1549    fn unicode_text_strategy() -> impl Strategy<Value = String> {
1550        prop::collection::vec(
1551            prop_oneof![
1552                Just("a".to_string()),
1553                Just(" ".to_string()),
1554                Just("\n".to_string()),
1555                Just("Γ©".to_string()),
1556                Just("δΈ–".to_string()),
1557                Just("πŸŽ‰".to_string()),
1558            ],
1559            0..50,
1560        )
1561        .prop_map(|v| v.join(""))
1562    }
1563
1564    proptest! {
1565        #![proptest_config(ProptestConfig::with_cases(100))]
1566
1567        // Property: Cursor is always within valid bounds after any operation
1568        #[test]
1569        fn cursor_always_in_bounds(text in text_strategy()) {
1570            let mut ed = Editor::with_text(&text);
1571
1572            // After creation
1573            let c = ed.cursor();
1574            prop_assert!(c.line < ed.line_count() || (c.line == 0 && ed.line_count() == 1));
1575
1576            // After various movements
1577            ed.move_left();
1578            let c = ed.cursor();
1579            prop_assert!(c.line < ed.line_count() || (c.line == 0 && ed.line_count() == 1));
1580
1581            ed.move_right();
1582            let c = ed.cursor();
1583            prop_assert!(c.line < ed.line_count() || (c.line == 0 && ed.line_count() == 1));
1584
1585            ed.move_up();
1586            let c = ed.cursor();
1587            prop_assert!(c.line < ed.line_count() || (c.line == 0 && ed.line_count() == 1));
1588
1589            ed.move_down();
1590            let c = ed.cursor();
1591            prop_assert!(c.line < ed.line_count() || (c.line == 0 && ed.line_count() == 1));
1592
1593            ed.move_to_line_start();
1594            let c = ed.cursor();
1595            prop_assert_eq!(c.grapheme, 0);
1596
1597            ed.move_to_document_start();
1598            let c = ed.cursor();
1599            prop_assert_eq!(c.line, 0);
1600            prop_assert_eq!(c.grapheme, 0);
1601        }
1602
1603        // Property: Undo after insert restores original text
1604        #[test]
1605        fn undo_insert_restores_text(base in text_strategy(), insert in "[a-z]{1,20}") {
1606            let mut ed = Editor::with_text(&base);
1607            let original = ed.text();
1608            ed.insert_text(&insert);
1609            prop_assert!(ed.can_undo());
1610            ed.undo();
1611            prop_assert_eq!(ed.text(), original);
1612        }
1613
1614        // Property: Undo after delete restores original text
1615        #[test]
1616        fn undo_delete_restores_text(text in "[a-zA-Z]{5,50}") {
1617            let mut ed = Editor::with_text(&text);
1618            let original = ed.text();
1619            if ed.delete_backward() {
1620                prop_assert!(ed.can_undo());
1621                ed.undo();
1622                prop_assert_eq!(ed.text(), original);
1623            }
1624        }
1625
1626        // Property: Redo after undo restores the edit
1627        #[test]
1628        fn redo_after_undo_restores(text in text_strategy(), insert in "[a-z]{1,10}") {
1629            let mut ed = Editor::with_text(&text);
1630            ed.insert_text(&insert);
1631            let after_insert = ed.text();
1632            ed.undo();
1633            prop_assert!(ed.can_redo());
1634            ed.redo();
1635            prop_assert_eq!(ed.text(), after_insert);
1636        }
1637
1638        // Property: select_all + delete = empty
1639        #[test]
1640        fn select_all_delete_empties(text in text_strategy()) {
1641            let mut ed = Editor::with_text(&text);
1642            ed.select_all();
1643            ed.delete_backward();
1644            prop_assert!(ed.is_empty());
1645        }
1646
1647        // Property: Line count equals newline count + 1
1648        #[test]
1649        fn line_count_matches_newlines(text in text_strategy()) {
1650            let ed = Editor::with_text(&text);
1651            let newline_count = text.matches('\n').count();
1652            // Line count is at least 1, and each \n adds a line
1653            prop_assert_eq!(ed.line_count(), newline_count + 1);
1654        }
1655
1656        // Property: text() roundtrip through set_text
1657        #[test]
1658        fn set_text_roundtrip(text in text_strategy()) {
1659            let mut ed = Editor::new();
1660            ed.set_text(&text);
1661            prop_assert_eq!(ed.text(), text);
1662        }
1663
1664        // Property: Cursor stays in bounds after unicode operations
1665        #[test]
1666        fn unicode_cursor_bounds(text in unicode_text_strategy()) {
1667            let mut ed = Editor::with_text(&text);
1668
1669            // Move around
1670            for _ in 0..10 {
1671                ed.move_left();
1672            }
1673            let c = ed.cursor();
1674            prop_assert!(c.line < ed.line_count() || ed.line_count() == 1);
1675
1676            for _ in 0..10 {
1677                ed.move_right();
1678            }
1679            let c = ed.cursor();
1680            prop_assert!(c.line < ed.line_count() || ed.line_count() == 1);
1681        }
1682
1683        // Property: insert_char then delete_backward = original (when no prior content at cursor)
1684        #[test]
1685        fn insert_delete_roundtrip(ch in prop::char::any().prop_filter("printable", |c| !c.is_control())) {
1686            let mut ed = Editor::new();
1687            ed.insert_char(ch);
1688            ed.delete_backward();
1689            prop_assert!(ed.is_empty());
1690        }
1691
1692        // Property: Multiple undos don't panic and eventually can't undo
1693        #[test]
1694        fn multiple_undos_safe(ops in prop::collection::vec(0..3u8, 0..20)) {
1695            let mut ed = Editor::new();
1696            for op in ops {
1697                match op {
1698                    0 => { ed.insert_char('x'); }
1699                    1 => { ed.delete_backward(); }
1700                    _ => { ed.undo(); }
1701                }
1702            }
1703            // Should be able to undo until stack is empty
1704            while ed.can_undo() {
1705                prop_assert!(ed.undo());
1706            }
1707            prop_assert!(!ed.can_undo());
1708        }
1709
1710        // Property: Selection byte_range always has start <= end
1711        #[test]
1712        fn selection_range_ordered(text in "[a-zA-Z]{10,50}") {
1713            use crate::cursor::CursorNavigator;
1714
1715            let mut ed = Editor::with_text(&text);
1716            ed.set_cursor(CursorPosition::new(0, 5, 5));
1717
1718            // Select in various directions
1719            ed.select_left();
1720            ed.select_left();
1721
1722            if let Some(sel) = ed.selection() {
1723                let nav = CursorNavigator::new(ed.rope());
1724                let (start, end) = sel.byte_range(&nav);
1725                prop_assert!(start <= end);
1726            }
1727
1728            ed.select_right();
1729            ed.select_right();
1730            ed.select_right();
1731            ed.select_right();
1732
1733            if let Some(sel) = ed.selection() {
1734                let nav = CursorNavigator::new(ed.rope());
1735                let (start, end) = sel.byte_range(&nav);
1736                prop_assert!(start <= end);
1737            }
1738        }
1739
1740        // Property: Word movement always makes progress or stays at boundary
1741        #[test]
1742        fn word_movement_progress(text in "[a-zA-Z ]{5,50}") {
1743            let mut ed = Editor::with_text(&text);
1744            ed.set_cursor(CursorPosition::new(0, 0, 0));
1745
1746            let start = ed.cursor();
1747            ed.move_word_right();
1748            let after = ed.cursor();
1749            // Either made progress or was already at end
1750            prop_assert!(after.grapheme >= start.grapheme);
1751
1752            ed.move_to_line_end();
1753            let end_pos = ed.cursor();
1754            ed.move_word_left();
1755            let after_left = ed.cursor();
1756            // Either made progress or was already at start
1757            prop_assert!(after_left.grapheme <= end_pos.grapheme);
1758        }
1759
1760        // Property: Document start/end are at expected positions
1761        #[test]
1762        fn document_bounds(text in text_strategy()) {
1763            let mut ed = Editor::with_text(&text);
1764
1765            ed.move_to_document_start();
1766            prop_assert_eq!(ed.cursor().line, 0);
1767            prop_assert_eq!(ed.cursor().grapheme, 0);
1768
1769            ed.move_to_document_end();
1770            let c = ed.cursor();
1771            let last_line = ed.line_count().saturating_sub(1);
1772            prop_assert_eq!(c.line, last_line);
1773        }
1774    }
1775}