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.undos
225            .iter()
226            .chain(&self.redos)
227            .map(|snapshot| snapshot.chars.len())
228            .sum()
229    }
230
231    /// End the current coalescing run, so the next edit starts a fresh undo
232    /// step. Callers use this at word boundaries and on paste.
233    pub fn break_undo(&mut self) {
234        self.run = None;
235    }
236
237    /// The selected span `(start, end)` as char indices, or `None` if the
238    /// selection is empty/collapsed.
239    pub fn selection(&self) -> Option<(usize, usize)> {
240        let a = self.anchor?;
241        (a != self.cursor).then(|| (a.min(self.cursor), a.max(self.cursor)))
242    }
243
244    pub fn has_selection(&self) -> bool {
245        self.selection().is_some()
246    }
247
248    /// The selected text, or `None` when nothing is selected.
249    pub fn selected_text(&self) -> Option<String> {
250        let (s, e) = self.selection()?;
251        Some(self.chars[s..e].iter().collect())
252    }
253
254    /// Select the whole line.
255    pub fn select_all(&mut self) {
256        if self.chars.is_empty() {
257            self.anchor = None;
258            return;
259        }
260        self.anchor = Some(0);
261        self.cursor = self.chars.len();
262    }
263
264    /// Drop any selection, keeping the cursor put.
265    pub fn clear_selection(&mut self) {
266        self.anchor = None;
267    }
268
269    pub fn collapse_selection_start(&mut self) -> bool {
270        let Some((start, _)) = self.selection() else {
271            return false;
272        };
273        self.cursor = start;
274        self.anchor = None;
275        true
276    }
277
278    pub fn collapse_selection_end(&mut self) -> bool {
279        let Some((_, end)) = self.selection() else {
280            return false;
281        };
282        self.cursor = end;
283        self.anchor = None;
284        true
285    }
286
287    /// Delete the selected text (if any), leaving the cursor at its start.
288    /// Returns whether anything was removed.
289    pub fn delete_selection(&mut self) -> bool {
290        if self.selection().is_none() {
291            return false;
292        }
293        self.record(EditKind::Delete);
294        self.take_selection()
295    }
296
297    /// [`delete_selection`](Self::delete_selection) without touching the undo
298    /// history, for the mutators that have already recorded their own step.
299    fn take_selection(&mut self) -> bool {
300        let Some((s, e)) = self.selection() else {
301            return false;
302        };
303        self.chars.drain(s..e);
304        self.cursor = s;
305        self.anchor = None;
306        true
307    }
308
309    /// Prepare for a cursor move: with `extend` (Shift held) anchor a selection
310    /// at the current cursor if one isn't already open; otherwise drop it.
311    pub fn pre_move(&mut self, extend: bool) {
312        if extend {
313            if self.anchor.is_none() {
314                self.anchor = Some(self.cursor);
315            }
316        } else {
317            self.anchor = None;
318        }
319    }
320
321    /// The text split around the selection: `(before, selected, after)`, or
322    /// `None` when nothing is selected.
323    pub fn split_selection(&self) -> Option<(String, String, String)> {
324        let (s, e) = self.selection()?;
325        Some((
326            self.chars[..s].iter().collect(),
327            self.chars[s..e].iter().collect(),
328            self.chars[e..].iter().collect(),
329        ))
330    }
331
332    /// Insert `s` at the cursor, replacing any selection, advancing past it.
333    pub fn insert(&mut self, s: &str) {
334        if s.is_empty() && !self.has_selection() {
335            return;
336        }
337        self.record(EditKind::Insert);
338        self.take_selection();
339        // `splice` shifts the tail once. Inserting char by char shifts it per
340        // character, which turns a large paste near the start of a long buffer
341        // into quadratic work on the UI thread.
342        let at = self.cursor;
343        self.chars.splice(at..at, s.chars());
344        self.cursor += s.chars().count();
345        // Typing a word is one undo step; the space after it ends that step so
346        // undo walks back word by word rather than wiping the whole line.
347        if s.chars().any(|c| c.is_whitespace()) {
348            self.run = None;
349        }
350    }
351
352    /// Replace the char range `range` with `s`, leaving the cursor after it.
353    /// This is the entry point the platform's text-input handler drives, so it
354    /// takes an explicit range rather than using the selection.
355    pub fn replace_range(&mut self, range: std::ops::Range<usize>, s: &str) {
356        let n = self.chars.len();
357        let start = range.start.min(n);
358        let end = range.end.clamp(start, n);
359        self.record(EditKind::Insert);
360        self.chars.splice(start..end, s.chars());
361        self.cursor = start + s.chars().count();
362        self.anchor = None;
363        if s.chars().any(|c| c.is_whitespace()) {
364            self.run = None;
365        }
366    }
367
368    /// Delete the selection, or the char before the cursor. Returns whether
369    /// anything changed.
370    pub fn backspace(&mut self) -> bool {
371        if self.has_selection() {
372            return self.delete_selection();
373        }
374        if self.cursor == 0 {
375            return false;
376        }
377        self.record(EditKind::Delete);
378        self.cursor -= 1;
379        self.chars.remove(self.cursor);
380        true
381    }
382
383    /// Delete the selection, or the char at the cursor. Returns whether anything
384    /// changed.
385    pub fn delete(&mut self) -> bool {
386        if self.has_selection() {
387            return self.delete_selection();
388        }
389        if self.cursor >= self.chars.len() {
390            return false;
391        }
392        self.record(EditKind::Delete);
393        self.chars.remove(self.cursor);
394        true
395    }
396
397    pub fn left(&mut self) {
398        self.cursor = self.cursor.saturating_sub(1);
399    }
400
401    pub fn right(&mut self) {
402        if self.cursor < self.chars.len() {
403            self.cursor += 1;
404        }
405    }
406
407    pub fn home(&mut self) {
408        self.cursor = 0;
409    }
410
411    pub fn end(&mut self) {
412        self.cursor = self.chars.len();
413    }
414
415    pub fn line_home(&mut self) {
416        while self.cursor > 0 && self.chars[self.cursor - 1] != '\n' {
417            self.cursor -= 1;
418        }
419    }
420
421    pub fn line_end(&mut self) {
422        while self.cursor < self.chars.len() && self.chars[self.cursor] != '\n' {
423            self.cursor += 1;
424        }
425    }
426
427    /// Move left to the start of the previous word (Option+Left on macOS).
428    pub fn word_left(&mut self) {
429        while self.cursor > 0 && !is_word(self.chars[self.cursor - 1]) {
430            self.cursor -= 1;
431        }
432        while self.cursor > 0 && is_word(self.chars[self.cursor - 1]) {
433            self.cursor -= 1;
434        }
435    }
436
437    /// Move right past the end of the next word (Option+Right on macOS).
438    pub fn word_right(&mut self) {
439        let n = self.chars.len();
440        while self.cursor < n && !is_word(self.chars[self.cursor]) {
441            self.cursor += 1;
442        }
443        while self.cursor < n && is_word(self.chars[self.cursor]) {
444            self.cursor += 1;
445        }
446    }
447
448    /// Delete the word before the cursor (Option+Backspace). Returns whether
449    /// anything changed.
450    pub fn delete_word_back(&mut self) -> bool {
451        if self.has_selection() {
452            return self.delete_selection();
453        }
454        let end = self.cursor;
455        self.word_left();
456        if self.cursor < end {
457            self.record(EditKind::Delete);
458            self.chars.drain(self.cursor..end);
459            self.run = None;
460            true
461        } else {
462            false
463        }
464    }
465
466    /// Delete the word after the cursor (Option+Delete). Returns whether
467    /// anything changed.
468    pub fn delete_word_forward(&mut self) -> bool {
469        if self.has_selection() {
470            return self.delete_selection();
471        }
472        let start = self.cursor;
473        let n = self.chars.len();
474        let mut end = self.cursor;
475        while end < n && !is_word(self.chars[end]) {
476            end += 1;
477        }
478        while end < n && is_word(self.chars[end]) {
479            end += 1;
480        }
481        if end > start {
482            self.record(EditKind::Delete);
483            self.chars.drain(start..end);
484            self.run = None;
485            true
486        } else {
487            false
488        }
489    }
490
491    /// Delete from the cursor to the line start (Cmd+Backspace). Returns
492    /// whether anything changed.
493    pub fn delete_to_start(&mut self) -> bool {
494        if self.has_selection() {
495            return self.delete_selection();
496        }
497        if self.cursor == 0 {
498            return false;
499        }
500        self.record(EditKind::Delete);
501        self.chars.drain(0..self.cursor);
502        self.cursor = 0;
503        self.run = None;
504        true
505    }
506
507    /// Delete from the cursor to the line end (Cmd+Delete / Ctrl+K). Returns
508    /// whether anything changed.
509    pub fn delete_to_end(&mut self) -> bool {
510        if self.has_selection() {
511            return self.delete_selection();
512        }
513        if self.cursor >= self.chars.len() {
514            return false;
515        }
516        self.record(EditKind::Delete);
517        self.chars.truncate(self.cursor);
518        self.run = None;
519        true
520    }
521
522    /// The text before and after the cursor, for rendering a caret between.
523    pub fn split(&self) -> (String, String) {
524        (
525            self.chars[..self.cursor].iter().collect(),
526            self.chars[self.cursor..].iter().collect(),
527        )
528    }
529
530    /// Move the cursor up one line, keeping the column where possible. Multiline
531    /// only (single-line text has nowhere to go).
532    pub fn up(&mut self) {
533        self.vmove(-1);
534    }
535
536    /// Move the cursor down one line, keeping the column where possible.
537    pub fn down(&mut self) {
538        self.vmove(1);
539    }
540
541    /// (line, column) of the cursor, counting `\n`-separated lines.
542    fn line_col(&self) -> (usize, usize) {
543        let mut line = 0;
544        let mut col = 0;
545        for &c in &self.chars[..self.cursor] {
546            if c == '\n' {
547                line += 1;
548                col = 0;
549            } else {
550                col += 1;
551            }
552        }
553        (line, col)
554    }
555
556    /// (start char index, length excluding newline) for each line.
557    fn line_bounds(&self) -> Vec<(usize, usize)> {
558        let mut out = Vec::new();
559        let mut start = 0;
560        let mut len = 0;
561        for (i, &c) in self.chars.iter().enumerate() {
562            if c == '\n' {
563                out.push((start, len));
564                start = i + 1;
565                len = 0;
566            } else {
567                len += 1;
568            }
569        }
570        out.push((start, len));
571        out
572    }
573
574    fn vmove(&mut self, dir: isize) {
575        let (line, col) = self.line_col();
576        let bounds = self.line_bounds();
577        let target = line as isize + dir;
578        if target < 0 || target as usize >= bounds.len() {
579            return;
580        }
581        let (start, len) = bounds[target as usize];
582        self.cursor = start + col.min(len);
583    }
584}
585
586/// Word characters for word-wise navigation/deletion.
587fn is_word(c: char) -> bool {
588    c.is_alphanumeric() || c == '_'
589}
590
591#[cfg(test)]
592mod tests {
593    use super::*;
594
595    #[test]
596    fn new_places_cursor_at_end() {
597        let e = TextEdit::new("abc");
598        assert_eq!(e.split(), ("abc".into(), "".into()));
599    }
600
601    #[test]
602    fn insert_at_cursor() {
603        let mut e = TextEdit::new("ac");
604        e.left();
605        e.insert("b");
606        assert_eq!(e.text(), "abc");
607        assert_eq!(e.split(), ("ab".into(), "c".into()));
608    }
609
610    #[test]
611    fn backspace_and_delete() {
612        let mut e = TextEdit::new("abc");
613        assert!(e.backspace());
614        assert_eq!(e.text(), "ab");
615        e.home();
616        assert!(e.delete());
617        assert_eq!(e.text(), "b");
618        e.home();
619        assert!(!e.backspace());
620        e.end();
621        assert!(!e.delete());
622    }
623
624    #[test]
625    fn handles_unicode() {
626        let mut e = TextEdit::new("café");
627        assert!(e.backspace());
628        assert_eq!(e.text(), "caf");
629        e.insert("é");
630        assert_eq!(e.text(), "café");
631    }
632
633    #[test]
634    fn vertical_movement_keeps_column() {
635        // Two lines: "hello" / "hi". Cursor starts at end ("hi").
636        let mut e = TextEdit::new("hello\nhi");
637        // Column 2 on line 1.
638        e.up();
639        // Same column (2) on line 0 → between "he" and "llo".
640        assert_eq!(e.split().0, "he");
641        e.down();
642        // Back to line 1; column clamped to its length (2) → end.
643        assert_eq!(e.split(), ("hello\nhi".into(), "".into()));
644    }
645
646    #[test]
647    fn vertical_movement_stops_at_edges() {
648        let mut e = TextEdit::new("a\nb");
649        e.home(); // line 1 has only the final char; home goes to absolute start
650        e.up(); // already on first line, no-op
651        assert_eq!(e.split().0, "");
652    }
653
654    #[test]
655    fn word_navigation() {
656        let mut e = TextEdit::new("foo bar baz");
657        e.word_left();
658        assert_eq!(e.split(), ("foo bar ".into(), "baz".into()));
659        e.word_left();
660        assert_eq!(e.split(), ("foo ".into(), "bar baz".into()));
661        e.word_right();
662        assert_eq!(e.split(), ("foo bar".into(), " baz".into()));
663    }
664
665    #[test]
666    fn delete_word_back_and_forward() {
667        let mut e = TextEdit::new("foo bar baz");
668        assert!(e.delete_word_back());
669        assert_eq!(e.text(), "foo bar ");
670        e.home();
671        assert!(e.delete_word_forward());
672        assert_eq!(e.text(), " bar ");
673        // Nothing before the cursor at home: no-op.
674        e.home();
675        assert!(!e.delete_word_back());
676    }
677
678    #[test]
679    fn select_all_then_type_replaces() {
680        let mut e = TextEdit::new("hello");
681        e.select_all();
682        assert_eq!(e.selected_text().as_deref(), Some("hello"));
683        e.insert("x");
684        assert_eq!(e.text(), "x");
685        assert!(!e.has_selection());
686    }
687
688    #[test]
689    fn shift_arrow_extends_selection() {
690        let mut e = TextEdit::new("abcd");
691        e.pre_move(true);
692        e.left(); // select "d"
693        e.pre_move(true);
694        e.left(); // select "cd"
695        assert_eq!(e.selected_text().as_deref(), Some("cd"));
696        let (before, sel, after) = e.split_selection().unwrap();
697        assert_eq!(
698            (before.as_str(), sel.as_str(), after.as_str()),
699            ("ab", "cd", "")
700        );
701    }
702
703    #[test]
704    fn plain_move_clears_selection() {
705        let mut e = TextEdit::new("abcd");
706        e.select_all();
707        e.pre_move(false);
708        e.left();
709        assert!(!e.has_selection());
710    }
711
712    #[test]
713    fn backspace_deletes_selection() {
714        let mut e = TextEdit::new("abcd");
715        e.select_all();
716        assert!(e.backspace());
717        assert_eq!(e.text(), "");
718    }
719
720    #[test]
721    fn modified_deletes_replace_the_selection() {
722        for delete in [
723            TextEdit::delete_word_back as fn(&mut TextEdit) -> bool,
724            TextEdit::delete_word_forward,
725            TextEdit::delete_to_start,
726            TextEdit::delete_to_end,
727        ] {
728            let mut edit = TextEdit::new("abcd");
729            edit.select_all();
730            assert!(delete(&mut edit));
731            assert_eq!(edit.text(), "");
732        }
733    }
734
735    #[test]
736    fn selection_collapses_to_the_requested_edge() {
737        let mut edit = TextEdit::new("abcd");
738        edit.select_all();
739        assert!(edit.collapse_selection_start());
740        assert_eq!(edit.split().0, "");
741        edit.select_all();
742        assert!(edit.collapse_selection_end());
743        assert_eq!(edit.split().0, "abcd");
744    }
745
746    #[test]
747    fn delete_to_line_edges() {
748        let mut e = TextEdit::new("hello world");
749        e.home();
750        e.right();
751        e.right();
752        assert!(e.delete_to_start());
753        assert_eq!(e.text(), "llo world");
754        assert!(e.delete_to_end());
755        assert_eq!(e.text(), "");
756        assert!(!e.delete_to_end());
757    }
758
759    #[test]
760    fn line_edges_stay_on_the_current_line() {
761        let mut e = TextEdit::new("one\ntwo\nthree");
762        e.up();
763        e.line_home();
764        assert_eq!(e.split().0, "one\n");
765        e.line_end();
766        assert_eq!(e.split().0, "one\ntwo");
767    }
768
769    /// An undo step is a whole copy of the buffer, so a big document must not
770    /// be able to turn a bounded step count into unbounded memory.
771    #[test]
772    fn undo_history_is_bounded_by_the_text_it_retains() {
773        let big = "x".repeat(64 * 1024);
774        let mut edit = TextEdit::new(&big);
775        // Each edit is its own step (the deletes break the coalescing run).
776        for i in 0..64 {
777            edit.insert(&format!("{i} "));
778            edit.backspace();
779        }
780        assert!(
781            edit.history_chars() <= UNDO_CHARS + big.chars().count(),
782            "history held {} chars",
783            edit.history_chars()
784        );
785        // The recent past still works even though the distant past was dropped.
786        assert!(edit.undo());
787        assert!(edit.undo());
788    }
789
790    #[test]
791    fn undo_accounting_survives_a_round_trip() {
792        let mut edit = TextEdit::new("");
793        edit.insert("alpha ");
794        edit.insert("beta");
795        let before = edit.history_chars();
796        assert!(edit.undo());
797        assert!(edit.redo());
798        assert_eq!(edit.history_chars(), before);
799        assert_eq!(edit.text(), "alpha beta");
800        // A fresh edit discards the redo stack and its accounting with it.
801        edit.insert("!");
802        assert!(edit.history_chars() > 0);
803    }
804
805    #[test]
806    fn set_text_releases_the_history() {
807        let mut edit = TextEdit::new(&"y".repeat(4096));
808        edit.insert("a");
809        edit.backspace();
810        assert!(edit.history_chars() > 0);
811        edit.set_text("small");
812        assert_eq!(edit.history_chars(), 0);
813        assert!(!edit.undo());
814    }
815
816    #[test]
817    fn chars_matches_text_without_allocating() {
818        let edit = TextEdit::new("caf\u{e9} \u{1f600}");
819        assert_eq!(edit.chars().iter().collect::<String>(), edit.text());
820        assert_eq!(edit.chars().len(), edit.len());
821    }
822}