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
631    .char_indices()
632    .nth(col)
633    .map(|(i, _)| i)
634    .unwrap_or(line.len())
635}
636
637/// Document text as lines, normalizing CRLF.
638fn split_lines(text: &str) -> Vec<String> {
639  let text = text.replace('\r', "");
640  text.split('\n').map(str::to_string).collect()
641}
642
643/// Word characters for word-wise navigation — mirrors `input::edit`.
644fn 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    // At the line start: joins with the previous line.
741    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    // At the line end: joins with the next line.
761    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    // Doc edges are no-ops.
828    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)); // clamped by the short line
842    m.move_down(false);
843    assert_eq!(m.cursor(), at(2, 4)); // goal column restored
844    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); // (1, 2), goal 4
854    m.move_left(false); // (1, 1), goal cleared
855    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    // Extending left of the anchor still yields a normalized range.
915    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); // back to the anchor
928    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"); // copy leaves the doc alone
976    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    // Mid-word.
995    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    // At a word end (cursor just past the last char).
1000    m.move_to(0, 3, false);
1001    m.select_word();
1002    assert_eq!(m.selected_text().as_deref(), Some("foo"));
1003    // On a non-word char (with no word char adjacent on the left):
1004    // selects just that char.
1005    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    // Empty line: nothing to select.
1010    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    // Backspace also breaks a run.
1084    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"); // single char, but it replaced a selection
1103    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)); // past the line end
1125    assert_eq!(m.pos_for_click(9, 1), at(1, 1)); // past the last line
1126    assert_eq!(m.pos_for_click(9, 99), at(1, 4)); // past both
1127  }
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    // Dragging backwards past the anchor flips the range.
1136    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)); // chars, not bytes
1145    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    // Vertical movement counts chars, and clamps clicks per line.
1161    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)); // é is a word char
1173    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}