1use std::borrow::Cow;
10
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
14pub struct Pos {
15 pub line: usize,
16 pub col: usize,
17}
18
19impl Pos {
20 pub fn new(line: usize, col: usize) -> Self {
21 Self { line, col }
22 }
23}
24
25#[derive(Debug, Clone)]
27struct Snapshot {
28 lines: Vec<String>,
29 cursor: Pos,
30}
31
32#[derive(Debug, Clone)]
39pub struct EditorModel {
40 lines: Vec<String>,
41 cursor: Pos,
42 anchor: Option<Pos>,
44 goal_col: Option<usize>,
46 undo: Vec<Snapshot>,
47 redo: Vec<Snapshot>,
48 coalescing: bool,
50 tab_size: usize,
52}
53
54impl Default for EditorModel {
55 fn default() -> Self {
56 Self::new("")
57 }
58}
59
60impl EditorModel {
61 pub fn new(text: &str) -> Self {
63 Self {
64 lines: split_lines(text),
65 cursor: Pos::default(),
66 anchor: None,
67 goal_col: None,
68 undo: Vec::new(),
69 redo: Vec::new(),
70 coalescing: false,
71 tab_size: 4,
72 }
73 }
74
75 pub fn text(&self) -> String {
78 self.lines.join("\n")
79 }
80
81 pub fn set_text(&mut self, text: &str) {
83 self.lines = split_lines(text);
84 self.cursor = Pos::default();
85 self.anchor = None;
86 self.goal_col = None;
87 self.undo.clear();
88 self.redo.clear();
89 self.coalescing = false;
90 }
91
92 pub fn is_empty(&self) -> bool {
93 self.lines.len() == 1 && self.lines[0].is_empty()
94 }
95
96 pub fn line_count(&self) -> usize {
97 self.lines.len()
98 }
99
100 pub fn line(&self, i: usize) -> Option<&str> {
102 self.lines.get(i).map(String::as_str)
103 }
104
105 pub fn lines(&self) -> &[String] {
107 &self.lines
108 }
109
110 pub fn cursor(&self) -> Pos {
111 self.cursor
112 }
113
114 pub fn tab_size(&self) -> usize {
115 self.tab_size
116 }
117
118 pub fn set_tab_size(&mut self, n: usize) {
120 self.tab_size = n.max(1);
121 }
122
123 pub fn insert(&mut self, s: &str) {
128 let s: Cow<str> = if s.contains('\r') {
129 Cow::Owned(s.replace('\r', ""))
130 } else {
131 Cow::Borrowed(s)
132 };
133 if s.is_empty() {
134 return;
135 }
136 let coalesce = self.selection().is_none() && s.chars().count() == 1 && !s.contains('\n');
137 self.push_undo(coalesce);
138 self.remove_selection();
139 self.insert_at_cursor(&s);
140 self.goal_col = None;
141 }
142
143 pub fn backspace(&mut self) -> bool {
146 if self.selection().is_some() {
147 return self.delete_selection();
148 }
149 if self.cursor == Pos::default() {
150 return false;
151 }
152 self.push_undo(false);
153 let start = self.prev_pos(self.cursor);
154 self.remove_range(start, self.cursor);
155 self.goal_col = None;
156 true
157 }
158
159 pub fn delete(&mut self) -> bool {
162 if self.selection().is_some() {
163 return self.delete_selection();
164 }
165 let end = self.next_pos(self.cursor);
166 if end == self.cursor {
167 return false;
168 }
169 self.push_undo(false);
170 self.remove_range(self.cursor, end);
171 self.goal_col = None;
172 true
173 }
174
175 pub fn newline(&mut self) {
179 self.push_undo(false);
180 self.remove_selection();
181 let indent: String = self.lines[self.cursor.line]
182 .chars()
183 .take(self.cursor.col)
184 .take_while(|c| c.is_whitespace())
185 .collect();
186 self.insert_at_cursor("\n");
187 self.insert_at_cursor(&indent);
188 self.goal_col = None;
189 }
190
191 pub fn tab(&mut self) {
193 self.push_undo(false);
194 self.remove_selection();
195 let n = self.tab_size - (self.cursor.col % self.tab_size);
196 self.insert_at_cursor(&" ".repeat(n));
197 self.goal_col = None;
198 }
199
200 pub fn move_left(&mut self, extend: bool) {
203 self.goal_col = None;
204 if !extend {
205 if let Some((start, _)) = self.selection() {
206 self.coalescing = false;
207 self.anchor = None;
208 self.cursor = start;
209 return;
210 }
211 }
212 self.start_move(extend);
213 self.cursor = self.prev_pos(self.cursor);
214 }
215
216 pub fn move_right(&mut self, extend: bool) {
217 self.goal_col = None;
218 if !extend {
219 if let Some((_, end)) = self.selection() {
220 self.coalescing = false;
221 self.anchor = None;
222 self.cursor = end;
223 return;
224 }
225 }
226 self.start_move(extend);
227 self.cursor = self.next_pos(self.cursor);
228 }
229
230 pub fn move_up(&mut self, extend: bool) {
232 self.start_move(extend);
233 let goal = self.goal_col.unwrap_or(self.cursor.col);
234 if self.cursor.line > 0 {
235 self.cursor.line -= 1;
236 self.cursor.col = goal.min(self.line_len(self.cursor.line));
237 }
238 self.goal_col = Some(goal);
239 }
240
241 pub fn move_down(&mut self, extend: bool) {
243 self.start_move(extend);
244 let goal = self.goal_col.unwrap_or(self.cursor.col);
245 if self.cursor.line + 1 < self.lines.len() {
246 self.cursor.line += 1;
247 self.cursor.col = goal.min(self.line_len(self.cursor.line));
248 }
249 self.goal_col = Some(goal);
250 }
251
252 pub fn home(&mut self, extend: bool) {
253 self.start_move(extend);
254 self.cursor.col = 0;
255 self.goal_col = None;
256 }
257
258 pub fn end(&mut self, extend: bool) {
259 self.start_move(extend);
260 self.cursor.col = self.line_len(self.cursor.line);
261 self.goal_col = None;
262 }
263
264 pub fn doc_start(&mut self, extend: bool) {
265 self.start_move(extend);
266 self.cursor = Pos::default();
267 self.goal_col = None;
268 }
269
270 pub fn doc_end(&mut self, extend: bool) {
271 self.start_move(extend);
272 let line = self.lines.len() - 1;
273 self.cursor = Pos::new(line, self.line_len(line));
274 self.goal_col = None;
275 }
276
277 pub fn word_left(&mut self, extend: bool) {
280 self.start_move(extend);
281 let mut p = self.cursor;
282 while let Some(c) = self.char_before(p) {
283 if is_word(c) {
284 break;
285 }
286 p = self.prev_pos(p);
287 }
288 while let Some(c) = self.char_before(p) {
289 if !is_word(c) {
290 break;
291 }
292 p = self.prev_pos(p);
293 }
294 self.cursor = p;
295 self.goal_col = None;
296 }
297
298 pub fn word_right(&mut self, extend: bool) {
301 self.start_move(extend);
302 let mut p = self.cursor;
303 while let Some(c) = self.char_at(p) {
304 if is_word(c) {
305 break;
306 }
307 p = self.next_pos(p);
308 }
309 while let Some(c) = self.char_at(p) {
310 if !is_word(c) {
311 break;
312 }
313 p = self.next_pos(p);
314 }
315 self.cursor = p;
316 self.goal_col = None;
317 }
318
319 pub fn pos_for_click(&self, line: usize, col: usize) -> Pos {
324 let line = line.min(self.lines.len() - 1);
325 Pos::new(line, col.min(self.line_len(line)))
326 }
327
328 pub fn move_to(&mut self, line: usize, col: usize, extend: bool) {
331 let pos = self.pos_for_click(line, col);
332 self.start_move(extend);
333 self.cursor = pos;
334 self.goal_col = None;
335 }
336
337 pub fn selection(&self) -> Option<(Pos, Pos)> {
342 let anchor = self.anchor?;
343 if anchor == self.cursor {
344 return None;
345 }
346 Some(if anchor < self.cursor {
347 (anchor, self.cursor)
348 } else {
349 (self.cursor, anchor)
350 })
351 }
352
353 pub fn clear_selection(&mut self) {
354 self.anchor = None;
355 }
356
357 pub fn select_all(&mut self) {
358 self.coalescing = false;
359 self.goal_col = None;
360 self.anchor = Some(Pos::default());
361 let line = self.lines.len() - 1;
362 self.cursor = Pos::new(line, self.line_len(line));
363 }
364
365 pub fn select_word(&mut self) {
368 self.coalescing = false;
369 self.goal_col = None;
370 let line = self.cursor.line;
371 let chars: Vec<char> = self.lines[line].chars().collect();
372 if chars.is_empty() {
373 return;
374 }
375 let col = self.cursor.col.min(chars.len());
376 let seed = if col < chars.len() && is_word(chars[col]) {
378 col
379 } else if col > 0 && is_word(chars[col - 1]) {
380 col - 1
381 } else {
382 let start = if col < chars.len() { col } else { col - 1 };
383 self.anchor = Some(Pos::new(line, start));
384 self.cursor = Pos::new(line, start + 1);
385 return;
386 };
387 let mut start = seed;
388 while start > 0 && is_word(chars[start - 1]) {
389 start -= 1;
390 }
391 let mut end = seed + 1;
392 while end < chars.len() && is_word(chars[end]) {
393 end += 1;
394 }
395 self.anchor = Some(Pos::new(line, start));
396 self.cursor = Pos::new(line, end);
397 }
398
399 pub fn select_line(&mut self) {
401 self.coalescing = false;
402 self.goal_col = None;
403 let line = self.cursor.line;
404 self.anchor = Some(Pos::new(line, 0));
405 self.cursor = Pos::new(line, self.line_len(line));
406 }
407
408 pub fn selected_text(&self) -> Option<String> {
410 let (start, end) = self.selection()?;
411 if start.line == end.line {
412 let line = &self.lines[start.line];
413 return Some(line[byte_idx(line, start.col)..byte_idx(line, end.col)].to_string());
414 }
415 let first = &self.lines[start.line];
416 let mut out = first[byte_idx(first, start.col)..].to_string();
417 for line in &self.lines[start.line + 1..end.line] {
418 out.push('\n');
419 out.push_str(line);
420 }
421 let last = &self.lines[end.line];
422 out.push('\n');
423 out.push_str(&last[..byte_idx(last, end.col)]);
424 Some(out)
425 }
426
427 pub fn delete_selection(&mut self) -> bool {
429 if self.selection().is_none() {
430 return false;
431 }
432 self.push_undo(false);
433 self.remove_selection();
434 self.goal_col = None;
435 true
436 }
437
438 pub fn cut(&mut self) -> Option<String> {
442 let text = self.selected_text()?;
443 self.push_undo(false);
444 self.remove_selection();
445 self.goal_col = None;
446 Some(text)
447 }
448
449 pub fn copy(&self) -> Option<String> {
451 self.selected_text()
452 }
453
454 pub fn can_undo(&self) -> bool {
457 !self.undo.is_empty()
458 }
459
460 pub fn can_redo(&self) -> bool {
461 !self.redo.is_empty()
462 }
463
464 pub fn undo(&mut self) -> bool {
467 let Some(snap) = self.undo.pop() else {
468 return false;
469 };
470 let lines = std::mem::replace(&mut self.lines, snap.lines);
471 self.redo.push(Snapshot {
472 lines,
473 cursor: self.cursor,
474 });
475 self.cursor = snap.cursor;
476 self.anchor = None;
477 self.goal_col = None;
478 self.coalescing = false;
479 true
480 }
481
482 pub fn redo(&mut self) -> bool {
484 let Some(snap) = self.redo.pop() else {
485 return false;
486 };
487 let lines = std::mem::replace(&mut self.lines, snap.lines);
488 self.undo.push(Snapshot {
489 lines,
490 cursor: self.cursor,
491 });
492 self.cursor = snap.cursor;
493 self.anchor = None;
494 self.goal_col = None;
495 self.coalescing = false;
496 true
497 }
498
499 fn line_len(&self, i: usize) -> usize {
502 self.lines[i].chars().count()
503 }
504
505 fn prev_pos(&self, p: Pos) -> Pos {
507 if p.col > 0 {
508 Pos::new(p.line, p.col - 1)
509 } else if p.line > 0 {
510 Pos::new(p.line - 1, self.line_len(p.line - 1))
511 } else {
512 p
513 }
514 }
515
516 fn next_pos(&self, p: Pos) -> Pos {
518 if p.col < self.line_len(p.line) {
519 Pos::new(p.line, p.col + 1)
520 } else if p.line + 1 < self.lines.len() {
521 Pos::new(p.line + 1, 0)
522 } else {
523 p
524 }
525 }
526
527 fn char_before(&self, p: Pos) -> Option<char> {
529 if p.col > 0 {
530 self.lines[p.line].chars().nth(p.col - 1)
531 } else if p.line > 0 {
532 Some('\n')
533 } else {
534 None
535 }
536 }
537
538 fn char_at(&self, p: Pos) -> Option<char> {
540 if p.col < self.line_len(p.line) {
541 self.lines[p.line].chars().nth(p.col)
542 } else if p.line + 1 < self.lines.len() {
543 Some('\n')
544 } else {
545 None
546 }
547 }
548
549 fn start_move(&mut self, extend: bool) {
551 self.coalescing = false;
552 if extend {
553 if self.anchor.is_none() {
554 self.anchor = Some(self.cursor);
555 }
556 } else {
557 self.anchor = None;
558 }
559 }
560
561 fn push_undo(&mut self, coalesce: bool) {
564 if !(coalesce && self.coalescing) {
565 self.undo.push(Snapshot {
566 lines: self.lines.clone(),
567 cursor: self.cursor,
568 });
569 }
570 self.coalescing = coalesce;
571 self.redo.clear();
572 }
573
574 fn remove_selection(&mut self) {
576 if let Some((start, end)) = self.selection() {
577 self.remove_range(start, end);
578 }
579 self.anchor = None;
580 }
581
582 fn remove_range(&mut self, start: Pos, end: Pos) {
584 if start.line == end.line {
585 let line = &mut self.lines[start.line];
586 let a = byte_idx(line, start.col);
587 let b = byte_idx(line, end.col);
588 line.replace_range(a..b, "");
589 } else {
590 let last = &self.lines[end.line];
591 let tail = last[byte_idx(last, end.col)..].to_string();
592 let first = &mut self.lines[start.line];
593 first.truncate(byte_idx(first, start.col));
594 first.push_str(&tail);
595 self.lines.drain(start.line + 1..=end.line);
596 }
597 self.cursor = start;
598 self.anchor = None;
599 }
600
601 fn insert_at_cursor(&mut self, s: &str) {
603 if s.is_empty() {
604 return;
605 }
606 let Pos { line, col } = self.cursor;
607 let at = byte_idx(&self.lines[line], col);
608 if !s.contains('\n') {
609 self.lines[line].insert_str(at, s);
610 self.cursor.col += s.chars().count();
611 return;
612 }
613 let tail = self.lines[line].split_off(at);
614 let mut parts = s.split('\n');
615 if let Some(first) = parts.next() {
616 self.lines[line].push_str(first);
617 }
618 let mut last = line;
619 for part in parts {
620 last += 1;
621 self.lines.insert(last, part.to_string());
622 }
623 self.cursor = Pos::new(last, self.line_len(last));
624 self.lines[last].push_str(&tail);
625 }
626}
627
628fn byte_idx(line: &str, col: usize) -> usize {
630 line.char_indices()
631 .nth(col)
632 .map(|(i, _)| i)
633 .unwrap_or(line.len())
634}
635
636fn split_lines(text: &str) -> Vec<String> {
638 let text = text.replace('\r', "");
639 text.split('\n').map(str::to_string).collect()
640}
641
642fn is_word(c: char) -> bool {
644 c.is_alphanumeric() || c == '_'
645}
646
647#[cfg(test)]
648mod tests {
649 use super::*;
650
651 fn at(line: usize, col: usize) -> Pos {
652 Pos::new(line, col)
653 }
654
655 #[test]
656 fn empty_doc_is_one_empty_line() {
657 let m = EditorModel::new("");
658 assert_eq!(m.line_count(), 1);
659 assert!(m.is_empty());
660 assert_eq!(m.text(), "");
661 assert_eq!(m.cursor(), at(0, 0));
662 }
663
664 #[test]
665 fn text_roundtrip() {
666 let m = EditorModel::new("a\nb\n\nc");
667 assert_eq!(m.line_count(), 4);
668 assert_eq!(m.line(2), Some(""));
669 assert_eq!(m.line(4), None);
670 assert_eq!(m.text(), "a\nb\n\nc");
671 }
672
673 #[test]
674 fn new_normalizes_crlf() {
675 let m = EditorModel::new("a\r\nb");
676 assert_eq!(m.text(), "a\nb");
677 assert_eq!(m.line_count(), 2);
678 }
679
680 #[test]
681 fn set_text_resets_cursor_selection_and_history() {
682 let mut m = EditorModel::new("abc");
683 m.doc_end(false);
684 m.insert("d");
685 m.select_all();
686 m.set_text("xyz");
687 assert_eq!(m.text(), "xyz");
688 assert_eq!(m.cursor(), at(0, 0));
689 assert_eq!(m.selection(), None);
690 assert!(!m.can_undo());
691 assert!(!m.can_redo());
692 }
693
694 #[test]
695 fn insert_advances_cursor() {
696 let mut m = EditorModel::new("ac");
697 m.move_right(false);
698 m.insert("b");
699 assert_eq!(m.text(), "abc");
700 assert_eq!(m.cursor(), at(0, 2));
701 }
702
703 #[test]
704 fn insert_multiline_splits_lines() {
705 let mut m = EditorModel::new("hello world");
706 m.move_to(0, 5, false);
707 m.insert("\nmid\n");
708 assert_eq!(m.text(), "hello\nmid\n world");
709 assert_eq!(m.cursor(), at(2, 0));
710 }
711
712 #[test]
713 fn insert_replaces_selection() {
714 let mut m = EditorModel::new("one two three");
715 m.move_to(0, 4, false);
716 m.move_to(0, 7, true);
717 m.insert("2");
718 assert_eq!(m.text(), "one 2 three");
719 assert_eq!(m.cursor(), at(0, 5));
720 assert_eq!(m.selection(), None);
721 }
722
723 #[test]
724 fn insert_replaces_multiline_selection() {
725 let mut m = EditorModel::new("aaa\nbbb\nccc");
726 m.move_to(0, 1, false);
727 m.move_to(2, 2, true);
728 m.insert("X");
729 assert_eq!(m.text(), "aXc");
730 assert_eq!(m.cursor(), at(0, 2));
731 }
732
733 #[test]
734 fn backspace_within_line_and_join() {
735 let mut m = EditorModel::new("ab\ncd");
736 m.move_to(1, 1, false);
737 assert!(m.backspace());
738 assert_eq!(m.text(), "ab\nd");
739 assert!(m.backspace());
741 assert_eq!(m.text(), "abd");
742 assert_eq!(m.cursor(), at(0, 2));
743 }
744
745 #[test]
746 fn backspace_at_doc_start_is_noop() {
747 let mut m = EditorModel::new("ab");
748 assert!(!m.backspace());
749 assert_eq!(m.text(), "ab");
750 assert!(!m.can_undo());
751 }
752
753 #[test]
754 fn delete_within_line_and_join() {
755 let mut m = EditorModel::new("ab\ncd");
756 m.move_to(0, 1, false);
757 assert!(m.delete());
758 assert_eq!(m.text(), "a\ncd");
759 assert!(m.delete());
761 assert_eq!(m.text(), "acd");
762 assert_eq!(m.cursor(), at(0, 1));
763 }
764
765 #[test]
766 fn delete_at_doc_end_is_noop() {
767 let mut m = EditorModel::new("ab");
768 m.doc_end(false);
769 assert!(!m.delete());
770 assert!(!m.can_undo());
771 }
772
773 #[test]
774 fn newline_copies_leading_whitespace() {
775 let mut m = EditorModel::new(" foo");
776 m.end(false);
777 m.newline();
778 assert_eq!(m.text(), " foo\n ");
779 assert_eq!(m.cursor(), at(1, 4));
780 }
781
782 #[test]
783 fn newline_inside_indent_caps_the_copy() {
784 let mut m = EditorModel::new(" foo");
785 m.move_to(0, 2, false);
786 m.newline();
787 assert_eq!(m.text(), " \n foo");
788 assert_eq!(m.cursor(), at(1, 2));
789 }
790
791 #[test]
792 fn newline_replaces_selection() {
793 let mut m = EditorModel::new(" ab cd");
794 m.move_to(0, 4, false);
795 m.move_to(0, 5, true);
796 m.newline();
797 assert_eq!(m.text(), " ab\n cd");
798 assert_eq!(m.cursor(), at(1, 2));
799 }
800
801 #[test]
802 fn tab_advances_to_next_stop() {
803 let mut m = EditorModel::new("");
804 m.tab();
805 assert_eq!(m.text(), " ");
806 let mut m = EditorModel::new("ab");
807 m.end(false);
808 m.tab();
809 assert_eq!(m.text(), "ab ");
810 assert_eq!(m.cursor(), at(0, 4));
811 let mut m = EditorModel::new("x");
812 m.set_tab_size(2);
813 m.end(false);
814 m.tab();
815 assert_eq!(m.text(), "x ");
816 }
817
818 #[test]
819 fn horizontal_movement_crosses_lines() {
820 let mut m = EditorModel::new("ab\ncd");
821 m.move_to(0, 2, false);
822 m.move_right(false);
823 assert_eq!(m.cursor(), at(1, 0));
824 m.move_left(false);
825 assert_eq!(m.cursor(), at(0, 2));
826 m.doc_start(false);
828 m.move_left(false);
829 assert_eq!(m.cursor(), at(0, 0));
830 m.doc_end(false);
831 m.move_right(false);
832 assert_eq!(m.cursor(), at(1, 2));
833 }
834
835 #[test]
836 fn vertical_movement_keeps_goal_column() {
837 let mut m = EditorModel::new("hello\nhi\nworld!");
838 m.move_to(0, 4, false);
839 m.move_down(false);
840 assert_eq!(m.cursor(), at(1, 2)); m.move_down(false);
842 assert_eq!(m.cursor(), at(2, 4)); m.move_up(false);
844 m.move_up(false);
845 assert_eq!(m.cursor(), at(0, 4));
846 }
847
848 #[test]
849 fn horizontal_movement_resets_goal_column() {
850 let mut m = EditorModel::new("hello\nhi\nworld!");
851 m.move_to(0, 4, false);
852 m.move_down(false); m.move_left(false); m.move_down(false);
855 assert_eq!(m.cursor(), at(2, 1));
856 }
857
858 #[test]
859 fn vertical_movement_stops_at_edges() {
860 let mut m = EditorModel::new("a\nb");
861 m.move_up(false);
862 assert_eq!(m.cursor(), at(0, 0));
863 m.doc_end(false);
864 m.move_down(false);
865 assert_eq!(m.cursor(), at(1, 1));
866 }
867
868 #[test]
869 fn home_end_and_doc_edges() {
870 let mut m = EditorModel::new("abc\ndef");
871 m.move_to(1, 1, false);
872 m.home(false);
873 assert_eq!(m.cursor(), at(1, 0));
874 m.end(false);
875 assert_eq!(m.cursor(), at(1, 3));
876 m.doc_start(false);
877 assert_eq!(m.cursor(), at(0, 0));
878 m.doc_end(false);
879 assert_eq!(m.cursor(), at(1, 3));
880 }
881
882 #[test]
883 fn word_movement_within_line() {
884 let mut m = EditorModel::new("foo bar baz");
885 m.doc_end(false);
886 m.word_left(false);
887 assert_eq!(m.cursor(), at(0, 8));
888 m.word_left(false);
889 assert_eq!(m.cursor(), at(0, 4));
890 m.word_right(false);
891 assert_eq!(m.cursor(), at(0, 7));
892 }
893
894 #[test]
895 fn word_movement_crosses_lines() {
896 let mut m = EditorModel::new("foo\nbar");
897 m.move_to(1, 0, false);
898 m.word_left(false);
899 assert_eq!(m.cursor(), at(0, 0));
900 m.word_right(false);
901 assert_eq!(m.cursor(), at(0, 3));
902 m.word_right(false);
903 assert_eq!(m.cursor(), at(1, 3));
904 }
905
906 #[test]
907 fn extend_grows_and_normalizes_selection() {
908 let mut m = EditorModel::new("abcdef");
909 m.move_to(0, 2, false);
910 m.move_right(true);
911 m.move_right(true);
912 assert_eq!(m.selection(), Some((at(0, 2), at(0, 4))));
913 let mut m = EditorModel::new("abcdef");
915 m.move_to(0, 4, false);
916 m.move_left(true);
917 m.move_left(true);
918 assert_eq!(m.selection(), Some((at(0, 2), at(0, 4))));
919 assert_eq!(m.selected_text().as_deref(), Some("cd"));
920 }
921
922 #[test]
923 fn empty_selection_is_none() {
924 let mut m = EditorModel::new("abc");
925 m.move_right(true);
926 m.move_left(true); assert_eq!(m.selection(), None);
928 assert_eq!(m.selected_text(), None);
929 }
930
931 #[test]
932 fn plain_move_collapses_selection_to_edge() {
933 let mut m = EditorModel::new("abcdef");
934 m.move_to(0, 2, false);
935 m.move_to(0, 4, true);
936 m.move_left(false);
937 assert_eq!(m.cursor(), at(0, 2));
938 assert_eq!(m.selection(), None);
939 let mut m = EditorModel::new("abcdef");
940 m.move_to(0, 2, false);
941 m.move_to(0, 4, true);
942 m.move_right(false);
943 assert_eq!(m.cursor(), at(0, 4));
944 assert_eq!(m.selection(), None);
945 }
946
947 #[test]
948 fn selected_text_multiline() {
949 let mut m = EditorModel::new("aaa\nbbb\nccc");
950 m.move_to(0, 1, false);
951 m.move_to(2, 2, true);
952 assert_eq!(m.selected_text().as_deref(), Some("aa\nbbb\ncc"));
953 }
954
955 #[test]
956 fn delete_selection_joins_lines() {
957 let mut m = EditorModel::new("aaa\nbbb\nccc");
958 m.move_to(0, 2, false);
959 m.move_to(2, 1, true);
960 assert!(m.delete_selection());
961 assert_eq!(m.text(), "aacc");
962 assert_eq!(m.cursor(), at(0, 2));
963 assert!(!m.delete_selection());
964 }
965
966 #[test]
967 fn cut_and_copy() {
968 let mut m = EditorModel::new("hello world");
969 assert_eq!(m.copy(), None);
970 assert_eq!(m.cut(), None);
971 m.move_to(0, 0, false);
972 m.word_right(true);
973 assert_eq!(m.copy().as_deref(), Some("hello"));
974 assert_eq!(m.text(), "hello world"); assert_eq!(m.cut().as_deref(), Some("hello"));
976 assert_eq!(m.text(), " world");
977 assert!(m.undo());
978 assert_eq!(m.text(), "hello world");
979 }
980
981 #[test]
982 fn select_all_spans_document() {
983 let mut m = EditorModel::new("ab\ncd");
984 m.select_all();
985 assert_eq!(m.selection(), Some((at(0, 0), at(1, 2))));
986 assert_eq!(m.selected_text().as_deref(), Some("ab\ncd"));
987 m.clear_selection();
988 assert_eq!(m.selection(), None);
989 }
990
991 #[test]
992 fn select_word_variants() {
993 let mut m = EditorModel::new("foo bar_baz qux");
995 m.move_to(0, 6, false);
996 m.select_word();
997 assert_eq!(m.selected_text().as_deref(), Some("bar_baz"));
998 m.move_to(0, 3, false);
1000 m.select_word();
1001 assert_eq!(m.selected_text().as_deref(), Some("foo"));
1002 let mut m = EditorModel::new("foo .. bar");
1005 m.move_to(0, 5, false);
1006 m.select_word();
1007 assert_eq!(m.selection(), Some((at(0, 5), at(0, 6))));
1008 let mut m = EditorModel::new("");
1010 m.select_word();
1011 assert_eq!(m.selection(), None);
1012 }
1013
1014 #[test]
1015 fn select_line_spans_line() {
1016 let mut m = EditorModel::new("abc\ndef");
1017 m.move_to(1, 1, false);
1018 m.select_line();
1019 assert_eq!(m.selection(), Some((at(1, 0), at(1, 3))));
1020 assert_eq!(m.selected_text().as_deref(), Some("def"));
1021 }
1022
1023 #[test]
1024 fn undo_redo_roundtrip() {
1025 let mut m = EditorModel::new("ab");
1026 m.doc_end(false);
1027 m.newline();
1028 m.insert("cd");
1029 assert_eq!(m.text(), "ab\ncd");
1030 assert!(m.undo());
1031 assert_eq!(m.text(), "ab\n");
1032 assert!(m.undo());
1033 assert_eq!(m.text(), "ab");
1034 assert_eq!(m.cursor(), at(0, 2));
1035 assert!(!m.undo());
1036 assert!(m.redo());
1037 assert_eq!(m.text(), "ab\n");
1038 assert!(m.redo());
1039 assert_eq!(m.text(), "ab\ncd");
1040 assert!(!m.redo());
1041 }
1042
1043 #[test]
1044 fn typing_coalesces_into_one_undo_step() {
1045 let mut m = EditorModel::new("");
1046 m.insert("a");
1047 m.insert("b");
1048 m.insert("c");
1049 assert!(m.undo());
1050 assert_eq!(m.text(), "");
1051 assert!(!m.undo());
1052 assert!(m.redo());
1053 assert_eq!(m.text(), "abc");
1054 }
1055
1056 #[test]
1057 fn movement_breaks_coalescing() {
1058 let mut m = EditorModel::new("");
1059 m.insert("a");
1060 m.insert("b");
1061 m.move_left(false);
1062 m.move_right(false);
1063 m.insert("c");
1064 assert!(m.undo());
1065 assert_eq!(m.text(), "ab");
1066 assert!(m.undo());
1067 assert_eq!(m.text(), "");
1068 }
1069
1070 #[test]
1071 fn structural_edits_break_coalescing() {
1072 let mut m = EditorModel::new("");
1073 m.insert("a");
1074 m.newline();
1075 m.insert("b");
1076 assert!(m.undo());
1077 assert_eq!(m.text(), "a\n");
1078 assert!(m.undo());
1079 assert_eq!(m.text(), "a");
1080 assert!(m.undo());
1081 assert_eq!(m.text(), "");
1082 let mut m = EditorModel::new("");
1084 m.insert("a");
1085 m.insert("b");
1086 m.backspace();
1087 m.insert("c");
1088 assert!(m.undo());
1089 assert_eq!(m.text(), "a");
1090 assert!(m.undo());
1091 assert_eq!(m.text(), "ab");
1092 assert!(m.undo());
1093 assert_eq!(m.text(), "");
1094 }
1095
1096 #[test]
1097 fn selection_replacement_is_its_own_undo_step() {
1098 let mut m = EditorModel::new("");
1099 m.insert("a");
1100 m.select_all();
1101 m.insert("b"); assert!(m.undo());
1103 assert_eq!(m.text(), "a");
1104 assert!(m.undo());
1105 assert_eq!(m.text(), "");
1106 }
1107
1108 #[test]
1109 fn new_edit_clears_redo() {
1110 let mut m = EditorModel::new("");
1111 m.insert("a");
1112 m.undo();
1113 assert!(m.can_redo());
1114 m.insert("b");
1115 assert!(!m.can_redo());
1116 assert!(!m.redo());
1117 assert_eq!(m.text(), "b");
1118 }
1119
1120 #[test]
1121 fn pos_for_click_clamps() {
1122 let m = EditorModel::new("ab\ncdef");
1123 assert_eq!(m.pos_for_click(0, 99), at(0, 2)); assert_eq!(m.pos_for_click(9, 1), at(1, 1)); assert_eq!(m.pos_for_click(9, 99), at(1, 4)); }
1127
1128 #[test]
1129 fn move_to_extend_selects_like_a_drag() {
1130 let mut m = EditorModel::new("hello\nworld");
1131 m.move_to(0, 1, false);
1132 m.move_to(1, 3, true);
1133 assert_eq!(m.selected_text().as_deref(), Some("ello\nwor"));
1134 m.move_to(0, 0, true);
1136 assert_eq!(m.selection(), Some((at(0, 0), at(0, 1))));
1137 }
1138
1139 #[test]
1140 fn utf8_editing() {
1141 let mut m = EditorModel::new("café");
1142 m.doc_end(false);
1143 assert_eq!(m.cursor(), at(0, 4)); assert!(m.backspace());
1145 assert_eq!(m.text(), "caf");
1146 m.insert("é");
1147 assert_eq!(m.text(), "café");
1148 }
1149
1150 #[test]
1151 fn utf8_cjk_positions() {
1152 let mut m = EditorModel::new("日本語\nhello");
1153 m.move_to(0, 1, false);
1154 m.insert("!");
1155 assert_eq!(m.text(), "日!本語\nhello");
1156 assert_eq!(m.cursor(), at(0, 2));
1157 assert!(m.delete());
1158 assert_eq!(m.text(), "日!語\nhello");
1159 m.end(false);
1161 m.move_down(false);
1162 assert_eq!(m.cursor(), at(1, 3));
1163 assert_eq!(m.pos_for_click(0, 99), at(0, 3));
1164 }
1165
1166 #[test]
1167 fn utf8_selection_and_words() {
1168 let mut m = EditorModel::new("voilà café");
1169 m.doc_end(false);
1170 m.word_left(false);
1171 assert_eq!(m.cursor(), at(0, 6)); m.word_left(true);
1173 assert_eq!(m.selected_text().as_deref(), Some("voilà "));
1174 m.move_to(0, 8, false);
1175 m.select_word();
1176 assert_eq!(m.selected_text().as_deref(), Some("café"));
1177 }
1178}