1#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
7#[derive(Default)]
8#[non_exhaustive]
9pub enum Safety {
10 #[default]
13 Safe,
14 PotentiallyUnsafe,
17 Unsafe,
20}
21
22#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub struct TextRange {
26 pub start: u32,
27 pub end: u32,
28}
29
30impl TextRange {
31 #[inline(always)]
32 #[must_use]
33 pub fn new(start: u32, end: u32) -> Self {
34 Self { start, end }
35 }
36
37 #[inline(always)]
39 #[must_use]
40 pub fn len(&self) -> u32 {
41 self.end - self.start
42 }
43
44 #[inline(always)]
46 #[must_use]
47 pub fn is_empty(&self) -> bool {
48 self.start == self.end
49 }
50
51 #[inline(always)]
71 #[must_use]
72 #[allow(clippy::suspicious_operation_groupings)]
73 pub fn overlaps(&self, other: &TextRange) -> bool {
74 match (self.is_empty(), other.is_empty()) {
75 (true, true) => false,
76 (true, false) => self.start > other.start && self.start < other.end,
77 (false, true) => other.start > self.start && other.start < self.end,
78 (false, false) => self.start < other.end && other.start < self.end,
79 }
80 }
81
82 #[inline(always)]
84 #[must_use]
85 pub fn contains(&self, offset: u32) -> bool {
86 offset >= self.start && offset < self.end
87 }
88}
89
90impl<T> From<T> for TextRange
91where
92 T: std::ops::RangeBounds<u32>,
93{
94 #[inline(always)]
95 fn from(r: T) -> Self {
96 let start = match r.start_bound() {
97 std::ops::Bound::Included(&s) => s,
98 std::ops::Bound::Excluded(&s) => s + 1,
99 std::ops::Bound::Unbounded => 0,
100 };
101
102 let end = match r.end_bound() {
103 std::ops::Bound::Included(&e) => e + 1,
104 std::ops::Bound::Excluded(&e) => e,
105 std::ops::Bound::Unbounded => u32::MAX, };
107
108 Self::new(start, end)
109 }
110}
111
112#[derive(Debug, Clone, Eq, PartialEq, Hash)]
117#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
118pub struct TextEdit {
119 pub range: TextRange,
121 pub new_text: Vec<u8>,
123 pub safety: Safety,
125}
126
127impl TextEdit {
128 #[inline]
130 #[must_use]
131 pub fn delete(range: impl Into<TextRange>) -> Self {
132 Self { range: range.into(), new_text: Vec::new(), safety: Safety::Safe }
133 }
134
135 #[inline]
137 #[must_use]
138 pub fn insert(offset: u32, text: impl Into<Vec<u8>>) -> Self {
139 Self { range: TextRange::new(offset, offset), new_text: text.into(), safety: Safety::Safe }
140 }
141
142 #[inline]
144 #[must_use]
145 pub fn replace(range: impl Into<TextRange>, text: impl Into<Vec<u8>>) -> Self {
146 Self { range: range.into(), new_text: text.into(), safety: Safety::Safe }
147 }
148
149 #[inline]
159 #[must_use]
160 pub fn with_safety(mut self, safety: Safety) -> Self {
161 self.safety = safety;
162 self
163 }
164}
165
166#[derive(Debug, Clone, Copy, Eq, PartialEq)]
167#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
168#[non_exhaustive]
169pub enum ApplyResult {
170 Applied,
172 OutOfBounds,
174 Overlap,
176 Rejected,
178 Unsafe,
180 PotentiallyUnsafe,
182}
183
184#[derive(Debug, Clone)]
189pub struct TextEditor<'src> {
190 original_text: &'src [u8],
191 original_len: u32,
192 edits: Vec<TextEdit>,
193 safety_threshold: Safety,
194}
195
196impl<'src> TextEditor<'src> {
197 #[inline]
199 #[must_use]
200 pub fn new(text: &'src [u8]) -> Self {
201 Self {
202 original_text: text,
203 original_len: text.len() as u32,
204 edits: Vec::new(),
205 safety_threshold: Safety::Unsafe,
206 }
207 }
208
209 #[inline]
221 #[must_use]
222 pub fn with_safety(text: &'src [u8], threshold: Safety) -> Self {
223 Self { original_text: text, original_len: text.len() as u32, edits: Vec::new(), safety_threshold: threshold }
224 }
225
226 #[inline]
229 fn check_safety(&self, edit_safety: Safety) -> Option<ApplyResult> {
230 if edit_safety > self.safety_threshold {
231 Some(match edit_safety {
232 Safety::Unsafe => ApplyResult::Unsafe,
233 Safety::PotentiallyUnsafe => ApplyResult::PotentiallyUnsafe,
234 Safety::Safe => ApplyResult::Unsafe,
235 })
236 } else {
237 None
238 }
239 }
240
241 #[inline]
246 pub fn apply<F>(&mut self, edit: TextEdit, checker: Option<F>) -> ApplyResult
247 where
248 F: FnOnce(&[u8]) -> bool,
249 {
250 if let Some(rejection) = self.check_safety(edit.safety) {
252 return rejection;
253 }
254
255 if edit.range.end > self.original_len || edit.range.start > edit.range.end {
256 return ApplyResult::OutOfBounds;
257 }
258
259 let search_idx = self.edits.partition_point(|e| e.range.end <= edit.range.start);
260
261 if let Some(existing) = self.edits.get(search_idx)
262 && existing.range.overlaps(&edit.range)
263 {
264 return ApplyResult::Overlap;
265 }
266
267 if let Some(check_fn) = checker {
268 let simulated_str = stitch_one(self.original_text, &self.edits, &edit);
269 if !check_fn(&simulated_str) {
270 return ApplyResult::Rejected;
271 }
272 }
273
274 self.edits.insert(search_idx, edit);
275
276 ApplyResult::Applied
277 }
278
279 #[inline]
284 pub fn apply_batch<F>(&mut self, mut new_edits: Vec<TextEdit>, checker: Option<F>) -> ApplyResult
285 where
286 F: FnOnce(&[u8]) -> bool,
287 {
288 if new_edits.is_empty() {
289 return ApplyResult::Applied;
290 }
291
292 for edit in &new_edits {
294 if let Some(rejection) = self.check_safety(edit.safety) {
295 return rejection;
296 }
297 }
298
299 new_edits.sort_by(|a, b| a.range.start.cmp(&b.range.start).then_with(|| a.range.end.cmp(&b.range.end)));
300
301 for i in 0..new_edits.len() {
302 let edit = &new_edits[i];
303
304 if edit.range.end > self.original_len || edit.range.start > edit.range.end {
305 return ApplyResult::OutOfBounds;
306 }
307
308 if i > 0 && new_edits[i - 1].range.overlaps(&edit.range) {
309 return ApplyResult::Overlap;
310 }
311 }
312
313 {
314 let mut old_iter = self.edits.iter();
315 let mut new_iter = new_edits.iter();
316 let mut next_old = old_iter.next();
317 let mut next_new = new_iter.next();
318
319 while let (Some(old), Some(new)) = (next_old, next_new) {
320 if old.range.overlaps(&new.range) {
321 return ApplyResult::Overlap;
322 }
323 if old.range.start < new.range.start {
324 next_old = old_iter.next();
325 } else {
326 next_new = new_iter.next();
327 }
328 }
329 }
330
331 if let Some(check_fn) = checker {
332 let simulated_str = stitch_merged(self.original_text, &self.edits, &new_edits);
333 if !check_fn(&simulated_str) {
334 return ApplyResult::Rejected;
335 }
336 }
337
338 self.edits.reserve(new_edits.len());
339 self.edits.extend(new_edits);
340 self.edits.sort_by(|a, b| a.range.start.cmp(&b.range.start).then_with(|| a.range.end.cmp(&b.range.end)));
341
342 ApplyResult::Applied
343 }
344
345 #[inline]
347 #[must_use]
348 pub fn finish(self) -> Vec<u8> {
349 stitch(self.original_text, &self.edits)
350 }
351
352 #[inline]
354 #[must_use]
355 pub fn get_edits(&self) -> &[TextEdit] {
356 &self.edits
357 }
358
359 #[inline]
361 #[must_use]
362 pub fn safety_threshold(&self) -> Safety {
363 self.safety_threshold
364 }
365}
366
367fn stitch(original: &[u8], edits: &[TextEdit]) -> Vec<u8> {
370 let mut final_len = original.len();
371 for edit in edits {
372 final_len = final_len.saturating_sub(edit.range.len() as usize).saturating_add(edit.new_text.len());
373 }
374
375 let mut output = Vec::with_capacity(final_len);
376 let mut last_processed = 0;
377
378 for edit in edits {
379 let start = edit.range.start as usize;
380 let end = edit.range.end as usize;
381
382 if start > last_processed {
383 output.extend_from_slice(&original[last_processed..start]);
384 }
385 output.extend_from_slice(&edit.new_text);
386 last_processed = end;
387 }
388
389 if last_processed < original.len() {
390 output.extend_from_slice(&original[last_processed..]);
391 }
392
393 output
394}
395
396fn stitch_one(original: &[u8], existing_edits: &[TextEdit], new_edit: &TextEdit) -> Vec<u8> {
398 let slice = std::slice::from_ref(new_edit);
399 stitch_merged(original, existing_edits, slice)
400}
401
402fn stitch_merged(original: &[u8], old_edits: &[TextEdit], new_edits: &[TextEdit]) -> Vec<u8> {
405 let mut final_len = original.len();
406 for e in old_edits {
407 final_len = final_len - e.range.len() as usize + e.new_text.len();
408 }
409 for e in new_edits {
410 final_len = final_len - e.range.len() as usize + e.new_text.len();
411 }
412
413 let mut output = Vec::with_capacity(final_len);
414 let mut last_processed = 0;
415
416 let mut old_iter = old_edits.iter();
417 let mut new_iter = new_edits.iter();
418 let mut next_old = old_iter.next();
419 let mut next_new = new_iter.next();
420
421 loop {
422 let next_edit = match (next_old, next_new) {
423 (Some(o), Some(n)) => {
424 if (o.range.start, o.range.end) <= (n.range.start, n.range.end) {
425 next_old = old_iter.next();
426 o
427 } else {
428 next_new = new_iter.next();
429 n
430 }
431 }
432 (Some(o), None) => {
433 next_old = old_iter.next();
434 o
435 }
436 (None, Some(n)) => {
437 next_new = new_iter.next();
438 n
439 }
440 (None, None) => break,
441 };
442
443 let start = next_edit.range.start as usize;
444 let end = next_edit.range.end as usize;
445
446 if start > last_processed {
447 output.extend_from_slice(&original[last_processed..start]);
448 }
449 output.extend_from_slice(&next_edit.new_text);
450 last_processed = end;
451 }
452
453 if last_processed < original.len() {
454 output.extend_from_slice(&original[last_processed..]);
455 }
456
457 output
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463
464 #[test]
465 fn test_apply_single() {
466 let mut editor = TextEditor::new(b"hello world");
467 editor.apply(TextEdit::replace(0..5, "hi"), None::<fn(&[u8]) -> bool>);
468 assert_eq!(editor.finish(), b"hi world");
469 }
470
471 #[test]
472 fn test_checker_fail() {
473 let mut editor = TextEditor::new(b"abc");
474 let res = editor.apply(TextEdit::delete(0..1), Some(|s: &[u8]| s.len() > 10));
477 assert_eq!(res, ApplyResult::Rejected);
479 assert_eq!(editor.finish(), b"abc"); }
481
482 #[test]
483 fn test_overlap_search() {
484 let mut editor = TextEditor::new(b"0123456789");
485 editor.apply(TextEdit::replace(2..4, "x"), None::<fn(&[u8]) -> bool>); assert_eq!(editor.apply(TextEdit::replace(3..5, "y"), None::<fn(&[u8]) -> bool>), ApplyResult::Overlap);
489
490 assert_eq!(editor.apply(TextEdit::replace(1..3, "y"), None::<fn(&[u8]) -> bool>), ApplyResult::Overlap);
492
493 assert_eq!(editor.apply(TextEdit::replace(4..5, "y"), None::<fn(&[u8]) -> bool>), ApplyResult::Applied);
495
496 assert_eq!(editor.finish(), b"01xy56789");
497 }
498
499 #[test]
500 fn test_batch_apply_ordering() {
501 let mut editor = TextEditor::new(b"abcdef");
502
503 let batch = vec![
505 TextEdit::replace(4..5, "E"), TextEdit::replace(0..1, "A"), ];
508
509 editor.apply_batch(batch, None::<fn(&[u8]) -> bool>);
510 assert_eq!(editor.finish(), b"AbcdEf");
511 }
512
513 #[test]
514 fn test_safety_default_is_safe() {
515 let edit = TextEdit::replace(0..1, b"x");
516 assert_eq!(edit.safety, Safety::Safe);
517 }
518
519 #[test]
520 fn test_with_safety_builder() {
521 let edit = TextEdit::replace(0..1, b"x").with_safety(Safety::Unsafe);
522 assert_eq!(edit.safety, Safety::Unsafe);
523
524 let edit = TextEdit::delete(0..1).with_safety(Safety::PotentiallyUnsafe);
525 assert_eq!(edit.safety, Safety::PotentiallyUnsafe);
526 }
527
528 #[test]
529 fn test_safety_threshold_safe_mode() {
530 let mut editor = TextEditor::with_safety(b"hello world", Safety::Safe);
531
532 let res = editor.apply(TextEdit::replace(0..5, b"hi"), None::<fn(&[u8]) -> bool>);
534 assert_eq!(res, ApplyResult::Applied);
535
536 let res = editor.apply(
538 TextEdit::replace(6..11, b"there").with_safety(Safety::PotentiallyUnsafe),
539 None::<fn(&[u8]) -> bool>,
540 );
541 assert_eq!(res, ApplyResult::PotentiallyUnsafe);
542
543 let res =
545 editor.apply(TextEdit::replace(6..11, b"there").with_safety(Safety::Unsafe), None::<fn(&[u8]) -> bool>);
546 assert_eq!(res, ApplyResult::Unsafe);
547
548 assert_eq!(editor.finish(), b"hi world"); }
550
551 #[test]
552 fn test_safety_threshold_potentially_unsafe_mode() {
553 let mut editor = TextEditor::with_safety(b"hello world", Safety::PotentiallyUnsafe);
554
555 let res = editor.apply(TextEdit::replace(0..5, "hi"), None::<fn(&[u8]) -> bool>);
557 assert_eq!(res, ApplyResult::Applied);
558
559 let res = editor
561 .apply(TextEdit::replace(6..11, "there").with_safety(Safety::PotentiallyUnsafe), None::<fn(&[u8]) -> bool>);
562 assert_eq!(res, ApplyResult::Applied);
563
564 assert_eq!(editor.finish(), b"hi there");
565 }
566
567 #[test]
568 fn test_safety_threshold_unsafe_mode() {
569 let mut editor = TextEditor::with_safety(b"hello world", Safety::Unsafe);
570
571 let res = editor.apply(TextEdit::replace(0..1, b"H"), None::<fn(&[u8]) -> bool>);
573 assert_eq!(res, ApplyResult::Applied);
574
575 let res = editor
576 .apply(TextEdit::replace(1..2, b"E").with_safety(Safety::PotentiallyUnsafe), None::<fn(&[u8]) -> bool>);
577 assert_eq!(res, ApplyResult::Applied);
578
579 let res = editor.apply(TextEdit::replace(2..3, b"L").with_safety(Safety::Unsafe), None::<fn(&[u8]) -> bool>);
580 assert_eq!(res, ApplyResult::Applied);
581
582 assert_eq!(editor.finish(), b"HELlo world");
583 }
584
585 #[test]
586 fn test_batch_safety_rejection() {
587 let mut editor = TextEditor::with_safety(b"hello", Safety::Safe);
588
589 let batch = vec![
591 TextEdit::replace(0..1, "H"), TextEdit::replace(1..2, "E").with_safety(Safety::Unsafe), ];
594
595 let res = editor.apply_batch(batch, None::<fn(&[u8]) -> bool>);
596 assert_eq!(res, ApplyResult::Unsafe);
597
598 assert_eq!(editor.finish(), b"hello");
600 }
601
602 #[test]
603 fn test_safety_ordering() {
604 assert!(Safety::Safe < Safety::PotentiallyUnsafe);
606 assert!(Safety::PotentiallyUnsafe < Safety::Unsafe);
607 assert!(Safety::Safe < Safety::Unsafe);
608 }
609
610 #[test]
611 fn test_insert_at_start_of_replace_applies_before_replacement() {
612 let mut editor = TextEditor::new(b"0123456789");
613
614 let res = editor.apply(TextEdit::replace(2..8, "replaced"), None::<fn(&[u8]) -> bool>);
615 assert_eq!(res, ApplyResult::Applied);
616
617 let res = editor.apply(TextEdit::insert(2, "inserted"), None::<fn(&[u8]) -> bool>);
618 assert_eq!(res, ApplyResult::Applied);
619
620 assert_eq!(editor.finish(), b"01insertedreplaced89");
621 }
622
623 #[test]
624 fn test_insert_after_replace_at_different_offset() {
625 let mut editor = TextEditor::new(b"0123456789");
626
627 let res = editor.apply(TextEdit::replace(2..5, "ABC"), None::<fn(&[u8]) -> bool>);
628 assert_eq!(res, ApplyResult::Applied);
629
630 let res = editor.apply(TextEdit::insert(6, "X"), None::<fn(&[u8]) -> bool>);
631 assert_eq!(res, ApplyResult::Applied);
632
633 assert_eq!(editor.finish(), b"01ABC5X6789");
634 }
635
636 #[test]
637 fn test_insert_at_start_of_replace_coexists() {
638 let mut editor = TextEditor::new(b"0123456789");
639
640 let res = editor.apply(TextEdit::replace(2..5, "ABC"), None::<fn(&[u8]) -> bool>);
641 assert_eq!(res, ApplyResult::Applied);
642
643 let res = editor.apply(TextEdit::insert(2, "X"), None::<fn(&[u8]) -> bool>);
644 assert_eq!(res, ApplyResult::Applied);
645
646 assert_eq!(editor.finish(), b"01XABC56789");
647 }
648
649 #[test]
650 fn test_batch_insert_and_replace_at_same_offset_coexist() {
651 let mut editor = TextEditor::new(b"0123456789");
652
653 let batch = vec![
654 TextEdit::insert(2, "inserted"), TextEdit::replace(2..5, "ABC"), ];
657
658 let res = editor.apply_batch(batch, None::<fn(&[u8]) -> bool>);
659 assert_eq!(res, ApplyResult::Applied);
660
661 assert_eq!(editor.finish(), b"01insertedABC56789");
662 }
663
664 #[test]
665 fn test_multiple_inserts_at_same_offset_stack_in_insertion_order() {
666 let mut editor = TextEditor::new(b"ABC");
667 let batch = vec![TextEdit::insert(0, "X"), TextEdit::insert(0, "Y"), TextEdit::insert(0, "Z")];
668 let res = editor.apply_batch(batch, None::<fn(&[u8]) -> bool>);
669 assert_eq!(res, ApplyResult::Applied);
670 assert_eq!(editor.finish(), b"XYZABC");
671 }
672
673 #[test]
674 fn test_insert_at_end_of_replace_applies_after_replacement() {
675 let mut editor = TextEditor::new(b"0123456789");
676 let res = editor.apply(TextEdit::replace(2..5, "ABC"), None::<fn(&[u8]) -> bool>);
677 assert_eq!(res, ApplyResult::Applied);
678 let res = editor.apply(TextEdit::insert(5, "X"), None::<fn(&[u8]) -> bool>);
679 assert_eq!(res, ApplyResult::Applied);
680 assert_eq!(editor.finish(), b"01ABCX56789");
681 }
682
683 #[test]
684 fn test_insert_inside_replace_overlaps() {
685 let mut editor = TextEditor::new(b"0123456789");
686 let res = editor.apply(TextEdit::replace(2..8, "ABCDEF"), None::<fn(&[u8]) -> bool>);
687 assert_eq!(res, ApplyResult::Applied);
688 let res = editor.apply(TextEdit::insert(5, "X"), None::<fn(&[u8]) -> bool>);
689 assert_eq!(res, ApplyResult::Overlap);
690 }
691
692 #[test]
693 fn test_issue_828_regression_both_edits_apply_correctly() {
694 let mut editor = TextEditor::new(b"function ($v) { return $v; }");
695 let batch = vec![TextEdit::insert(0, "static "), TextEdit::replace(0..8, "fn")];
696 let res = editor.apply_batch(batch, None::<fn(&[u8]) -> bool>);
697 assert_eq!(res, ApplyResult::Applied);
698 assert_eq!(editor.finish(), b"static fn ($v) { return $v; }");
699 }
700
701 #[test]
702 fn test_checker_simulation_matches_final_output_for_stacked_inserts() {
703 let mut editor = TextEditor::new(b"ABC");
704 editor.apply(TextEdit::insert(0, b"X"), None::<fn(&[u8]) -> bool>);
705
706 let simulated: std::cell::RefCell<Option<Vec<u8>>> = std::cell::RefCell::new(None);
707 let checker = |s: &[u8]| {
708 *simulated.borrow_mut() = Some(s.to_vec());
709 true
710 };
711 let batch = vec![TextEdit::insert(0, b"Y")];
712 assert_eq!(editor.apply_batch(batch, Some(checker)), ApplyResult::Applied);
713
714 #[allow(clippy::expect_used)]
715 let simulated = simulated.borrow().clone().expect("checker called");
716 let final_str = editor.finish();
717 assert_eq!(simulated, final_str);
718 assert_eq!(final_str, b"XYABC");
719 }
720
721 #[test]
722 fn test_touching_non_empty_ranges_do_not_overlap() {
723 let range1 = TextRange::new(0, 5);
724 let range2 = TextRange::new(5, 10);
725 assert!(!range1.overlaps(&range2));
726 assert!(!range2.overlaps(&range1));
727 }
728
729 #[test]
730 fn test_insert_at_boundary_of_replace_does_not_overlap() {
731 let insert_at_start = TextRange::new(5, 5);
732 let insert_at_end = TextRange::new(10, 10);
733 let replace_range = TextRange::new(5, 10);
734 assert!(!insert_at_start.overlaps(&replace_range));
735 assert!(!replace_range.overlaps(&insert_at_start));
736 assert!(!insert_at_end.overlaps(&replace_range));
737 assert!(!replace_range.overlaps(&insert_at_end));
738 }
739
740 #[test]
741 fn test_insert_inside_non_empty_range_overlaps() {
742 let insert = TextRange::new(7, 7);
743 let replace = TextRange::new(5, 10);
744 assert!(insert.overlaps(&replace));
745 assert!(replace.overlaps(&insert));
746 }
747
748 #[test]
749 fn test_two_empty_ranges_at_same_offset_do_not_overlap() {
750 let a = TextRange::new(5, 5);
751 let b = TextRange::new(5, 5);
752 assert!(!a.overlaps(&b));
753 assert!(!b.overlaps(&a));
754 }
755}