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
225 .undos
226 .iter()
227 .chain(&self.redos)
228 .map(|snapshot| snapshot.chars.len())
229 .sum()
230 }
231
232 pub fn break_undo(&mut self) {
235 self.run = None;
236 }
237
238 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 pub fn selected_text(&self) -> Option<String> {
251 let (s, e) = self.selection()?;
252 Some(self.chars[s..e].iter().collect())
253 }
254
255 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 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 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 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 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 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 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 let at = self.cursor;
344 self.chars.splice(at..at, s.chars());
345 self.cursor += s.chars().count();
346 if s.chars().any(|c| c.is_whitespace()) {
349 self.run = None;
350 }
351 }
352
353 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 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 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 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 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 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 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 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 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 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 pub fn up(&mut self) {
534 self.vmove(-1);
535 }
536
537 pub fn down(&mut self) {
539 self.vmove(1);
540 }
541
542 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 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
587fn 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 let mut e = TextEdit::new("hello\nhi");
638 e.up();
640 assert_eq!(e.split().0, "he");
642 e.down();
643 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(); e.up(); 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 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(); e.pre_move(true);
695 e.left(); 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 #[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 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 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 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}