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}