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