1#![forbid(unsafe_code)]
2
3use crate::cursor::{CursorNavigator, CursorPosition};
30use crate::rope::Rope;
31
32#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub struct Selection {
87 pub anchor: CursorPosition,
89 pub head: CursorPosition,
91}
92
93impl Selection {
94 #[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 #[must_use]
104 pub fn is_empty(&self) -> bool {
105 self.anchor == self.head
106 }
107}
108
109#[derive(Debug, Clone)]
115pub struct Editor {
116 rope: Rope,
118 cursor: CursorPosition,
120 selection: Option<Selection>,
122 undo_stack: Vec<(EditOp, CursorPosition)>,
124 redo_stack: Vec<(EditOp, CursorPosition)>,
126 max_history: usize,
128 current_undo_size: usize,
130 max_undo_size: usize,
132}
133
134impl Default for Editor {
135 fn default() -> Self {
136 Self::new()
137 }
138}
139
140impl Editor {
141 #[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, }
154 }
155
156 #[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 pub fn set_max_history(&mut self, max: usize) {
176 self.max_history = max;
177 }
178
179 pub fn set_max_undo_size(&mut self, bytes: usize) {
181 self.max_undo_size = bytes;
182 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 #[must_use]
191 pub fn text(&self) -> String {
192 self.rope.to_string()
193 }
194
195 #[must_use]
197 pub fn rope(&self) -> &Rope {
198 &self.rope
199 }
200
201 #[must_use]
203 pub fn cursor(&self) -> CursorPosition {
204 self.cursor
205 }
206
207 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 #[must_use]
216 pub fn selection(&self) -> Option<Selection> {
217 self.selection
218 }
219
220 #[must_use]
222 pub fn can_undo(&self) -> bool {
223 !self.undo_stack.is_empty()
224 }
225
226 #[must_use]
228 pub fn can_redo(&self) -> bool {
229 !self.redo_stack.is_empty()
230 }
231
232 #[must_use]
234 pub fn is_empty(&self) -> bool {
235 self.rope.is_empty()
236 }
237
238 #[must_use]
240 pub fn line_count(&self) -> usize {
241 self.rope.len_lines()
242 }
243
244 #[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 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 pub fn insert_text(&mut self, text: &str) {
269 if text.is_empty() {
270 return;
271 }
272
273 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 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 pub fn insert_newline(&mut self) {
318 self.insert_text("\n");
319 }
320
321 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; }
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 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; }
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 let nav = CursorNavigator::new(&self.rope);
388 self.cursor = nav.clamp(self.cursor);
389 true
390 }
391
392 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 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 let nav = CursorNavigator::new(&self.rope);
455 self.cursor = nav.clamp(self.cursor);
456 true
457 }
458
459 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 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 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 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 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 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 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 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 let nav = CursorNavigator::new(&self.rope);
557 self.cursor = nav.clamp(self.cursor);
558 true
559 }
560
561 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn select_left(&mut self) {
703 self.extend_selection(|nav, pos| nav.move_left(pos));
704 }
705
706 pub fn select_right(&mut self) {
708 self.extend_selection(|nav, pos| nav.move_right(pos));
709 }
710
711 pub fn select_up(&mut self) {
713 self.extend_selection(|nav, pos| nav.move_up(pos));
714 }
715
716 pub fn select_down(&mut self) {
718 self.extend_selection(|nav, pos| nav.move_down(pos));
719 }
720
721 pub fn select_word_left(&mut self) {
723 self.extend_selection(|nav, pos| nav.move_word_left(pos));
724 }
725
726 pub fn select_word_right(&mut self) {
728 self.extend_selection(|nav, pos| nav.move_word_right(pos));
729 }
730
731 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 pub fn clear_selection(&mut self) {
745 self.selection = None;
746 }
747
748 #[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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 #[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 #[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 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 ed.move_left(); ed.move_left(); ed.move_left(); ed.move_left(); ed.move_left(); ed.move_left(); ed.move_left(); ed.delete_backward(); assert_eq!(ed.text(), "helloπ world");
1308 }
1309
1310 #[test]
1311 fn unicode_combining_character() {
1312 let mut ed = Editor::new();
1313 ed.insert_text("caf\u{0065}\u{0301}");
1315 assert_eq!(ed.text(), "caf\u{0065}\u{0301}");
1317 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 ed.insert_text("π©\u{200D}π");
1327 let text = ed.text();
1328 assert!(text.contains("π©"));
1329 ed.move_left();
1331 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 assert!(ed.delete_word_backward());
1394 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 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 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 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 ed.select_right();
1457 ed.select_left();
1458 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 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 for _ in 0..5 {
1479 ed.select_right();
1480 }
1481 ed.insert_text("universe");
1482 assert_eq!(ed.text(), "hello universe");
1483
1484 ed.undo();
1486 assert_eq!(ed.text(), "hello world");
1487
1488 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 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 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 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 ed.insert_text("hello\x1bworld\x07\n\t!");
1568 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 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 assert!(start <= end);
1628 assert_eq!(end - start, 3);
1629 }
1630}
1631
1632#[cfg(test)]
1637mod proptests {
1638 use super::*;
1639 use proptest::prelude::*;
1640
1641 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 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 #[test]
1669 fn cursor_always_in_bounds(text in text_strategy()) {
1670 let mut ed = Editor::with_text(&text);
1671
1672 let c = ed.cursor();
1674 prop_assert!(c.line < ed.line_count() || (c.line == 0 && ed.line_count() == 1));
1675
1676 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 #[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 #[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 #[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 #[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 #[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 prop_assert_eq!(ed.line_count(), newline_count + 1);
1754 }
1755
1756 #[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 #[test]
1766 fn unicode_cursor_bounds(text in unicode_text_strategy()) {
1767 let mut ed = Editor::with_text(&text);
1768
1769 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 #[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 #[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 while ed.can_undo() {
1805 prop_assert!(ed.undo());
1806 }
1807 prop_assert!(!ed.can_undo());
1808 }
1809
1810 #[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 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 #[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 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 prop_assert!(after_left.grapheme <= end_pos.grapheme);
1858 }
1859
1860 #[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}