Skip to main content

guise/input/
edit.rs

1//! Pure single-line text-editing model: a string plus a char-index cursor,
2//! with the operations a text field needs. No UI — fully unit-testable; the
3//! `TextInput` entity drives it from key events and renders from `split`.
4
5/// How many undo steps a field remembers.
6const UNDO_DEPTH: usize = 128;
7
8/// …and how much text those steps may hold between them, in chars.
9///
10/// A step is a whole copy of the buffer, which is nothing for a one-line field
11/// and a great deal for a `TextArea` holding a document: 128 steps of a 200 KB
12/// buffer is 100 MB of `Vec<char>`. Bounding the depth alone leaves that hole,
13/// so history is bounded by what it *retains* as well, and the oldest steps
14/// are dropped first. A quarter-million chars is far more history than anyone
15/// undoes through, and it costs a megabyte at worst.
16const UNDO_CHARS: usize = 256 * 1024;
17
18/// What the last mutation was, so a run of the same kind coalesces into one
19/// undo step instead of one step per keystroke.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21enum EditKind {
22  Insert,
23  Delete,
24}
25
26/// A restorable point in the edit history.
27#[derive(Debug, Clone)]
28struct Snapshot {
29  chars: Vec<char>,
30  cursor: usize,
31  anchor: Option<usize>,
32}
33
34/// An editable line of text with a cursor and an optional selection.
35#[derive(Debug, Clone, Default)]
36pub struct TextEdit {
37  chars: Vec<char>,
38  /// Cursor position as a char index in `0..=chars.len()`.
39  cursor: usize,
40  /// Selection anchor; a selection spans `anchor..cursor` in either order.
41  /// `None` means no selection.
42  anchor: Option<usize>,
43  undos: Vec<Snapshot>,
44  redos: Vec<Snapshot>,
45  /// The kind of the mutation that produced the newest undo entry, so the
46  /// next one of the same kind can fold into it.
47  run: Option<EditKind>,
48}
49
50impl TextEdit {
51  /// Start editing `text` with the cursor at the end.
52  pub fn new(text: &str) -> Self {
53    let chars: Vec<char> = text.chars().collect();
54    let cursor = chars.len();
55    Self {
56      chars,
57      cursor,
58      anchor: None,
59      undos: Vec::new(),
60      redos: Vec::new(),
61      run: None,
62    }
63  }
64
65  pub fn text(&self) -> String {
66    self.chars.iter().collect()
67  }
68
69  /// The buffer itself. Callers that only need to measure or slice the text
70  /// use this instead of [`text`](Self::text), which allocates a fresh
71  /// `String` every call — and the platform's input handler asks several
72  /// times per keystroke.
73  pub fn chars(&self) -> &[char] {
74    &self.chars
75  }
76
77  pub fn is_empty(&self) -> bool {
78    self.chars.is_empty()
79  }
80
81  /// Length in chars, which is also the last valid cursor position.
82  pub fn len(&self) -> usize {
83    self.chars.len()
84  }
85
86  /// The cursor as a char index.
87  pub fn cursor(&self) -> usize {
88    self.cursor
89  }
90
91  /// Move the cursor, dropping any selection. Out-of-range values clamp.
92  pub fn set_cursor(&mut self, index: usize) {
93    self.cursor = index.min(self.chars.len());
94    self.anchor = None;
95    self.run = None;
96  }
97
98  /// Select `start..end` and leave the cursor at `end`, so a following
99  /// Shift+Arrow extends from the edge the user last moved.
100  pub fn set_selection(&mut self, start: usize, end: usize) {
101    let n = self.chars.len();
102    self.anchor = Some(start.min(n));
103    self.cursor = end.min(n);
104    self.run = None;
105  }
106
107  /// Move the selection's free edge to `index`, opening a selection anchored
108  /// at the cursor if there isn't one. This is Shift+click and mouse drag.
109  pub fn extend_to(&mut self, index: usize) {
110    if self.anchor.is_none() {
111      self.anchor = Some(self.cursor);
112    }
113    self.cursor = index.min(self.chars.len());
114    self.run = None;
115  }
116
117  /// Replace everything, as a programmatic set: the history is dropped
118  /// because the old text is no longer something the user can undo back to.
119  /// That also releases whatever the history was holding.
120  pub fn set_text(&mut self, text: &str) {
121    *self = TextEdit::new(text);
122  }
123
124  /// Byte offset into [`text`](Self::text) for a char index. Shaped-line
125  /// geometry is addressed in bytes, the edit model in chars.
126  pub fn byte_of(&self, index: usize) -> usize {
127    self.chars[..index.min(self.chars.len())]
128      .iter()
129      .map(|c| c.len_utf8())
130      .sum()
131  }
132
133  /// Char index for a byte offset, rounding up to the next char boundary.
134  pub fn char_of(&self, byte: usize) -> usize {
135    let mut at = 0;
136    for (index, c) in self.chars.iter().enumerate() {
137      if at >= byte {
138        return index;
139      }
140      at += c.len_utf8();
141    }
142    self.chars.len()
143  }
144
145  /// The word surrounding `index` as `(start, end)` char indices — what a
146  /// double-click selects. A click in whitespace takes the whitespace run.
147  pub fn word_at(&self, index: usize) -> (usize, usize) {
148    let n = self.chars.len();
149    if n == 0 {
150      return (0, 0);
151    }
152    // A click at the very end, or just past a word, belongs to the char
153    // before it — the same way a browser resolves the boundary case.
154    let probe = index.min(n - 1);
155    let wordish = is_word(self.chars[probe]);
156    let mut start = probe;
157    while start > 0 && is_word(self.chars[start - 1]) == wordish {
158      start -= 1;
159    }
160    let mut end = probe;
161    while end < n && is_word(self.chars[end]) == wordish {
162      end += 1;
163    }
164    (start, end)
165  }
166
167  /// Undo the last edit. Returns whether there was one.
168  pub fn undo(&mut self) -> bool {
169    let Some(previous) = self.undos.pop() else {
170      return false;
171    };
172    self.redos.push(self.snapshot());
173    self.restore(previous);
174    true
175  }
176
177  /// Redo the last undone edit. Returns whether there was one.
178  pub fn redo(&mut self) -> bool {
179    let Some(next) = self.redos.pop() else {
180      return false;
181    };
182    self.undos.push(self.snapshot());
183    self.restore(next);
184    true
185  }
186
187  fn snapshot(&self) -> Snapshot {
188    Snapshot {
189      chars: self.chars.clone(),
190      cursor: self.cursor,
191      anchor: self.anchor,
192    }
193  }
194
195  fn restore(&mut self, snapshot: Snapshot) {
196    self.chars = snapshot.chars;
197    self.cursor = snapshot.cursor.min(self.chars.len());
198    self.anchor = snapshot.anchor.map(|a| a.min(self.chars.len()));
199    self.run = None;
200  }
201
202  /// Remember the pre-edit state, unless this continues a run of the same
203  /// kind — typing a word is one undo step, not one per letter.
204  fn record(&mut self, kind: EditKind) {
205    self.redos.clear();
206    if self.run == Some(kind) {
207      return;
208    }
209    self.undos.push(self.snapshot());
210    // Oldest first: the step you are least likely to reach for is the one
211    // furthest back.
212    while self.undos.len() > UNDO_DEPTH
213      || (self.history_chars() > UNDO_CHARS && self.undos.len() > 1)
214    {
215      self.undos.remove(0);
216    }
217    self.run = Some(kind);
218  }
219
220  /// Chars held by the undo history. Summed on demand rather than tracked
221  /// incrementally: the stacks are at most [`UNDO_DEPTH`] deep and this is
222  /// only asked while trimming, which is nowhere near a hot path.
223  pub(crate) fn history_chars(&self) -> usize {
224    self
225      .undos
226      .iter()
227      .chain(&self.redos)
228      .map(|snapshot| snapshot.chars.len())
229      .sum()
230  }
231
232  /// End the current coalescing run, so the next edit starts a fresh undo
233  /// step. Callers use this at word boundaries and on paste.
234  pub fn break_undo(&mut self) {
235    self.run = None;
236  }
237
238  /// The selected span `(start, end)` as char indices, or `None` if the
239  /// selection is empty/collapsed.
240  pub fn selection(&self) -> Option<(usize, usize)> {
241    let a = self.anchor?;
242    (a != self.cursor).then(|| (a.min(self.cursor), a.max(self.cursor)))
243  }
244
245  pub fn has_selection(&self) -> bool {
246    self.selection().is_some()
247  }
248
249  /// The selected text, or `None` when nothing is selected.
250  pub fn selected_text(&self) -> Option<String> {
251    let (s, e) = self.selection()?;
252    Some(self.chars[s..e].iter().collect())
253  }
254
255  /// Select the whole line.
256  pub fn select_all(&mut self) {
257    if self.chars.is_empty() {
258      self.anchor = None;
259      return;
260    }
261    self.anchor = Some(0);
262    self.cursor = self.chars.len();
263  }
264
265  /// Drop any selection, keeping the cursor put.
266  pub fn clear_selection(&mut self) {
267    self.anchor = None;
268  }
269
270  pub fn collapse_selection_start(&mut self) -> bool {
271    let Some((start, _)) = self.selection() else {
272      return false;
273    };
274    self.cursor = start;
275    self.anchor = None;
276    true
277  }
278
279  pub fn collapse_selection_end(&mut self) -> bool {
280    let Some((_, end)) = self.selection() else {
281      return false;
282    };
283    self.cursor = end;
284    self.anchor = None;
285    true
286  }
287
288  /// Delete the selected text (if any), leaving the cursor at its start.
289  /// Returns whether anything was removed.
290  pub fn delete_selection(&mut self) -> bool {
291    if self.selection().is_none() {
292      return false;
293    }
294    self.record(EditKind::Delete);
295    self.take_selection()
296  }
297
298  /// [`delete_selection`](Self::delete_selection) without touching the undo
299  /// history, for the mutators that have already recorded their own step.
300  fn take_selection(&mut self) -> bool {
301    let Some((s, e)) = self.selection() else {
302      return false;
303    };
304    self.chars.drain(s..e);
305    self.cursor = s;
306    self.anchor = None;
307    true
308  }
309
310  /// Prepare for a cursor move: with `extend` (Shift held) anchor a selection
311  /// at the current cursor if one isn't already open; otherwise drop it.
312  pub fn pre_move(&mut self, extend: bool) {
313    if extend {
314      if self.anchor.is_none() {
315        self.anchor = Some(self.cursor);
316      }
317    } else {
318      self.anchor = None;
319    }
320  }
321
322  /// The text split around the selection: `(before, selected, after)`, or
323  /// `None` when nothing is selected.
324  pub fn split_selection(&self) -> Option<(String, String, String)> {
325    let (s, e) = self.selection()?;
326    Some((
327      self.chars[..s].iter().collect(),
328      self.chars[s..e].iter().collect(),
329      self.chars[e..].iter().collect(),
330    ))
331  }
332
333  /// Insert `s` at the cursor, replacing any selection, advancing past it.
334  pub fn insert(&mut self, s: &str) {
335    if s.is_empty() && !self.has_selection() {
336      return;
337    }
338    self.record(EditKind::Insert);
339    self.take_selection();
340    // `splice` shifts the tail once. Inserting char by char shifts it per
341    // character, which turns a large paste near the start of a long buffer
342    // into quadratic work on the UI thread.
343    let at = self.cursor;
344    self.chars.splice(at..at, s.chars());
345    self.cursor += s.chars().count();
346    // Typing a word is one undo step; the space after it ends that step so
347    // undo walks back word by word rather than wiping the whole line.
348    if s.chars().any(|c| c.is_whitespace()) {
349      self.run = None;
350    }
351  }
352
353  /// Replace the char range `range` with `s`, leaving the cursor after it.
354  /// This is the entry point the platform's text-input handler drives, so it
355  /// takes an explicit range rather than using the selection.
356  pub fn replace_range(&mut self, range: std::ops::Range<usize>, s: &str) {
357    let n = self.chars.len();
358    let start = range.start.min(n);
359    let end = range.end.clamp(start, n);
360    self.record(EditKind::Insert);
361    self.chars.splice(start..end, s.chars());
362    self.cursor = start + s.chars().count();
363    self.anchor = None;
364    if s.chars().any(|c| c.is_whitespace()) {
365      self.run = None;
366    }
367  }
368
369  /// Delete the selection, or the char before the cursor. Returns whether
370  /// anything changed.
371  pub fn backspace(&mut self) -> bool {
372    if self.has_selection() {
373      return self.delete_selection();
374    }
375    if self.cursor == 0 {
376      return false;
377    }
378    self.record(EditKind::Delete);
379    self.cursor -= 1;
380    self.chars.remove(self.cursor);
381    true
382  }
383
384  /// Delete the selection, or the char at the cursor. Returns whether anything
385  /// changed.
386  pub fn delete(&mut self) -> bool {
387    if self.has_selection() {
388      return self.delete_selection();
389    }
390    if self.cursor >= self.chars.len() {
391      return false;
392    }
393    self.record(EditKind::Delete);
394    self.chars.remove(self.cursor);
395    true
396  }
397
398  pub fn left(&mut self) {
399    self.cursor = self.cursor.saturating_sub(1);
400  }
401
402  pub fn right(&mut self) {
403    if self.cursor < self.chars.len() {
404      self.cursor += 1;
405    }
406  }
407
408  pub fn home(&mut self) {
409    self.cursor = 0;
410  }
411
412  pub fn end(&mut self) {
413    self.cursor = self.chars.len();
414  }
415
416  pub fn line_home(&mut self) {
417    while self.cursor > 0 && self.chars[self.cursor - 1] != '\n' {
418      self.cursor -= 1;
419    }
420  }
421
422  pub fn line_end(&mut self) {
423    while self.cursor < self.chars.len() && self.chars[self.cursor] != '\n' {
424      self.cursor += 1;
425    }
426  }
427
428  /// Move left to the start of the previous word (Option+Left on macOS).
429  pub fn word_left(&mut self) {
430    while self.cursor > 0 && !is_word(self.chars[self.cursor - 1]) {
431      self.cursor -= 1;
432    }
433    while self.cursor > 0 && is_word(self.chars[self.cursor - 1]) {
434      self.cursor -= 1;
435    }
436  }
437
438  /// Move right past the end of the next word (Option+Right on macOS).
439  pub fn word_right(&mut self) {
440    let n = self.chars.len();
441    while self.cursor < n && !is_word(self.chars[self.cursor]) {
442      self.cursor += 1;
443    }
444    while self.cursor < n && is_word(self.chars[self.cursor]) {
445      self.cursor += 1;
446    }
447  }
448
449  /// Delete the word before the cursor (Option+Backspace). Returns whether
450  /// anything changed.
451  pub fn delete_word_back(&mut self) -> bool {
452    if self.has_selection() {
453      return self.delete_selection();
454    }
455    let end = self.cursor;
456    self.word_left();
457    if self.cursor < end {
458      self.record(EditKind::Delete);
459      self.chars.drain(self.cursor..end);
460      self.run = None;
461      true
462    } else {
463      false
464    }
465  }
466
467  /// Delete the word after the cursor (Option+Delete). Returns whether
468  /// anything changed.
469  pub fn delete_word_forward(&mut self) -> bool {
470    if self.has_selection() {
471      return self.delete_selection();
472    }
473    let start = self.cursor;
474    let n = self.chars.len();
475    let mut end = self.cursor;
476    while end < n && !is_word(self.chars[end]) {
477      end += 1;
478    }
479    while end < n && is_word(self.chars[end]) {
480      end += 1;
481    }
482    if end > start {
483      self.record(EditKind::Delete);
484      self.chars.drain(start..end);
485      self.run = None;
486      true
487    } else {
488      false
489    }
490  }
491
492  /// Delete from the cursor to the line start (Cmd+Backspace). Returns
493  /// whether anything changed.
494  pub fn delete_to_start(&mut self) -> bool {
495    if self.has_selection() {
496      return self.delete_selection();
497    }
498    if self.cursor == 0 {
499      return false;
500    }
501    self.record(EditKind::Delete);
502    self.chars.drain(0..self.cursor);
503    self.cursor = 0;
504    self.run = None;
505    true
506  }
507
508  /// Delete from the cursor to the line end (Cmd+Delete / Ctrl+K). Returns
509  /// whether anything changed.
510  pub fn delete_to_end(&mut self) -> bool {
511    if self.has_selection() {
512      return self.delete_selection();
513    }
514    if self.cursor >= self.chars.len() {
515      return false;
516    }
517    self.record(EditKind::Delete);
518    self.chars.truncate(self.cursor);
519    self.run = None;
520    true
521  }
522
523  /// The text before and after the cursor, for rendering a caret between.
524  pub fn split(&self) -> (String, String) {
525    (
526      self.chars[..self.cursor].iter().collect(),
527      self.chars[self.cursor..].iter().collect(),
528    )
529  }
530
531  /// Move the cursor up one line, keeping the column where possible. Multiline
532  /// only (single-line text has nowhere to go).
533  pub fn up(&mut self) {
534    self.vmove(-1);
535  }
536
537  /// Move the cursor down one line, keeping the column where possible.
538  pub fn down(&mut self) {
539    self.vmove(1);
540  }
541
542  /// (line, column) of the cursor, counting `\n`-separated lines.
543  fn line_col(&self) -> (usize, usize) {
544    let mut line = 0;
545    let mut col = 0;
546    for &c in &self.chars[..self.cursor] {
547      if c == '\n' {
548        line += 1;
549        col = 0;
550      } else {
551        col += 1;
552      }
553    }
554    (line, col)
555  }
556
557  /// (start char index, length excluding newline) for each line.
558  fn line_bounds(&self) -> Vec<(usize, usize)> {
559    let mut out = Vec::new();
560    let mut start = 0;
561    let mut len = 0;
562    for (i, &c) in self.chars.iter().enumerate() {
563      if c == '\n' {
564        out.push((start, len));
565        start = i + 1;
566        len = 0;
567      } else {
568        len += 1;
569      }
570    }
571    out.push((start, len));
572    out
573  }
574
575  fn vmove(&mut self, dir: isize) {
576    let (line, col) = self.line_col();
577    let bounds = self.line_bounds();
578    let target = line as isize + dir;
579    if target < 0 || target as usize >= bounds.len() {
580      return;
581    }
582    let (start, len) = bounds[target as usize];
583    self.cursor = start + col.min(len);
584  }
585}
586
587/// Word characters for word-wise navigation/deletion.
588fn is_word(c: char) -> bool {
589  c.is_alphanumeric() || c == '_'
590}
591
592#[cfg(test)]
593mod tests {
594  use super::*;
595
596  #[test]
597  fn new_places_cursor_at_end() {
598    let e = TextEdit::new("abc");
599    assert_eq!(e.split(), ("abc".into(), "".into()));
600  }
601
602  #[test]
603  fn insert_at_cursor() {
604    let mut e = TextEdit::new("ac");
605    e.left();
606    e.insert("b");
607    assert_eq!(e.text(), "abc");
608    assert_eq!(e.split(), ("ab".into(), "c".into()));
609  }
610
611  #[test]
612  fn backspace_and_delete() {
613    let mut e = TextEdit::new("abc");
614    assert!(e.backspace());
615    assert_eq!(e.text(), "ab");
616    e.home();
617    assert!(e.delete());
618    assert_eq!(e.text(), "b");
619    e.home();
620    assert!(!e.backspace());
621    e.end();
622    assert!(!e.delete());
623  }
624
625  #[test]
626  fn handles_unicode() {
627    let mut e = TextEdit::new("café");
628    assert!(e.backspace());
629    assert_eq!(e.text(), "caf");
630    e.insert("é");
631    assert_eq!(e.text(), "café");
632  }
633
634  #[test]
635  fn vertical_movement_keeps_column() {
636    // Two lines: "hello" / "hi". Cursor starts at end ("hi").
637    let mut e = TextEdit::new("hello\nhi");
638    // Column 2 on line 1.
639    e.up();
640    // Same column (2) on line 0 → between "he" and "llo".
641    assert_eq!(e.split().0, "he");
642    e.down();
643    // Back to line 1; column clamped to its length (2) → end.
644    assert_eq!(e.split(), ("hello\nhi".into(), "".into()));
645  }
646
647  #[test]
648  fn vertical_movement_stops_at_edges() {
649    let mut e = TextEdit::new("a\nb");
650    e.home(); // line 1 has only the final char; home goes to absolute start
651    e.up(); // already on first line, no-op
652    assert_eq!(e.split().0, "");
653  }
654
655  #[test]
656  fn word_navigation() {
657    let mut e = TextEdit::new("foo bar baz");
658    e.word_left();
659    assert_eq!(e.split(), ("foo bar ".into(), "baz".into()));
660    e.word_left();
661    assert_eq!(e.split(), ("foo ".into(), "bar baz".into()));
662    e.word_right();
663    assert_eq!(e.split(), ("foo bar".into(), " baz".into()));
664  }
665
666  #[test]
667  fn delete_word_back_and_forward() {
668    let mut e = TextEdit::new("foo bar baz");
669    assert!(e.delete_word_back());
670    assert_eq!(e.text(), "foo bar ");
671    e.home();
672    assert!(e.delete_word_forward());
673    assert_eq!(e.text(), " bar ");
674    // Nothing before the cursor at home: no-op.
675    e.home();
676    assert!(!e.delete_word_back());
677  }
678
679  #[test]
680  fn select_all_then_type_replaces() {
681    let mut e = TextEdit::new("hello");
682    e.select_all();
683    assert_eq!(e.selected_text().as_deref(), Some("hello"));
684    e.insert("x");
685    assert_eq!(e.text(), "x");
686    assert!(!e.has_selection());
687  }
688
689  #[test]
690  fn shift_arrow_extends_selection() {
691    let mut e = TextEdit::new("abcd");
692    e.pre_move(true);
693    e.left(); // select "d"
694    e.pre_move(true);
695    e.left(); // select "cd"
696    assert_eq!(e.selected_text().as_deref(), Some("cd"));
697    let (before, sel, after) = e.split_selection().unwrap();
698    assert_eq!(
699      (before.as_str(), sel.as_str(), after.as_str()),
700      ("ab", "cd", "")
701    );
702  }
703
704  #[test]
705  fn plain_move_clears_selection() {
706    let mut e = TextEdit::new("abcd");
707    e.select_all();
708    e.pre_move(false);
709    e.left();
710    assert!(!e.has_selection());
711  }
712
713  #[test]
714  fn backspace_deletes_selection() {
715    let mut e = TextEdit::new("abcd");
716    e.select_all();
717    assert!(e.backspace());
718    assert_eq!(e.text(), "");
719  }
720
721  #[test]
722  fn modified_deletes_replace_the_selection() {
723    for delete in [
724      TextEdit::delete_word_back as fn(&mut TextEdit) -> bool,
725      TextEdit::delete_word_forward,
726      TextEdit::delete_to_start,
727      TextEdit::delete_to_end,
728    ] {
729      let mut edit = TextEdit::new("abcd");
730      edit.select_all();
731      assert!(delete(&mut edit));
732      assert_eq!(edit.text(), "");
733    }
734  }
735
736  #[test]
737  fn selection_collapses_to_the_requested_edge() {
738    let mut edit = TextEdit::new("abcd");
739    edit.select_all();
740    assert!(edit.collapse_selection_start());
741    assert_eq!(edit.split().0, "");
742    edit.select_all();
743    assert!(edit.collapse_selection_end());
744    assert_eq!(edit.split().0, "abcd");
745  }
746
747  #[test]
748  fn delete_to_line_edges() {
749    let mut e = TextEdit::new("hello world");
750    e.home();
751    e.right();
752    e.right();
753    assert!(e.delete_to_start());
754    assert_eq!(e.text(), "llo world");
755    assert!(e.delete_to_end());
756    assert_eq!(e.text(), "");
757    assert!(!e.delete_to_end());
758  }
759
760  #[test]
761  fn line_edges_stay_on_the_current_line() {
762    let mut e = TextEdit::new("one\ntwo\nthree");
763    e.up();
764    e.line_home();
765    assert_eq!(e.split().0, "one\n");
766    e.line_end();
767    assert_eq!(e.split().0, "one\ntwo");
768  }
769
770  /// An undo step is a whole copy of the buffer, so a big document must not
771  /// be able to turn a bounded step count into unbounded memory.
772  #[test]
773  fn undo_history_is_bounded_by_the_text_it_retains() {
774    let big = "x".repeat(64 * 1024);
775    let mut edit = TextEdit::new(&big);
776    // Each edit is its own step (the deletes break the coalescing run).
777    for i in 0..64 {
778      edit.insert(&format!("{i} "));
779      edit.backspace();
780    }
781    assert!(
782      edit.history_chars() <= UNDO_CHARS + big.chars().count(),
783      "history held {} chars",
784      edit.history_chars()
785    );
786    // The recent past still works even though the distant past was dropped.
787    assert!(edit.undo());
788    assert!(edit.undo());
789  }
790
791  #[test]
792  fn undo_accounting_survives_a_round_trip() {
793    let mut edit = TextEdit::new("");
794    edit.insert("alpha ");
795    edit.insert("beta");
796    let before = edit.history_chars();
797    assert!(edit.undo());
798    assert!(edit.redo());
799    assert_eq!(edit.history_chars(), before);
800    assert_eq!(edit.text(), "alpha beta");
801    // A fresh edit discards the redo stack and its accounting with it.
802    edit.insert("!");
803    assert!(edit.history_chars() > 0);
804  }
805
806  #[test]
807  fn set_text_releases_the_history() {
808    let mut edit = TextEdit::new(&"y".repeat(4096));
809    edit.insert("a");
810    edit.backspace();
811    assert!(edit.history_chars() > 0);
812    edit.set_text("small");
813    assert_eq!(edit.history_chars(), 0);
814    assert!(!edit.undo());
815  }
816
817  #[test]
818  fn chars_matches_text_without_allocating() {
819    let edit = TextEdit::new("caf\u{e9} \u{1f600}");
820    assert_eq!(edit.chars().iter().collect::<String>(), edit.text());
821    assert_eq!(edit.chars().len(), edit.len());
822  }
823}