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
631 .char_indices()
632 .nth(col)
633 .map(|(i, _)| i)
634 .unwrap_or(line.len())
635}
636
637fn split_lines(text: &str) -> Vec<String> {
639 let text = text.replace('\r', "");
640 text.split('\n').map(str::to_string).collect()
641}
642
643fn is_word(c: char) -> bool {
645 c.is_alphanumeric() || c == '_'
646}
647
648#[cfg(test)]
649mod tests {
650 use super::*;
651
652 fn at(line: usize, col: usize) -> Pos {
653 Pos::new(line, col)
654 }
655
656 #[test]
657 fn empty_doc_is_one_empty_line() {
658 let m = EditorModel::new("");
659 assert_eq!(m.line_count(), 1);
660 assert!(m.is_empty());
661 assert_eq!(m.text(), "");
662 assert_eq!(m.cursor(), at(0, 0));
663 }
664
665 #[test]
666 fn text_roundtrip() {
667 let m = EditorModel::new("a\nb\n\nc");
668 assert_eq!(m.line_count(), 4);
669 assert_eq!(m.line(2), Some(""));
670 assert_eq!(m.line(4), None);
671 assert_eq!(m.text(), "a\nb\n\nc");
672 }
673
674 #[test]
675 fn new_normalizes_crlf() {
676 let m = EditorModel::new("a\r\nb");
677 assert_eq!(m.text(), "a\nb");
678 assert_eq!(m.line_count(), 2);
679 }
680
681 #[test]
682 fn set_text_resets_cursor_selection_and_history() {
683 let mut m = EditorModel::new("abc");
684 m.doc_end(false);
685 m.insert("d");
686 m.select_all();
687 m.set_text("xyz");
688 assert_eq!(m.text(), "xyz");
689 assert_eq!(m.cursor(), at(0, 0));
690 assert_eq!(m.selection(), None);
691 assert!(!m.can_undo());
692 assert!(!m.can_redo());
693 }
694
695 #[test]
696 fn insert_advances_cursor() {
697 let mut m = EditorModel::new("ac");
698 m.move_right(false);
699 m.insert("b");
700 assert_eq!(m.text(), "abc");
701 assert_eq!(m.cursor(), at(0, 2));
702 }
703
704 #[test]
705 fn insert_multiline_splits_lines() {
706 let mut m = EditorModel::new("hello world");
707 m.move_to(0, 5, false);
708 m.insert("\nmid\n");
709 assert_eq!(m.text(), "hello\nmid\n world");
710 assert_eq!(m.cursor(), at(2, 0));
711 }
712
713 #[test]
714 fn insert_replaces_selection() {
715 let mut m = EditorModel::new("one two three");
716 m.move_to(0, 4, false);
717 m.move_to(0, 7, true);
718 m.insert("2");
719 assert_eq!(m.text(), "one 2 three");
720 assert_eq!(m.cursor(), at(0, 5));
721 assert_eq!(m.selection(), None);
722 }
723
724 #[test]
725 fn insert_replaces_multiline_selection() {
726 let mut m = EditorModel::new("aaa\nbbb\nccc");
727 m.move_to(0, 1, false);
728 m.move_to(2, 2, true);
729 m.insert("X");
730 assert_eq!(m.text(), "aXc");
731 assert_eq!(m.cursor(), at(0, 2));
732 }
733
734 #[test]
735 fn backspace_within_line_and_join() {
736 let mut m = EditorModel::new("ab\ncd");
737 m.move_to(1, 1, false);
738 assert!(m.backspace());
739 assert_eq!(m.text(), "ab\nd");
740 assert!(m.backspace());
742 assert_eq!(m.text(), "abd");
743 assert_eq!(m.cursor(), at(0, 2));
744 }
745
746 #[test]
747 fn backspace_at_doc_start_is_noop() {
748 let mut m = EditorModel::new("ab");
749 assert!(!m.backspace());
750 assert_eq!(m.text(), "ab");
751 assert!(!m.can_undo());
752 }
753
754 #[test]
755 fn delete_within_line_and_join() {
756 let mut m = EditorModel::new("ab\ncd");
757 m.move_to(0, 1, false);
758 assert!(m.delete());
759 assert_eq!(m.text(), "a\ncd");
760 assert!(m.delete());
762 assert_eq!(m.text(), "acd");
763 assert_eq!(m.cursor(), at(0, 1));
764 }
765
766 #[test]
767 fn delete_at_doc_end_is_noop() {
768 let mut m = EditorModel::new("ab");
769 m.doc_end(false);
770 assert!(!m.delete());
771 assert!(!m.can_undo());
772 }
773
774 #[test]
775 fn newline_copies_leading_whitespace() {
776 let mut m = EditorModel::new(" foo");
777 m.end(false);
778 m.newline();
779 assert_eq!(m.text(), " foo\n ");
780 assert_eq!(m.cursor(), at(1, 4));
781 }
782
783 #[test]
784 fn newline_inside_indent_caps_the_copy() {
785 let mut m = EditorModel::new(" foo");
786 m.move_to(0, 2, false);
787 m.newline();
788 assert_eq!(m.text(), " \n foo");
789 assert_eq!(m.cursor(), at(1, 2));
790 }
791
792 #[test]
793 fn newline_replaces_selection() {
794 let mut m = EditorModel::new(" ab cd");
795 m.move_to(0, 4, false);
796 m.move_to(0, 5, true);
797 m.newline();
798 assert_eq!(m.text(), " ab\n cd");
799 assert_eq!(m.cursor(), at(1, 2));
800 }
801
802 #[test]
803 fn tab_advances_to_next_stop() {
804 let mut m = EditorModel::new("");
805 m.tab();
806 assert_eq!(m.text(), " ");
807 let mut m = EditorModel::new("ab");
808 m.end(false);
809 m.tab();
810 assert_eq!(m.text(), "ab ");
811 assert_eq!(m.cursor(), at(0, 4));
812 let mut m = EditorModel::new("x");
813 m.set_tab_size(2);
814 m.end(false);
815 m.tab();
816 assert_eq!(m.text(), "x ");
817 }
818
819 #[test]
820 fn horizontal_movement_crosses_lines() {
821 let mut m = EditorModel::new("ab\ncd");
822 m.move_to(0, 2, false);
823 m.move_right(false);
824 assert_eq!(m.cursor(), at(1, 0));
825 m.move_left(false);
826 assert_eq!(m.cursor(), at(0, 2));
827 m.doc_start(false);
829 m.move_left(false);
830 assert_eq!(m.cursor(), at(0, 0));
831 m.doc_end(false);
832 m.move_right(false);
833 assert_eq!(m.cursor(), at(1, 2));
834 }
835
836 #[test]
837 fn vertical_movement_keeps_goal_column() {
838 let mut m = EditorModel::new("hello\nhi\nworld!");
839 m.move_to(0, 4, false);
840 m.move_down(false);
841 assert_eq!(m.cursor(), at(1, 2)); m.move_down(false);
843 assert_eq!(m.cursor(), at(2, 4)); m.move_up(false);
845 m.move_up(false);
846 assert_eq!(m.cursor(), at(0, 4));
847 }
848
849 #[test]
850 fn horizontal_movement_resets_goal_column() {
851 let mut m = EditorModel::new("hello\nhi\nworld!");
852 m.move_to(0, 4, false);
853 m.move_down(false); m.move_left(false); m.move_down(false);
856 assert_eq!(m.cursor(), at(2, 1));
857 }
858
859 #[test]
860 fn vertical_movement_stops_at_edges() {
861 let mut m = EditorModel::new("a\nb");
862 m.move_up(false);
863 assert_eq!(m.cursor(), at(0, 0));
864 m.doc_end(false);
865 m.move_down(false);
866 assert_eq!(m.cursor(), at(1, 1));
867 }
868
869 #[test]
870 fn home_end_and_doc_edges() {
871 let mut m = EditorModel::new("abc\ndef");
872 m.move_to(1, 1, false);
873 m.home(false);
874 assert_eq!(m.cursor(), at(1, 0));
875 m.end(false);
876 assert_eq!(m.cursor(), at(1, 3));
877 m.doc_start(false);
878 assert_eq!(m.cursor(), at(0, 0));
879 m.doc_end(false);
880 assert_eq!(m.cursor(), at(1, 3));
881 }
882
883 #[test]
884 fn word_movement_within_line() {
885 let mut m = EditorModel::new("foo bar baz");
886 m.doc_end(false);
887 m.word_left(false);
888 assert_eq!(m.cursor(), at(0, 8));
889 m.word_left(false);
890 assert_eq!(m.cursor(), at(0, 4));
891 m.word_right(false);
892 assert_eq!(m.cursor(), at(0, 7));
893 }
894
895 #[test]
896 fn word_movement_crosses_lines() {
897 let mut m = EditorModel::new("foo\nbar");
898 m.move_to(1, 0, false);
899 m.word_left(false);
900 assert_eq!(m.cursor(), at(0, 0));
901 m.word_right(false);
902 assert_eq!(m.cursor(), at(0, 3));
903 m.word_right(false);
904 assert_eq!(m.cursor(), at(1, 3));
905 }
906
907 #[test]
908 fn extend_grows_and_normalizes_selection() {
909 let mut m = EditorModel::new("abcdef");
910 m.move_to(0, 2, false);
911 m.move_right(true);
912 m.move_right(true);
913 assert_eq!(m.selection(), Some((at(0, 2), at(0, 4))));
914 let mut m = EditorModel::new("abcdef");
916 m.move_to(0, 4, false);
917 m.move_left(true);
918 m.move_left(true);
919 assert_eq!(m.selection(), Some((at(0, 2), at(0, 4))));
920 assert_eq!(m.selected_text().as_deref(), Some("cd"));
921 }
922
923 #[test]
924 fn empty_selection_is_none() {
925 let mut m = EditorModel::new("abc");
926 m.move_right(true);
927 m.move_left(true); assert_eq!(m.selection(), None);
929 assert_eq!(m.selected_text(), None);
930 }
931
932 #[test]
933 fn plain_move_collapses_selection_to_edge() {
934 let mut m = EditorModel::new("abcdef");
935 m.move_to(0, 2, false);
936 m.move_to(0, 4, true);
937 m.move_left(false);
938 assert_eq!(m.cursor(), at(0, 2));
939 assert_eq!(m.selection(), None);
940 let mut m = EditorModel::new("abcdef");
941 m.move_to(0, 2, false);
942 m.move_to(0, 4, true);
943 m.move_right(false);
944 assert_eq!(m.cursor(), at(0, 4));
945 assert_eq!(m.selection(), None);
946 }
947
948 #[test]
949 fn selected_text_multiline() {
950 let mut m = EditorModel::new("aaa\nbbb\nccc");
951 m.move_to(0, 1, false);
952 m.move_to(2, 2, true);
953 assert_eq!(m.selected_text().as_deref(), Some("aa\nbbb\ncc"));
954 }
955
956 #[test]
957 fn delete_selection_joins_lines() {
958 let mut m = EditorModel::new("aaa\nbbb\nccc");
959 m.move_to(0, 2, false);
960 m.move_to(2, 1, true);
961 assert!(m.delete_selection());
962 assert_eq!(m.text(), "aacc");
963 assert_eq!(m.cursor(), at(0, 2));
964 assert!(!m.delete_selection());
965 }
966
967 #[test]
968 fn cut_and_copy() {
969 let mut m = EditorModel::new("hello world");
970 assert_eq!(m.copy(), None);
971 assert_eq!(m.cut(), None);
972 m.move_to(0, 0, false);
973 m.word_right(true);
974 assert_eq!(m.copy().as_deref(), Some("hello"));
975 assert_eq!(m.text(), "hello world"); assert_eq!(m.cut().as_deref(), Some("hello"));
977 assert_eq!(m.text(), " world");
978 assert!(m.undo());
979 assert_eq!(m.text(), "hello world");
980 }
981
982 #[test]
983 fn select_all_spans_document() {
984 let mut m = EditorModel::new("ab\ncd");
985 m.select_all();
986 assert_eq!(m.selection(), Some((at(0, 0), at(1, 2))));
987 assert_eq!(m.selected_text().as_deref(), Some("ab\ncd"));
988 m.clear_selection();
989 assert_eq!(m.selection(), None);
990 }
991
992 #[test]
993 fn select_word_variants() {
994 let mut m = EditorModel::new("foo bar_baz qux");
996 m.move_to(0, 6, false);
997 m.select_word();
998 assert_eq!(m.selected_text().as_deref(), Some("bar_baz"));
999 m.move_to(0, 3, false);
1001 m.select_word();
1002 assert_eq!(m.selected_text().as_deref(), Some("foo"));
1003 let mut m = EditorModel::new("foo .. bar");
1006 m.move_to(0, 5, false);
1007 m.select_word();
1008 assert_eq!(m.selection(), Some((at(0, 5), at(0, 6))));
1009 let mut m = EditorModel::new("");
1011 m.select_word();
1012 assert_eq!(m.selection(), None);
1013 }
1014
1015 #[test]
1016 fn select_line_spans_line() {
1017 let mut m = EditorModel::new("abc\ndef");
1018 m.move_to(1, 1, false);
1019 m.select_line();
1020 assert_eq!(m.selection(), Some((at(1, 0), at(1, 3))));
1021 assert_eq!(m.selected_text().as_deref(), Some("def"));
1022 }
1023
1024 #[test]
1025 fn undo_redo_roundtrip() {
1026 let mut m = EditorModel::new("ab");
1027 m.doc_end(false);
1028 m.newline();
1029 m.insert("cd");
1030 assert_eq!(m.text(), "ab\ncd");
1031 assert!(m.undo());
1032 assert_eq!(m.text(), "ab\n");
1033 assert!(m.undo());
1034 assert_eq!(m.text(), "ab");
1035 assert_eq!(m.cursor(), at(0, 2));
1036 assert!(!m.undo());
1037 assert!(m.redo());
1038 assert_eq!(m.text(), "ab\n");
1039 assert!(m.redo());
1040 assert_eq!(m.text(), "ab\ncd");
1041 assert!(!m.redo());
1042 }
1043
1044 #[test]
1045 fn typing_coalesces_into_one_undo_step() {
1046 let mut m = EditorModel::new("");
1047 m.insert("a");
1048 m.insert("b");
1049 m.insert("c");
1050 assert!(m.undo());
1051 assert_eq!(m.text(), "");
1052 assert!(!m.undo());
1053 assert!(m.redo());
1054 assert_eq!(m.text(), "abc");
1055 }
1056
1057 #[test]
1058 fn movement_breaks_coalescing() {
1059 let mut m = EditorModel::new("");
1060 m.insert("a");
1061 m.insert("b");
1062 m.move_left(false);
1063 m.move_right(false);
1064 m.insert("c");
1065 assert!(m.undo());
1066 assert_eq!(m.text(), "ab");
1067 assert!(m.undo());
1068 assert_eq!(m.text(), "");
1069 }
1070
1071 #[test]
1072 fn structural_edits_break_coalescing() {
1073 let mut m = EditorModel::new("");
1074 m.insert("a");
1075 m.newline();
1076 m.insert("b");
1077 assert!(m.undo());
1078 assert_eq!(m.text(), "a\n");
1079 assert!(m.undo());
1080 assert_eq!(m.text(), "a");
1081 assert!(m.undo());
1082 assert_eq!(m.text(), "");
1083 let mut m = EditorModel::new("");
1085 m.insert("a");
1086 m.insert("b");
1087 m.backspace();
1088 m.insert("c");
1089 assert!(m.undo());
1090 assert_eq!(m.text(), "a");
1091 assert!(m.undo());
1092 assert_eq!(m.text(), "ab");
1093 assert!(m.undo());
1094 assert_eq!(m.text(), "");
1095 }
1096
1097 #[test]
1098 fn selection_replacement_is_its_own_undo_step() {
1099 let mut m = EditorModel::new("");
1100 m.insert("a");
1101 m.select_all();
1102 m.insert("b"); assert!(m.undo());
1104 assert_eq!(m.text(), "a");
1105 assert!(m.undo());
1106 assert_eq!(m.text(), "");
1107 }
1108
1109 #[test]
1110 fn new_edit_clears_redo() {
1111 let mut m = EditorModel::new("");
1112 m.insert("a");
1113 m.undo();
1114 assert!(m.can_redo());
1115 m.insert("b");
1116 assert!(!m.can_redo());
1117 assert!(!m.redo());
1118 assert_eq!(m.text(), "b");
1119 }
1120
1121 #[test]
1122 fn pos_for_click_clamps() {
1123 let m = EditorModel::new("ab\ncdef");
1124 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)); }
1128
1129 #[test]
1130 fn move_to_extend_selects_like_a_drag() {
1131 let mut m = EditorModel::new("hello\nworld");
1132 m.move_to(0, 1, false);
1133 m.move_to(1, 3, true);
1134 assert_eq!(m.selected_text().as_deref(), Some("ello\nwor"));
1135 m.move_to(0, 0, true);
1137 assert_eq!(m.selection(), Some((at(0, 0), at(0, 1))));
1138 }
1139
1140 #[test]
1141 fn utf8_editing() {
1142 let mut m = EditorModel::new("café");
1143 m.doc_end(false);
1144 assert_eq!(m.cursor(), at(0, 4)); assert!(m.backspace());
1146 assert_eq!(m.text(), "caf");
1147 m.insert("é");
1148 assert_eq!(m.text(), "café");
1149 }
1150
1151 #[test]
1152 fn utf8_cjk_positions() {
1153 let mut m = EditorModel::new("日本語\nhello");
1154 m.move_to(0, 1, false);
1155 m.insert("!");
1156 assert_eq!(m.text(), "日!本語\nhello");
1157 assert_eq!(m.cursor(), at(0, 2));
1158 assert!(m.delete());
1159 assert_eq!(m.text(), "日!語\nhello");
1160 m.end(false);
1162 m.move_down(false);
1163 assert_eq!(m.cursor(), at(1, 3));
1164 assert_eq!(m.pos_for_click(0, 99), at(0, 3));
1165 }
1166
1167 #[test]
1168 fn utf8_selection_and_words() {
1169 let mut m = EditorModel::new("voilà café");
1170 m.doc_end(false);
1171 m.word_left(false);
1172 assert_eq!(m.cursor(), at(0, 6)); m.word_left(true);
1174 assert_eq!(m.selected_text().as_deref(), Some("voilà "));
1175 m.move_to(0, 8, false);
1176 m.select_word();
1177 assert_eq!(m.selected_text().as_deref(), Some("café"));
1178 }
1179}