Skip to main content

mago_text_edit/
lib.rs

1/// Represents the safety of applying a specific edit.
2///
3/// Ordered from most safe to least safe.
4#[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    /// Safe to apply automatically. The semantic meaning of the code is preserved.
11    /// Example: Formatting, renaming a local variable.
12    #[default]
13    Safe,
14    /// Likely safe, but changes semantics slightly or relies on heuristics.
15    /// Example: Removing an unused variable (might have side effects in constructor).
16    PotentiallyUnsafe,
17    /// Requires manual user review. Valid code, but changes logic significantly.
18    /// Example: Changing type casts, altering control flow logic.
19    Unsafe,
20}
21
22/// Represents a range in the source text identified by byte offsets.
23#[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    /// Returns the length of the range in bytes.
38    #[inline(always)]
39    #[must_use]
40    pub fn len(&self) -> u32 {
41        self.end - self.start
42    }
43
44    /// Returns true if the range has a length of zero.
45    #[inline(always)]
46    #[must_use]
47    pub fn is_empty(&self) -> bool {
48        self.start == self.end
49    }
50
51    /// Checks if this range overlaps with another.
52    ///
53    /// Two ranges conflict only when they share byte positions; i.e. when
54    /// applying both would write the same byte twice or write into bytes the
55    /// other is deleting. Adjacency at a boundary is fine:
56    ///
57    /// - Adjacent non-empty ranges (e.g. `0..5` and `5..10`) do not overlap;
58    ///   they replace different bytes.
59    /// - Two empty ranges at the same offset stack in insertion order; they
60    ///   each write their own bytes without touching the other's.
61    /// - An empty range at the exact boundary of a non-empty one (e.g. an
62    ///   insert at `5` with a replace of `5..10`, or an insert at `10` with
63    ///   a replace of `5..10`) does not overlap; the stitcher resolves the
64    ///   order deterministically (insert-at-start goes before replacement;
65    ///   insert-at-end goes after).
66    ///
67    /// Only *interior* containment of an empty range inside a non-empty one
68    /// is treated as overlap, as is any interior overlap between two
69    /// non-empty ranges.
70    #[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    /// Checks if this range contains a specific offset.
83    #[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, // Will fail bounds check later
106        };
107
108        Self::new(start, end)
109    }
110}
111
112/// A unified atomic edit operation.
113///
114/// This struct holds the data for a modification but does not execute it.
115/// It always refers to the byte offsets in the **ORIGINAL** source code.
116#[derive(Debug, Clone, Eq, PartialEq, Hash)]
117#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
118pub struct TextEdit {
119    /// The range in the original text to be replaced.
120    pub range: TextRange,
121    /// The new text to replace the range with.
122    pub new_text: Vec<u8>,
123    /// How safe this specific edit is.
124    pub safety: Safety,
125}
126
127impl TextEdit {
128    /// Creates a delete edit (defaults to Safe).
129    #[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    /// Creates an insert edit (defaults to Safe).
136    #[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    /// Creates a replace edit (defaults to Safe).
143    #[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    /// Builder method to change the safety level of this edit.
150    ///
151    /// # Example
152    /// ```
153    /// use mago_text_edit::{TextEdit, Safety};
154    ///
155    /// let edit = TextEdit::replace(1..2, "b").with_safety(Safety::Unsafe);
156    /// assert_eq!(edit.safety, Safety::Unsafe);
157    /// ```
158    #[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    /// The edits were successfully applied.
171    Applied,
172    /// The edits were invalid (e.g., start > end or > file length).
173    OutOfBounds,
174    /// The edits overlapped with previously confirmed edits or each other.
175    Overlap,
176    /// The provided checker function returned `false`.
177    Rejected,
178    /// Edit rejected because it's unsafe and we're in safe or potentially-unsafe mode.
179    Unsafe,
180    /// Edit rejected because it's potentially-unsafe and we're in safe mode.
181    PotentiallyUnsafe,
182}
183
184/// A high-performance, transactional text editor.
185///
186/// It accumulates edits and applies them in a single pass when `finish()` is called.
187/// It ensures all edits are valid, non-overlapping, and safe according to optional user checks.
188#[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    /// Creates a new TextEditor with the default safety threshold (Unsafe - accepts all edits).
198    #[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    /// Creates a new TextEditor with a specific safety threshold.
210    ///
211    /// Edits with a safety level above the threshold will be rejected.
212    ///
213    /// # Example
214    /// ```
215    /// use mago_text_edit::{TextEditor, Safety};
216    ///
217    /// // Only accept Safe edits
218    /// let editor = TextEditor::with_safety(b"hello", Safety::Safe);
219    /// ```
220    #[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    /// Checks if an edit's safety level exceeds the threshold.
227    /// Returns the appropriate rejection result, or None if the edit is acceptable.
228    #[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    /// Applies a single edit.
242    ///
243    /// Uses binary search to check for overlaps in O(log N).
244    /// Rejects edits that exceed the safety threshold.
245    #[inline]
246    pub fn apply<F>(&mut self, edit: TextEdit, checker: Option<F>) -> ApplyResult
247    where
248        F: FnOnce(&[u8]) -> bool,
249    {
250        // Check safety first
251        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    /// Applies a batch of edits atomically.
280    ///
281    /// Either all edits are applied, or none are (if overlap/check/safety fails).
282    /// If any edit in the batch exceeds the safety threshold, the entire batch is rejected.
283    #[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        // Check safety of all edits first
293        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    /// Consumes the editor and returns the final modified string.
346    #[inline]
347    #[must_use]
348    pub fn finish(self) -> Vec<u8> {
349        stitch(self.original_text, &self.edits)
350    }
351
352    /// Returns a slice of the currently applied edits.
353    #[inline]
354    #[must_use]
355    pub fn get_edits(&self) -> &[TextEdit] {
356        &self.edits
357    }
358
359    /// Returns the current safety threshold.
360    #[inline]
361    #[must_use]
362    pub fn safety_threshold(&self) -> Safety {
363        self.safety_threshold
364    }
365}
366
367/// Standard stitching of a sorted list.
368/// Calculates exact capacity first to guarantee exactly 1 allocation.
369fn 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
396/// Simulation for a single new edit (avoids creating a new vector).
397fn 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
402/// Simulation for merging two sorted lists of edits without mutating the original.
403/// Used by the checker to verify validity before committing.
404fn 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        // Fail if length of result > 10 (it won't be, so this passes check logic is inverted? No.)
475        // Checker logic: return TRUE if valid.
476        let res = editor.apply(TextEdit::delete(0..1), Some(|s: &[u8]| s.len() > 10));
477        // "bc" len is 2. 2 > 10 is false. Checker returns false.
478        assert_eq!(res, ApplyResult::Rejected);
479        assert_eq!(editor.finish(), b"abc"); // Unchanged
480    }
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>); // 2,3
486
487        // Try edit at 3..5 (Overlaps 2..4)
488        assert_eq!(editor.apply(TextEdit::replace(3..5, "y"), None::<fn(&[u8]) -> bool>), ApplyResult::Overlap);
489
490        // Try edit at 1..3 (Overlaps 2..4)
491        assert_eq!(editor.apply(TextEdit::replace(1..3, "y"), None::<fn(&[u8]) -> bool>), ApplyResult::Overlap);
492
493        // Try edit at 4..5 (Safe)
494        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        // Batch with mixed order inputs
504        let batch = vec![
505            TextEdit::replace(4..5, "E"), // e -> E
506            TextEdit::replace(0..1, "A"), // a -> A
507        ];
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        // Safe edit should be accepted
533        let res = editor.apply(TextEdit::replace(0..5, b"hi"), None::<fn(&[u8]) -> bool>);
534        assert_eq!(res, ApplyResult::Applied);
535
536        // PotentiallyUnsafe edit should be rejected
537        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        // Unsafe edit should be rejected
544        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"); // Only safe edit applied
549    }
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        // Safe edit should be accepted
556        let res = editor.apply(TextEdit::replace(0..5, "hi"), None::<fn(&[u8]) -> bool>);
557        assert_eq!(res, ApplyResult::Applied);
558
559        // PotentiallyUnsafe edit should be accepted
560        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        // All safety levels should be accepted
572        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        // Batch with one unsafe edit should reject entire batch
590        let batch = vec![
591            TextEdit::replace(0..1, "H"),                             // Safe
592            TextEdit::replace(1..2, "E").with_safety(Safety::Unsafe), // Unsafe - should cause rejection
593        ];
594
595        let res = editor.apply_batch(batch, None::<fn(&[u8]) -> bool>);
596        assert_eq!(res, ApplyResult::Unsafe);
597
598        // Original text unchanged
599        assert_eq!(editor.finish(), b"hello");
600    }
601
602    #[test]
603    fn test_safety_ordering() {
604        // Test that Safety enum orders correctly (Safe < PotentiallyUnsafe < Unsafe)
605        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"), // insert at 2
655            TextEdit::replace(2..5, "ABC"),  // replace starting at 2
656        ];
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}