1const UNDO_DEPTH: usize = 128;
7
8const UNDO_CHARS: usize = 256 * 1024;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21enum EditKind {
22 Insert,
23 Delete,
24}
25
26#[derive(Debug, Clone)]
28struct Snapshot {
29 chars: Vec<char>,
30 cursor: usize,
31 anchor: Option<usize>,
32}
33
34#[derive(Debug, Clone, Default)]
36pub struct TextEdit {
37 chars: Vec<char>,
38 cursor: usize,
40 anchor: Option<usize>,
43 undos: Vec<Snapshot>,
44 redos: Vec<Snapshot>,
45 run: Option<EditKind>,
48}
49
50impl TextEdit {
51 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 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 pub fn len(&self) -> usize {
83 self.chars.len()
84 }
85
86 pub fn cursor(&self) -> usize {
88 self.cursor
89 }
90
91 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 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 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 pub fn set_text(&mut self, text: &str) {
121 *self = TextEdit::new(text);
122 }
123
124 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 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 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 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 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 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 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 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 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 pub fn break_undo(&mut self) {
234 self.run = None;
235 }
236
237 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 pub fn selected_text(&self) -> Option<String> {
250 let (s, e) = self.selection()?;
251 Some(self.chars[s..e].iter().collect())
252 }
253
254 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 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 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 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 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 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 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 let at = self.cursor;
343 self.chars.splice(at..at, s.chars());
344 self.cursor += s.chars().count();
345 if s.chars().any(|c| c.is_whitespace()) {
348 self.run = None;
349 }
350 }
351
352 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 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 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 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 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 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 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 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 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 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 pub fn up(&mut self) {
533 self.vmove(-1);
534 }
535
536 pub fn down(&mut self) {
538 self.vmove(1);
539 }
540
541 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 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
586fn 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 let mut e = TextEdit::new("hello\nhi");
637 e.up();
639 assert_eq!(e.split().0, "he");
641 e.down();
642 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(); e.up(); 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 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(); e.pre_move(true);
694 e.left(); 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 #[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 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 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 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}