Skip to main content

fresh/model/
marker_tree.rs

1use std::cell::RefCell;
2use std::cmp::{max, Ordering};
3use std::collections::HashMap;
4use std::mem;
5use std::rc::{Rc, Weak};
6
7/// Use a simple u64 for marker IDs
8pub type MarkerId = u64;
9
10// ---
11// 1. Core Data Structures and Pointers
12// ---
13
14#[derive(Debug, Clone, PartialEq)]
15pub struct Interval {
16    pub start: u64,
17    pub end: u64,
18}
19
20/// Type of marker - either a position marker or a line anchor
21#[derive(Debug, Clone, PartialEq)]
22pub enum MarkerType {
23    /// Regular position marker (for overlays, cursors, etc.)
24    Position,
25    /// Line anchor with estimated/exact line number
26    LineAnchor {
27        estimated_line: usize,
28        confidence: AnchorConfidence,
29    },
30}
31
32/// Confidence level for line anchor estimates
33#[derive(Debug, Clone, PartialEq)]
34pub enum AnchorConfidence {
35    /// Exact line number (scanned from known position)
36    Exact,
37    /// Estimated from average line length
38    Estimated,
39    /// Relative to another anchor
40    Relative(MarkerId),
41}
42
43#[derive(Debug, Clone, PartialEq)]
44pub struct Marker {
45    pub id: MarkerId,
46    pub interval: Interval,
47    pub marker_type: MarkerType,
48    /// Insertion gravity at the marker's exact position.
49    /// - `true` (right gravity, default): text inserted exactly at the marker
50    ///   position pushes the marker forward (the marker moves after the text).
51    /// - `false` (left gravity): text inserted exactly at the marker position
52    ///   leaves it in place (the marker stays before the text).
53    ///
54    /// Only matters for insertions landing on the marker's own position;
55    /// insertions strictly before always shift it, insertions strictly after
56    /// never do. Used to keep search-match highlights from growing when text
57    /// is typed immediately after a match (issue #2053).
58    pub right_gravity: bool,
59}
60
61/// A Strong pointer to a tree node (child/sibling/map reference)
62type NodePtr = Option<Rc<RefCell<Node>>>;
63/// A Weak pointer to a tree node (parent reference, doesn't count for ownership)
64type WeakNodePtr = Weak<RefCell<Node>>;
65
66/// The internal tree node
67#[derive(Debug)]
68struct Node {
69    pub marker: Marker,
70
71    /// AVL: Height of this node's subtree
72    pub height: i32,
73    /// Augmentation: The max 'end' value in this node's subtree
74    pub max_end: u64,
75    /// VSCode-style: The delta to be applied to this node and its children
76    pub lazy_delta: i64,
77
78    pub parent: WeakNodePtr,
79    pub left: NodePtr,
80    pub right: NodePtr,
81}
82
83/// The main Interval Tree structure
84#[derive(Debug, Default)]
85pub struct IntervalTree {
86    root: NodePtr,
87    next_id: u64,
88    /// ID-to-Node map for O(1) lookups
89    marker_map: HashMap<MarkerId, Rc<RefCell<Node>>>,
90}
91
92// ---
93// 2. Node Helpers (Pushing Deltas, Stats, Heights)
94// ---
95
96impl Node {
97    fn new(marker: Marker, parent: WeakNodePtr) -> Rc<RefCell<Self>> {
98        // Fix E0382: Calculate max_end before moving ownership of `marker` into the struct.
99        let max_end_val = marker.interval.end;
100
101        Rc::new(RefCell::new(Node {
102            marker,
103            height: 1,
104            max_end: max_end_val,
105            lazy_delta: 0,
106            parent,
107            left: None,
108            right: None,
109        }))
110    }
111
112    /// Gets the height of a node (0 for None).
113    fn height(node: &NodePtr) -> i32 {
114        node.as_ref().map_or(0, |n| n.borrow().height)
115    }
116
117    /// Calculates the balance factor of a node (height(left) - height(right)).
118    fn balance_factor(node: &Rc<RefCell<Self>>) -> i32 {
119        let n = node.borrow();
120        Self::height(&n.left) - Self::height(&n.right)
121    }
122
123    /// Pushes this node's lazy_delta down to its immediate children.
124    fn push_delta(node_rc: &Rc<RefCell<Self>>) {
125        let mut node = node_rc.borrow_mut();
126        if node.lazy_delta == 0 {
127            return;
128        }
129
130        let delta = node.lazy_delta;
131
132        // Apply delta to self (start and end)
133        node.marker.interval.start = (node.marker.interval.start as i64 + delta) as u64;
134        node.marker.interval.end = (node.marker.interval.end as i64 + delta) as u64;
135
136        // Apply delta to children (only update their lazy_delta fields)
137        if let Some(ref left) = node.left {
138            left.borrow_mut().lazy_delta += delta;
139        }
140        if let Some(ref right) = node.right {
141            right.borrow_mut().lazy_delta += delta;
142        }
143
144        node.lazy_delta = 0;
145
146        // The max_end needs to be updated after the push
147        let max_l = node.left.as_ref().map_or(0, |l| l.borrow().max_end);
148        let max_r = node.right.as_ref().map_or(0, |r| r.borrow().max_end);
149        node.max_end = max(node.marker.interval.end, max(max_l, max_r));
150    }
151
152    /// Updates a node's height and max_end based on its children.
153    fn update_stats(node: &Rc<RefCell<Self>>) {
154        let mut n = node.borrow_mut();
155        let height_l = Self::height(&n.left);
156        let height_r = Self::height(&n.right);
157
158        n.height = 1 + max(height_l, height_r);
159
160        let max_l = n.left.as_ref().map_or(0, |l| l.borrow().max_end);
161        let max_r = n.right.as_ref().map_or(0, |r| r.borrow().max_end);
162        n.max_end = max(n.marker.interval.end, max(max_l, max_r));
163    }
164}
165
166// ---
167// 3. Main Public API
168// ---
169
170impl IntervalTree {
171    pub fn new() -> Self {
172        Self::default()
173    }
174
175    /// Inserts a new marker interval. Performance: O(log n)
176    pub fn insert(&mut self, start: u64, end: u64) -> MarkerId {
177        self.insert_with_type(start, end, MarkerType::Position)
178    }
179
180    /// Inserts a new left-gravity marker interval: text inserted exactly at the
181    /// marker's position leaves it in place rather than pushing it forward.
182    /// Performance: O(log n)
183    pub fn insert_left_gravity(&mut self, start: u64, end: u64) -> MarkerId {
184        self.insert_full(start, end, MarkerType::Position, false)
185    }
186
187    /// Insert a marker with a specific ID and type (for set_position).
188    /// The caller must ensure the ID is not already in use.
189    fn insert_with_id(
190        &mut self,
191        id: MarkerId,
192        start: u64,
193        end: u64,
194        marker_type: MarkerType,
195        right_gravity: bool,
196    ) {
197        debug_assert!(
198            id < self.next_id,
199            "insert_with_id: id {} must be < next_id {}",
200            id,
201            self.next_id
202        );
203        debug_assert!(
204            !self.marker_map.contains_key(&id),
205            "insert_with_id: id {} already in use",
206            id
207        );
208        let marker = Marker {
209            id,
210            interval: Interval { start, end },
211            marker_type,
212            right_gravity,
213        };
214
215        let new_node = Node::new(marker.clone(), Weak::new());
216        self.root = Self::insert_recursive(self.root.take(), new_node.clone());
217        self.marker_map.insert(id, new_node);
218    }
219
220    /// Insert a marker with a specific type (right gravity).
221    pub fn insert_with_type(&mut self, start: u64, end: u64, marker_type: MarkerType) -> MarkerId {
222        self.insert_full(start, end, marker_type, true)
223    }
224
225    /// Insert a marker with a specific type and gravity.
226    fn insert_full(
227        &mut self,
228        start: u64,
229        end: u64,
230        marker_type: MarkerType,
231        right_gravity: bool,
232    ) -> MarkerId {
233        let id = self.next_id;
234        self.next_id += 1;
235        let marker = Marker {
236            id,
237            interval: Interval { start, end },
238            marker_type,
239            right_gravity,
240        };
241
242        let new_node = Node::new(marker.clone(), Weak::new());
243        self.root = Self::insert_recursive(self.root.take(), new_node.clone());
244
245        self.marker_map.insert(id, new_node);
246        id
247    }
248
249    /// Insert a line anchor at a specific position
250    pub fn insert_line_anchor(
251        &mut self,
252        start: u64,
253        end: u64,
254        estimated_line: usize,
255        confidence: AnchorConfidence,
256    ) -> MarkerId {
257        self.insert_with_type(
258            start,
259            end,
260            MarkerType::LineAnchor {
261                estimated_line,
262                confidence,
263            },
264        )
265    }
266
267    /// Finds the current true position of a marker by its ID. Performance: O(log n)
268    pub fn get_position(&self, id: MarkerId) -> Option<(u64, u64)> {
269        let node_rc = self.marker_map.get(&id)?;
270        let mut node_opt = Some(Rc::clone(node_rc));
271        let mut current_delta: i64 = 0;
272
273        // Walk up the tree, collecting all deltas that haven't been applied yet.
274        while let Some(current_rc) = node_opt {
275            let current = current_rc.borrow();
276
277            // Add this node's delta (if any)
278            current_delta += current.lazy_delta;
279
280            // Move up to the parent
281            node_opt = current.parent.upgrade();
282        }
283
284        let raw_marker = node_rc.borrow().marker.interval.clone();
285
286        let start = (raw_marker.start as i64 + current_delta) as u64;
287        let end = (raw_marker.end as i64 + current_delta) as u64;
288
289        Some((start, end))
290    }
291
292    /// Deletes a marker by its ID. Performance: O(log n)
293    ///
294    /// Locates the node via `marker_map` and removes it using parent pointers
295    /// rather than a `(start, id)` key search. Edits can transiently leave the
296    /// tree position-ordered but *not* `(start, id)`-ordered — e.g. a deletion
297    /// clamps two markers to the same position, and their ids contradict the
298    /// order they reached that position. A key-routed delete would then take a
299    /// wrong turn and silently fail to remove the node; identity-based removal
300    /// is immune to that.
301    pub fn delete(&mut self, id: MarkerId) -> bool {
302        let node_rc = match self.marker_map.get(&id) {
303            Some(n) => Rc::clone(n),
304            None => return false,
305        };
306
307        // Flush pending lazy deltas from the root down to this node so the node
308        // (and every ancestor) holds its true interval with no pending ancestor
309        // delta, making structural surgery safe.
310        Self::push_to_node(&node_rc);
311
312        // Decide which physical node to splice out. With two children we swap
313        // this node's marker with its in-order successor (which has no left
314        // child) and remove the successor's node instead.
315        let two_children = {
316            let n = node_rc.borrow();
317            n.left.is_some() && n.right.is_some()
318        };
319
320        let remove_rc = if two_children {
321            let right = node_rc.borrow().right.as_ref().unwrap().clone();
322            let succ = Self::min_node(&right);
323            let succ_id = succ.borrow().marker.id;
324
325            mem::swap(
326                &mut node_rc.borrow_mut().marker,
327                &mut succ.borrow_mut().marker,
328            );
329            // `node_rc` now carries the successor's marker; redirect the map.
330            self.marker_map.insert(succ_id, Rc::clone(&node_rc));
331            succ
332        } else {
333            Rc::clone(&node_rc)
334        };
335
336        // `remove_rc` now has at most one child. Splice that child (if any) into
337        // remove_rc's slot under its parent.
338        let child = {
339            let mut rb = remove_rc.borrow_mut();
340            rb.left.take().or_else(|| rb.right.take())
341        };
342        let parent = remove_rc.borrow().parent.upgrade();
343        if let Some(ref ch) = child {
344            ch.borrow_mut().parent = remove_rc.borrow().parent.clone();
345        }
346        match parent {
347            None => self.root = child,
348            Some(ref p) => {
349                let is_left = p
350                    .borrow()
351                    .left
352                    .as_ref()
353                    .is_some_and(|l| Rc::ptr_eq(l, &remove_rc));
354                if is_left {
355                    p.borrow_mut().left = child;
356                } else {
357                    p.borrow_mut().right = child;
358                }
359            }
360        }
361
362        self.marker_map.remove(&id);
363
364        // Rebalance from the splice point up to the root.
365        self.rebalance_upward(parent);
366        true
367    }
368
369    /// Pushes pending lazy deltas from the root down to (and including)
370    /// `node_rc`, so the node holds its true interval and all ancestors on the
371    /// path have a zero `lazy_delta`.
372    fn push_to_node(node_rc: &Rc<RefCell<Node>>) {
373        let mut path: Vec<Rc<RefCell<Node>>> = Vec::new();
374        let mut cur = Some(Rc::clone(node_rc));
375        while let Some(c) = cur {
376            let parent = c.borrow().parent.upgrade();
377            path.push(c);
378            cur = parent;
379        }
380        for n in path.into_iter().rev() {
381            Node::push_delta(&n);
382        }
383    }
384
385    /// Walks from `start` up to the root, refreshing stats and AVL-balancing
386    /// each node, fixing the parent links and `self.root` as subtrees rotate.
387    fn rebalance_upward(&mut self, start: NodePtr) {
388        let mut cur = start;
389        while let Some(n) = cur {
390            let parent = n.borrow().parent.upgrade();
391            Node::update_stats(&n);
392            let new_sub = Self::balance(Rc::clone(&n));
393            match &parent {
394                None => {
395                    if let Some(ref ns) = new_sub {
396                        ns.borrow_mut().parent = Weak::new();
397                    }
398                    self.root = new_sub;
399                }
400                Some(p) => {
401                    let is_left = p.borrow().left.as_ref().is_some_and(|l| Rc::ptr_eq(l, &n));
402                    if let Some(ref ns) = new_sub {
403                        ns.borrow_mut().parent = Rc::downgrade(p);
404                    }
405                    if is_left {
406                        p.borrow_mut().left = new_sub;
407                    } else {
408                        p.borrow_mut().right = new_sub;
409                    }
410                }
411            }
412            cur = parent;
413        }
414    }
415
416    /// Move a marker to a new position, preserving its ID and type.
417    /// Implemented as delete + reinsert with the same ID.
418    /// Returns false if the marker doesn't exist.
419    /// Performance: O(log n)
420    pub fn set_position(&mut self, id: MarkerId, new_start: u64, new_end: u64) -> bool {
421        // Get the marker's type and gravity before deleting
422        let (marker_type, right_gravity) = match self.get_marker(id) {
423            Some(m) => (m.marker_type, m.right_gravity),
424            None => return false,
425        };
426
427        // Delete from tree
428        if !self.delete(id) {
429            return false;
430        }
431
432        // Reinsert with same ID
433        self.insert_with_id(id, new_start, new_end, marker_type, right_gravity);
434        true
435    }
436
437    /// Adjusts all markers for a text edit (insertion or deletion).
438    /// Performance: O(log n) due to lazy delta propagation.
439    pub fn adjust_for_edit(&mut self, pos: u64, delta: i64) {
440        // Special case: an insertion landing exactly on a position shared by a
441        // left-gravity marker (which stays put) and a right-gravity marker
442        // (which moves forward). The two markers' relative order is *reversed*
443        // by the edit, but this tree is a positional BST keyed on `(start, id)`
444        // and ordered when each marker was inserted — it cannot represent that
445        // reversal in place. Leaving it would corrupt the BST invariant and
446        // make later start-keyed traversals (adjust/delete/query) misroute,
447        // silently dropping edits to the displaced markers.
448        //
449        // To keep the invariant intact: pull the left-gravity "stayers" out of
450        // the tree first (while ordering is still valid so delete can find
451        // them), shift everything else, then re-insert the stayers at their
452        // correct post-edit interval so the BST is rebuilt in proper order.
453        // Only needed when a co-located right-gravity marker actually moves;
454        // otherwise the in-place adjust already keeps stayers correctly placed.
455        if delta > 0 {
456            // Collect every marker whose start is exactly `pos` by descending
457            // the BST on `start` alone (no reliance on the `max_end`
458            // augmentation, which can be transiently stale under lazy-delta
459            // propagation). The tree is position-ordered here, so this is a
460            // reliable O(log n + k) lookup.
461            let mut at_pos: Vec<(MarkerId, u64, bool, MarkerType)> = Vec::new();
462            Self::collect_starts_at(&self.root, 0, pos, &mut at_pos);
463
464            let has_mover = at_pos.iter().any(|(_, _, rg, _)| *rg);
465            let stayers: Vec<(MarkerId, u64, bool, MarkerType)> =
466                at_pos.into_iter().filter(|(_, _, rg, _)| !*rg).collect();
467
468            if has_mover && !stayers.is_empty() {
469                for (id, _, _, _) in &stayers {
470                    self.delete(*id);
471                }
472                Self::adjust_recursive(&mut self.root, pos, delta);
473                for (id, end, _rg, mtype) in stayers {
474                    // Left-gravity: the start stays at `pos`; the end shifts only
475                    // if it is strictly after `pos`, mirroring the gravity logic
476                    // in adjust_recursive.
477                    let new_end = if end > pos {
478                        (end as i64 + delta) as u64
479                    } else {
480                        end
481                    };
482                    self.insert_with_id(id, pos, new_end, mtype, false);
483                }
484                return;
485            }
486        }
487
488        Self::adjust_recursive(&mut self.root, pos, delta);
489    }
490
491    /// Collects `(id, true_end, right_gravity, marker_type)` for every marker
492    /// whose true start equals `pos`. Read-only descent that accumulates lazy
493    /// deltas manually and routes purely on `start`; relies only on the BST's
494    /// position ordering, not on `max_end`.
495    fn collect_starts_at(
496        node: &NodePtr,
497        acc_delta: i64,
498        pos: u64,
499        out: &mut Vec<(MarkerId, u64, bool, MarkerType)>,
500    ) {
501        let Some(n) = node else { return };
502        let nb = n.borrow();
503        let d = acc_delta + nb.lazy_delta;
504        let start = (nb.marker.interval.start as i64 + d) as u64;
505        match pos.cmp(&start) {
506            Ordering::Less => Self::collect_starts_at(&nb.left, d, pos, out),
507            Ordering::Greater => Self::collect_starts_at(&nb.right, d, pos, out),
508            Ordering::Equal => {
509                let end = (nb.marker.interval.end as i64 + d) as u64;
510                out.push((
511                    nb.marker.id,
512                    end,
513                    nb.marker.right_gravity,
514                    nb.marker.marker_type.clone(),
515                ));
516                // Equal-start markers can sit in either subtree (BST tie-break
517                // by id), so search both.
518                Self::collect_starts_at(&nb.left, d, pos, out);
519                Self::collect_starts_at(&nb.right, d, pos, out);
520            }
521        }
522    }
523
524    /// Finds all markers that overlap a given query range.
525    /// Performance: O(log n + k)
526    pub fn query(&self, query_start: u64, query_end: u64) -> Vec<Marker> {
527        let mut results = Vec::new();
528        Self::query_recursive(&self.root, query_start, query_end, &mut results);
529        results
530    }
531
532    /// Get the marker data for a given marker ID
533    pub fn get_marker(&self, id: MarkerId) -> Option<Marker> {
534        let node_rc = self.marker_map.get(&id)?;
535        Some(node_rc.borrow().marker.clone())
536    }
537
538    /// Update a line anchor's estimated line number and confidence
539    pub fn update_line_anchor(
540        &mut self,
541        id: MarkerId,
542        estimated_line: usize,
543        confidence: AnchorConfidence,
544    ) -> bool {
545        if let Some(node_rc) = self.marker_map.get(&id) {
546            let mut node = node_rc.borrow_mut();
547            node.marker.marker_type = MarkerType::LineAnchor {
548                estimated_line,
549                confidence,
550            };
551            true
552        } else {
553            false
554        }
555    }
556
557    /// Query only line anchors in a range
558    pub fn query_line_anchors(&self, query_start: u64, query_end: u64) -> Vec<Marker> {
559        self.query(query_start, query_end)
560            .into_iter()
561            .filter(|m| matches!(m.marker_type, MarkerType::LineAnchor { .. }))
562            .collect()
563    }
564}
565
566// ---
567// 4. Recursive Implementation Details (Insert, Delete, Adjust)
568// ---
569
570impl IntervalTree {
571    /// Recursive helper for insert
572    fn insert_recursive(root: NodePtr, new_node: Rc<RefCell<Node>>) -> NodePtr {
573        // Remove unnecessary 'mut'
574        let root = match root {
575            Some(r) => r,
576            None => return Some(new_node),
577        };
578
579        Node::push_delta(&root);
580
581        let (start, id) = (
582            new_node.borrow().marker.interval.start,
583            new_node.borrow().marker.id,
584        );
585
586        let mut root_mut = root.borrow_mut();
587        let (root_start, root_id) = (root_mut.marker.interval.start, root_mut.marker.id);
588
589        if start < root_start || (start == root_start && id < root_id) {
590            root_mut.left = Self::insert_recursive(root_mut.left.take(), Rc::clone(&new_node));
591            root_mut.left.as_ref().unwrap().borrow_mut().parent = Rc::downgrade(&root);
592        } else {
593            root_mut.right = Self::insert_recursive(root_mut.right.take(), Rc::clone(&new_node));
594            root_mut.right.as_ref().unwrap().borrow_mut().parent = Rc::downgrade(&root);
595        }
596
597        drop(root_mut);
598        Node::update_stats(&root);
599        Self::balance(root)
600    }
601
602    /// Finds the minimum node in a subtree (for deletion)
603    fn min_node(node_rc: &Rc<RefCell<Node>>) -> Rc<RefCell<Node>> {
604        let mut current = Rc::clone(node_rc);
605        loop {
606            Node::push_delta(&current);
607
608            // Fix E0506: Clone the next node pointer before the borrow (Ref<Node>) on
609            // `current` is dropped and potentially prevents reassignment.
610            let next_left_opt = current.borrow().left.clone();
611
612            if let Some(next) = next_left_opt {
613                current = next;
614            } else {
615                break current;
616            }
617        }
618    }
619
620    /// CORRECTED Recursive helper for `adjust_for_edit` (O(log n) lazy update)
621    fn adjust_recursive(node_opt: &mut NodePtr, pos: u64, delta: i64) {
622        let node_rc = match node_opt {
623            Some(n) => n,
624            None => return,
625        };
626
627        Node::push_delta(node_rc);
628
629        let mut node = node_rc.borrow_mut();
630        let start = node.marker.interval.start;
631        // Left-gravity markers stay put when text is inserted exactly at their
632        // position (the insertion goes after them); right-gravity markers (the
633        // default) are pushed forward. Gravity only changes behaviour for
634        // insertions (delta > 0) landing exactly on the boundary.
635        let left_gravity = !node.marker.right_gravity;
636
637        if pos <= start {
638            // CASE 1: Edit is at or before this node's start.
639            // This node and everything to its right must be shifted.
640
641            // Whether this node's own start should shift. A left-gravity marker
642            // does NOT move when the insertion lands exactly on it.
643            let stay_put = left_gravity && delta > 0 && pos == start;
644
645            // 1. Shift the current node's start position directly, clamping at `pos` if needed.
646            if !stay_put {
647                if delta < 0 {
648                    node.marker.interval.start = (start as i64 + delta).max(pos as i64) as u64;
649                } else {
650                    node.marker.interval.start = (start as i64 + delta) as u64;
651                }
652            }
653
654            // 2. Handle the right subtree.
655            // For insertions strictly before this node's start, every node to
656            // the right has start > pos and shifts uniformly, so lazy
657            // propagation is safe and efficient. When the insertion lands
658            // exactly on this node's start (pos == start), the right subtree may
659            // contain other markers also sitting at `pos` whose gravity must be
660            // respected individually, so recurse instead of shifting them all.
661            // Deletions always recurse so nodes can clamp to `pos`.
662            if delta < 0 || pos == start {
663                Self::adjust_recursive(&mut node.right, pos, delta);
664            } else if let Some(ref right) = node.right {
665                right.borrow_mut().lazy_delta += delta;
666            }
667
668            // 3. Recurse left, as it may contain markers spanning the edit pos.
669            Self::adjust_recursive(&mut node.left, pos, delta);
670        } else {
671            // pos > start
672            // CASE 2: This node's start is BEFORE the edit.
673            // Its start is unaffected. We only need to check the right subtree
674            // for nodes that might be affected.
675            Self::adjust_recursive(&mut node.right, pos, delta);
676        }
677
678        // Handle the interval span case (where the edit falls inside [start, end]).
679        // A left-gravity marker's end stays put for an insertion landing exactly
680        // on it, matching its start behaviour above.
681        let end = node.marker.interval.end;
682        let shift_end = if left_gravity && delta > 0 {
683            end > pos
684        } else {
685            end >= pos
686        };
687        if shift_end {
688            node.marker.interval.end =
689                (end as i64 + delta).max(node.marker.interval.start as i64) as u64;
690        }
691
692        drop(node);
693        Node::update_stats(node_rc);
694    }
695
696    /// Recursive helper for query
697    fn query_recursive(
698        node_opt: &NodePtr,
699        query_start: u64,
700        query_end: u64,
701        results: &mut Vec<Marker>,
702    ) {
703        let node_rc = match node_opt {
704            Some(n) => n,
705            None => return,
706        };
707
708        Node::push_delta(node_rc);
709        let node = node_rc.borrow();
710
711        let i = &node.marker.interval;
712        if i.start <= query_end && i.end >= query_start {
713            results.push(node.marker.clone());
714        }
715
716        if node.left.is_some() && node.left.as_ref().unwrap().borrow().max_end >= query_start {
717            Self::query_recursive(&node.left, query_start, query_end, results);
718        }
719
720        if node.right.is_some() && node.marker.interval.start <= query_end {
721            Self::query_recursive(&node.right, query_start, query_end, results);
722        }
723    }
724
725    // --- AVL Balancing ---
726
727    fn balance(node: Rc<RefCell<Node>>) -> NodePtr {
728        let bf = Node::balance_factor(&node);
729
730        if bf > 1 {
731            let left_rc = node.borrow().left.as_ref().unwrap().clone();
732            if Node::balance_factor(&left_rc) < 0 {
733                // Fix RefCell borrow issue: extract left child before rotating
734                let left_child = node.borrow_mut().left.take().unwrap();
735                node.borrow_mut().left = Self::rotate_left(left_child);
736            }
737            Self::rotate_right(node)
738        } else if bf < -1 {
739            let right_rc = node.borrow().right.as_ref().unwrap().clone();
740            if Node::balance_factor(&right_rc) > 0 {
741                // Fix RefCell borrow issue: extract right child before rotating
742                let right_child = node.borrow_mut().right.take().unwrap();
743                node.borrow_mut().right = Self::rotate_right(right_child);
744            }
745            Self::rotate_left(node)
746        } else {
747            Some(node)
748        }
749    }
750
751    fn rotate_left(node_rc: Rc<RefCell<Node>>) -> NodePtr {
752        Node::push_delta(&node_rc);
753        let x_rc = node_rc.borrow_mut().right.take().unwrap();
754        Node::push_delta(&x_rc);
755
756        let mut y = node_rc.borrow_mut();
757        let mut x = x_rc.borrow_mut();
758
759        y.right = x.left.take();
760        if let Some(ref r) = y.right {
761            r.borrow_mut().parent = Rc::downgrade(&node_rc);
762        }
763        x.left = Some(Rc::clone(&node_rc));
764        x.parent = y.parent.clone();
765        y.parent = Rc::downgrade(&x_rc);
766
767        drop(x);
768        drop(y);
769
770        Node::update_stats(&node_rc);
771        Node::update_stats(&x_rc);
772        Some(x_rc)
773    }
774
775    fn rotate_right(node_rc: Rc<RefCell<Node>>) -> NodePtr {
776        Node::push_delta(&node_rc);
777        let x_rc = node_rc.borrow_mut().left.take().unwrap();
778        Node::push_delta(&x_rc);
779
780        let mut y = node_rc.borrow_mut();
781        let mut x = x_rc.borrow_mut();
782
783        y.left = x.right.take();
784        if let Some(ref l) = y.left {
785            l.borrow_mut().parent = Rc::downgrade(&node_rc);
786        }
787        x.right = Some(Rc::clone(&node_rc));
788        x.parent = y.parent.clone();
789        y.parent = Rc::downgrade(&x_rc);
790
791        drop(x);
792        drop(y);
793
794        Node::update_stats(&node_rc);
795        Node::update_stats(&x_rc);
796        Some(x_rc)
797    }
798}
799
800#[cfg(test)]
801impl IntervalTree {
802    fn debug_dump(&self) -> Vec<(MarkerId, u64, u64, bool)> {
803        let mut out = Vec::new();
804        Self::debug_collect(&self.root, 0, &mut out);
805        out
806    }
807    fn debug_collect(node: &NodePtr, ad: i64, out: &mut Vec<(MarkerId, u64, u64, bool)>) {
808        if let Some(n) = node {
809            let nb = n.borrow();
810            let d = ad + nb.lazy_delta;
811            Self::debug_collect(&nb.left, d, out);
812            out.push((
813                nb.marker.id,
814                (nb.marker.interval.start as i64 + d) as u64,
815                (nb.marker.interval.end as i64 + d) as u64,
816                nb.marker.right_gravity,
817            ));
818            Self::debug_collect(&nb.right, d, out);
819        }
820    }
821}
822
823#[cfg(test)]
824mod tests {
825    use super::*;
826
827    /// Helper to insert and return the ID, making test setup cleaner.
828    fn insert_marker(tree: &mut IntervalTree, start: u64, end: u64) -> MarkerId {
829        tree.insert(start, end)
830    }
831
832    /// Helper to get position and unwrap, or panic with a clear message.
833    fn get_pos(tree: &IntervalTree, id: MarkerId) -> (u64, u64) {
834        tree.get_position(id)
835            .unwrap_or_else(|| panic!("Marker ID {} not found.", id))
836    }
837
838    // --- Insertion gravity (issue #2053) ---
839
840    #[test]
841    fn test_left_gravity_marker_stays_on_insert_at_position() {
842        // A left-gravity point marker models the end of a fixed-width
843        // highlight (e.g. a search match). Inserting text exactly at its
844        // position must NOT move it, so the highlight does not grow.
845        let mut tree = IntervalTree::new();
846        let m = tree.insert_left_gravity(3, 3);
847
848        // Insert 4 bytes immediately at the marker.
849        tree.adjust_for_edit(3, 4);
850
851        assert_eq!(
852            get_pos(&tree, m),
853            (3, 3),
854            "left-gravity marker must stay put when text is inserted at its position"
855        );
856    }
857
858    #[test]
859    fn test_right_gravity_marker_moves_on_insert_at_position() {
860        // The default (right gravity) point marker moves forward when text is
861        // inserted exactly at its position.
862        let mut tree = IntervalTree::new();
863        let m = tree.insert(3, 3);
864
865        tree.adjust_for_edit(3, 4);
866
867        assert_eq!(get_pos(&tree, m), (7, 7));
868    }
869
870    #[test]
871    fn test_left_gravity_marker_still_shifts_on_insert_before() {
872        // Left gravity only changes the exact-boundary case; insertions
873        // strictly before the marker still shift it.
874        let mut tree = IntervalTree::new();
875        let m = tree.insert_left_gravity(5, 5);
876
877        tree.adjust_for_edit(2, 3);
878
879        assert_eq!(get_pos(&tree, m), (8, 8));
880    }
881
882    #[test]
883    fn test_search_match_does_not_grow_on_adjacent_insert() {
884        // Reproduces #2053 at the marker level: a match highlight spanning
885        // [0, 3) is modelled by a right-gravity start marker at 0 and a
886        // left-gravity end marker at 3. Typing immediately after the match
887        // (at position 3) must leave both markers anchored to the match.
888        let mut tree = IntervalTree::new();
889        let start = tree.insert(0, 0);
890        let end = tree.insert_left_gravity(3, 3);
891
892        // User types "X" right after the match.
893        tree.adjust_for_edit(3, 1);
894
895        assert_eq!(get_pos(&tree, start), (0, 0));
896        assert_eq!(
897            get_pos(&tree, end),
898            (3, 3),
899            "highlight end must not extend over text typed after the match"
900        );
901    }
902
903    #[test]
904    fn test_adjacent_matches_keep_independent_gravity_at_shared_boundary() {
905        // Two adjacent matches (e.g. searching "ab" in "abab") share a
906        // boundary at position 2: the first match's left-gravity end and the
907        // second match's right-gravity start both sit at 2. Inserting there
908        // must keep the first match fixed while the second match shifts.
909        let mut tree = IntervalTree::new();
910        let m1_start = tree.insert(0, 0);
911        let m1_end = tree.insert_left_gravity(2, 2);
912        let m2_start = tree.insert(2, 2);
913        let m2_end = tree.insert_left_gravity(4, 4);
914
915        // Insert 3 bytes exactly at the shared boundary.
916        tree.adjust_for_edit(2, 3);
917
918        assert_eq!(get_pos(&tree, m1_start), (0, 0));
919        assert_eq!(get_pos(&tree, m1_end), (2, 2), "first match must not grow");
920        assert_eq!(get_pos(&tree, m2_start), (5, 5), "second match shifts");
921        assert_eq!(get_pos(&tree, m2_end), (7, 7));
922    }
923
924    #[test]
925    fn test_gravity_reversal_preserves_bst_for_later_edits() {
926        // Regression for prop_shadow_model_matches_tree: a left-gravity marker
927        // and a right-gravity marker created in id-order at distinct positions
928        // can later collide on the same position. An insertion there makes the
929        // right-gravity marker hop *past* the left-gravity one, reversing their
930        // relative order. The positional BST must be rebuilt so that a
931        // *subsequent* edit still routes to the displaced marker — the original
932        // bug left the tree corrupt and silently dropped the later deletion.
933        let mut tree = IntervalTree::new();
934
935        // Created in this id order while positions are distinct.
936        let right = tree.insert(10, 10); // right-gravity, lower id
937        let left = tree.insert_left_gravity(11, 11); // left-gravity, higher id
938
939        // Make them collide at position 10: pull `left` back by deleting the
940        // single byte between them.
941        tree.adjust_for_edit(10, -1);
942        assert_eq!(get_pos(&tree, right), (10, 10));
943        assert_eq!(get_pos(&tree, left), (10, 10));
944
945        // Insert at 10. `right` (right-gravity) hops to 15; `left` stays at 10.
946        // Their order is now reversed relative to the (start, id) BST key.
947        tree.adjust_for_edit(10, 5);
948        assert_eq!(get_pos(&tree, left), (10, 10), "left-gravity marker stays");
949        assert_eq!(
950            get_pos(&tree, right),
951            (15, 15),
952            "right-gravity marker moves"
953        );
954
955        // The real test: a later edit between the two positions must still
956        // reach `right`. Before the fix this deletion never visited `right`'s
957        // node because the BST was corrupt, leaving it stuck at 15.
958        tree.adjust_for_edit(12, -1);
959        assert_eq!(
960            get_pos(&tree, right),
961            (14, 14),
962            "later deletion must still route to the displaced marker"
963        );
964        assert_eq!(get_pos(&tree, left), (10, 10));
965    }
966
967    #[test]
968    fn test_gravity_reversal_after_clamp_with_unordered_ids() {
969        // Production markers get ids in *creation* order, not position order, so
970        // a higher-id marker can sit at a lower position. A deletion can then
971        // clamp two markers to the same point, and a following insertion there
972        // (left-gravity stayer + right-gravity mover) must still be handled
973        // without losing or duplicating a marker.
974        let mut tree = IntervalTree::new();
975        let a = tree.insert(100, 100); // id 0, right-gravity, higher position
976        let b = tree.insert_left_gravity(50, 50); // id 1, left-gravity, lower position
977
978        // Clamp both to position 50.
979        tree.adjust_for_edit(50, -60);
980        assert_eq!(get_pos(&tree, a), (50, 50));
981        assert_eq!(get_pos(&tree, b), (50, 50));
982
983        // Insert at 50: `a` (right-gravity) moves to 55, `b` (left-gravity) stays.
984        tree.adjust_for_edit(50, 5);
985        assert_eq!(get_pos(&tree, b), (50, 50), "left-gravity stayer");
986        assert_eq!(get_pos(&tree, a), (55, 55), "right-gravity mover");
987
988        // Both markers must still be present and a later edit must route to both.
989        tree.adjust_for_edit(52, -1);
990        assert_eq!(get_pos(&tree, a), (54, 54));
991        assert_eq!(get_pos(&tree, b), (50, 50));
992
993        // The tree must hold exactly two physical nodes — no orphan/duplicate
994        // left behind by a misrouted internal delete.
995        assert_eq!(
996            tree.debug_dump().len(),
997            2,
998            "tree leaked a duplicate node: {:?}",
999            tree.debug_dump()
1000        );
1001    }
1002
1003    #[test]
1004    fn test_initial_insert_and_delete() {
1005        let mut tree = IntervalTree::new();
1006        let id1 = insert_marker(&mut tree, 10, 20);
1007        let id2 = insert_marker(&mut tree, 30, 40);
1008
1009        assert_eq!(get_pos(&tree, id1), (10, 20));
1010        assert_eq!(get_pos(&tree, id2), (30, 40));
1011
1012        assert!(tree.delete(id1));
1013        assert_eq!(tree.get_position(id1), None);
1014        assert_eq!(get_pos(&tree, id2), (30, 40));
1015    }
1016
1017    #[test]
1018    fn test_basic_edit_adjustment() {
1019        let mut tree = IntervalTree::new();
1020        let id1 = insert_marker(&mut tree, 10, 20); // Before edit
1021        let id2 = insert_marker(&mut tree, 30, 40); // At/After edit
1022
1023        // Insert 5 characters at position 30
1024        tree.adjust_for_edit(30, 5);
1025
1026        // id1 (10-20) should not move
1027        assert_eq!(
1028            get_pos(&tree, id1),
1029            (10, 20),
1030            "Marker before edit should not move."
1031        );
1032
1033        // id2 (30-40) should move to (35-45)
1034        assert_eq!(
1035            get_pos(&tree, id2),
1036            (35, 45),
1037            "Marker at/after edit should move."
1038        );
1039
1040        // Delete 10 characters at position 5
1041        tree.adjust_for_edit(5, -10); // All markers are after position 5
1042
1043        // id1 (10-20) is inside the deletion [5, 15) and should be clamped and shrunk.
1044        assert_eq!(
1045            get_pos(&tree, id1),
1046            (5, 10),
1047            "Marker moved back by deletion."
1048        );
1049
1050        // id2 (35-45) -> (25-35)
1051        assert_eq!(
1052            get_pos(&tree, id2),
1053            (25, 35),
1054            "Marker moved back by deletion."
1055        );
1056    }
1057
1058    #[test]
1059    fn test_problematic_lazy_delta_scenario() {
1060        // This test replicates the tricky tree structure to ensure the O(log n) lazy
1061        // delta propagation works correctly and doesn't over-propagate to left children.
1062
1063        let mut tree = IntervalTree::new();
1064
1065        // Setup the tree with specific positions to force a parent/child relationship
1066        // that caused the previous bug:
1067        // L(100) -> P(200) <- R(300)
1068        let id_p = insert_marker(&mut tree, 200, 250); // Parent node (P)
1069        let id_r = insert_marker(&mut tree, 300, 350); // Right child (R)
1070        let id_l = insert_marker(&mut tree, 100, 150); // Left child (L)
1071
1072        // --- Verify initial state ---
1073        assert_eq!(
1074            get_pos(&tree, id_l),
1075            (100, 150),
1076            "L initial position incorrect."
1077        );
1078        assert_eq!(
1079            get_pos(&tree, id_p),
1080            (200, 250),
1081            "P initial position incorrect."
1082        );
1083        assert_eq!(
1084            get_pos(&tree, id_r),
1085            (300, 350),
1086            "R initial position incorrect."
1087        );
1088
1089        // --- Apply the problematic edit ---
1090        // Edit: Insert 50 characters at position 150 (P=150, delta=+50)
1091        // L(100) should NOT move (100 < 150).
1092        // P(200) and R(300) should move (+50).
1093        tree.adjust_for_edit(150, 50);
1094
1095        // --- Verify corrected final state ---
1096
1097        // L(100) should have its end expanded (100 < 150, but 150 >= 150).
1098        assert_eq!(
1099            get_pos(&tree, id_l),
1100            (100, 200),
1101            "L(100) should expand to (100, 200)."
1102        );
1103
1104        // P(200) should be shifted (200 >= 150) -> 250
1105        assert_eq!(
1106            get_pos(&tree, id_p),
1107            (250, 300),
1108            "P(200) did not shift correctly. Should be 250."
1109        );
1110
1111        // R(300) should be shifted (300 >= 150) -> 350
1112        assert_eq!(
1113            get_pos(&tree, id_r),
1114            (350, 400),
1115            "R(300) did not shift correctly. Should be 350."
1116        );
1117    }
1118
1119    #[test]
1120    fn test_interval_spanning_edit() {
1121        let mut tree = IntervalTree::new();
1122        // Marker S starts before edit, but spans it.
1123        let id_s = insert_marker(&mut tree, 50, 200);
1124
1125        // Edit: Insert 10 characters at position 100 (P=100, delta=+10)
1126        tree.adjust_for_edit(100, 10);
1127
1128        // S(50, 200) starts before 100, so its start (50) is fixed.
1129        // Its end (200) is at/after 100, so its end should move to 210.
1130        assert_eq!(
1131            get_pos(&tree, id_s),
1132            (50, 210),
1133            "Spanning marker end did not move correctly."
1134        );
1135    }
1136
1137    #[test]
1138    fn test_deletion_engulfing_marker_start() {
1139        let mut tree = IntervalTree::new();
1140        let id1 = insert_marker(&mut tree, 8, 20);
1141
1142        // Delete 10 chars at pos 5. Deletion is on [5, 15).
1143        // Marker is on [8, 20). The part [8, 15) is deleted.
1144        // New start should be clamped at the deletion position: 5.
1145        // End is adjusted by delta: 20 - 10 = 10.
1146        // So new interval should be (5, 10).
1147        tree.adjust_for_edit(5, -10);
1148
1149        assert_eq!(
1150            get_pos(&tree, id1),
1151            (5, 10),
1152            "Marker should be clamped and shrunk."
1153        );
1154    }
1155
1156    #[test]
1157    fn test_zero_length_marker() {
1158        let mut tree = IntervalTree::new();
1159        let id1 = insert_marker(&mut tree, 10, 10);
1160
1161        // Insertion at the marker's position should push it.
1162        tree.adjust_for_edit(10, 5);
1163        assert_eq!(
1164            get_pos(&tree, id1),
1165            (15, 15),
1166            "Insertion at zero-length marker."
1167        );
1168
1169        // Insertion before should also push it.
1170        tree.adjust_for_edit(5, 5);
1171        assert_eq!(
1172            get_pos(&tree, id1),
1173            (20, 20),
1174            "Insertion before zero-length marker."
1175        );
1176
1177        // Deletion after should not affect it.
1178        tree.adjust_for_edit(25, -5);
1179        assert_eq!(
1180            get_pos(&tree, id1),
1181            (20, 20),
1182            "Deletion after zero-length marker."
1183        );
1184
1185        // Deletion that contains the marker.
1186        tree.adjust_for_edit(15, -10);
1187        // Marker at 20. Deletion on [15, 25).
1188        // Start becomes max(15, 20-10) = 15.
1189        // End becomes max(new_start, 20-10) = max(15, 10) = 15.
1190        assert_eq!(
1191            get_pos(&tree, id1),
1192            (15, 15),
1193            "Deletion containing zero-length marker."
1194        );
1195    }
1196
1197    #[test]
1198    fn test_edit_at_pos_zero() {
1199        let mut tree = IntervalTree::new();
1200        let id1 = insert_marker(&mut tree, 10, 20);
1201
1202        // Insertion at pos 0
1203        tree.adjust_for_edit(0, 5);
1204        assert_eq!(get_pos(&tree, id1), (15, 25), "Insertion at pos 0.");
1205
1206        // Deletion at pos 0
1207        tree.adjust_for_edit(0, -5);
1208        assert_eq!(get_pos(&tree, id1), (10, 20), "Deletion at pos 0.");
1209
1210        // Deletion at pos 0 that engulfs the start.
1211        tree.adjust_for_edit(0, -15);
1212        // Marker at (10, 20). Deletion on [0, 15).
1213        // New start becomes max(0, 10-15) = 0.
1214        // New end becomes max(new_start, 20-15) = max(0, 5) = 5.
1215        assert_eq!(get_pos(&tree, id1), (0, 5), "Engulfing deletion at pos 0.");
1216    }
1217
1218    #[test]
1219    fn test_deletion_preserves_marker_ordering() {
1220        // This test reproduces the bug found in prop_marker_ordering_preserved
1221        // where lazy delta propagation causes ordering violations.
1222        let mut tree = IntervalTree::new();
1223
1224        // Create markers in order: [0, 10, 20, 30, 40] (spacing=10)
1225        let id0 = insert_marker(&mut tree, 0, 0);
1226        let id1 = insert_marker(&mut tree, 10, 10);
1227        let id2 = insert_marker(&mut tree, 20, 20);
1228        let id3 = insert_marker(&mut tree, 30, 30);
1229        let id4 = insert_marker(&mut tree, 40, 40);
1230
1231        // Verify initial state
1232        assert_eq!(get_pos(&tree, id0), (0, 0));
1233        assert_eq!(get_pos(&tree, id1), (10, 10));
1234        assert_eq!(get_pos(&tree, id2), (20, 20));
1235        assert_eq!(get_pos(&tree, id3), (30, 30));
1236        assert_eq!(get_pos(&tree, id4), (40, 40));
1237
1238        // Delete 16 bytes starting at position 5
1239        // This deletes range [5, 21)
1240        // Expected positions after: [0, 5, 5, 14, 24]
1241        tree.adjust_for_edit(5, -16);
1242
1243        // Get all positions
1244        let positions = vec![
1245            get_pos(&tree, id0).0,
1246            get_pos(&tree, id1).0,
1247            get_pos(&tree, id2).0,
1248            get_pos(&tree, id3).0,
1249            get_pos(&tree, id4).0,
1250        ];
1251
1252        // Verify ordering is preserved (no inversions)
1253        for i in 0..positions.len() - 1 {
1254            assert!(
1255                positions[i] <= positions[i + 1],
1256                "Ordering violated at index {}: {:?}[{}]={} > {:?}[{}]={}",
1257                i,
1258                positions,
1259                i,
1260                positions[i],
1261                positions,
1262                i + 1,
1263                positions[i + 1]
1264            );
1265        }
1266
1267        // Verify specific expected positions
1268        assert_eq!(get_pos(&tree, id0), (0, 0), "Marker at 0 should stay at 0");
1269        assert_eq!(
1270            get_pos(&tree, id1),
1271            (5, 5),
1272            "Marker at 10 should clamp to 5"
1273        );
1274        assert_eq!(
1275            get_pos(&tree, id2),
1276            (5, 5),
1277            "Marker at 20 should clamp to 5"
1278        );
1279        assert_eq!(
1280            get_pos(&tree, id3),
1281            (14, 14),
1282            "Marker at 30 should shift to 14"
1283        );
1284        assert_eq!(
1285            get_pos(&tree, id4),
1286            (24, 24),
1287            "Marker at 40 should shift to 24"
1288        );
1289    }
1290
1291    // Property tests exercising the tree directly with creation-order ids
1292    // (decoupled from position), mixed gravity, clamping deletes, and explicit
1293    // marker deletes — the combination that exposed the BST-ordering and
1294    // delete-routing bugs.
1295    mod property_tests {
1296        use super::*;
1297        use proptest::prelude::*;
1298
1299        #[derive(Debug, Clone)]
1300        enum Op {
1301            Insert { pos: u64, len: u64 },
1302            Delete { pos: u64, len: u64 },
1303            CreateMarker { pos: u64, right_gravity: bool },
1304            DeleteMarker { idx: usize },
1305        }
1306
1307        fn arb_op(max: u64) -> impl Strategy<Value = Op> {
1308            prop_oneof![
1309                (0..=max, 1..=50u64).prop_map(|(pos, len)| Op::Insert { pos, len }),
1310                (0..=max, 1..=30u64).prop_map(|(pos, len)| Op::Delete { pos, len }),
1311                (0..=max, any::<bool>())
1312                    .prop_map(|(pos, right_gravity)| Op::CreateMarker { pos, right_gravity }),
1313                (0..200usize).prop_map(|idx| Op::DeleteMarker { idx }),
1314            ]
1315        }
1316
1317        proptest! {
1318            /// The tree's reported positions must always match a naive shadow
1319            /// model that slides/clamps point markers independently, regardless
1320            /// of marker creation order, gravity, clamping, or interleaved
1321            /// marker deletions.
1322            #[test]
1323            fn prop_tree_matches_shadow_with_unordered_ids(
1324                init in prop::collection::vec((0..1000u64, any::<bool>()), 0..15),
1325                ops in prop::collection::vec(arb_op(1000), 1..40),
1326            ) {
1327                let mut tree = IntervalTree::new();
1328                // shadow: (id, Option<pos>, right_gravity)
1329                let mut shadow: Vec<(MarkerId, Option<u64>, bool)> = Vec::new();
1330
1331                for (pos, rg) in init {
1332                    let id = if rg {
1333                        tree.insert(pos, pos)
1334                    } else {
1335                        tree.insert_left_gravity(pos, pos)
1336                    };
1337                    shadow.push((id, Some(pos), rg));
1338                }
1339
1340                for op in ops {
1341                    match op {
1342                        Op::Insert { pos, len } => {
1343                            tree.adjust_for_edit(pos, len as i64);
1344                            for (_id, p, rg) in shadow.iter_mut() {
1345                                if let Some(cur) = p {
1346                                    let shifts = if *rg { *cur >= pos } else { *cur > pos };
1347                                    if shifts {
1348                                        *cur += len;
1349                                    }
1350                                }
1351                            }
1352                        }
1353                        Op::Delete { pos, len } => {
1354                            tree.adjust_for_edit(pos, -(len as i64));
1355                            for (_id, p, _rg) in shadow.iter_mut() {
1356                                if let Some(cur) = p {
1357                                    if *cur >= pos + len {
1358                                        *cur -= len;
1359                                    } else if *cur > pos {
1360                                        *cur = pos;
1361                                    }
1362                                }
1363                            }
1364                        }
1365                        Op::CreateMarker { pos, right_gravity } => {
1366                            let id = if right_gravity {
1367                                tree.insert(pos, pos)
1368                            } else {
1369                                tree.insert_left_gravity(pos, pos)
1370                            };
1371                            shadow.push((id, Some(pos), right_gravity));
1372                        }
1373                        Op::DeleteMarker { idx } => {
1374                            if !shadow.is_empty() {
1375                                let i = idx % shadow.len();
1376                                if let (id, Some(_), _) = shadow[i] {
1377                                    tree.delete(id);
1378                                    shadow[i].1 = None;
1379                                }
1380                            }
1381                        }
1382                    }
1383
1384                    // Every live marker must match its shadow position.
1385                    for (id, p, _rg) in &shadow {
1386                        if let Some(expected) = p {
1387                            let actual = tree.get_position(*id).map(|x| x.0);
1388                            prop_assert_eq!(
1389                                actual,
1390                                Some(*expected),
1391                                "marker {} expected at {} but tree says {:?}",
1392                                id,
1393                                expected,
1394                                actual
1395                            );
1396                        }
1397                    }
1398
1399                    // The tree must remain position-ordered (in-order
1400                    // non-decreasing) and free of leaked/duplicate nodes.
1401                    let dump = tree.debug_dump();
1402                    for w in dump.windows(2) {
1403                        prop_assert!(
1404                            w[0].1 <= w[1].1,
1405                            "BST position order violated: id {}@{} before id {}@{}",
1406                            w[0].0, w[0].1, w[1].0, w[1].1
1407                        );
1408                    }
1409                    let live = shadow.iter().filter(|(_, p, _)| p.is_some()).count();
1410                    prop_assert_eq!(
1411                        dump.len(),
1412                        live,
1413                        "tree node count {} != live marker count {}",
1414                        dump.len(),
1415                        live
1416                    );
1417                }
1418            }
1419        }
1420    }
1421}