Skip to main content

guise/editor/
model.rs

1//! Pure multiline text-editing model: the document as lines, a char-index
2//! cursor, an anchor-based selection, and snapshot undo/redo with coalesced
3//! typing. No UI and no gpui — fully unit-testable; the `Editor` entity
4//! drives it from key/mouse events and renders from `lines()`/`selection()`.
5//!
6//! The multiline successor to [`TextEdit`](crate::input::TextEdit): same
7//! char-index cursor and word-boundary semantics, plus selections and history.
8
9use std::borrow::Cow;
10
11/// A position in the document: `line` index plus `col` as a **char** index
12/// (not bytes) in `0..=line_len`. Ordering is document order.
13#[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/// One undo/redo step: the document and cursor as they were before an edit.
26#[derive(Debug, Clone)]
27struct Snapshot {
28    lines: Vec<String>,
29    cursor: Pos,
30}
31
32/// A multiline editing model with cursor, selection, and undo history.
33///
34/// The document is a `Vec<String>` of lines (no trailing `\n` stored); an
35/// empty document is one empty line. All columns are char indices, so
36/// multibyte text (é, 日本語) edits correctly. Runs of single-char typing
37/// coalesce into one undo step; any other edit or movement breaks the run.
38#[derive(Debug, Clone)]
39pub struct EditorModel {
40    lines: Vec<String>,
41    cursor: Pos,
42    /// Selection anchor; the selection spans anchor..cursor in either order.
43    anchor: Option<Pos>,
44    /// Sticky column for vertical movement through short lines.
45    goal_col: Option<usize>,
46    undo: Vec<Snapshot>,
47    redo: Vec<Snapshot>,
48    /// Whether the last edit was a coalescable single-char insert.
49    coalescing: bool,
50    /// Spaces per tab stop for [`tab`](Self::tab).
51    tab_size: usize,
52}
53
54impl Default for EditorModel {
55    fn default() -> Self {
56        Self::new("")
57    }
58}
59
60impl EditorModel {
61    /// Start editing `text` with the cursor at the document start.
62    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    // ---- document access ----
76
77    pub fn text(&self) -> String {
78        self.lines.join("\n")
79    }
80
81    /// Replace the whole document, resetting cursor, selection, and history.
82    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    /// The text of line `i`, if it exists.
101    pub fn line(&self, i: usize) -> Option<&str> {
102        self.lines.get(i).map(String::as_str)
103    }
104
105    /// All lines, for rendering.
106    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    /// Spaces per tab stop (min 1, default 4).
119    pub fn set_tab_size(&mut self, n: usize) {
120        self.tab_size = n.max(1);
121    }
122
123    // ---- editing ----
124
125    /// Insert `s` at the cursor, replacing the selection if there is one.
126    /// Embedded newlines split lines; CRLF is normalized to `\n`.
127    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    /// Delete the selection, or the char before the cursor (joining lines at
144    /// a line start). Returns whether anything changed.
145    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    /// Delete the selection, or the char at the cursor (joining lines at a
160    /// line end). Returns whether anything changed.
161    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    /// Split the line at the cursor, auto-indenting the new line with the
176    /// current line's leading whitespace (capped at the cursor column, so
177    /// splitting inside the indent doesn't over-indent).
178    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    /// Insert spaces up to the next tab stop (see [`set_tab_size`](Self::set_tab_size)).
192    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    // ---- movement (extend = shift held: grow the selection) ----
201
202    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    /// Move up one line, keeping the goal column through shorter lines.
231    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    /// Move down one line, keeping the goal column through shorter lines.
242    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    /// Move left to the start of the previous word (Option+Left), crossing
278    /// line boundaries.
279    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    /// Move right past the end of the next word (Option+Right), crossing
299    /// line boundaries.
300    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    // ---- mouse ----
320
321    /// Clamp a raw (line, col) from mouse hit-testing to a valid position:
322    /// line into the document, col to that line's char length.
323    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    /// Move the cursor to a clicked position (clamped), extending the
329    /// selection when `extend` (shift-click or drag).
330    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    // ---- selection ----
338
339    /// The selection as a normalized (start, end) pair in document order, or
340    /// `None` when there is no selection (or it is empty).
341    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    /// Select the word around the cursor (double-click). On a non-word char,
366    /// selects just that char.
367    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        // The word char under the cursor, or the one before at a word end.
377        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    /// Select the cursor's whole line (triple-click).
400    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    /// The selected text, `None` when nothing is selected.
409    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    /// Delete the selected text. Returns whether anything changed.
428    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    // ---- clipboard (the entity layer talks to the OS) ----
439
440    /// Remove and return the selected text; `None` when nothing is selected.
441    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    /// The selected text without modifying the document.
450    pub fn copy(&self) -> Option<String> {
451        self.selected_text()
452    }
453
454    // ---- history ----
455
456    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    /// Revert the last edit (a coalesced typing run counts as one). Returns
465    /// whether anything changed.
466    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    /// Re-apply the last undone edit. Returns whether anything changed.
483    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    // ---- internals ----
500
501    fn line_len(&self, i: usize) -> usize {
502        self.lines[i].chars().count()
503    }
504
505    /// The position one char before `p` (line boundaries count as one char).
506    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    /// The position one char after `p` (line boundaries count as one char).
517    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    /// The char before `p`, with `\n` at line starts; `None` at doc start.
528    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    /// The char at `p`, with `\n` at line ends; `None` at doc end.
539    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    /// Movement prologue: break undo coalescing and set/clear the anchor.
550    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    /// Record the current state for undo unless coalescing with the previous
562    /// single-char insert. Any edit invalidates the redo stack.
563    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    /// Remove the selected range if any, leaving the cursor at its start.
575    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    /// Remove `start..end` (document order), leaving the cursor at `start`.
583    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    /// Insert already-normalized text at the cursor, advancing past it.
602    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
628/// Byte offset of char index `col` in `line` (`line.len()` past the end).
629fn 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
636/// Document text as lines, normalizing CRLF.
637fn split_lines(text: &str) -> Vec<String> {
638    let text = text.replace('\r', "");
639    text.split('\n').map(str::to_string).collect()
640}
641
642/// Word characters for word-wise navigation — mirrors `input::edit`.
643fn 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        // At the line start: joins with the previous line.
740        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        // At the line end: joins with the next line.
760        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        // Doc edges are no-ops.
827        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)); // clamped by the short line
841        m.move_down(false);
842        assert_eq!(m.cursor(), at(2, 4)); // goal column restored
843        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); // (1, 2), goal 4
853        m.move_left(false); // (1, 1), goal cleared
854        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        // Extending left of the anchor still yields a normalized range.
914        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); // back to the anchor
927        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"); // copy leaves the doc alone
975        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        // Mid-word.
994        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        // At a word end (cursor just past the last char).
999        m.move_to(0, 3, false);
1000        m.select_word();
1001        assert_eq!(m.selected_text().as_deref(), Some("foo"));
1002        // On a non-word char (with no word char adjacent on the left):
1003        // selects just that char.
1004        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        // Empty line: nothing to select.
1009        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        // Backspace also breaks a run.
1083        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"); // single char, but it replaced a selection
1102        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)); // past the line end
1124        assert_eq!(m.pos_for_click(9, 1), at(1, 1)); // past the last line
1125        assert_eq!(m.pos_for_click(9, 99), at(1, 4)); // past both
1126    }
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        // Dragging backwards past the anchor flips the range.
1135        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)); // chars, not bytes
1144        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        // Vertical movement counts chars, and clamps clicks per line.
1160        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)); // é is a word char
1172        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}