Skip to main content

fresh/model/
piece_tree_diff.rs

1use std::ops::Range;
2use std::sync::Arc;
3
4use crate::model::piece_tree::{LeafData, PieceTreeNode};
5
6/// Summary of differences between two piece tree roots.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct PieceTreeDiff {
9    /// Whether the two trees represent identical piece sequences.
10    pub equal: bool,
11    /// Changed byte ranges in the "after" tree (exclusive end). Empty when `equal` is true.
12    pub byte_ranges: Vec<Range<usize>>,
13    /// Changed line ranges in the "after" tree (exclusive end). `None` when line counts are unknown.
14    pub line_ranges: Option<Vec<Range<usize>>>,
15    /// Number of tree nodes visited during the diff walk.
16    /// When `Arc::ptr_eq` short-circuits effectively, this should be
17    /// proportional to the edited region, not the entire tree.
18    pub nodes_visited: usize,
19}
20
21/// Compute a diff between two piece tree roots.
22///
23/// Uses structural sharing (Arc::ptr_eq) to skip identical subtrees in O(1),
24/// falling back to leaf-level comparison only for subtrees that actually differ.
25/// After path-copying edits, this is O(changed_path) instead of O(all_leaves).
26///
27/// `line_counter` should return the number of line feeds in a slice of a leaf.
28/// If it returns None for any consulted slice, the diff will have line_range=None.
29pub fn diff_piece_trees(
30    before: &Arc<PieceTreeNode>,
31    after: &Arc<PieceTreeNode>,
32    line_counter: &dyn Fn(&LeafData, usize, usize) -> Option<usize>,
33) -> PieceTreeDiff {
34    // Fast path: identical subtree (same Arc pointer)
35    if Arc::ptr_eq(before, after) {
36        return PieceTreeDiff {
37            equal: true,
38            byte_ranges: Vec::new(),
39            line_ranges: Some(Vec::new()),
40            nodes_visited: 1,
41        };
42    }
43
44    // Collect leaves only from differing subtrees using structural walk.
45    // Spans include document-absolute byte offsets.
46    let mut before_spans = Vec::new();
47    let mut after_spans = Vec::new();
48    let mut nodes_visited: usize = 0;
49    let mut before_doc_offset: usize = 0;
50    let mut after_doc_offset: usize = 0;
51    let mut after_doc_lf: Option<usize> = Some(0);
52    diff_collect_leaves(
53        before,
54        after,
55        &mut before_spans,
56        &mut after_spans,
57        &mut nodes_visited,
58        &mut before_doc_offset,
59        &mut after_doc_offset,
60        &mut after_doc_lf,
61    );
62
63    let before_spans = normalize_spans(before_spans);
64    let after_spans = normalize_spans(after_spans);
65
66    // Fast-path: identical leaf sequences (same content, different tree structure).
67    if span_slices_equal(&before_spans, &after_spans) {
68        return PieceTreeDiff {
69            equal: true,
70            byte_ranges: Vec::new(),
71            line_ranges: Some(Vec::new()),
72            nodes_visited,
73        };
74    }
75
76    // Longest common prefix at byte granularity.
77    let prefix = common_prefix_bytes(&before_spans, &after_spans);
78    // Longest common suffix without overlapping prefix.
79    let suffix = common_suffix_bytes(&before_spans, &after_spans, prefix);
80
81    let ranges = collect_diff_ranges(&before_spans, &after_spans, prefix, suffix);
82
83    // Map byte ranges to line ranges (best effort).
84    let line_ranges = line_ranges(&after_spans, &ranges, line_counter);
85
86    PieceTreeDiff {
87        equal: false,
88        byte_ranges: ranges,
89        line_ranges,
90        nodes_visited,
91    }
92}
93
94/// Total bytes stored under a node (without calling private methods).
95fn node_bytes(node: &PieceTreeNode) -> usize {
96    match node {
97        PieceTreeNode::Internal {
98            left_bytes, right, ..
99        } => left_bytes + node_bytes(right),
100        PieceTreeNode::Leaf { bytes, .. } => *bytes,
101    }
102}
103
104/// Total line feeds under a node. Returns None if any leaf has unknown count.
105fn node_line_feeds(node: &PieceTreeNode) -> Option<usize> {
106    match node {
107        PieceTreeNode::Internal { lf_left, right, .. } => {
108            let left_lf = (*lf_left)?;
109            let right_lf = node_line_feeds(right)?;
110            Some(left_lf + right_lf)
111        }
112        PieceTreeNode::Leaf { line_feed_cnt, .. } => *line_feed_cnt,
113    }
114}
115
116/// Parallel tree walk that uses Arc::ptr_eq to skip identical subtrees.
117/// Only collects leaves from subtrees that differ between before and after.
118/// Tracks running document byte offsets so collected spans have absolute positions.
119/// Also tracks line feeds in all subtrees to populate doc_lf_offset.
120fn diff_collect_leaves(
121    before: &Arc<PieceTreeNode>,
122    after: &Arc<PieceTreeNode>,
123    before_out: &mut Vec<Span>,
124    after_out: &mut Vec<Span>,
125    nodes_visited: &mut usize,
126    before_doc_offset: &mut usize,
127    after_doc_offset: &mut usize,
128    after_doc_lf: &mut Option<usize>,
129) {
130    *nodes_visited += 2; // counting both before and after nodes
131
132    // Identical subtree - skip entirely, advancing document offsets
133    if Arc::ptr_eq(before, after) {
134        let bytes = node_bytes(before);
135        *before_doc_offset += bytes;
136        *after_doc_offset += bytes;
137        *after_doc_lf = match (*after_doc_lf, node_line_feeds(after)) {
138            (Some(a), Some(b)) => Some(a + b),
139            _ => None,
140        };
141        return;
142    }
143
144    match (before.as_ref(), after.as_ref()) {
145        // Both internal: recurse into children
146        (
147            PieceTreeNode::Internal {
148                left: b_left,
149                right: b_right,
150                ..
151            },
152            PieceTreeNode::Internal {
153                left: a_left,
154                right: a_right,
155                ..
156            },
157        ) => {
158            diff_collect_leaves(
159                b_left,
160                a_left,
161                before_out,
162                after_out,
163                nodes_visited,
164                before_doc_offset,
165                after_doc_offset,
166                after_doc_lf,
167            );
168            diff_collect_leaves(
169                b_right,
170                a_right,
171                before_out,
172                after_out,
173                nodes_visited,
174                before_doc_offset,
175                after_doc_offset,
176                after_doc_lf,
177            );
178        }
179        // Structure mismatch - fall back to full leaf collection for both subtrees
180        _ => {
181            let mut before_lf_dummy = Some(0); // Not needed for 'before' ranges
182            collect_leaves_with_offsets(
183                before,
184                before_out,
185                nodes_visited,
186                before_doc_offset,
187                &mut before_lf_dummy,
188            );
189            collect_leaves_with_offsets(
190                after,
191                after_out,
192                nodes_visited,
193                after_doc_offset,
194                after_doc_lf,
195            );
196        }
197    }
198}
199
200fn collect_leaves_with_offsets(
201    node: &Arc<PieceTreeNode>,
202    out: &mut Vec<Span>,
203    nodes_visited: &mut usize,
204    doc_offset: &mut usize,
205    doc_lf: &mut Option<usize>,
206) {
207    *nodes_visited += 1;
208    match node.as_ref() {
209        PieceTreeNode::Internal { left, right, .. } => {
210            collect_leaves_with_offsets(left, out, nodes_visited, doc_offset, doc_lf);
211            collect_leaves_with_offsets(right, out, nodes_visited, doc_offset, doc_lf);
212        }
213        PieceTreeNode::Leaf {
214            location,
215            offset,
216            bytes,
217            line_feed_cnt,
218        } => {
219            let leaf = LeafData::new(*location, *offset, *bytes, *line_feed_cnt);
220            out.push(Span {
221                leaf,
222                doc_offset: *doc_offset,
223                doc_lf_offset: doc_lf.unwrap_or(0),
224            });
225            *doc_offset += bytes;
226            *doc_lf = match (*doc_lf, line_feed_cnt) {
227                (Some(a), Some(b)) => Some(a + b),
228                _ => None,
229            };
230        }
231    }
232}
233
234#[derive(Clone)]
235struct Span {
236    leaf: LeafData,
237    doc_offset: usize,
238    doc_lf_offset: usize,
239}
240
241fn spans_equal(a: &Span, b: &Span) -> bool {
242    a.leaf.location == b.leaf.location
243        && a.leaf.offset == b.leaf.offset
244        && a.leaf.bytes == b.leaf.bytes
245}
246
247fn span_slices_equal(a: &[Span], b: &[Span]) -> bool {
248    a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| spans_equal(x, y))
249}
250
251fn normalize_spans(spans: Vec<Span>) -> Vec<Span> {
252    if spans.is_empty() {
253        return spans;
254    }
255
256    let mut it = spans.into_iter();
257    let mut current = it.next().unwrap();
258    let mut normalized = Vec::new();
259
260    for span in it {
261        let contiguous = current.leaf.location == span.leaf.location
262            && current.leaf.offset + current.leaf.bytes == span.leaf.offset
263            && current.doc_offset + current.leaf.bytes == span.doc_offset;
264        if contiguous {
265            current.leaf.bytes += span.leaf.bytes;
266            current.leaf.line_feed_cnt = match (current.leaf.line_feed_cnt, span.leaf.line_feed_cnt)
267            {
268                (Some(a), Some(b)) => Some(a + b),
269                _ => None,
270            };
271        } else {
272            normalized.push(current);
273            current = span;
274        }
275    }
276
277    normalized.push(current);
278    normalized
279}
280
281fn common_prefix_bytes(before: &[Span], after: &[Span]) -> usize {
282    let mut b_idx = 0;
283    let mut a_idx = 0;
284    let mut b_off = 0;
285    let mut a_off = 0;
286    let mut consumed = 0;
287
288    while b_idx < before.len() && a_idx < after.len() {
289        let b_span = &before[b_idx];
290        let a_span = &after[a_idx];
291        let b = &b_span.leaf;
292        let a = &a_span.leaf;
293
294        let b_pos = b.offset + b_off;
295        let a_pos = a.offset + a_off;
296
297        // Must also ensure they are at the same document relative position
298        // if they were separated by gaps.
299        if b.location == a.location
300            && b_pos == a_pos
301            && (b_span.doc_offset + b_off) == (a_span.doc_offset + a_off)
302        {
303            let b_rem = b.bytes - b_off;
304            let a_rem = a.bytes - a_off;
305            let take = b_rem.min(a_rem);
306
307            consumed += take;
308            b_off += take;
309            a_off += take;
310
311            if b_off == b.bytes {
312                b_idx += 1;
313                b_off = 0;
314            }
315            if a_off == a.bytes {
316                a_idx += 1;
317                a_off = 0;
318            }
319        } else {
320            break;
321        }
322    }
323
324    consumed
325}
326
327fn common_suffix_bytes(before: &[Span], after: &[Span], prefix_bytes: usize) -> usize {
328    let total_before = before
329        .last()
330        .map(|s| s.doc_offset + s.leaf.bytes)
331        .unwrap_or(0);
332    let total_after = after
333        .last()
334        .map(|s| s.doc_offset + s.leaf.bytes)
335        .unwrap_or(0);
336
337    let mut b_idx: isize = before.len() as isize - 1;
338    let mut a_idx: isize = after.len() as isize - 1;
339    let mut b_off = 0;
340    let mut a_off = 0;
341    let mut consumed = 0;
342
343    while b_idx >= 0
344        && a_idx >= 0
345        && (total_before - consumed) > prefix_bytes
346        && (total_after - consumed) > prefix_bytes
347    {
348        let b_span = &before[b_idx as usize];
349        let a_span = &after[a_idx as usize];
350        let b_leaf = &b_span.leaf;
351        let a_leaf = &a_span.leaf;
352
353        let b_pos = b_leaf.offset + b_leaf.bytes - b_off;
354        let a_pos = a_leaf.offset + a_leaf.bytes - a_off;
355
356        // Compare by buffer identity only (location + offset). Suffix bytes are
357        // at different doc_offsets in before vs after when insertions/deletions
358        // change the total size, but they still contain the same data.
359        if b_leaf.location == a_leaf.location && b_pos == a_pos {
360            let b_rem = b_leaf.bytes - b_off;
361            let a_rem = a_leaf.bytes - a_off;
362            let take = b_rem.min(a_rem);
363
364            consumed += take;
365            b_off += take;
366            a_off += take;
367
368            if b_off == b_leaf.bytes {
369                b_idx -= 1;
370                b_off = 0;
371            }
372            if a_off == a_leaf.bytes {
373                a_idx -= 1;
374                a_off = 0;
375            }
376        } else {
377            break;
378        }
379    }
380
381    consumed.min(total_after.saturating_sub(prefix_bytes))
382}
383
384fn collect_diff_ranges(
385    before: &[Span],
386    after: &[Span],
387    prefix: usize,
388    suffix: usize,
389) -> Vec<Range<usize>> {
390    let mut ranges = Vec::new();
391    let mut b_idx = 0;
392    let mut a_idx = 0;
393    let mut b_off = 0;
394    let mut a_off = 0;
395    let mut matched_prefix = 0;
396
397    // Skip matching prefix
398    while matched_prefix < prefix && b_idx < before.len() && a_idx < after.len() {
399        let b = &before[b_idx].leaf;
400        let a = &after[a_idx].leaf;
401        let b_rem = b.bytes - b_off;
402        let a_rem = a.bytes - a_off;
403        let take = b_rem.min(a_rem).min(prefix - matched_prefix);
404        matched_prefix += take;
405        b_off += take;
406        a_off += take;
407        if b_off == b.bytes {
408            b_idx += 1;
409            b_off = 0;
410        }
411        if a_off == a.bytes {
412            a_idx += 1;
413            a_off = 0;
414        }
415    }
416
417    // compare_limit in document-absolute space: end of collected spans minus suffix
418    let doc_end = after
419        .last()
420        .map(|s| s.doc_offset + s.leaf.bytes)
421        .unwrap_or(0);
422    let compare_limit = doc_end.saturating_sub(suffix);
423
424    // prefix_start in document-absolute space
425    let doc_start = after.first().map(|s| s.doc_offset).unwrap_or(0);
426
427    let mut current_start: Option<usize> = None;
428    let mut current_end: usize = 0;
429
430    while a_idx < after.len() {
431        let a = &after[a_idx];
432        let pos = a.doc_offset + a_off;
433        if pos >= compare_limit {
434            break;
435        }
436
437        // If we have a current range but there's a gap in document offset,
438        // it means there was an identical subtree that was skipped.
439        // We must break the range here.
440        if let Some(start) = current_start {
441            if pos > current_end {
442                ranges.push(start..current_end);
443                current_start = None;
444            }
445        }
446
447        let matches = if b_idx < before.len() {
448            let b = &before[b_idx].leaf;
449            let b_pos = b.offset + b_off;
450            let a_pos = a.leaf.offset + a_off;
451            // Compare by buffer identity only. Insertions/deletions shift
452            // doc_offsets, but matching buffer location + offset means same data.
453            b.location == a.leaf.location && b_pos == a_pos
454        } else {
455            false
456        };
457
458        if matches {
459            if let Some(start) = current_start.take() {
460                ranges.push(start..current_end);
461            }
462
463            let b = &before[b_idx].leaf;
464            let b_rem = b.bytes - b_off;
465            let a_rem = a.leaf.bytes - a_off;
466            let take = b_rem.min(a_rem).min(compare_limit.saturating_sub(pos));
467
468            b_off += take;
469            a_off += take;
470
471            if b_off == b.bytes {
472                b_idx += 1;
473                b_off = 0;
474            }
475            if a_off == a.leaf.bytes {
476                a_idx += 1;
477                a_off = 0;
478            }
479        } else {
480            if current_start.is_none() {
481                current_start = Some(pos);
482            }
483            let take = (a.leaf.bytes - a_off).min(compare_limit.saturating_sub(pos));
484            current_end = pos + take;
485            a_off += take;
486            if a_off == a.leaf.bytes {
487                a_idx += 1;
488                a_off = 0;
489            }
490        }
491    }
492
493    if let Some(start) = current_start {
494        ranges.push(start..current_end);
495    }
496
497    // Any trailing unmatched "after" spans up to suffix boundary
498    while a_idx < after.len() {
499        let start = after[a_idx].doc_offset + a_off;
500        if start >= compare_limit {
501            break;
502        }
503        let end = (after[a_idx].doc_offset + after[a_idx].leaf.bytes).min(compare_limit);
504        ranges.push(start..end);
505        a_idx += 1;
506        a_off = 0;
507    }
508
509    if ranges.is_empty() {
510        // Anchor range: either there's content between prefix and suffix, or
511        // the trees differ only in size (deletion) — report the anchor point.
512        ranges.push((doc_start + prefix)..compare_limit);
513    }
514
515    ranges
516}
517
518fn count_lines_in_range(
519    spans: &[Span],
520    start: usize,
521    end: usize,
522    line_counter: &dyn Fn(&LeafData, usize, usize) -> Option<usize>,
523) -> Option<usize> {
524    if start >= end {
525        return Some(0);
526    }
527
528    let mut line_feeds = 0usize;
529    for span in spans {
530        let span_start = span.doc_offset;
531        let span_end = span_start + span.leaf.bytes;
532
533        if end <= span_start {
534            break;
535        }
536        if start >= span_end {
537            continue;
538        }
539
540        let overlap_start = start.max(span_start);
541        let overlap_end = end.min(span_end);
542        let local_start = overlap_start - span_start;
543        let len = overlap_end - overlap_start;
544
545        let chunk_lines = line_counter(&span.leaf, local_start, len)?;
546        line_feeds += chunk_lines;
547    }
548
549    Some(line_feeds)
550}
551
552fn line_ranges(
553    after_spans: &[Span],
554    byte_ranges: &[Range<usize>],
555    line_counter: &dyn Fn(&LeafData, usize, usize) -> Option<usize>,
556) -> Option<Vec<Range<usize>>> {
557    let mut accum = Vec::with_capacity(byte_ranges.len());
558    for range in byte_ranges {
559        // Find the span containing the start of the range to get its doc_lf_offset.
560        // Use <= for the upper bound so deletion anchors at span boundaries are found.
561        let span = after_spans
562            .iter()
563            .find(|s| range.start >= s.doc_offset && range.start <= s.doc_offset + s.leaf.bytes)?;
564
565        let offset_into_span = (range.start - span.doc_offset).min(span.leaf.bytes);
566        let lf_at_span_start = span.doc_lf_offset;
567        let lf_to_range_start = line_counter(&span.leaf, 0, offset_into_span)?;
568        let start_line = lf_at_span_start + lf_to_range_start;
569
570        let lf_in_range = count_lines_in_range(after_spans, range.start, range.end, line_counter)?;
571
572        let end_line = if range.start == range.end {
573            start_line + 1
574        } else {
575            start_line + lf_in_range + 1
576        };
577        accum.push(start_line..end_line);
578    }
579
580    Some(accum)
581}
582
583#[cfg(test)]
584#[allow(clippy::single_range_in_vec_init)]
585mod tests {
586    use super::*;
587    use crate::model::piece_tree::BufferLocation;
588
589    fn sum_bytes(leaves: &[LeafData]) -> usize {
590        leaves.iter().map(|leaf| leaf.bytes).sum()
591    }
592
593    fn leaf(loc: BufferLocation, offset: usize, bytes: usize, lfs: Option<usize>) -> LeafData {
594        LeafData::new(loc, offset, bytes, lfs)
595    }
596
597    // Minimal balanced builder for tests.
598    fn build(leaves: &[LeafData]) -> Arc<PieceTreeNode> {
599        if leaves.is_empty() {
600            return Arc::new(PieceTreeNode::Leaf {
601                location: BufferLocation::Stored(0),
602                offset: 0,
603                bytes: 0,
604                line_feed_cnt: Some(0),
605            });
606        }
607        if leaves.len() == 1 {
608            let l = leaves[0];
609            return Arc::new(PieceTreeNode::Leaf {
610                location: l.location,
611                offset: l.offset,
612                bytes: l.bytes,
613                line_feed_cnt: l.line_feed_cnt,
614            });
615        }
616
617        let mid = leaves.len() / 2;
618        let left = build(&leaves[..mid]);
619        let right = build(&leaves[mid..]);
620
621        Arc::new(PieceTreeNode::Internal {
622            left_bytes: sum_bytes(&leaves[..mid]),
623            lf_left: leaves[..mid]
624                .iter()
625                .map(|l| l.line_feed_cnt)
626                .try_fold(0usize, |acc, v| v.map(|b| acc + b)),
627            left,
628            right,
629        })
630    }
631
632    fn count_line_feeds(leaf: &LeafData, start: usize, len: usize) -> Option<usize> {
633        if len == 0 {
634            return Some(0);
635        }
636        // If we know total LFs, assume uniform distribution only when full coverage.
637        if start == 0 && len == leaf.bytes {
638            return leaf.line_feed_cnt;
639        }
640        None
641    }
642
643    #[test]
644    fn detects_identical_trees() {
645        let leaves = vec![leaf(BufferLocation::Stored(0), 0, 10, Some(0))];
646        let before = build(&leaves);
647        let after = build(&leaves);
648
649        let diff = diff_piece_trees(&before, &after, &count_line_feeds);
650        assert!(diff.equal);
651        assert!(diff.byte_ranges.is_empty());
652        assert_eq!(diff.line_ranges, Some(Vec::new()));
653    }
654
655    #[test]
656    fn detects_single_line_change() {
657        let before = build(&[leaf(BufferLocation::Stored(0), 0, 5, Some(0))]);
658        let after = build(&[leaf(BufferLocation::Added(1), 0, 5, Some(0))]);
659
660        let diff = diff_piece_trees(&before, &after, &count_line_feeds);
661        assert!(!diff.equal);
662        assert_eq!(diff.byte_ranges, vec![0..5]);
663        assert_eq!(diff.line_ranges, Some(vec![0..1])); // same line, different content
664    }
665
666    #[test]
667    fn tracks_newlines_in_changed_span() {
668        let before = build(&[leaf(BufferLocation::Stored(0), 0, 6, Some(0))]);
669        let after = build(&[leaf(BufferLocation::Added(1), 0, 6, Some(1))]); // introduces a newline
670
671        let diff = diff_piece_trees(&before, &after, &count_line_feeds);
672        assert!(!diff.equal);
673        assert_eq!(diff.byte_ranges, vec![0..6]);
674        assert_eq!(diff.line_ranges, Some(vec![0..2])); // spans two lines after change
675    }
676
677    #[test]
678    fn handles_deletion_by_marking_anchor_line() {
679        let before = build(&[
680            leaf(BufferLocation::Stored(0), 0, 6, Some(1)), // two lines
681            leaf(BufferLocation::Stored(0), 6, 4, Some(0)), // trailing text
682        ]);
683        let after = build(&[leaf(BufferLocation::Stored(0), 0, 6, Some(1))]);
684
685        let diff = diff_piece_trees(&before, &after, &count_line_feeds);
686        assert!(!diff.equal);
687        assert_eq!(diff.byte_ranges, vec![6..6]); // no bytes remain at the change site
688        assert_eq!(diff.line_ranges, Some(vec![1..2])); // anchor after deleted span
689    }
690
691    /// Uses real PieceTree::insert (path-copy) near EOF.
692    /// The diff must produce document-absolute offsets.
693    #[test]
694    fn diff_after_path_copy_insert_at_eof() {
695        use crate::model::piece_tree::{PieceTree, StringBuffer};
696
697        // 10 KB file, split into 1 KB chunks → 10 leaves.
698        let chunk_size = 1000;
699        let total = 10_000;
700        // Build content: 10 LFs per 1000-byte chunk (one every 100 bytes).
701        let content: Vec<u8> = (0..total)
702            .map(|i| if i % 100 == 99 { b'\n' } else { b'A' })
703            .collect();
704        let buf = StringBuffer::new_loaded(0, content, false);
705
706        let mut saved_tree = PieceTree::new(BufferLocation::Stored(0), 0, total, None);
707        saved_tree.split_leaves_to_chunk_size(chunk_size);
708        // Set line feeds for each leaf.
709        let lf_updates: Vec<(usize, usize)> = (0..10).map(|i| (i, 10)).collect();
710        saved_tree.update_leaf_line_feeds(&lf_updates);
711        let saved_root = saved_tree.root();
712
713        // Insert 5 bytes near EOF via real path-copy.
714        let mut after_tree = saved_tree;
715        let insert_offset = total - 100; // 9900
716        let insert_buf = StringBuffer::new_loaded(1, b"HELLO".to_vec(), false);
717        after_tree.insert(
718            insert_offset,
719            BufferLocation::Added(1),
720            0,
721            5,
722            Some(0),
723            &[buf, insert_buf],
724        );
725        let after_root = after_tree.root();
726
727        let diff = diff_piece_trees(&saved_root, &after_root, &count_line_feeds);
728        assert!(!diff.equal);
729
730        assert!(
731            diff.byte_ranges[0].start >= total - 200,
732            "byte_ranges should be document-absolute (near EOF): got {:?}, expected near {}",
733            diff.byte_ranges,
734            insert_offset,
735        );
736
737        if let Some(ref lr) = diff.line_ranges {
738            assert!(
739                lr[0].start >= 90,
740                "line_ranges should be document-absolute (>= 90), got {:?}",
741                lr
742            );
743        }
744    }
745
746    /// After rebalance, Arc sharing is destroyed. The diff must still produce
747    /// the same byte_ranges and line_ranges as the path-copy version.
748    #[test]
749    fn diff_after_rebalance_matches_path_copy_diff() {
750        use crate::model::piece_tree::{PieceTree, StringBuffer};
751
752        let chunk_size = 1000;
753        let total = 10_000;
754        let content: Vec<u8> = (0..total)
755            .map(|i| if i % 100 == 99 { b'\n' } else { b'A' })
756            .collect();
757        let buf = StringBuffer::new_loaded(0, content, false);
758
759        let mut saved_tree = PieceTree::new(BufferLocation::Stored(0), 0, total, None);
760        saved_tree.split_leaves_to_chunk_size(chunk_size);
761        let lf_updates: Vec<(usize, usize)> = (0..10).map(|i| (i, 10)).collect();
762        saved_tree.update_leaf_line_feeds(&lf_updates);
763        let saved_root = saved_tree.root();
764
765        // Insert near EOF via path-copy.
766        let mut after_tree = saved_tree;
767        let insert_buf = StringBuffer::new_loaded(1, b"HELLO".to_vec(), false);
768        after_tree.insert(
769            total - 100,
770            BufferLocation::Added(1),
771            0,
772            5,
773            Some(0),
774            &[buf.clone(), insert_buf.clone()],
775        );
776
777        // Diff with Arc sharing (path-copy).
778        let diff_shared = diff_piece_trees(&saved_root, &after_tree.root(), &count_line_feeds);
779
780        // Rebalance → destroys all Arc sharing.
781        after_tree.rebalance();
782        let diff_rebalanced = diff_piece_trees(&saved_root, &after_tree.root(), &count_line_feeds);
783
784        assert!(!diff_shared.equal);
785        assert!(!diff_rebalanced.equal);
786        assert_eq!(
787            diff_shared.byte_ranges, diff_rebalanced.byte_ranges,
788            "byte_ranges should be identical whether or not Arc sharing exists"
789        );
790        assert_eq!(
791            diff_shared.line_ranges, diff_rebalanced.line_ranges,
792            "line_ranges should be identical whether or not Arc sharing exists"
793        );
794    }
795
796    #[test]
797    fn tolerates_split_leaves_with_same_content_prefix() {
798        let before = build(&[leaf(BufferLocation::Stored(0), 0, 100, Some(1))]);
799        let after = build(&[
800            leaf(BufferLocation::Stored(0), 0, 50, Some(0)),
801            leaf(BufferLocation::Added(1), 0, 10, Some(0)),
802            leaf(BufferLocation::Stored(0), 50, 50, Some(1)),
803        ]);
804
805        let diff = diff_piece_trees(&before, &after, &count_line_feeds);
806        assert!(!diff.equal);
807        // Only the inserted span should be marked.
808        assert_eq!(diff.byte_ranges, vec![50..60]);
809    }
810
811    #[test]
812    fn diff_with_disjoint_changes_reports_correct_line_numbers() {
813        let leaf1_before = leaf(BufferLocation::Stored(0), 0, 10, Some(0));
814        let leaf2 = leaf(BufferLocation::Stored(0), 10, 10, Some(1)); // Has 1 LF
815        let leaf3_before = leaf(BufferLocation::Stored(0), 20, 10, Some(0));
816
817        let leaf1_after = leaf(BufferLocation::Added(1), 0, 10, Some(0));
818        let leaf3_after = leaf(BufferLocation::Added(1), 10, 10, Some(0));
819
820        // Manually build trees to ensure structural sharing of leaf2
821        let leaf2_arc = Arc::new(PieceTreeNode::Leaf {
822            location: leaf2.location,
823            offset: leaf2.offset,
824            bytes: leaf2.bytes,
825            line_feed_cnt: leaf2.line_feed_cnt,
826        });
827
828        let before = Arc::new(PieceTreeNode::Internal {
829            left_bytes: 10,
830            lf_left: Some(0),
831            left: Arc::new(PieceTreeNode::Leaf {
832                location: leaf1_before.location,
833                offset: leaf1_before.offset,
834                bytes: leaf1_before.bytes,
835                line_feed_cnt: leaf1_before.line_feed_cnt,
836            }),
837            right: Arc::new(PieceTreeNode::Internal {
838                left_bytes: 10,
839                lf_left: Some(1),
840                left: Arc::clone(&leaf2_arc),
841                right: Arc::new(PieceTreeNode::Leaf {
842                    location: leaf3_before.location,
843                    offset: leaf3_before.offset,
844                    bytes: leaf3_before.bytes,
845                    line_feed_cnt: leaf3_before.line_feed_cnt,
846                }),
847            }),
848        });
849
850        let after = Arc::new(PieceTreeNode::Internal {
851            left_bytes: 10,
852            lf_left: Some(0),
853            left: Arc::new(PieceTreeNode::Leaf {
854                location: leaf1_after.location,
855                offset: leaf1_after.offset,
856                bytes: leaf1_after.bytes,
857                line_feed_cnt: leaf1_after.line_feed_cnt,
858            }),
859            right: Arc::new(PieceTreeNode::Internal {
860                left_bytes: 10,
861                lf_left: Some(1),
862                left: Arc::clone(&leaf2_arc), // Shared!
863                right: Arc::new(PieceTreeNode::Leaf {
864                    location: leaf3_after.location,
865                    offset: leaf3_after.offset,
866                    bytes: leaf3_after.bytes,
867                    line_feed_cnt: leaf3_after.line_feed_cnt,
868                }),
869            }),
870        });
871
872        let diff = diff_piece_trees(&before, &after, &count_line_feeds);
873
874        assert_eq!(diff.byte_ranges, vec![0..10, 20..30]);
875        // Change 1 is at line 0.
876        // leaf2 has 1 LF, so Change 2 (leaf3) starts at line 1.
877        assert_eq!(
878            diff.line_ranges,
879            Some(vec![0..1, 1..2]),
880            "Change after a skipped subtree with line feeds should have correct line number"
881        );
882    }
883}