1#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
19pub enum RangeError {
20 #[error("invalid text range: start {start} > end {end}")]
22 StartAfterEnd {
23 start: usize,
25 end: usize,
27 },
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
32pub enum EditError {
33 #[error(transparent)]
35 Range(#[from] RangeError),
36 #[error("range end {end} is out of bounds for source of length {len}")]
38 OutOfBounds {
39 end: usize,
41 len: usize,
43 },
44 #[error("byte offset {offset} is not on a UTF-8 character boundary")]
46 NotCharBoundary {
47 offset: usize,
49 },
50 #[error("stale edit: expected {expected:?} at range, found {found:?}")]
54 StaleEdit {
55 expected: String,
57 found: String,
59 },
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct TextRange {
65 pub start: usize,
67 pub end: usize,
69}
70
71impl TextRange {
72 pub fn try_new(start: usize, end: usize) -> Result<Self, RangeError> {
77 if start > end {
78 return Err(RangeError::StartAfterEnd { start, end });
79 }
80 Ok(Self { start, end })
81 }
82
83 #[deprecated(note = "panics on invalid input (start > end); use try_new")]
85 pub fn new(start: usize, end: usize) -> Self {
86 Self::try_new(start, end).expect("TextRange::new called with start > end; use try_new")
87 }
88
89 pub fn len(&self) -> usize {
91 self.end.saturating_sub(self.start)
92 }
93
94 pub fn is_empty(&self) -> bool {
96 self.len() == 0
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct DocumentChange {
110 pub range: TextRange,
112 pub new_text: String,
114 pub old_text: Option<String>,
117}
118
119impl DocumentChange {
120 pub fn replace(range: TextRange, new_text: impl Into<String>) -> Self {
124 Self {
125 range,
126 new_text: new_text.into(),
127 old_text: None,
128 }
129 }
130
131 pub fn replace_expecting(
136 range: TextRange,
137 new_text: impl Into<String>,
138 old_text: impl Into<String>,
139 ) -> Self {
140 Self {
141 range,
142 new_text: new_text.into(),
143 old_text: Some(old_text.into()),
144 }
145 }
146
147 #[deprecated(note = "trusts caller-authored old_text (unverified undo evidence); \
149 use replace/replace_expecting + try_apply")]
150 pub fn new(range: TextRange, new_text: impl Into<String>, old_text: impl Into<String>) -> Self {
151 Self::replace_expecting(range, new_text, old_text)
152 }
153
154 pub fn try_apply(&self, source: &str) -> Result<AppliedChange, EditError> {
180 let TextRange { start, end } = self.range;
181 TextRange::try_new(start, end)?;
184 if end > source.len() {
185 return Err(EditError::OutOfBounds {
186 end,
187 len: source.len(),
188 });
189 }
190 for offset in [start, end] {
191 if !source.is_char_boundary(offset) {
192 return Err(EditError::NotCharBoundary { offset });
193 }
194 }
195
196 let removed = &source[start..end];
197 if let Some(expected) = &self.old_text {
198 if expected != removed {
199 return Err(EditError::StaleEdit {
200 expected: expected.clone(),
201 found: removed.to_string(),
202 });
203 }
204 }
205
206 let mut new_text =
207 String::with_capacity(source.len() - self.range.len() + self.new_text.len());
208 new_text.push_str(&source[..start]);
209 new_text.push_str(&self.new_text);
210 new_text.push_str(&source[end..]);
211
212 let inverse = Self {
213 range: TextRange {
214 start,
215 end: start + self.new_text.len(),
216 },
217 new_text: removed.to_string(),
218 old_text: Some(self.new_text.clone()),
219 };
220
221 Ok(AppliedChange { new_text, inverse })
222 }
223
224 #[deprecated(note = "panics on invalid input; use try_apply")]
230 pub fn apply(&self, source: &str) -> String {
231 self.try_apply(source)
232 .expect("DocumentChange::apply called with an invalid change; use try_apply")
233 .new_text
234 }
235
236 fn inverse_unverified(&self) -> Self {
244 Self {
245 range: TextRange {
246 start: self.range.start,
247 end: self.range.start + self.new_text.len(),
248 },
249 new_text: self.old_text.clone().unwrap_or_default(),
250 old_text: Some(self.new_text.clone()),
251 }
252 }
253
254 #[deprecated(note = "built from caller-authored old_text, which may not match the \
259 document; use the derived AppliedChange::inverse from try_apply")]
260 pub fn inverse(&self) -> Self {
261 self.inverse_unverified()
262 }
263}
264
265#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct AppliedChange {
269 pub new_text: String,
271 pub inverse: DocumentChange,
275}
276
277#[derive(Debug, Default)]
286pub struct DocumentHistory {
287 past: Vec<DocumentChange>,
288 future: Vec<DocumentChange>,
289}
290
291impl DocumentHistory {
292 pub fn new() -> Self {
294 Self::default()
295 }
296
297 pub fn push_applied(&mut self, applied: &AppliedChange) {
303 self.push(applied.inverse.inverse_unverified());
307 }
308
309 pub fn push(&mut self, change: DocumentChange) {
315 self.past.push(change);
316 self.future.clear();
317 }
318
319 pub fn undo(&mut self) -> Option<&DocumentChange> {
324 let change = self.past.pop()?;
325 self.future.push(change.inverse_unverified());
326 self.future.last()
329 }
330
331 pub fn redo(&mut self) -> Option<&DocumentChange> {
334 let change = self.future.pop()?;
335 self.past.push(change.inverse_unverified());
338 self.past.last()
339 }
340
341 pub fn can_undo(&self) -> bool {
343 !self.past.is_empty()
344 }
345
346 pub fn can_redo(&self) -> bool {
348 !self.future.is_empty()
349 }
350}
351
352#[cfg(test)]
357mod tests {
358 use super::*;
359
360 fn range(start: usize, end: usize) -> TextRange {
361 TextRange::try_new(start, end).expect("test range must be well-formed")
362 }
363
364 #[test]
367 fn test_try_new_accepts_ordered_and_empty_ranges() {
368 assert_eq!(TextRange::try_new(2, 7), Ok(TextRange { start: 2, end: 7 }));
369 assert_eq!(TextRange::try_new(3, 3), Ok(TextRange { start: 3, end: 3 }));
370 assert_eq!(TextRange::try_new(0, 0), Ok(TextRange { start: 0, end: 0 }));
371 }
372
373 #[test]
374 fn test_try_new_rejects_reversed_range() {
375 assert_eq!(
376 TextRange::try_new(7, 2),
377 Err(RangeError::StartAfterEnd { start: 7, end: 2 })
378 );
379 }
380
381 #[test]
382 fn test_range_error_display() {
383 let err = RangeError::StartAfterEnd { start: 7, end: 2 };
384 assert_eq!(err.to_string(), "invalid text range: start 7 > end 2");
385 }
386
387 #[test]
388 fn test_text_range_len() {
389 assert_eq!(range(2, 7).len(), 5);
390 assert_eq!(range(3, 3).len(), 0);
391 }
392
393 #[test]
394 fn test_text_range_is_empty() {
395 assert!(range(5, 5).is_empty());
396 assert!(!range(5, 6).is_empty());
397 }
398
399 #[test]
402 fn test_try_apply_replaces_range() {
403 let change = DocumentChange::replace_expecting(range(7, 12), "Rust", "world");
404 let applied = change.try_apply("Hello, world!").expect("valid edit");
405 assert_eq!(applied.new_text, "Hello, Rust!");
406 }
407
408 #[test]
409 fn test_try_apply_empty_insertion_at_start() {
410 let change = DocumentChange::replace(range(0, 0), "Hello ");
411 let applied = change.try_apply("world").expect("valid edit");
412 assert_eq!(applied.new_text, "Hello world");
413 }
414
415 #[test]
416 fn test_try_apply_empty_insertion_at_end() {
417 let source = "Hello";
418 let change = DocumentChange::replace(range(source.len(), source.len()), "!");
419 let applied = change.try_apply(source).expect("valid edit");
420 assert_eq!(applied.new_text, "Hello!");
421 }
422
423 #[test]
424 fn test_try_apply_empty_insertion_into_empty_source() {
425 let change = DocumentChange::replace(range(0, 0), "seed");
426 let applied = change.try_apply("").expect("valid edit");
427 assert_eq!(applied.new_text, "seed");
428 }
429
430 #[test]
431 fn test_try_apply_delete_whole_source() {
432 let source = "Hello, world!";
433 let change = DocumentChange::replace(range(0, source.len()), "");
434 let applied = change.try_apply(source).expect("valid edit");
435 assert_eq!(applied.new_text, "");
436 assert_eq!(applied.inverse.new_text, source);
438 }
439
440 #[test]
441 fn test_try_apply_no_op_change() {
442 let change = DocumentChange::replace(range(3, 3), "");
443 let applied = change.try_apply("abcdef").expect("valid edit");
444 assert_eq!(applied.new_text, "abcdef");
445 }
446
447 #[test]
448 fn test_try_apply_multibyte_content_replacement() {
449 let source = "héllo 🦀";
451 let change = DocumentChange::replace(range(7, 11), "🐍");
452 let applied = change.try_apply(source).expect("valid edit");
453 assert_eq!(applied.new_text, "héllo 🐍");
454 assert_eq!(applied.inverse.new_text, "🦀");
455 }
456
457 #[test]
460 fn test_try_apply_rejects_reversed_range() {
461 let change = DocumentChange {
463 range: TextRange { start: 5, end: 2 },
464 new_text: "x".to_string(),
465 old_text: None,
466 };
467 assert_eq!(
468 change.try_apply("Hello, world!"),
469 Err(EditError::Range(RangeError::StartAfterEnd {
470 start: 5,
471 end: 2
472 }))
473 );
474 }
475
476 #[test]
477 fn test_try_apply_rejects_out_of_bounds_end() {
478 let change = DocumentChange::replace(range(0, 99), "x");
479 assert_eq!(
480 change.try_apply("short"),
481 Err(EditError::OutOfBounds { end: 99, len: 5 })
482 );
483 }
484
485 #[test]
486 fn test_try_apply_rejects_out_of_bounds_empty_insertion() {
487 let change = DocumentChange::replace(range(6, 6), "x");
488 assert_eq!(
489 change.try_apply("short"),
490 Err(EditError::OutOfBounds { end: 6, len: 5 })
491 );
492 }
493
494 #[test]
495 fn test_try_apply_rejects_split_two_byte_char() {
496 let source = "café";
498 assert_eq!(source.len(), 5);
499 let change = DocumentChange::replace(range(0, 4), "");
500 assert_eq!(
501 change.try_apply(source),
502 Err(EditError::NotCharBoundary { offset: 4 })
503 );
504 }
505
506 #[test]
507 fn test_try_apply_rejects_split_emoji() {
508 let change = DocumentChange::replace(range(0, 2), "");
510 assert_eq!(
511 change.try_apply("🦀"),
512 Err(EditError::NotCharBoundary { offset: 2 })
513 );
514 }
515
516 #[test]
517 fn test_try_apply_rejects_split_start_offset() {
518 let change = DocumentChange::replace(range(1, 4), "");
519 assert_eq!(
520 change.try_apply("🦀!"),
521 Err(EditError::NotCharBoundary { offset: 1 })
522 );
523 }
524
525 #[test]
526 fn test_try_apply_rejects_split_combining_mark() {
527 let source = "e\u{0301}";
530 let change = DocumentChange::replace(range(0, 2), "");
531 assert_eq!(
532 change.try_apply(source),
533 Err(EditError::NotCharBoundary { offset: 2 })
534 );
535 }
536
537 #[test]
538 fn test_try_apply_allows_char_boundary_between_base_and_combining_mark() {
539 let source = "e\u{0301}";
543 let change = DocumentChange::replace(range(1, 1), "x");
544 let applied = change.try_apply(source).expect("char boundary is valid");
545 assert_eq!(applied.new_text, "ex\u{0301}");
546 }
547
548 #[test]
549 fn test_try_apply_rejects_stale_old_text() {
550 let change = DocumentChange::replace_expecting(range(7, 12), "Rust", "world");
551 assert_eq!(
553 change.try_apply("Hello, there!"),
554 Err(EditError::StaleEdit {
555 expected: "world".to_string(),
556 found: "there".to_string(),
557 })
558 );
559 }
560
561 #[test]
562 fn test_try_apply_without_old_text_skips_stale_check() {
563 let change = DocumentChange::replace(range(7, 12), "Rust");
564 let applied = change.try_apply("Hello, there!").expect("no precondition");
565 assert_eq!(applied.new_text, "Hello, Rust!");
566 assert_eq!(applied.inverse.new_text, "there");
568 }
569
570 #[test]
571 fn test_edit_error_display() {
572 let stale = EditError::StaleEdit {
573 expected: "a".to_string(),
574 found: "b".to_string(),
575 };
576 assert_eq!(
577 stale.to_string(),
578 "stale edit: expected \"a\" at range, found \"b\""
579 );
580 let oob = EditError::OutOfBounds { end: 9, len: 3 };
581 assert_eq!(
582 oob.to_string(),
583 "range end 9 is out of bounds for source of length 3"
584 );
585 let boundary = EditError::NotCharBoundary { offset: 2 };
586 assert_eq!(
587 boundary.to_string(),
588 "byte offset 2 is not on a UTF-8 character boundary"
589 );
590 let range_err = EditError::Range(RangeError::StartAfterEnd { start: 3, end: 1 });
592 assert_eq!(range_err.to_string(), "invalid text range: start 3 > end 1");
593 }
594
595 #[test]
598 fn test_apply_then_inverse_restores_original() {
599 let source = "Hello, world!";
600 let change = DocumentChange::replace(range(7, 12), "Rust");
601 let applied = change.try_apply(source).expect("valid edit");
602 assert_eq!(applied.new_text, "Hello, Rust!");
603
604 let restored = applied
605 .inverse
606 .try_apply(&applied.new_text)
607 .expect("derived inverse is valid");
608 assert_eq!(restored.new_text, source);
609 }
610
611 #[test]
612 fn test_inverse_of_insertion_deletes() {
613 let source = "ab";
614 let change = DocumentChange::replace(range(1, 1), "XYZ");
615 let applied = change.try_apply(source).expect("valid edit");
616 assert_eq!(applied.new_text, "aXYZb");
617
618 let restored = applied
619 .inverse
620 .try_apply(&applied.new_text)
621 .expect("derived inverse is valid");
622 assert_eq!(restored.new_text, source);
623 }
624
625 #[test]
626 fn test_inverse_of_deletion_reinserts_exact_bytes() {
627 let source = "héllo 🦀 wörld";
628 let change = DocumentChange::replace(range(6, 11), "");
629 let applied = change.try_apply(source).expect("valid edit");
630 assert_eq!(applied.new_text, "héllo wörld");
631
632 let restored = applied
633 .inverse
634 .try_apply(&applied.new_text)
635 .expect("derived inverse is valid");
636 assert_eq!(restored.new_text, source);
637 assert_eq!(restored.new_text.as_bytes(), source.as_bytes());
639 }
640
641 #[test]
642 fn test_inverse_round_trip_multibyte_replacement() {
643 let source = "🦀🐍🦀";
644 let change = DocumentChange::replace(range(4, 8), "e\u{0301}");
645 let applied = change.try_apply(source).expect("valid edit");
646 assert_eq!(applied.new_text, "🦀e\u{0301}🦀");
647
648 let restored = applied
649 .inverse
650 .try_apply(&applied.new_text)
651 .expect("derived inverse is valid");
652 assert_eq!(restored.new_text.as_bytes(), source.as_bytes());
653 }
654
655 #[test]
656 fn test_derived_inverse_carries_verified_evidence() {
657 let change = DocumentChange::replace(range(0, 5), "Howdy");
658 let applied = change.try_apply("Hello, world!").expect("valid edit");
659 assert_eq!(applied.inverse.old_text.as_deref(), Some("Howdy"));
661 assert_eq!(applied.inverse.new_text, "Hello");
663 assert_eq!(applied.inverse.range, TextRange { start: 0, end: 5 });
664 }
665
666 #[test]
669 fn test_history_can_undo_after_push_applied() {
670 let mut h = DocumentHistory::new();
671 assert!(!h.can_undo());
672 let applied = DocumentChange::replace(range(0, 0), "x")
673 .try_apply("")
674 .expect("valid edit");
675 h.push_applied(&applied);
676 assert!(h.can_undo());
677 }
678
679 #[test]
680 fn test_history_undo_returns_verified_inverse() {
681 let source = "Hello, world!";
682 let applied = DocumentChange::replace(range(7, 12), "Rust")
683 .try_apply(source)
684 .expect("valid edit");
685
686 let mut h = DocumentHistory::new();
687 h.push_applied(&applied);
688
689 let undo_change = h.undo().expect("should have undo");
690 assert_eq!(undo_change.new_text, "world");
692 assert_eq!(undo_change.old_text.as_deref(), Some("Rust"));
693 let restored = undo_change
694 .try_apply(&applied.new_text)
695 .expect("undo is valid against the edited document");
696 assert_eq!(restored.new_text, source);
697 }
698
699 #[test]
700 fn test_history_undo_enables_redo() {
701 let applied = DocumentChange::replace(range(0, 1), "b")
702 .try_apply("a")
703 .expect("valid edit");
704 let mut h = DocumentHistory::new();
705 h.push_applied(&applied);
706 assert!(!h.can_redo());
707 h.undo();
708 assert!(h.can_redo());
709 assert!(!h.can_undo());
710 }
711
712 #[test]
713 fn test_history_redo_re_applies() {
714 let source = "a";
715 let applied = DocumentChange::replace(range(0, 1), "b")
716 .try_apply(source)
717 .expect("valid edit"); let mut h = DocumentHistory::new();
720 h.push_applied(&applied);
721
722 let undo = h.undo().expect("undo").clone();
723 let after_undo = undo.try_apply(&applied.new_text).expect("valid undo");
724 assert_eq!(after_undo.new_text, "a");
725
726 let redo = h.redo().expect("redo").clone();
727 let after_redo = redo.try_apply(&after_undo.new_text).expect("valid redo");
728 assert_eq!(after_redo.new_text, "b");
729 }
730
731 #[test]
732 fn test_push_clears_redo_stack() {
733 let applied = DocumentChange::replace(range(0, 1), "b")
734 .try_apply("a")
735 .expect("valid edit");
736 let mut h = DocumentHistory::new();
737 h.push_applied(&applied);
738 h.undo();
739 assert!(h.can_redo());
740
741 let applied2 = DocumentChange::replace(range(0, 1), "c")
743 .try_apply("a")
744 .expect("valid edit");
745 h.push_applied(&applied2);
746 assert!(!h.can_redo());
747 }
748
749 #[test]
750 fn test_multi_step_undo_redo() {
751 let mut source = String::from("a");
752 let mut h = DocumentHistory::new();
753
754 let a1 = DocumentChange::replace(range(1, 1), "b")
755 .try_apply(&source)
756 .expect("valid edit");
757 h.push_applied(&a1);
758 source = a1.new_text; let a2 = DocumentChange::replace(range(2, 2), "c")
761 .try_apply(&source)
762 .expect("valid edit");
763 h.push_applied(&a2);
764 source = a2.new_text; let u2 = h.undo().expect("undo c2").clone();
768 source = u2.try_apply(&source).expect("valid undo").new_text;
769 assert_eq!(source, "ab");
770
771 let u1 = h.undo().expect("undo c1").clone();
773 source = u1.try_apply(&source).expect("valid undo").new_text;
774 assert_eq!(source, "a");
775
776 let r1 = h.redo().expect("redo c1").clone();
778 source = r1.try_apply(&source).expect("valid redo").new_text;
779 assert_eq!(source, "ab");
780
781 let r2 = h.redo().expect("redo c2").clone();
783 source = r2.try_apply(&source).expect("valid redo").new_text;
784 assert_eq!(source, "abc");
785 }
786
787 mod deprecated_compat {
792 #![allow(deprecated)]
793
794 use super::super::*;
795
796 #[test]
797 fn test_deprecated_apply_still_works_on_valid_input() {
798 let change = DocumentChange::new(TextRange::new(7, 12), "Rust", "world");
799 assert_eq!(change.apply("Hello, world!"), "Hello, Rust!");
800 }
801
802 #[test]
803 #[should_panic(expected = "invalid change")]
804 fn test_deprecated_apply_still_panics_on_out_of_bounds() {
805 let change = DocumentChange::new(TextRange::new(0, 99), "x", "y");
806 change.apply("short");
807 }
808
809 #[test]
810 #[should_panic(expected = "start > end")]
811 fn test_deprecated_text_range_new_panics_on_reversed_range() {
812 TextRange::new(7, 2);
813 }
814
815 #[test]
816 fn test_deprecated_inverse_round_trips_when_old_text_accurate() {
817 let source = "Hello, world!";
818 let change = DocumentChange::new(TextRange::new(7, 12), "Rust", "world");
819 let modified = change.apply(source);
820 let restored = change.inverse().apply(&modified);
821 assert_eq!(restored, source);
822 }
823
824 #[test]
825 fn test_deprecated_new_populates_old_text_precondition() {
826 let change = DocumentChange::new(TextRange::new(0, 5), "new", "old_t");
827 assert_eq!(change.old_text.as_deref(), Some("old_t"));
828 }
829 }
830}