Skip to main content

fresh/model/
marker.rs

1/// Marker system for content-anchored positions
2///
3/// This module provides a marker system where markers automatically adjust
4/// their positions when text is inserted or deleted.
5///
6/// **Implementation Note:**
7/// The MarkerList struct provides backward-compatible API using the old Vec-based
8/// implementation (O(n) operations). For performance-critical use cases with many
9/// markers, use IntervalTree directly from marker_tree module (O(log n) operations).
10///
11/// The Vec-based implementation is kept for compatibility and simplicity in
12/// situations where marker count is low (<100).
13use std::collections::HashMap;
14
15use crate::model::marker_tree::IntervalTree;
16
17/// Unique identifier for a marker
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub struct MarkerId(pub u64);
20
21/// Entry in the marker list - either a gap (content bytes) or a marker
22#[derive(Debug, Clone, PartialEq)]
23pub enum MarkerEntry {
24    /// A gap representing N bytes of buffer content
25    Gap(usize),
26
27    /// A marker at this position
28    Marker {
29        id: MarkerId,
30        /// Insertion affinity:
31        /// - true (left): marker stays before text inserted at this position
32        /// - false (right): marker moves after text inserted at this position
33        left_affinity: bool,
34    },
35}
36
37/// Marker list implementation using IntervalTree for O(log n) operations
38///
39/// This provides a backward-compatible API for the old Vec-based implementation,
40/// but uses IntervalTree internally for better performance with many markers.
41///
42/// Point markers (single positions) are represented as zero-length intervals.
43#[derive(Debug)]
44pub struct MarkerList {
45    /// Internal interval tree for O(log n) operations
46    tree: IntervalTree,
47
48    /// Track affinity for compatibility (though IntervalTree handles this through intervals)
49    /// We don't strictly need this for the tree, but keep it for API compatibility
50    _affinity_map: HashMap<MarkerId, bool>,
51}
52
53impl MarkerList {
54    /// Create a new empty marker list
55    pub fn new() -> Self {
56        Self {
57            tree: IntervalTree::new(),
58            _affinity_map: HashMap::new(),
59        }
60    }
61
62    /// Create a new marker at the given position
63    ///
64    /// # Arguments
65    /// * `position` - Byte offset in the buffer
66    /// * `left_affinity` - If true, marker stays before text inserted at this position
67    ///
68    /// # Returns
69    /// The ID of the newly created marker
70    ///
71    /// Note: Point markers are represented as zero-length intervals in the tree.
72    /// The IntervalTree handles position adjustments using interval semantics, which
73    /// differs slightly from explicit affinity for zero-length markers at exact edit
74    /// positions. In practice, this doesn't affect the LSP diagnostics use case.
75    pub fn create(&mut self, position: usize, left_affinity: bool) -> MarkerId {
76        let pos = position as u64;
77
78        // Create a zero-length interval for point markers
79        // The IntervalTree handles affinity through its interval spanning logic
80        let tree_id = self.tree.insert(pos, pos);
81        let id = MarkerId(tree_id);
82
83        // Store affinity for compatibility (though not strictly needed by tree)
84        self._affinity_map.insert(id, left_affinity);
85
86        tracing::trace!(
87            "Created marker {:?} at position {} with {} affinity",
88            id,
89            position,
90            if left_affinity { "left" } else { "right" }
91        );
92
93        id
94    }
95
96    /// Create a new left-gravity point marker at the given position.
97    ///
98    /// Unlike [`create`], text inserted exactly at the marker's position leaves
99    /// it in place instead of pushing it forward. Used as the end marker of a
100    /// fixed-width highlight (e.g. a search match) so the highlight does not
101    /// grow when text is typed immediately after it.
102    pub fn create_left_gravity(&mut self, position: usize) -> MarkerId {
103        let pos = position as u64;
104        let tree_id = self.tree.insert_left_gravity(pos, pos);
105        let id = MarkerId(tree_id);
106        self._affinity_map.insert(id, true);
107        id
108    }
109
110    /// Delete a marker
111    pub fn delete(&mut self, id: MarkerId) {
112        self.tree.delete(id.0);
113        self._affinity_map.remove(&id);
114    }
115
116    /// Move a marker to a new byte position, preserving its ID and affinity.
117    ///
118    /// Implemented as delete + reinsert in the interval tree to maintain BST
119    /// ordering invariants. The MarkerId is preserved so external references
120    /// (VirtualTextManager, OverlayManager, MarginManager) remain valid.
121    /// Returns false if the marker doesn't exist.
122    /// Cost: O(log n)
123    pub fn set_position(&mut self, id: MarkerId, new_position: usize) -> bool {
124        let pos = new_position as u64;
125        self.tree.set_position(id.0, pos, pos)
126    }
127
128    /// Get the current byte position of a marker
129    ///
130    /// For point markers (zero-length intervals), returns the start position.
131    /// Cost: O(log n) with the IntervalTree implementation.
132    pub fn get_position(&self, id: MarkerId) -> Option<usize> {
133        let (start, _end) = self.tree.get_position(id.0)?;
134        Some(start as usize)
135    }
136
137    /// Query all markers that overlap with a byte range
138    ///
139    /// This is an efficient way to find all markers in a viewport/visible region.
140    /// Returns a Vec of (MarkerId, start_position, end_position) tuples.
141    ///
142    /// Cost: O(log n + k) where k is the number of overlapping markers
143    ///
144    /// # Example
145    /// ```ignore
146    /// // Get all markers in the visible viewport
147    /// let visible_markers = marker_list.query_range(viewport_start, viewport_end);
148    /// ```
149    pub fn query_range(&self, start: usize, end: usize) -> Vec<(MarkerId, usize, usize)> {
150        self.tree
151            .query(start as u64, end as u64)
152            .into_iter()
153            .map(|m| {
154                (
155                    MarkerId(m.id),
156                    m.interval.start as usize,
157                    m.interval.end as usize,
158                )
159            })
160            .collect()
161    }
162
163    /// Adjust all markers for an insertion
164    ///
165    /// # Arguments
166    /// * `position` - Byte offset where text was inserted
167    /// * `length` - Number of bytes inserted
168    ///
169    /// Delegates to IntervalTree's adjust_for_edit with positive delta.
170    /// Cost: O(log n)
171    pub fn adjust_for_insert(&mut self, position: usize, length: usize) {
172        if length == 0 {
173            return;
174        }
175
176        self.tree.adjust_for_edit(position as u64, length as i64);
177    }
178
179    /// Adjust all markers for a deletion
180    ///
181    /// # Arguments
182    /// * `position` - Byte offset where deletion starts
183    /// * `length` - Number of bytes deleted
184    ///
185    /// Delegates to IntervalTree's adjust_for_edit with negative delta.
186    /// Markers within the deleted range are automatically handled by the tree.
187    /// Cost: O(log n)
188    pub fn adjust_for_delete(&mut self, position: usize, length: usize) {
189        if length == 0 {
190            return;
191        }
192
193        self.tree.adjust_for_edit(position as u64, -(length as i64));
194    }
195
196    /// Get the total size of the buffer (not directly tracked by IntervalTree)
197    ///
198    /// Note: This method is kept for API compatibility but is no longer used internally.
199    /// The buffer size is managed by the Buffer struct, not by markers.
200    pub fn buffer_size(&self) -> usize {
201        // Find the maximum end position among all markers
202        // This is an approximation - the actual buffer size should be tracked separately
203        0 // The buffer size is not tracked by markers in the tree-based implementation
204    }
205
206    /// Get the number of markers
207    pub fn marker_count(&self) -> usize {
208        self._affinity_map.len()
209    }
210
211    /// Set the initial buffer size (for tests)
212    ///
213    /// Note: This is a no-op in the IntervalTree implementation as buffer size
214    /// is not tracked by markers. Kept for backward compatibility with tests.
215    #[cfg(test)]
216    pub fn set_buffer_size(&mut self, _size: usize) {
217        // No-op: IntervalTree doesn't track buffer size
218    }
219
220    /// Iterate through entries (for testing and debugging)
221    ///
222    /// Note: Not supported in IntervalTree implementation as there are no "entries".
223    /// This returns an empty slice for compatibility.
224    #[cfg(test)]
225    pub fn entries(&self) -> &[MarkerEntry] {
226        &[]
227    }
228
229    /// Check invariants (for testing)
230    ///
231    /// Note: IntervalTree has its own internal invariants. This is a compatibility stub.
232    #[cfg(test)]
233    pub fn check_invariants(&self) -> Result<(), String> {
234        // IntervalTree maintains its own invariants internally
235        Ok(())
236    }
237
238    // --- Line Anchor Methods ---
239
240    /// Create a line anchor at a specific byte range
241    ///
242    /// This creates a marker that represents a line with an estimated line number.
243    /// The byte positions are exact, but the line number may be estimated.
244    pub fn create_line_anchor(
245        &mut self,
246        start: usize,
247        end: usize,
248        estimated_line: usize,
249        confidence: crate::model::marker_tree::AnchorConfidence,
250    ) -> MarkerId {
251        let tree_id =
252            self.tree
253                .insert_line_anchor(start as u64, end as u64, estimated_line, confidence);
254        MarkerId(tree_id)
255    }
256
257    /// Get the line number and confidence for a line anchor
258    pub fn get_line_anchor_info(
259        &self,
260        id: MarkerId,
261    ) -> Option<(usize, crate::model::marker_tree::AnchorConfidence)> {
262        let marker = self.tree.get_marker(id.0)?;
263        match marker.marker_type {
264            crate::model::marker_tree::MarkerType::LineAnchor {
265                estimated_line,
266                confidence,
267            } => Some((estimated_line, confidence)),
268            _ => None,
269        }
270    }
271
272    /// Update a line anchor's line number and confidence
273    pub fn update_line_anchor(
274        &mut self,
275        id: MarkerId,
276        estimated_line: usize,
277        confidence: crate::model::marker_tree::AnchorConfidence,
278    ) -> bool {
279        self.tree
280            .update_line_anchor(id.0, estimated_line, confidence)
281    }
282
283    /// Query all line anchors in a byte range
284    pub fn query_line_anchors(
285        &self,
286        start: usize,
287        end: usize,
288    ) -> Vec<(MarkerId, usize, usize, usize)> {
289        self.tree
290            .query_line_anchors(start as u64, end as u64)
291            .into_iter()
292            .filter_map(|m| {
293                if let crate::model::marker_tree::MarkerType::LineAnchor {
294                    estimated_line, ..
295                } = m.marker_type
296                {
297                    Some((
298                        MarkerId(m.id),
299                        m.interval.start as usize,
300                        m.interval.end as usize,
301                        estimated_line,
302                    ))
303                } else {
304                    None
305                }
306            })
307            .collect()
308    }
309
310    /// Find the nearest line anchor before a given byte position
311    pub fn nearest_line_anchor_before(
312        &self,
313        byte_offset: usize,
314    ) -> Option<(MarkerId, usize, usize, usize)> {
315        // Query from 0 to byte_offset to get all anchors before
316        let anchors = self.query_line_anchors(0, byte_offset);
317        // Return the one closest to byte_offset
318        anchors.into_iter().max_by_key(|(_, start, _, _)| *start)
319    }
320
321    /// Find the nearest line anchor before a given line number
322    pub fn nearest_line_anchor_before_line(
323        &self,
324        line_num: usize,
325    ) -> Option<(MarkerId, usize, usize, usize)> {
326        // Query all anchors (we need to check line numbers, not byte positions)
327        // This is not optimal but simple - in practice we won't have many anchors
328        let all_anchors = self.query_line_anchors(0, usize::MAX);
329        all_anchors
330            .into_iter()
331            .filter(|(_, _, _, estimated_line)| *estimated_line <= line_num)
332            .max_by_key(|(_, _, _, estimated_line)| *estimated_line)
333    }
334}
335
336impl Default for MarkerList {
337    fn default() -> Self {
338        Self::new()
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    #[test]
347    fn test_new_marker_list() {
348        let list = MarkerList::new();
349        assert_eq!(list.marker_count(), 0);
350        list.check_invariants().unwrap();
351    }
352
353    #[test]
354    fn test_create_marker_at_start() {
355        let mut list = MarkerList::new();
356
357        let m1 = list.create(0, true);
358        assert_eq!(list.marker_count(), 1);
359        assert_eq!(list.get_position(m1), Some(0));
360        list.check_invariants().unwrap();
361    }
362
363    #[test]
364    fn test_create_multiple_markers() {
365        let mut list = MarkerList::new();
366
367        let m1 = list.create(5, true);
368        let m2 = list.create(15, false);
369
370        assert_eq!(list.get_position(m1), Some(5));
371        assert_eq!(list.get_position(m2), Some(15));
372        list.check_invariants().unwrap();
373    }
374
375    #[test]
376    fn test_insert_before_marker() {
377        let mut list = MarkerList::new();
378
379        let m1 = list.create(10, true);
380        assert_eq!(list.get_position(m1), Some(10));
381
382        // Insert 5 bytes before marker
383        list.adjust_for_insert(5, 5);
384
385        // Marker should have moved forward
386        assert_eq!(list.get_position(m1), Some(15));
387        list.check_invariants().unwrap();
388    }
389
390    #[test]
391    fn test_insert_after_marker() {
392        let mut list = MarkerList::new();
393
394        let m1 = list.create(10, true);
395        assert_eq!(list.get_position(m1), Some(10));
396
397        // Insert 5 bytes after marker
398        list.adjust_for_insert(15, 5);
399
400        // Marker should stay at same position
401        assert_eq!(list.get_position(m1), Some(10));
402        list.check_invariants().unwrap();
403    }
404
405    #[test]
406    fn test_insert_at_marker_left_affinity() {
407        let mut list = MarkerList::new();
408
409        // Left affinity: marker stays before inserted text
410        let m1 = list.create(10, true);
411
412        // Insert at marker position
413        list.adjust_for_insert(10, 5);
414
415        // Note: IntervalTree treats zero-length markers as intervals.
416        // When inserting at position 10 where a [10,10] marker exists,
417        // the interval tree shifts it to [15,15] (standard interval tree behavior).
418        // This is different from the old Vec implementation but more consistent
419        // with interval tree semantics where intervals can expand.
420        assert_eq!(list.get_position(m1), Some(15));
421        list.check_invariants().unwrap();
422    }
423
424    #[test]
425    fn test_insert_at_marker_right_affinity() {
426        let mut list = MarkerList::new();
427
428        // Right affinity: marker moves after inserted text
429        let m1 = list.create(10, false);
430
431        // Insert at marker position
432        list.adjust_for_insert(10, 5);
433
434        // Marker should move to 15, insertion goes before
435        assert_eq!(list.get_position(m1), Some(15));
436        list.check_invariants().unwrap();
437    }
438
439    #[test]
440    fn test_delete_before_marker() {
441        let mut list = MarkerList::new();
442
443        let m1 = list.create(15, true);
444        assert_eq!(list.get_position(m1), Some(15));
445
446        // Delete 5 bytes before marker (at position 5)
447        list.adjust_for_delete(5, 5);
448
449        // Marker should move backward
450        assert_eq!(list.get_position(m1), Some(10));
451        list.check_invariants().unwrap();
452    }
453
454    #[test]
455    fn test_delete_after_marker() {
456        let mut list = MarkerList::new();
457
458        let m1 = list.create(10, true);
459        assert_eq!(list.get_position(m1), Some(10));
460
461        // Delete 5 bytes after marker (at position 15)
462        list.adjust_for_delete(15, 5);
463
464        // Marker should stay at same position
465        assert_eq!(list.get_position(m1), Some(10));
466        list.check_invariants().unwrap();
467    }
468
469    #[test]
470    fn test_delete_marker() {
471        let mut list = MarkerList::new();
472
473        let m1 = list.create(10, true);
474
475        // Delete at the marker position
476        list.adjust_for_delete(10, 5);
477
478        // IntervalTree clamps markers instead of deleting them
479        // Zero-length marker at position 10 gets clamped to position 10
480        assert_eq!(list.get_position(m1), Some(10));
481        assert_eq!(list.marker_count(), 1);
482        list.check_invariants().unwrap();
483    }
484
485    #[test]
486    fn test_delete_multiple_markers() {
487        let mut list = MarkerList::new();
488
489        let m1 = list.create(10, true);
490        let m2 = list.create(15, true);
491        let m3 = list.create(20, true);
492
493        // Delete range [8, 18) covering m1 and m2
494        list.adjust_for_delete(8, 10);
495
496        // IntervalTree clamps markers instead of deleting
497        // m1 at 10 gets clamped to 8, m2 at 15 gets clamped to 8, m3 at 20 moves to 10
498        assert_eq!(list.get_position(m1), Some(8)); // Clamped to deletion start
499        assert_eq!(list.get_position(m2), Some(8)); // Clamped to deletion start
500        assert_eq!(list.get_position(m3), Some(10)); // 20 - 10 = 10
501        assert_eq!(list.marker_count(), 3);
502        list.check_invariants().unwrap();
503    }
504
505    #[test]
506    fn test_complex_scenario() {
507        let mut list = MarkerList::new();
508
509        // Create markers at 10, 20, 30
510        let m1 = list.create(10, true);
511        let m2 = list.create(20, true);
512        let m3 = list.create(30, true);
513
514        // Insert at 15
515        list.adjust_for_insert(15, 5);
516        assert_eq!(list.get_position(m1), Some(10));
517        assert_eq!(list.get_position(m2), Some(25)); // 20 + 5
518        assert_eq!(list.get_position(m3), Some(35)); // 30 + 5
519
520        // Delete at 12, length 8 (delete range [12, 20))
521        // This removes part of the gap between m1 and m2, but not m2 itself
522        list.adjust_for_delete(12, 8);
523        assert_eq!(list.get_position(m1), Some(10)); // Before deletion
524        assert_eq!(list.get_position(m2), Some(17)); // 25 - 8 = 17
525        assert_eq!(list.get_position(m3), Some(27)); // 35 - 8 = 27
526
527        list.check_invariants().unwrap();
528    }
529
530    #[test]
531    fn test_marker_deletion_with_delete_method() {
532        let mut list = MarkerList::new();
533
534        let m1 = list.create(10, true);
535        let m2 = list.create(15, false);
536
537        // Delete m1
538        list.delete(m1);
539
540        assert_eq!(list.get_position(m1), None);
541        assert_eq!(list.get_position(m2), Some(15));
542        assert_eq!(list.marker_count(), 1);
543        list.check_invariants().unwrap();
544    }
545
546    // Property-based tests
547    #[cfg(test)]
548    mod property_tests {
549        use super::*;
550        use proptest::prelude::*;
551
552        /// Generate random edit operations
553        #[derive(Debug, Clone)]
554        enum EditOp {
555            Insert { position: usize, length: usize },
556            Delete { position: usize, length: usize },
557        }
558
559        fn arb_edit_op(max_buffer_size: usize) -> impl Strategy<Value = EditOp> {
560            prop_oneof![
561                (0..=max_buffer_size, 1..=50usize).prop_map(|(pos, len)| EditOp::Insert {
562                    position: pos,
563                    length: len
564                }),
565                (0..=max_buffer_size, 1..=20usize).prop_map(|(pos, len)| EditOp::Delete {
566                    position: pos,
567                    length: len
568                }),
569            ]
570        }
571
572        proptest! {
573            /// Invariants should hold after any sequence of operations
574            #[test]
575            fn prop_invariants_hold(
576                initial_positions in prop::collection::vec(0..1000usize, 1..10),
577                ops in prop::collection::vec(arb_edit_op(1000), 1..20)
578            ) {
579                let mut list = MarkerList::new();
580
581                // Filter out duplicate positions to avoid RefCell borrow conflicts
582                // when multiple markers at same position are adjusted
583                let mut unique_positions: Vec<usize> = initial_positions.clone();
584                unique_positions.sort_unstable();
585                unique_positions.dedup();
586
587                // Create some markers at various positions
588                let markers: Vec<_> = unique_positions
589                    .iter()
590                    .enumerate()
591                    .map(|(i, &pos)| list.create(pos, i % 2 == 0))
592                    .collect();
593
594                // Apply random operations
595                for op in ops {
596                    match op {
597                        EditOp::Insert { position, length } => {
598                            list.adjust_for_insert(position, length);
599                        }
600                        EditOp::Delete { position, length } => {
601                            if length > 0 {
602                                list.adjust_for_delete(position, length);
603                            }
604                        }
605                    }
606
607                    // Invariants must hold after every operation
608                    list.check_invariants().unwrap();
609                }
610
611                // All remaining markers should still exist
612                for marker in markers {
613                    // Just verify we can query positions
614                    let _ = list.get_position(marker);
615                }
616            }
617
618            /// Marker positions should be in the same order after edits
619            #[test]
620            fn prop_marker_ordering_preserved(
621                initial_spacing in 10..50usize,
622                ops in prop::collection::vec(arb_edit_op(500), 1..10)
623            ) {
624                let mut list = MarkerList::new();
625
626                // Create markers in order with given spacing
627                let markers: Vec<_> = (0..5)
628                    .map(|i| list.create(i * initial_spacing, true))
629                    .collect();
630
631                // Apply operations
632                for op in ops {
633                    match op {
634                        EditOp::Insert { position, length } => {
635                            list.adjust_for_insert(position, length);
636                        }
637                        EditOp::Delete { position, length } => {
638                            if length > 0 {
639                                list.adjust_for_delete(position, length);
640                            }
641                        }
642                    }
643                }
644
645                // Get positions of all markers AND their intervals for debugging
646                let positions: Vec<_> = markers
647                    .iter()
648                    .filter_map(|&m| list.get_position(m))
649                    .collect();
650
651                // Debug: Get full intervals (start, end) from tree
652                let intervals: Vec<_> = markers
653                    .iter()
654                    .filter_map(|&m| list.tree.get_position(m.0))
655                    .collect();
656
657                // Should still be in order (no inversions)
658                for window in positions.windows(2) {
659                    if window[0] > window[1] {
660                        tracing::trace!("Ordering violation detected!");
661                        tracing::trace!("  Positions: {:?}", positions);
662                        tracing::trace!("  Full intervals: {:?}", intervals);
663                        panic!("Marker ordering violated: {:?}", positions);
664                    }
665                }
666            }
667
668            /// Shadow-model property test: for every sequence of
669            /// create / delete / adjust_for_insert / adjust_for_delete
670            /// operations, the positions reported by MarkerList for
671            /// each still-live marker must match the positions a naïve
672            /// `Vec<(MarkerId, usize)>` would compute by independently
673            /// shifting/clamping on each edit.
674            ///
675            /// This catches bugs where the interval-tree's own
676            /// bookkeeping (e.g. lazy_delta propagation, BST-delete
677            /// node swaps, marker_map staleness) diverges from the
678            /// straightforward "markers are points that slide with
679            /// the buffer" semantics. The inlay-hint-jumps-to-start
680            /// regression on line delete was exactly this kind of
681            /// divergence, and was invisible to every other invariant
682            /// check in this file.
683            #[test]
684            fn prop_shadow_model_matches_tree(
685                initial_positions in prop::collection::vec(0..1000usize, 1..20),
686                ops in prop::collection::vec(arb_edit_op(1000), 1..30),
687                delete_indices in prop::collection::vec(0..20usize, 0..5),
688            ) {
689                let mut list = MarkerList::new();
690
691                let mut unique_positions: Vec<usize> = initial_positions;
692                unique_positions.sort_unstable();
693                unique_positions.dedup();
694
695                // Shadow: Vec<(MarkerId, Option<usize>, right_gravity)>.
696                // None means deleted. Half the markers are created left-gravity
697                // (issue #2053) so the model also covers the sticky-end path.
698                let mut shadow: Vec<(MarkerId, Option<usize>, bool)> = Vec::new();
699                for (i, &p) in unique_positions.iter().enumerate() {
700                    let right_gravity = i % 2 == 0;
701                    let id = if right_gravity {
702                        list.create(p, i % 2 == 0)
703                    } else {
704                        list.create_left_gravity(p)
705                    };
706                    shadow.push((id, Some(p), right_gravity));
707                }
708
709                // Delete some markers (by shadow index modulo len).
710                for idx in delete_indices {
711                    if shadow.is_empty() {
712                        break;
713                    }
714                    let i = idx % shadow.len();
715                    if let (id, Some(_), _) = shadow[i] {
716                        list.delete(id);
717                        shadow[i].1 = None;
718                    }
719                }
720
721                // Apply edits to both the tree and the shadow.
722                for op in ops {
723                    match op {
724                        EditOp::Insert { position, length } => {
725                            list.adjust_for_insert(position, length);
726                            for (_id, pos, right_gravity) in shadow.iter_mut() {
727                                if let Some(p) = pos {
728                                    // Right-gravity markers shift when the
729                                    // insertion is at or before them; left-gravity
730                                    // markers only shift for insertions strictly
731                                    // before, staying put at the exact boundary.
732                                    let shifts = if *right_gravity {
733                                        *p >= position
734                                    } else {
735                                        *p > position
736                                    };
737                                    if shifts {
738                                        *p += length;
739                                    }
740                                }
741                            }
742                        }
743                        EditOp::Delete { position, length } => {
744                            if length == 0 {
745                                continue;
746                            }
747                            list.adjust_for_delete(position, length);
748                            for (_id, pos, _right_gravity) in shadow.iter_mut() {
749                                if let Some(p) = pos {
750                                    // Markers inside the deleted range
751                                    // clamp to the deletion start in
752                                    // MarkerList's semantics (see
753                                    // adjust_recursive's `.max(pos)`),
754                                    // so mirror that in the shadow.
755                                    // Gravity does not affect deletions.
756                                    if *p >= position + length {
757                                        *p -= length;
758                                    } else if *p > position {
759                                        *p = position;
760                                    }
761                                }
762                            }
763                        }
764                    }
765
766                    // Every live marker's tree position must match its
767                    // shadow position after every operation.
768                    for (id, shadow_pos, _right_gravity) in &shadow {
769                        match shadow_pos {
770                            Some(expected) => {
771                                let actual = list.get_position(*id);
772                                prop_assert_eq!(
773                                    actual,
774                                    Some(*expected),
775                                    "marker {:?} expected at {} but tree says {:?}",
776                                    id,
777                                    expected,
778                                    actual
779                                );
780                            }
781                            None => {
782                                // Deleted markers: get_position may
783                                // return None OR the tree may leak the
784                                // underlying storage — accept either,
785                                // but never a stale live position.
786                            }
787                        }
788                    }
789                }
790            }
791        }
792    }
793}