Skip to main content

fresh/model/
piece_tree.rs

1use std::io;
2use std::path::PathBuf;
3use std::sync::Arc;
4
5/// A position in the document (line and column)
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct Position {
8    pub line: usize,   // 0-indexed line number
9    pub column: usize, // Byte offset within the line
10}
11
12/// Data storage for a buffer - either loaded in memory or unloaded (file reference)
13#[derive(Debug, Clone)]
14pub enum BufferData {
15    /// Loaded in memory with optional line indexing
16    Loaded {
17        data: Vec<u8>,
18        line_starts: Option<Vec<usize>>, // None = not indexed (large file mode)
19    },
20    /// Not yet loaded from file
21    Unloaded {
22        file_path: PathBuf,
23        file_offset: usize, // Where in file this buffer starts
24        bytes: usize,       // Length of this region
25    },
26}
27
28/// A string buffer containing a chunk of text data and its line metadata
29/// This is the fundamental storage unit - piece tree nodes reference these buffers
30#[derive(Debug, Clone)]
31pub struct StringBuffer {
32    /// Unique identifier for this buffer
33    pub id: usize,
34    /// The buffer data - either loaded or unloaded
35    pub data: BufferData,
36    /// For buffers created by loading a chunk from a stored (on-disk) buffer:
37    /// the original file offset. This allows `rebuild_with_pristine_saved_root`
38    /// to recognize loaded chunks as saved content rather than user edits.
39    pub stored_file_offset: Option<usize>,
40}
41
42impl StringBuffer {
43    /// Create a new string buffer with line metadata (legacy constructor)
44    /// Automatically computes line starts
45    pub fn new(id: usize, data: Vec<u8>) -> Self {
46        let line_starts = Self::compute_line_starts(&data);
47        StringBuffer {
48            id,
49            data: BufferData::Loaded {
50                data,
51                line_starts: Some(line_starts),
52            },
53            stored_file_offset: None,
54        }
55    }
56
57    /// Create a loaded buffer with optional line indexing
58    pub fn new_loaded(id: usize, data: Vec<u8>, compute_lines: bool) -> Self {
59        let line_starts = if compute_lines {
60            Some(Self::compute_line_starts(&data))
61        } else {
62            None
63        };
64        StringBuffer {
65            id,
66            data: BufferData::Loaded { data, line_starts },
67            stored_file_offset: None,
68        }
69    }
70
71    /// Create buffer for file region (not yet loaded)
72    pub fn new_unloaded(id: usize, file_path: PathBuf, file_offset: usize, bytes: usize) -> Self {
73        StringBuffer {
74            id,
75            data: BufferData::Unloaded {
76                file_path,
77                file_offset,
78                bytes,
79            },
80            stored_file_offset: None,
81        }
82    }
83
84    /// Check if buffer is loaded
85    pub fn is_loaded(&self) -> bool {
86        matches!(self.data, BufferData::Loaded { .. })
87    }
88
89    /// Returns the total byte count of an unloaded buffer, or `None` if loaded.
90    pub fn unloaded_bytes(&self) -> Option<usize> {
91        match &self.data {
92            BufferData::Unloaded { bytes, .. } => Some(*bytes),
93            BufferData::Loaded { .. } => None,
94        }
95    }
96
97    /// Get data reference if loaded, None if unloaded
98    ///
99    /// NOTE: This is a low-level API. External code should use TextBuffer::get_text_range_mut()
100    /// which provides automatic lazy loading. This method is pub(crate) to prevent misuse.
101    pub(crate) fn get_data(&self) -> Option<&[u8]> {
102        match &self.data {
103            BufferData::Loaded { data, .. } => Some(data),
104            BufferData::Unloaded { .. } => None,
105        }
106    }
107
108    /// Find the byte offset where `line_in_piece`-th line starts within a piece.
109    /// `piece_start`/`piece_end` define the piece's byte range in this buffer.
110    /// `line_in_piece` is 0-indexed: 0 means the first line of the piece (returns `piece_start`).
111    /// Returns `None` if line starts aren't available or the target line isn't in this piece.
112    pub fn nth_line_start_in_piece(
113        &self,
114        piece_start: usize,
115        piece_end: usize,
116        line_in_piece: usize,
117    ) -> Option<usize> {
118        if line_in_piece == 0 {
119            return Some(piece_start);
120        }
121        let line_starts = self.get_line_starts()?;
122        let start_idx = line_starts.partition_point(|&ls| ls <= piece_start);
123        let target_idx = start_idx + line_in_piece - 1;
124        let &ls = line_starts.get(target_idx)?;
125        if ls < piece_end {
126            Some(ls)
127        } else {
128            None
129        }
130    }
131
132    /// Get line starts if available
133    pub fn get_line_starts(&self) -> Option<&[usize]> {
134        match &self.data {
135            BufferData::Loaded { line_starts, .. } => line_starts.as_deref(),
136            BufferData::Unloaded { .. } => None,
137        }
138    }
139
140    /// Load buffer data from file using a FileSystem (for unloaded buffers)
141    /// Returns error if buffer is not unloaded or if I/O fails
142    pub fn load(&mut self, fs: &dyn crate::model::filesystem::FileSystem) -> io::Result<()> {
143        match &self.data {
144            BufferData::Loaded { .. } => Ok(()), // Already loaded
145            BufferData::Unloaded {
146                file_path,
147                file_offset,
148                bytes,
149            } => {
150                // Load from file using the FileSystem trait
151                let buffer = fs.read_range(file_path, *file_offset as u64, *bytes)?;
152
153                // Defense-in-depth: panic if the FileSystem returned fewer bytes than requested.
154                // This violates the read_range contract (should return exactly `len` bytes or error).
155                // The piece tree references this buffer using the `bytes` value, so a mismatch
156                // would cause a panic later during save when slicing - better to fail fast here.
157                assert_eq!(
158                    buffer.len(),
159                    *bytes,
160                    "FileSystem::read_range violated contract: expected {} bytes at offset {}, got {} (path: {})",
161                    bytes,
162                    file_offset,
163                    buffer.len(),
164                    file_path.display()
165                );
166
167                // Replace with loaded data, computing line starts so that
168                // line-based APIs (line_start_offset, indent folding, etc.)
169                // work on large-file chunks.
170                let line_starts = Self::compute_line_starts(&buffer);
171                self.data = BufferData::Loaded {
172                    data: buffer,
173                    line_starts: Some(line_starts),
174                };
175
176                Ok(())
177            }
178        }
179    }
180
181    /// Create a new unloaded buffer representing a chunk of this buffer
182    /// This is used for splitting large unloaded buffers into smaller chunks
183    ///
184    /// # Arguments
185    /// * `new_id` - The ID for the new buffer
186    /// * `chunk_offset` - Offset within this buffer where the chunk starts
187    /// * `chunk_bytes` - Number of bytes in the chunk
188    ///
189    /// # Returns
190    /// A new StringBuffer referencing the chunk, or None if this buffer is not unloaded
191    /// or if the chunk range is invalid
192    pub fn create_chunk_buffer(
193        &self,
194        new_id: usize,
195        chunk_offset: usize,
196        chunk_bytes: usize,
197    ) -> Option<StringBuffer> {
198        match &self.data {
199            BufferData::Unloaded {
200                file_path,
201                file_offset,
202                bytes,
203            } => {
204                // Validate chunk range
205                if chunk_offset + chunk_bytes > *bytes {
206                    return None;
207                }
208
209                let absolute_file_offset = file_offset + chunk_offset;
210                let mut buf = StringBuffer::new_unloaded(
211                    new_id,
212                    file_path.clone(),
213                    absolute_file_offset,
214                    chunk_bytes,
215                );
216                buf.stored_file_offset = Some(absolute_file_offset);
217                Some(buf)
218            }
219            BufferData::Loaded { .. } => None, // Can't create chunk from loaded buffer
220        }
221    }
222
223    /// Compute line start offsets for a buffer
224    fn compute_line_starts(data: &[u8]) -> Vec<usize> {
225        let mut line_starts = vec![0];
226        for (i, &byte) in data.iter().enumerate() {
227            if byte == b'\n' {
228                line_starts.push(i + 1);
229            }
230        }
231        line_starts
232    }
233
234    /// Get the number of line feeds (newlines) in this buffer
235    /// Returns None if line indexing was not computed or buffer is unloaded
236    pub fn line_feed_count(&self) -> Option<usize> {
237        match &self.data {
238            BufferData::Loaded { line_starts, .. } => line_starts
239                .as_ref()
240                .map(|starts| starts.len().saturating_sub(1)),
241            BufferData::Unloaded { .. } => None,
242        }
243    }
244
245    /// Append data to this buffer and recompute line starts
246    /// Returns the offset where the appended data starts
247    /// Only works for loaded buffers with line starts
248    pub fn append(&mut self, data_to_append: &[u8]) -> usize {
249        match &mut self.data {
250            BufferData::Loaded { data, line_starts } => {
251                let start_offset = data.len();
252                data.extend_from_slice(data_to_append);
253
254                // Add new line starts if we're tracking them
255                if let Some(ref mut line_starts) = line_starts {
256                    for (i, &byte) in data_to_append.iter().enumerate() {
257                        if byte == b'\n' {
258                            line_starts.push(start_offset + i + 1);
259                        }
260                    }
261                }
262
263                start_offset
264            }
265            BufferData::Unloaded { .. } => {
266                // Can't append to unloaded buffer
267                0
268            }
269        }
270    }
271}
272
273/// Identifies which buffer a piece of text comes from
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum BufferLocation {
276    /// Data is in the original stored/persisted buffer
277    Stored(usize), // buffer_id
278    /// Data is in the added/modified buffer
279    Added(usize), // buffer_id
280}
281
282impl BufferLocation {
283    /// Get the buffer ID
284    pub fn buffer_id(&self) -> usize {
285        match self {
286            Self::Stored(id) | Self::Added(id) => *id,
287        }
288    }
289}
290
291/// A node in the piece tree with integrated line tracking
292#[derive(Debug, Clone)]
293pub enum PieceTreeNode {
294    /// Internal node with left and right children
295    Internal {
296        left_bytes: usize,      // Total bytes in left subtree
297        lf_left: Option<usize>, // Total line feeds in left subtree (None if unknown)
298        left: Arc<PieceTreeNode>,
299        right: Arc<PieceTreeNode>,
300    },
301    /// Leaf node representing an actual piece
302    Leaf {
303        location: BufferLocation, // Where this piece's data is (includes buffer_id)
304        offset: usize,            // Offset within the buffer
305        bytes: usize,             // Number of bytes in this piece
306        line_feed_cnt: Option<usize>, // Number of line feeds in this piece (None if unknown)
307    },
308}
309
310/// Information about a piece at a specific location
311#[derive(Debug, Clone)]
312pub struct PieceInfo {
313    pub location: BufferLocation,       // Which buffer (Stored or Added)
314    pub offset: usize,                  // Starting offset of this piece within that buffer
315    pub bytes: usize,                   // Length of this piece in bytes
316    pub offset_in_piece: Option<usize>, // For queries: how far into this piece the query point is
317    pub line_feeds: Option<usize>,      // Number of line feeds in this piece (None if unknown)
318}
319
320/// Result from finding a piece by byte offset
321#[derive(Debug, Clone)]
322struct OffsetFindResult {
323    info: PieceInfo,
324    bytes_before: usize, // Total bytes in all pieces before this one
325}
326
327/// A cursor position in the document
328#[derive(Debug, Clone)]
329pub struct Cursor {
330    pub byte_offset: usize, // Absolute byte offset in document
331    pub line: usize,        // Line number (0-indexed)
332    pub col: usize,         // Column within line (byte offset)
333}
334
335/// Represents the data for a leaf node in the piece tree
336#[derive(Debug, Clone, Copy)]
337pub struct LeafData {
338    pub location: BufferLocation,
339    pub offset: usize,
340    pub bytes: usize,
341    pub line_feed_cnt: Option<usize>,
342}
343
344impl LeafData {
345    pub fn new(
346        location: BufferLocation,
347        offset: usize,
348        bytes: usize,
349        line_feed_cnt: Option<usize>,
350    ) -> Self {
351        LeafData {
352            location,
353            offset,
354            bytes,
355            line_feed_cnt,
356        }
357    }
358}
359
360/// Statistics about the piece tree structure
361#[derive(Debug, Clone, Copy)]
362pub struct TreeStats {
363    pub total_bytes: usize,
364    pub depth: usize,
365    pub leaf_count: usize,
366    pub line_feed_count: Option<usize>,
367}
368
369// Line iteration can be implemented by:
370// 1. Maintaining a cursor position (current piece + offset within piece)
371// 2. For next_line(): scan forward in the current piece's buffer until '\n',
372//    or move to the next piece if we reach the end
373// 3. For prev_line(): scan backward similarly
374// The iterator would need access to the actual buffer data (Stored/Added)
375// which is managed externally, so this is deferred until buffer integration.
376
377impl PieceTreeNode {
378    /// Find the piece containing the given byte offset
379    fn find_by_offset(&self, offset: usize) -> Option<OffsetFindResult> {
380        match self {
381            Self::Internal {
382                left_bytes,
383                left,
384                right,
385                ..
386            } => {
387                if offset < *left_bytes {
388                    left.find_by_offset(offset)
389                } else {
390                    // Search in right subtree
391                    right.find_by_offset(offset - left_bytes).map(|mut result| {
392                        // Adjust bytes_before to account for left subtree
393                        result.bytes_before += left_bytes;
394                        result
395                    })
396                }
397            }
398            Self::Leaf {
399                location,
400                offset: piece_offset,
401                bytes,
402                line_feed_cnt,
403            } => {
404                if offset < *bytes {
405                    Some(OffsetFindResult {
406                        info: PieceInfo {
407                            location: *location,
408                            offset: *piece_offset,
409                            bytes: *bytes,
410                            offset_in_piece: Some(offset),
411                            line_feeds: *line_feed_cnt,
412                        },
413                        bytes_before: 0,
414                    })
415                } else {
416                    None
417                }
418            }
419        }
420    }
421
422    /// Get total bytes in this node
423    fn total_bytes(&self) -> usize {
424        match self {
425            Self::Internal {
426                left_bytes, right, ..
427            } => left_bytes + right.total_bytes(),
428            Self::Leaf { bytes, .. } => *bytes,
429        }
430    }
431
432    /// Get total line feeds in this node
433    /// Returns None if any piece has unknown line count
434    fn total_line_feeds(&self) -> Option<usize> {
435        match self {
436            Self::Internal { lf_left, right, .. } => match (lf_left, right.total_line_feeds()) {
437                (Some(left), Some(right)) => Some(left + right),
438                _ => None,
439            },
440            Self::Leaf { line_feed_cnt, .. } => *line_feed_cnt,
441        }
442    }
443
444    /// Get the depth of this tree
445    fn depth(&self) -> usize {
446        match self {
447            Self::Internal { left, right, .. } => 1 + left.depth().max(right.depth()),
448            Self::Leaf { .. } => 1,
449        }
450    }
451
452    /// Count the number of leaf nodes
453    fn count_leaves(&self) -> usize {
454        match self {
455            Self::Internal { left, right, .. } => left.count_leaves() + right.count_leaves(),
456            Self::Leaf { .. } => 1,
457        }
458    }
459
460    /// Path-copy update of a single leaf's line_feed_cnt by leaf index.
461    /// Returns (new_node, true) if the leaf was found and updated, or
462    /// (clone of self, false) if leaf_index was out of range.
463    /// Only clones nodes along the root-to-leaf path; sibling subtrees
464    /// are shared via Arc, preserving Arc::ptr_eq for unmodified regions.
465    fn update_leaf_lf_by_index(
466        self: &Arc<Self>,
467        leaf_index: usize,
468        lf_count: usize,
469    ) -> (Arc<Self>, bool) {
470        match self.as_ref() {
471            Self::Leaf {
472                location,
473                offset,
474                bytes,
475                ..
476            } => {
477                if leaf_index == 0 {
478                    (
479                        Arc::new(Self::Leaf {
480                            location: *location,
481                            offset: *offset,
482                            bytes: *bytes,
483                            line_feed_cnt: Some(lf_count),
484                        }),
485                        true,
486                    )
487                } else {
488                    (Arc::clone(self), false)
489                }
490            }
491            Self::Internal {
492                left_bytes,
493                lf_left,
494                left,
495                right,
496            } => {
497                let left_leaf_count = left.count_leaves();
498                if leaf_index < left_leaf_count {
499                    let (new_left, found) = left.update_leaf_lf_by_index(leaf_index, lf_count);
500                    if found {
501                        let new_lf_left = new_left.total_line_feeds();
502                        (
503                            Arc::new(Self::Internal {
504                                left_bytes: *left_bytes,
505                                lf_left: new_lf_left,
506                                left: new_left,
507                                right: Arc::clone(right),
508                            }),
509                            true,
510                        )
511                    } else {
512                        (Arc::clone(self), false)
513                    }
514                } else {
515                    let (new_right, found) =
516                        right.update_leaf_lf_by_index(leaf_index - left_leaf_count, lf_count);
517                    if found {
518                        (
519                            Arc::new(Self::Internal {
520                                left_bytes: *left_bytes,
521                                lf_left: *lf_left,
522                                left: Arc::clone(left),
523                                right: new_right,
524                            }),
525                            true,
526                        )
527                    } else {
528                        (Arc::clone(self), false)
529                    }
530                }
531            }
532        }
533    }
534
535    /// Collect all leaves in order
536    pub(crate) fn collect_leaves(&self, leaves: &mut Vec<LeafData>) {
537        match self {
538            Self::Internal { left, right, .. } => {
539                left.collect_leaves(leaves);
540                right.collect_leaves(leaves);
541            }
542            Self::Leaf {
543                location,
544                offset,
545                bytes,
546                line_feed_cnt,
547            } => {
548                leaves.push(LeafData::new(*location, *offset, *bytes, *line_feed_cnt));
549            }
550        }
551    }
552
553    /// Count line feeds in a byte range [start, end)
554    /// current_offset: byte offset at the start of this node
555    /// Returns None if any piece in the range has unknown line count
556    fn count_lines_in_byte_range(
557        &self,
558        current_offset: usize,
559        start: usize,
560        end: usize,
561    ) -> Option<usize> {
562        match self {
563            Self::Internal {
564                left_bytes,
565                left,
566                right,
567                ..
568            } => {
569                let left_end = current_offset + left_bytes;
570
571                if end <= current_offset || start >= current_offset + self.total_bytes() {
572                    Some(0) // Range is completely before or after this node
573                } else if start <= current_offset && end >= current_offset + self.total_bytes() {
574                    // Range completely contains this node
575                    self.total_line_feeds()
576                } else if end <= left_end {
577                    // Range is completely in left subtree
578                    left.count_lines_in_byte_range(current_offset, start, end)
579                } else if start >= left_end {
580                    // Range is completely in right subtree
581                    right.count_lines_in_byte_range(left_end, start, end)
582                } else {
583                    // Range spans both subtrees
584                    let left_count = left.count_lines_in_byte_range(current_offset, start, end)?;
585                    let right_count = right.count_lines_in_byte_range(left_end, start, end)?;
586                    Some(left_count + right_count)
587                }
588            }
589            Self::Leaf {
590                line_feed_cnt,
591                bytes,
592                ..
593            } => {
594                let node_end = current_offset + bytes;
595
596                if end <= current_offset || start >= node_end {
597                    Some(0) // No overlap
598                } else if start <= current_offset && end >= node_end {
599                    // Range completely contains this leaf
600                    *line_feed_cnt
601                } else {
602                    // Partial overlap - for simplicity, return the full count
603                    // (accurate counting would require scanning the buffer)
604                    *line_feed_cnt
605                }
606            }
607        }
608    }
609
610    /// Find byte offset for a given line/column position
611    /// current_offset: byte offset at start of this node
612    /// lines_before: number of complete lines before this node
613    fn find_byte_offset_for_line(
614        &self,
615        current_offset: usize,
616        lines_before: usize,
617        target_line: usize,
618        column: usize,
619        buffers: &[StringBuffer],
620    ) -> Option<usize> {
621        match self {
622            Self::Internal {
623                left_bytes,
624                lf_left,
625                left,
626                right,
627            } => {
628                // If line count is unknown, we can't do line-based navigation
629                let lf_left = lf_left.as_ref()?;
630                let lines_after_left = lines_before + lf_left;
631
632                // When looking for line start (column == 0), we want the leftmost piece containing the line
633                // So use <= instead of < to prefer going left when the line boundary is exactly at lines_after_left
634                let go_left = if column == 0 {
635                    target_line <= lines_after_left
636                } else {
637                    target_line < lines_after_left
638                };
639
640                if go_left {
641                    // Target is in left subtree
642                    let result = left.find_byte_offset_for_line(
643                        current_offset,
644                        lines_before,
645                        target_line,
646                        column,
647                        buffers,
648                    );
649                    // If left returns None, try right as fallback (happens when line starts after a newline)
650                    result.or_else(|| {
651                        right.find_byte_offset_for_line(
652                            current_offset + left_bytes,
653                            lines_after_left,
654                            target_line,
655                            column,
656                            buffers,
657                        )
658                    })
659                } else {
660                    // Target is in right subtree
661                    right.find_byte_offset_for_line(
662                        current_offset + left_bytes,
663                        lines_after_left,
664                        target_line,
665                        column,
666                        buffers,
667                    )
668                }
669            }
670            Self::Leaf {
671                location,
672                offset,
673                bytes,
674                line_feed_cnt,
675            } => {
676                // If line count is unknown, we can't do line-based navigation
677                let line_feed_cnt = line_feed_cnt.as_ref()?;
678                let lines_in_piece = lines_before + line_feed_cnt;
679
680                // Special case: when looking for column==0 of line N where N == lines_in_piece,
681                // the line might start in the NEXT piece if this piece ends with a newline.
682                // Check if the last byte of this piece is a newline.
683                if column == 0 && target_line == lines_in_piece && target_line > lines_before {
684                    let buffer = buffers.get(location.buffer_id())?;
685                    let data = buffer.get_data()?;
686                    let last_byte_offset = offset + bytes - 1;
687                    let last_byte = data.get(last_byte_offset)?;
688
689                    if *last_byte == b'\n' {
690                        // Piece ends with newline, so the next line starts in the next piece
691                        return None;
692                    }
693                    // Otherwise, line starts within this piece after a newline
694                }
695
696                if target_line < lines_before || target_line > lines_in_piece {
697                    // Target line not in this piece
698                    return None;
699                }
700
701                // Get the buffer for this piece
702                let buffer_id = location.buffer_id();
703                let buffer = buffers.get(buffer_id)?;
704
705                // Find the line within the piece
706                let line_in_piece = target_line - lines_before;
707                let line_start_in_buffer =
708                    buffer.nth_line_start_in_piece(*offset, offset + bytes, line_in_piece)?;
709
710                // Add column offset
711                let target_offset_in_buffer = line_start_in_buffer + column;
712
713                // Convert to document offset
714                let offset_in_piece = target_offset_in_buffer.saturating_sub(*offset);
715                Some(current_offset + offset_in_piece.min(*bytes))
716            }
717        }
718    }
719
720    /// Find the piece (leaf) containing a target line using only tree metadata.
721    /// Returns `(doc_byte_offset, buffer_id, piece_offset, piece_bytes, lines_before)`
722    fn find_piece_for_line(
723        &self,
724        current_offset: usize,
725        lines_before: usize,
726        target_line: usize,
727    ) -> Option<(usize, usize, usize, usize, usize)> {
728        match self {
729            Self::Internal {
730                left_bytes,
731                lf_left,
732                left,
733                right,
734            } => {
735                let lf_left = lf_left.as_ref()?;
736                let lines_after_left = lines_before + lf_left;
737
738                if target_line <= lines_after_left {
739                    let result =
740                        left.find_piece_for_line(current_offset, lines_before, target_line);
741                    // Fall back to right if left doesn't contain it
742                    result.or_else(|| {
743                        right.find_piece_for_line(
744                            current_offset + left_bytes,
745                            lines_after_left,
746                            target_line,
747                        )
748                    })
749                } else {
750                    right.find_piece_for_line(
751                        current_offset + left_bytes,
752                        lines_after_left,
753                        target_line,
754                    )
755                }
756            }
757            Self::Leaf {
758                location,
759                offset,
760                bytes,
761                line_feed_cnt,
762            } => {
763                let line_feed_cnt = line_feed_cnt.as_ref()?;
764                let lines_in_piece = lines_before + line_feed_cnt;
765
766                if target_line >= lines_before && target_line <= lines_in_piece {
767                    Some((
768                        current_offset,
769                        location.buffer_id(),
770                        *offset,
771                        *bytes,
772                        lines_before,
773                    ))
774                } else {
775                    None
776                }
777            }
778        }
779    }
780}
781
782/// The main piece table structure with integrated line tracking
783#[derive(Debug, Clone)]
784pub struct PieceTree {
785    root: Arc<PieceTreeNode>,
786    total_bytes: usize,
787}
788
789impl PieceTree {
790    /// Create a new piece table with a single initial piece
791    pub fn new(
792        location: BufferLocation,
793        offset: usize,
794        bytes: usize,
795        line_feed_cnt: Option<usize>,
796    ) -> Self {
797        PieceTree {
798            root: Arc::new(PieceTreeNode::Leaf {
799                location,
800                offset,
801                bytes,
802                line_feed_cnt,
803            }),
804            total_bytes: bytes,
805        }
806    }
807
808    /// Create an empty piece table
809    pub fn empty() -> Self {
810        PieceTree {
811            root: Arc::new(PieceTreeNode::Leaf {
812                location: BufferLocation::Stored(0),
813                offset: 0,
814                bytes: 0,
815                line_feed_cnt: Some(0), // Empty has zero line feeds (known)
816            }),
817            total_bytes: 0,
818        }
819    }
820
821    /// Build a `PieceTree` from an explicit list of leaves.
822    pub fn from_leaves(leaves: &[LeafData]) -> Self {
823        let total_bytes: usize = leaves.iter().map(|l| l.bytes).sum();
824        PieceTree {
825            root: Self::build_balanced(leaves),
826            total_bytes,
827        }
828    }
829
830    /// Build a balanced tree from a list of leaves
831    fn build_balanced(leaves: &[LeafData]) -> Arc<PieceTreeNode> {
832        if leaves.is_empty() {
833            return Arc::new(PieceTreeNode::Leaf {
834                location: BufferLocation::Stored(0),
835                offset: 0,
836                bytes: 0,
837                line_feed_cnt: Some(0), // Empty has zero line feeds (known)
838            });
839        }
840
841        if leaves.len() == 1 {
842            let leaf = leaves[0];
843            return Arc::new(PieceTreeNode::Leaf {
844                location: leaf.location,
845                offset: leaf.offset,
846                bytes: leaf.bytes,
847                line_feed_cnt: leaf.line_feed_cnt,
848            });
849        }
850
851        // Split in the middle
852        let mid = leaves.len() / 2;
853        let left = Self::build_balanced(&leaves[..mid]);
854        let right = Self::build_balanced(&leaves[mid..]);
855
856        let left_bytes = left.total_bytes();
857        let lf_left = left.total_line_feeds();
858
859        Arc::new(PieceTreeNode::Internal {
860            left_bytes,
861            lf_left,
862            left,
863            right,
864        })
865    }
866
867    /// Path-copying insert: walk from root to the target leaf, split only that leaf,
868    /// and Arc::clone all unmodified siblings. Returns the new root.
869    fn path_copy_insert(
870        node: &Arc<PieceTreeNode>,
871        current_offset: usize,
872        insert_offset: usize,
873        insert_leaf: LeafData,
874        buffers: &[StringBuffer],
875    ) -> Arc<PieceTreeNode> {
876        match node.as_ref() {
877            PieceTreeNode::Internal {
878                left_bytes,
879                lf_left,
880                left,
881                right,
882                ..
883            } => {
884                let left_end = current_offset + left_bytes;
885                if insert_offset < left_end {
886                    // Insert is in left subtree - recurse left, clone right
887                    let new_left = Self::path_copy_insert(
888                        left,
889                        current_offset,
890                        insert_offset,
891                        insert_leaf,
892                        buffers,
893                    );
894                    let new_left_bytes = new_left.total_bytes();
895                    let new_lf_left = new_left.total_line_feeds();
896                    Arc::new(PieceTreeNode::Internal {
897                        left_bytes: new_left_bytes,
898                        lf_left: new_lf_left,
899                        left: new_left,
900                        right: Arc::clone(right),
901                    })
902                } else if insert_offset > left_end {
903                    // Insert is in right subtree - clone left, recurse right
904                    let new_right = Self::path_copy_insert(
905                        right,
906                        left_end,
907                        insert_offset,
908                        insert_leaf,
909                        buffers,
910                    );
911                    Arc::new(PieceTreeNode::Internal {
912                        left_bytes: *left_bytes,
913                        lf_left: *lf_left,
914                        left: Arc::clone(left),
915                        right: new_right,
916                    })
917                } else {
918                    // insert_offset == left_end: boundary between left and right.
919                    // Check if right subtree starts at insert_offset - if so,
920                    // insert at the beginning of the right subtree.
921                    // We need to recurse into right to prepend.
922                    let new_right = Self::path_copy_insert(
923                        right,
924                        left_end,
925                        insert_offset,
926                        insert_leaf,
927                        buffers,
928                    );
929                    Arc::new(PieceTreeNode::Internal {
930                        left_bytes: *left_bytes,
931                        lf_left: *lf_left,
932                        left: Arc::clone(left),
933                        right: new_right,
934                    })
935                }
936            }
937            PieceTreeNode::Leaf {
938                location,
939                offset,
940                bytes,
941                line_feed_cnt,
942            } => {
943                let piece_end = current_offset + bytes;
944                let offset_in_piece = insert_offset.saturating_sub(current_offset);
945
946                if offset_in_piece > 0 && insert_offset < piece_end {
947                    // Split this leaf at insert_offset
948                    let before_bytes = offset_in_piece;
949                    let after_bytes = bytes - offset_in_piece;
950
951                    let lf_before =
952                        Self::compute_line_feeds_static(buffers, *location, *offset, before_bytes);
953                    let lf_after = Self::compute_line_feeds_static(
954                        buffers,
955                        *location,
956                        offset + before_bytes,
957                        after_bytes,
958                    );
959
960                    let leaf_before = LeafData::new(*location, *offset, before_bytes, lf_before);
961                    let leaf_after =
962                        LeafData::new(*location, offset + before_bytes, after_bytes, lf_after);
963
964                    // Build a small subtree: [before, insert, after]
965                    Self::build_balanced(&[leaf_before, insert_leaf, leaf_after])
966                } else if insert_offset <= current_offset {
967                    // Insert before this leaf
968                    if *bytes == 0 {
969                        // Replace empty leaf with the insert
970                        Arc::new(PieceTreeNode::Leaf {
971                            location: insert_leaf.location,
972                            offset: insert_leaf.offset,
973                            bytes: insert_leaf.bytes,
974                            line_feed_cnt: insert_leaf.line_feed_cnt,
975                        })
976                    } else {
977                        let existing = LeafData::new(*location, *offset, *bytes, *line_feed_cnt);
978                        Self::build_balanced(&[insert_leaf, existing])
979                    }
980                } else {
981                    // Insert after this leaf (insert_offset >= piece_end)
982                    let existing = LeafData::new(*location, *offset, *bytes, *line_feed_cnt);
983                    Self::build_balanced(&[existing, insert_leaf])
984                }
985            }
986        }
987    }
988
989    /// Path-copying delete: walk from root, only rebuilding nodes along
990    /// the path to the deleted range, Arc::clone-ing unaffected siblings.
991    /// Returns the new root. The delete range is [delete_start, delete_end) in document offsets.
992    fn path_copy_delete(
993        node: &Arc<PieceTreeNode>,
994        current_offset: usize,
995        delete_start: usize,
996        delete_end: usize,
997        buffers: &[StringBuffer],
998    ) -> Arc<PieceTreeNode> {
999        match node.as_ref() {
1000            PieceTreeNode::Internal {
1001                left_bytes,
1002                lf_left,
1003                left,
1004                right,
1005                ..
1006            } => {
1007                let left_end = current_offset + left_bytes;
1008
1009                // Delete range entirely in left subtree
1010                if delete_end <= left_end {
1011                    let new_left = Self::path_copy_delete(
1012                        left,
1013                        current_offset,
1014                        delete_start,
1015                        delete_end,
1016                        buffers,
1017                    );
1018                    let new_left_bytes = new_left.total_bytes();
1019                    if new_left_bytes == 0 {
1020                        // Left is empty after delete, promote right
1021                        return Arc::clone(right);
1022                    }
1023                    let new_lf_left = new_left.total_line_feeds();
1024                    return Arc::new(PieceTreeNode::Internal {
1025                        left_bytes: new_left_bytes,
1026                        lf_left: new_lf_left,
1027                        left: new_left,
1028                        right: Arc::clone(right),
1029                    });
1030                }
1031
1032                // Delete range entirely in right subtree
1033                if delete_start >= left_end {
1034                    let new_right =
1035                        Self::path_copy_delete(right, left_end, delete_start, delete_end, buffers);
1036                    let new_right_bytes = new_right.total_bytes();
1037                    if new_right_bytes == 0 {
1038                        // Right is empty after delete, promote left
1039                        return Arc::clone(left);
1040                    }
1041                    return Arc::new(PieceTreeNode::Internal {
1042                        left_bytes: *left_bytes,
1043                        lf_left: *lf_left,
1044                        left: Arc::clone(left),
1045                        right: new_right,
1046                    });
1047                }
1048
1049                // Delete range spans both children
1050                let new_left = Self::path_copy_delete(
1051                    left,
1052                    current_offset,
1053                    delete_start,
1054                    left_end.min(delete_end),
1055                    buffers,
1056                );
1057                let new_right = Self::path_copy_delete(
1058                    right,
1059                    left_end,
1060                    left_end.max(delete_start),
1061                    delete_end,
1062                    buffers,
1063                );
1064
1065                let new_left_bytes = new_left.total_bytes();
1066                let new_right_bytes = new_right.total_bytes();
1067
1068                if new_left_bytes == 0 && new_right_bytes == 0 {
1069                    // Both empty - return empty leaf
1070                    return Arc::new(PieceTreeNode::Leaf {
1071                        location: BufferLocation::Stored(0),
1072                        offset: 0,
1073                        bytes: 0,
1074                        line_feed_cnt: Some(0),
1075                    });
1076                }
1077                if new_left_bytes == 0 {
1078                    return new_right;
1079                }
1080                if new_right_bytes == 0 {
1081                    return new_left;
1082                }
1083
1084                let new_lf_left = new_left.total_line_feeds();
1085                Arc::new(PieceTreeNode::Internal {
1086                    left_bytes: new_left_bytes,
1087                    lf_left: new_lf_left,
1088                    left: new_left,
1089                    right: new_right,
1090                })
1091            }
1092            PieceTreeNode::Leaf {
1093                location,
1094                offset,
1095                bytes,
1096                line_feed_cnt: _,
1097            } => {
1098                let piece_start = current_offset;
1099                let piece_end = current_offset + bytes;
1100
1101                // No overlap - keep as is
1102                if piece_end <= delete_start || piece_start >= delete_end {
1103                    return Arc::clone(node);
1104                }
1105
1106                // Completely deleted
1107                if delete_start <= piece_start && delete_end >= piece_end {
1108                    return Arc::new(PieceTreeNode::Leaf {
1109                        location: BufferLocation::Stored(0),
1110                        offset: 0,
1111                        bytes: 0,
1112                        line_feed_cnt: Some(0),
1113                    });
1114                }
1115
1116                // Partial overlap - keep parts outside delete range
1117                let before_bytes = delete_start.saturating_sub(piece_start);
1118                let after_bytes = piece_end.saturating_sub(delete_end);
1119
1120                if before_bytes > 0 && after_bytes > 0 {
1121                    // Delete in the middle of this leaf - split into two
1122                    let lf_before =
1123                        Self::compute_line_feeds_static(buffers, *location, *offset, before_bytes);
1124                    let lf_after = Self::compute_line_feeds_static(
1125                        buffers,
1126                        *location,
1127                        offset + bytes - after_bytes,
1128                        after_bytes,
1129                    );
1130                    let leaf_before = LeafData::new(*location, *offset, before_bytes, lf_before);
1131                    let leaf_after = LeafData::new(
1132                        *location,
1133                        offset + bytes - after_bytes,
1134                        after_bytes,
1135                        lf_after,
1136                    );
1137                    Self::build_balanced(&[leaf_before, leaf_after])
1138                } else if before_bytes > 0 {
1139                    // Keep only the part before the delete
1140                    let lf_cnt =
1141                        Self::compute_line_feeds_static(buffers, *location, *offset, before_bytes);
1142                    Arc::new(PieceTreeNode::Leaf {
1143                        location: *location,
1144                        offset: *offset,
1145                        bytes: before_bytes,
1146                        line_feed_cnt: lf_cnt,
1147                    })
1148                } else {
1149                    // Keep only the part after the delete
1150                    let skip = delete_end - piece_start;
1151                    let lf_cnt = Self::compute_line_feeds_static(
1152                        buffers,
1153                        *location,
1154                        offset + skip,
1155                        after_bytes,
1156                    );
1157                    Arc::new(PieceTreeNode::Leaf {
1158                        location: *location,
1159                        offset: offset + skip,
1160                        bytes: after_bytes,
1161                        line_feed_cnt: lf_cnt,
1162                    })
1163                }
1164            }
1165        }
1166    }
1167
1168    /// Rebuild the tree to be balanced
1169    pub(crate) fn rebalance(&mut self) {
1170        let mut leaves = Vec::new();
1171        self.root.collect_leaves(&mut leaves);
1172        self.root = Self::build_balanced(&leaves);
1173    }
1174
1175    /// Check if rebalancing is needed and do it
1176    fn check_and_rebalance(&mut self) {
1177        let count = self.root.count_leaves();
1178        if count < 2 {
1179            return;
1180        }
1181
1182        let depth = self.root.depth();
1183        let max_depth = 2 * (count as f64).log2().ceil() as usize;
1184
1185        if depth > max_depth {
1186            self.rebalance();
1187        }
1188    }
1189
1190    /// Find the piece at the given byte offset
1191    pub fn find_by_offset(&self, offset: usize) -> Option<PieceInfo> {
1192        if offset >= self.total_bytes {
1193            return None;
1194        }
1195        self.root.find_by_offset(offset).map(|result| result.info)
1196    }
1197
1198    /// Create a cursor at the given byte offset
1199    /// Note: line/col calculation should be done by LineIndex
1200    pub fn cursor_at_offset(&self, offset: usize) -> Cursor {
1201        Cursor {
1202            byte_offset: offset.min(self.total_bytes),
1203            line: 0,
1204            col: 0,
1205        }
1206    }
1207
1208    /// Insert text at the given offset
1209    /// Returns new cursor after the inserted text
1210    /// line_feed_cnt: number of line feeds in the inserted text (None if unknown/not computed)
1211    /// buffers: reference to the string buffers for computing line feeds during splits
1212    pub fn insert(
1213        &mut self,
1214        offset: usize,
1215        location: BufferLocation,
1216        buffer_offset: usize,
1217        bytes: usize,
1218        line_feed_cnt: Option<usize>,
1219        buffers: &[StringBuffer],
1220    ) -> Cursor {
1221        if bytes == 0 {
1222            return self.cursor_at_offset(offset);
1223        }
1224
1225        let insert_leaf = LeafData::new(location, buffer_offset, bytes, line_feed_cnt);
1226        self.root = Self::path_copy_insert(&self.root, 0, offset, insert_leaf, buffers);
1227        self.total_bytes += bytes;
1228        self.check_and_rebalance();
1229
1230        self.cursor_at_offset(offset + bytes)
1231    }
1232
1233    /// Get a clone of the root node (shared via Arc)
1234    pub fn root(&self) -> Arc<PieceTreeNode> {
1235        Arc::clone(&self.root)
1236    }
1237
1238    /// Insert text at the given position (line, column)
1239    /// Returns new cursor after the inserted text
1240    /// This converts position to offset, then uses path-copying insert
1241    #[allow(clippy::too_many_arguments)]
1242    pub fn insert_at_position(
1243        &mut self,
1244        line: usize,
1245        column: usize,
1246        location: BufferLocation,
1247        buffer_offset: usize,
1248        bytes: usize,
1249        line_feed_cnt: usize,
1250        buffers: &[StringBuffer],
1251    ) -> Cursor {
1252        let offset = self.position_to_offset(line, column, buffers);
1253        self.insert(
1254            offset,
1255            location,
1256            buffer_offset,
1257            bytes,
1258            Some(line_feed_cnt),
1259            buffers,
1260        )
1261    }
1262
1263    /// Helper to collect leaves while splitting at insertion point
1264    fn collect_leaves_with_split(
1265        &self,
1266        node: &Arc<PieceTreeNode>,
1267        current_offset: usize,
1268        split_offset: usize,
1269        insert: Option<LeafData>,
1270        leaves: &mut Vec<LeafData>,
1271        buffers: &[StringBuffer],
1272    ) {
1273        match node.as_ref() {
1274            PieceTreeNode::Internal {
1275                left_bytes,
1276                left,
1277                right,
1278                ..
1279            } => {
1280                // Only pass `insert` to the subtree containing the split point
1281                if split_offset < current_offset + left_bytes {
1282                    // Split is in left subtree
1283                    self.collect_leaves_with_split(
1284                        left,
1285                        current_offset,
1286                        split_offset,
1287                        insert,
1288                        leaves,
1289                        buffers,
1290                    );
1291                    self.collect_leaves_with_split(
1292                        right,
1293                        current_offset + left_bytes,
1294                        split_offset,
1295                        None,
1296                        leaves,
1297                        buffers,
1298                    );
1299                } else {
1300                    // Split is in right subtree (or at boundary)
1301                    self.collect_leaves_with_split(
1302                        left,
1303                        current_offset,
1304                        split_offset,
1305                        None,
1306                        leaves,
1307                        buffers,
1308                    );
1309                    self.collect_leaves_with_split(
1310                        right,
1311                        current_offset + left_bytes,
1312                        split_offset,
1313                        insert,
1314                        leaves,
1315                        buffers,
1316                    );
1317                }
1318            }
1319            PieceTreeNode::Leaf {
1320                location,
1321                offset,
1322                bytes,
1323                line_feed_cnt,
1324            } => {
1325                let piece_end = current_offset + bytes;
1326
1327                if split_offset > current_offset && split_offset < piece_end {
1328                    // Split this piece - need to compute line feeds for each part
1329                    let offset_in_piece = split_offset - current_offset;
1330
1331                    // First part (before split)
1332                    if offset_in_piece > 0 {
1333                        let lf_cnt = Self::compute_line_feeds_static(
1334                            buffers,
1335                            *location,
1336                            *offset,
1337                            offset_in_piece,
1338                        );
1339                        leaves.push(LeafData::new(*location, *offset, offset_in_piece, lf_cnt));
1340                    }
1341
1342                    // Inserted piece
1343                    if let Some(insert_leaf) = insert {
1344                        leaves.push(insert_leaf);
1345                    }
1346
1347                    // Second part (after split)
1348                    let remaining = bytes - offset_in_piece;
1349                    if remaining > 0 {
1350                        let lf_cnt = Self::compute_line_feeds_static(
1351                            buffers,
1352                            *location,
1353                            offset + offset_in_piece,
1354                            remaining,
1355                        );
1356                        leaves.push(LeafData::new(
1357                            *location,
1358                            offset + offset_in_piece,
1359                            remaining,
1360                            lf_cnt,
1361                        ));
1362                    }
1363                } else if split_offset == current_offset {
1364                    // Insert before this piece
1365                    if let Some(insert_leaf) = insert {
1366                        leaves.push(insert_leaf);
1367                    }
1368                    leaves.push(LeafData::new(*location, *offset, *bytes, *line_feed_cnt));
1369                } else {
1370                    // Don't split, just add the piece
1371                    leaves.push(LeafData::new(*location, *offset, *bytes, *line_feed_cnt));
1372                }
1373            }
1374        }
1375    }
1376
1377    /// Helper to compute line feeds in a buffer range
1378    fn compute_line_feeds_static(
1379        buffers: &[StringBuffer],
1380        location: BufferLocation,
1381        offset: usize,
1382        bytes: usize,
1383    ) -> Option<usize> {
1384        let buffer_id = location.buffer_id();
1385        if let Some(buffer) = buffers.get(buffer_id) {
1386            if let Some(data) = buffer.get_data() {
1387                let end = (offset + bytes).min(data.len());
1388                Some(data[offset..end].iter().filter(|&&b| b == b'\n').count())
1389            } else {
1390                // Buffer is unloaded - return None
1391                None
1392            }
1393        } else {
1394            // Buffer not available - return None
1395            None
1396        }
1397    }
1398
1399    /// Split a piece at the given offset without inserting anything
1400    /// This is useful for isolating a chunk of a large piece for partial loading
1401    ///
1402    /// If the offset is in the middle of a piece, that piece will be split into two pieces.
1403    /// If the offset is at a piece boundary, nothing changes.
1404    /// Does nothing if offset is 0 or >= total_bytes.
1405    pub fn split_at_offset(&mut self, offset: usize, buffers: &[StringBuffer]) {
1406        if offset == 0 || offset >= self.total_bytes {
1407            return;
1408        }
1409
1410        // Check if we need to split (offset must be in middle of a piece)
1411        if let Some(_result) = self.root.find_by_offset(offset) {
1412            // Split the piece at the offset (with no insertion)
1413            let mut leaves = Vec::new();
1414            self.collect_leaves_with_split(&self.root, 0, offset, None, &mut leaves, buffers);
1415
1416            self.root = Self::build_balanced(&leaves);
1417            self.check_and_rebalance();
1418        }
1419    }
1420
1421    /// Replace buffer references in pieces
1422    /// This is used when creating chunk buffers from large unloaded buffers
1423    ///
1424    /// Finds all pieces that reference the old buffer at the specified offset/bytes
1425    /// and updates them to reference the new buffer at offset 0.
1426    pub fn replace_buffer_reference(
1427        &mut self,
1428        old_buffer_id: usize,
1429        old_buffer_offset: usize,
1430        old_buffer_bytes: usize,
1431        new_buffer_location: BufferLocation,
1432    ) {
1433        let mut leaves = Vec::new();
1434        self.root.collect_leaves(&mut leaves);
1435
1436        // Find and update matching pieces
1437        let mut modified = false;
1438        for leaf in &mut leaves {
1439            if leaf.location.buffer_id() == old_buffer_id
1440                && leaf.offset == old_buffer_offset
1441                && leaf.bytes == old_buffer_bytes
1442            {
1443                leaf.location = new_buffer_location;
1444                leaf.offset = 0; // New buffer starts at 0
1445                modified = true;
1446            }
1447        }
1448
1449        // Rebuild tree if we made changes
1450        if modified {
1451            self.root = Self::build_balanced(&leaves);
1452            self.check_and_rebalance();
1453        }
1454    }
1455
1456    /// Delete text starting at offset for the given number of bytes
1457    pub fn delete(&mut self, offset: usize, delete_bytes: usize, buffers: &[StringBuffer]) {
1458        if delete_bytes == 0 || offset >= self.total_bytes {
1459            return;
1460        }
1461
1462        let delete_bytes = delete_bytes.min(self.total_bytes - offset);
1463        let end_offset = offset + delete_bytes;
1464
1465        self.root = Self::path_copy_delete(&self.root, 0, offset, end_offset, buffers);
1466        self.total_bytes -= delete_bytes;
1467
1468        self.check_and_rebalance();
1469    }
1470
1471    /// Delete text in a range specified by positions (start_line, start_col) to (end_line, end_col)
1472    /// Converts positions to offsets, then uses path-copying delete
1473    pub fn delete_position_range(
1474        &mut self,
1475        start_line: usize,
1476        start_column: usize,
1477        end_line: usize,
1478        end_column: usize,
1479        buffers: &[StringBuffer],
1480    ) {
1481        // Edge case: empty range
1482        if start_line == end_line && start_column == end_column {
1483            return;
1484        }
1485
1486        let start_offset = self.position_to_offset(start_line, start_column, buffers);
1487        let end_offset = self.position_to_offset(end_line, end_column, buffers);
1488
1489        if end_offset > start_offset {
1490            self.delete(start_offset, end_offset - start_offset, buffers);
1491        }
1492    }
1493
1494    /// Get the total number of bytes in the document
1495    pub fn total_bytes(&self) -> usize {
1496        self.total_bytes
1497    }
1498
1499    /// Get the total number of lines in the document
1500    /// Line count = line feeds + 1
1501    /// Returns None if any piece has unknown line count
1502    pub fn line_count(&self) -> Option<usize> {
1503        self.root.total_line_feeds().map(|lf| lf + 1)
1504    }
1505
1506    /// Find the document byte offset and metadata for the piece containing a target line.
1507    ///
1508    /// Uses only the tree's `lf_left` / `line_feed_cnt` metadata (no buffer data needed).
1509    /// Returns `(doc_byte_offset, buffer_id, piece_offset, piece_bytes, lines_before_piece)`
1510    /// where `lines_before_piece` is the number of complete lines before this piece.
1511    pub fn piece_info_for_line(
1512        &self,
1513        target_line: usize,
1514    ) -> Option<(usize, usize, usize, usize, usize)> {
1515        self.root.find_piece_for_line(0, 0, target_line)
1516    }
1517
1518    /// Rebuild the tree with updated line feed counts from external data.
1519    ///
1520    /// Takes a slice of `(leaf_index, line_feed_count)` pairs. Each entry updates
1521    /// the corresponding leaf (by order in the tree) with the given line feed count.
1522    /// After updating, rebuilds the tree so that `lf_left` values on internal nodes
1523    /// are correct.
1524    ///
1525    /// This keeps the piece tree free of I/O — the caller is responsible for
1526    /// reading chunk data and counting line feeds externally.
1527    pub fn update_leaf_line_feeds(&mut self, updates: &[(usize, usize)]) {
1528        if updates.is_empty() {
1529            return;
1530        }
1531
1532        let mut leaves = Vec::new();
1533        self.root.collect_leaves(&mut leaves);
1534
1535        for &(idx, lf_count) in updates {
1536            if let Some(leaf) = leaves.get_mut(idx) {
1537                leaf.line_feed_cnt = Some(lf_count);
1538            }
1539        }
1540
1541        self.root = Self::build_balanced(&leaves);
1542    }
1543
1544    /// Update leaf line feed counts using path-copying.
1545    /// Like `update_leaf_line_feeds` but only clones nodes along each updated
1546    /// leaf's root-to-leaf path, preserving `Arc::ptr_eq` for unmodified subtrees.
1547    pub fn update_leaf_line_feeds_path_copy(&mut self, updates: &[(usize, usize)]) {
1548        for &(idx, lf_count) in updates {
1549            let (new_root, _) = self.root.update_leaf_lf_by_index(idx, lf_count);
1550            self.root = new_root;
1551        }
1552    }
1553
1554    /// Get tree statistics for debugging
1555    pub fn stats(&self) -> TreeStats {
1556        TreeStats {
1557            total_bytes: self.total_bytes,
1558            depth: self.root.depth(),
1559            leaf_count: self.root.count_leaves(),
1560            line_feed_count: self.root.total_line_feeds(),
1561        }
1562    }
1563
1564    /// Get all leaves in order (for debugging)
1565    pub fn get_leaves(&self) -> Vec<LeafData> {
1566        let mut leaves = Vec::new();
1567        self.root.collect_leaves(&mut leaves);
1568        leaves
1569    }
1570
1571    /// Split any leaves larger than `max_bytes` into smaller pieces.
1572    ///
1573    /// This is a bulk operation that collects all leaves, subdivides oversized
1574    /// ones, and rebuilds the tree once.  Line-feed counts on the new sub-leaves
1575    /// are set to `None` (the caller is expected to fill them in via scanning).
1576    pub fn split_leaves_to_chunk_size(&mut self, max_bytes: usize) {
1577        let old_leaves = self.get_leaves();
1578        let mut new_leaves = Vec::with_capacity(old_leaves.len());
1579        let mut changed = false;
1580
1581        for leaf in &old_leaves {
1582            if leaf.bytes <= max_bytes {
1583                new_leaves.push(*leaf);
1584            } else {
1585                changed = true;
1586                let mut off = 0;
1587                while off < leaf.bytes {
1588                    let len = max_bytes.min(leaf.bytes - off);
1589                    new_leaves.push(LeafData::new(leaf.location, leaf.offset + off, len, None));
1590                    off += len;
1591                }
1592            }
1593        }
1594
1595        if changed {
1596            self.root = Self::build_balanced(&new_leaves);
1597        }
1598    }
1599
1600    /// Convert byte offset to line/column position using tree's line metadata
1601    pub fn offset_to_position(
1602        &self,
1603        offset: usize,
1604        buffers: &[StringBuffer],
1605    ) -> Option<(usize, usize)> {
1606        if offset == 0 {
1607            return Some((0, 0));
1608        }
1609
1610        let offset = offset.min(self.total_bytes);
1611
1612        // Find the piece containing this offset
1613        if let Some(result) = self.root.find_by_offset(offset) {
1614            let piece_info = result.info;
1615            let bytes_before = result.bytes_before;
1616
1617            // Count lines before this piece
1618            // If line count is unknown, return None - we can't reliably compute position
1619            let lines_before = match self.count_lines_before_offset(bytes_before) {
1620                Some(count) => count,
1621                None => {
1622                    // No line metadata available - cannot compute position reliably
1623                    return None;
1624                }
1625            };
1626
1627            // Get the buffer for this piece
1628            let buffer_id = piece_info.location.buffer_id();
1629            if let Some(buffer) = buffers.get(buffer_id) {
1630                let offset_in_piece = piece_info.offset_in_piece.unwrap_or(0);
1631
1632                // Check if we have line starts available
1633                if let Some(line_starts) = buffer.get_line_starts() {
1634                    let byte_offset_in_buffer = piece_info.offset + offset_in_piece;
1635
1636                    // Find which line within the buffer
1637                    let line_in_buffer = line_starts
1638                        .binary_search(&byte_offset_in_buffer)
1639                        .unwrap_or_else(|i| i.saturating_sub(1));
1640
1641                    // Find which line the piece starts at in the buffer
1642                    let piece_start_line = line_starts
1643                        .binary_search(&piece_info.offset)
1644                        .unwrap_or_else(|i| i.saturating_sub(1));
1645
1646                    // Calculate line relative to piece start (not buffer start)
1647                    let line_in_piece = line_in_buffer - piece_start_line;
1648
1649                    // Calculate the document line number
1650                    let doc_line = lines_before + line_in_piece;
1651
1652                    // Calculate column
1653                    let column = if line_in_piece == 0 && bytes_before == 0 {
1654                        // Fast path: piece is at document start, so column is just offset within piece
1655                        offset_in_piece
1656                    } else if line_in_piece == 0 {
1657                        // We're on the first line of this piece, but the document line may have
1658                        // started before this piece (after modifications). Find the actual line start.
1659                        let line_start = self.position_to_offset(doc_line, 0, buffers);
1660                        offset.saturating_sub(line_start)
1661                    } else {
1662                        // Line starts within this piece
1663                        let line_start_in_buf = buffer
1664                            .nth_line_start_in_piece(
1665                                piece_info.offset,
1666                                piece_info.offset + piece_info.bytes,
1667                                line_in_piece,
1668                            )
1669                            .unwrap_or(piece_info.offset);
1670                        let line_start_offset_in_piece = line_start_in_buf - piece_info.offset;
1671                        offset_in_piece - line_start_offset_in_piece
1672                    };
1673
1674                    return Some((doc_line, column));
1675                }
1676
1677                // No line_starts index, but buffer data may be loaded (e.g. scanned
1678                // large file with lazy-loaded chunks). Count newlines directly.
1679                if let Some(data) = buffer.get_data() {
1680                    let piece_start = piece_info.offset;
1681                    let scan_end = (piece_start + offset_in_piece).min(data.len());
1682                    let line_in_piece = data[piece_start..scan_end]
1683                        .iter()
1684                        .filter(|&&b| b == b'\n')
1685                        .count();
1686                    let doc_line = lines_before + line_in_piece;
1687
1688                    // Column: bytes since last newline (or piece start)
1689                    let column = if line_in_piece == 0 {
1690                        if bytes_before == 0 {
1691                            offset_in_piece
1692                        } else {
1693                            // First line of piece may span from previous piece
1694                            let line_start = self.position_to_offset(doc_line, 0, buffers);
1695                            offset.saturating_sub(line_start)
1696                        }
1697                    } else {
1698                        // Find last newline before our offset within the piece
1699                        let last_nl = data[piece_start..scan_end]
1700                            .iter()
1701                            .rposition(|&b| b == b'\n')
1702                            .unwrap_or(0);
1703                        offset_in_piece - last_nl - 1
1704                    };
1705
1706                    return Some((doc_line, column));
1707                }
1708                // Buffer unloaded and no line starts.
1709                // If we have line feed counts (scanned), estimate proportionally
1710                // within the piece: lines_before + (offset_in_piece / piece_bytes) * piece_line_feeds
1711                if let Some(lf_count) = piece_info.line_feeds {
1712                    let piece_bytes = piece_info.bytes;
1713                    let lines_in_partial = if piece_bytes > 0 {
1714                        (offset_in_piece as u64 * lf_count as u64 / piece_bytes as u64) as usize
1715                    } else {
1716                        0
1717                    };
1718                    let doc_line = lines_before + lines_in_partial;
1719                    // Column estimate: offset_in_piece modulo average bytes per line
1720                    let avg_bytes_per_line = if lf_count > 0 {
1721                        piece_bytes / lf_count
1722                    } else {
1723                        80
1724                    };
1725                    let column = if avg_bytes_per_line > 0 {
1726                        offset_in_piece % avg_bytes_per_line
1727                    } else {
1728                        0
1729                    };
1730                    return Some((doc_line, column));
1731                }
1732            }
1733        }
1734
1735        // Fallback: end of document or no metadata
1736        // Only if we have line metadata and the offset is actually at/near the end
1737        if offset >= self.total_bytes {
1738            match self.line_count() {
1739                Some(line_count) => {
1740                    let last_line = line_count.saturating_sub(1);
1741                    let line_start = self.position_to_offset(last_line, 0, buffers);
1742                    let column = self.total_bytes.saturating_sub(line_start);
1743                    return Some((last_line, column));
1744                }
1745                None => return None,
1746            }
1747        }
1748        None
1749    }
1750
1751    /// Convert line/column position to byte offset using tree's line metadata
1752    pub fn position_to_offset(
1753        &self,
1754        line: usize,
1755        column: usize,
1756        buffers: &[StringBuffer],
1757    ) -> usize {
1758        if line == 0 && column == 0 {
1759            return 0;
1760        }
1761
1762        // Traverse tree to find the piece containing the target line
1763        self.find_offset_for_line(line, column, buffers)
1764            .unwrap_or(self.total_bytes)
1765    }
1766
1767    /// Helper: count line feeds before a given byte offset
1768    /// Returns None if any piece has unknown line count
1769    fn count_lines_before_offset(&self, byte_offset: usize) -> Option<usize> {
1770        self.count_lines_in_range(0, byte_offset)
1771    }
1772
1773    /// Helper: count line feeds in a byte range
1774    /// Returns None if any piece has unknown line count
1775    fn count_lines_in_range(&self, start: usize, end: usize) -> Option<usize> {
1776        if start >= end {
1777            return Some(0);
1778        }
1779
1780        self.root.count_lines_in_byte_range(0, start, end)
1781    }
1782
1783    /// Helper: find byte offset for a given line/column
1784    fn find_offset_for_line(
1785        &self,
1786        target_line: usize,
1787        column: usize,
1788        buffers: &[StringBuffer],
1789    ) -> Option<usize> {
1790        self.root
1791            .find_byte_offset_for_line(0, 0, target_line, column, buffers)
1792    }
1793
1794    /// Get the byte range for a specific line
1795    pub fn line_range(
1796        &self,
1797        line: usize,
1798        buffers: &[StringBuffer],
1799    ) -> Option<(usize, Option<usize>)> {
1800        // Check if line exists
1801        let line_count = self.line_count()?;
1802        if line >= line_count {
1803            return None;
1804        }
1805
1806        let start = self.position_to_offset(line, 0, buffers);
1807        let end = if line + 1 < line_count {
1808            Some(self.position_to_offset(line + 1, 0, buffers))
1809        } else {
1810            None
1811        };
1812        Some((start, end))
1813    }
1814
1815    /// Iterate through pieces overlapping a byte range
1816    /// Does ONE O(log n) tree traversal, then iterates sequentially
1817    pub fn iter_pieces_in_range(&self, start: usize, end: usize) -> PieceRangeIter {
1818        PieceRangeIter::new(&self.root, start, end)
1819    }
1820
1821    /// Apply multiple edits in a single tree traversal + rebuild
1822    ///
1823    /// # Arguments
1824    /// * `edits` - Vec of (position, delete_len, insert_text), MUST be sorted descending by position
1825    /// * `buffers` - Reference to string buffers (for line feed computation)
1826    /// * `add_text_fn` - Function to add text to buffer, returns (BufferLocation, offset, bytes)
1827    ///
1828    /// # Complexity
1829    /// O(pieces + edits) instead of O(pieces × edits)
1830    ///
1831    /// # Returns
1832    /// The net change in total bytes
1833    pub fn apply_bulk_edits<F>(
1834        &mut self,
1835        edits: &[(usize, usize, &str)],
1836        buffers: &[StringBuffer],
1837        mut add_text_fn: F,
1838    ) -> isize
1839    where
1840        F: FnMut(&str) -> (BufferLocation, usize, usize, Option<usize>),
1841    {
1842        if edits.is_empty() {
1843            return 0;
1844        }
1845
1846        // 1. Collect all split points (both start and end of each edit range)
1847        let mut split_points: Vec<usize> = Vec::with_capacity(edits.len() * 2);
1848        for (pos, del_len, _) in edits {
1849            split_points.push(*pos);
1850            if *del_len > 0 {
1851                let end = pos.saturating_add(*del_len).min(self.total_bytes);
1852                if end > *pos {
1853                    split_points.push(end);
1854                }
1855            }
1856        }
1857        split_points.sort_unstable();
1858        split_points.dedup();
1859
1860        // 2. Collect all leaves, splitting at all required points
1861        let mut leaves = Vec::new();
1862        self.collect_leaves_with_multi_split(
1863            &self.root.clone(),
1864            0,
1865            &split_points,
1866            &mut leaves,
1867            buffers,
1868        );
1869
1870        // 3. Build edit ranges for quick lookup (sorted descending by position)
1871        // Each edit: (start, end, insert_leaf)
1872        let mut edit_ranges: Vec<(usize, usize, Option<LeafData>)> =
1873            Vec::with_capacity(edits.len());
1874        for (pos, del_len, text) in edits {
1875            let del_end = pos.saturating_add(*del_len).min(self.total_bytes);
1876            let insert_leaf = if !text.is_empty() {
1877                let (location, offset, bytes, lf_cnt) = add_text_fn(text);
1878                Some(LeafData::new(location, offset, bytes, lf_cnt))
1879            } else {
1880                None
1881            };
1882            edit_ranges.push((*pos, del_end, insert_leaf));
1883        }
1884
1885        // 4. Apply edits to leaves
1886        // Edits are sorted descending by position, so:
1887        //   edit_ranges[len-1] has smallest position, edit_ranges[0] has largest
1888        // We iterate leaves ascending, and for each leaf we:
1889        //   1. First add any inserts that belong BEFORE this leaf
1890        //   2. Then add the leaf (if not deleted)
1891        let mut new_leaves: Vec<LeafData> = Vec::with_capacity(leaves.len() + edits.len());
1892        let mut current_offset = 0;
1893        let mut edit_idx = edit_ranges.len(); // Points past the end; we access [edit_idx-1]
1894
1895        for leaf in leaves {
1896            let leaf_start = current_offset;
1897            let leaf_end = current_offset + leaf.bytes;
1898
1899            // First, add any inserts whose position is <= leaf_start
1900            // These inserts should appear BEFORE this leaf's content
1901            while edit_idx > 0 {
1902                let (edit_start, _edit_end, ref insert_leaf) = edit_ranges[edit_idx - 1];
1903
1904                // If this edit's position is after where we are, stop
1905                if edit_start > leaf_start {
1906                    break;
1907                }
1908
1909                // Insert belongs at or before leaf_start
1910                if let Some(insert) = insert_leaf {
1911                    new_leaves.push(*insert);
1912                }
1913                edit_idx -= 1;
1914            }
1915
1916            // Check if this leaf overlaps with ANY edit's delete range
1917            // We check ALL edits, not just remaining ones, because edits
1918            // processed in the insert loop above may still have deletions
1919            let mut keep_leaf = true;
1920
1921            for (edit_start, edit_end, _) in &edit_ranges {
1922                // If edit's delete range is entirely after this leaf, skip
1923                if *edit_start >= leaf_end {
1924                    continue;
1925                }
1926
1927                // If edit has no deletion (edit_start == edit_end), skip
1928                if *edit_start == *edit_end {
1929                    continue;
1930                }
1931
1932                // If edit's delete range is entirely before this leaf, skip
1933                if *edit_end <= leaf_start {
1934                    continue;
1935                }
1936
1937                // Leaf overlaps with this edit's delete range - filter it out
1938                if leaf_start >= *edit_start && leaf_end <= *edit_end {
1939                    keep_leaf = false;
1940                    break;
1941                }
1942            }
1943
1944            if keep_leaf {
1945                new_leaves.push(leaf);
1946            }
1947
1948            current_offset = leaf_end;
1949        }
1950
1951        // Handle any remaining inserts at the end of document
1952        while edit_idx > 0 {
1953            if let Some(insert) = &edit_ranges[edit_idx - 1].2 {
1954                new_leaves.push(*insert);
1955            }
1956            edit_idx -= 1;
1957        }
1958
1959        // 5. Calculate byte delta
1960        let old_bytes = self.total_bytes;
1961        let mut new_bytes: usize = 0;
1962        for leaf in &new_leaves {
1963            new_bytes += leaf.bytes;
1964        }
1965        let delta = new_bytes as isize - old_bytes as isize;
1966
1967        // 6. Single balanced tree rebuild
1968        self.root = Self::build_balanced(&new_leaves);
1969        self.total_bytes = new_bytes;
1970
1971        delta
1972    }
1973
1974    /// Collect leaves, splitting at multiple points in one traversal
1975    fn collect_leaves_with_multi_split(
1976        &self,
1977        node: &Arc<PieceTreeNode>,
1978        current_offset: usize,
1979        split_points: &[usize],
1980        leaves: &mut Vec<LeafData>,
1981        buffers: &[StringBuffer],
1982    ) {
1983        match node.as_ref() {
1984            PieceTreeNode::Internal {
1985                left_bytes,
1986                left,
1987                right,
1988                ..
1989            } => {
1990                // Recurse into both subtrees
1991                self.collect_leaves_with_multi_split(
1992                    left,
1993                    current_offset,
1994                    split_points,
1995                    leaves,
1996                    buffers,
1997                );
1998                self.collect_leaves_with_multi_split(
1999                    right,
2000                    current_offset + left_bytes,
2001                    split_points,
2002                    leaves,
2003                    buffers,
2004                );
2005            }
2006            PieceTreeNode::Leaf {
2007                location,
2008                offset,
2009                bytes,
2010                line_feed_cnt,
2011            } => {
2012                if *bytes == 0 {
2013                    return;
2014                }
2015
2016                let piece_start = current_offset;
2017                let piece_end = current_offset + bytes;
2018
2019                // Find split points within this piece
2020                let mut split_offsets: Vec<usize> = Vec::new();
2021                for &sp in split_points {
2022                    if sp > piece_start && sp < piece_end {
2023                        split_offsets.push(sp - piece_start);
2024                    }
2025                }
2026
2027                if split_offsets.is_empty() {
2028                    // No splits needed, add entire piece
2029                    leaves.push(LeafData::new(*location, *offset, *bytes, *line_feed_cnt));
2030                } else {
2031                    // Split the piece at each point
2032                    split_offsets.sort_unstable();
2033                    split_offsets.dedup();
2034
2035                    let mut prev_offset = 0;
2036                    for split_offset in split_offsets {
2037                        if split_offset > prev_offset {
2038                            let chunk_bytes = split_offset - prev_offset;
2039                            let lf_cnt = Self::compute_line_feeds_static(
2040                                buffers,
2041                                *location,
2042                                offset + prev_offset,
2043                                chunk_bytes,
2044                            );
2045                            leaves.push(LeafData::new(
2046                                *location,
2047                                offset + prev_offset,
2048                                chunk_bytes,
2049                                lf_cnt,
2050                            ));
2051                        }
2052                        prev_offset = split_offset;
2053                    }
2054
2055                    // Add remaining part after last split
2056                    if prev_offset < *bytes {
2057                        let remaining = bytes - prev_offset;
2058                        let lf_cnt = Self::compute_line_feeds_static(
2059                            buffers,
2060                            *location,
2061                            offset + prev_offset,
2062                            remaining,
2063                        );
2064                        leaves.push(LeafData::new(
2065                            *location,
2066                            offset + prev_offset,
2067                            remaining,
2068                            lf_cnt,
2069                        ));
2070                    }
2071                }
2072            }
2073        }
2074    }
2075}
2076
2077/// A view into a piece's data within the document
2078#[derive(Debug, Clone)]
2079pub struct PieceView {
2080    /// The location of this piece (which buffer it references)
2081    pub location: BufferLocation,
2082    /// Offset within the source buffer where this piece starts
2083    pub buffer_offset: usize,
2084    /// Number of bytes in this piece
2085    pub bytes: usize,
2086    /// Byte offset where this piece starts in the document
2087    pub doc_offset: usize,
2088    /// Number of line feeds in this piece (None if unknown for large files)
2089    pub line_feed_cnt: Option<usize>,
2090}
2091
2092/// Iterator over pieces in a byte range
2093/// Performs ONE O(log n) traversal to collect pieces, then iterates in O(1) per piece
2094pub struct PieceRangeIter {
2095    pieces: Vec<PieceView>,
2096    current_index: usize,
2097}
2098
2099impl PieceRangeIter {
2100    fn new(root: &Arc<PieceTreeNode>, start: usize, end: usize) -> Self {
2101        let mut pieces = Vec::new();
2102        Self::collect_pieces(root, 0, start, end, &mut pieces);
2103        PieceRangeIter {
2104            pieces,
2105            current_index: 0,
2106        }
2107    }
2108
2109    /// Recursively collect all pieces that overlap [start, end)
2110    fn collect_pieces(
2111        node: &Arc<PieceTreeNode>,
2112        doc_offset: usize,
2113        range_start: usize,
2114        range_end: usize,
2115        pieces: &mut Vec<PieceView>,
2116    ) {
2117        match node.as_ref() {
2118            PieceTreeNode::Internal {
2119                left_bytes,
2120                left,
2121                right,
2122                ..
2123            } => {
2124                let left_end = doc_offset + left_bytes;
2125
2126                // Check if left subtree overlaps with range
2127                if range_start < left_end {
2128                    Self::collect_pieces(left, doc_offset, range_start, range_end, pieces);
2129                }
2130
2131                // Check if right subtree overlaps with range
2132                if range_end > left_end {
2133                    Self::collect_pieces(right, left_end, range_start, range_end, pieces);
2134                }
2135            }
2136            PieceTreeNode::Leaf {
2137                location,
2138                offset,
2139                bytes,
2140                line_feed_cnt,
2141            } => {
2142                let piece_end = doc_offset + bytes;
2143
2144                // Check if this piece overlaps with the range
2145                if doc_offset < range_end && piece_end > range_start {
2146                    pieces.push(PieceView {
2147                        location: *location,
2148                        buffer_offset: *offset,
2149                        bytes: *bytes,
2150                        doc_offset,
2151                        line_feed_cnt: *line_feed_cnt,
2152                    });
2153                }
2154            }
2155        }
2156    }
2157}
2158
2159impl Iterator for PieceRangeIter {
2160    type Item = PieceView;
2161
2162    fn next(&mut self) -> Option<Self::Item> {
2163        if self.current_index < self.pieces.len() {
2164            let piece = self.pieces[self.current_index].clone();
2165            self.current_index += 1;
2166            Some(piece)
2167        } else {
2168            None
2169        }
2170    }
2171}
2172
2173#[cfg(test)]
2174mod tests {
2175    use super::*;
2176
2177    // Helper to create test buffers
2178    fn test_buffers() -> Vec<StringBuffer> {
2179        vec![
2180            StringBuffer::new(0, vec![b'a'; 100]), // Buffer 0: 100 'a's
2181            StringBuffer::new(1, vec![b'b'; 50]),  // Buffer 1: 50 'b's
2182            StringBuffer::new(2, vec![b'c'; 25]),  // Buffer 2: 25 'c's
2183        ]
2184    }
2185
2186    #[test]
2187    fn test_create_empty() {
2188        let tree = PieceTree::empty();
2189        assert_eq!(tree.total_bytes(), 0);
2190    }
2191
2192    #[test]
2193    fn test_create_with_initial_piece() {
2194        let tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
2195        assert_eq!(tree.total_bytes(), 100);
2196    }
2197
2198    #[test]
2199    fn test_insert_at_end() {
2200        let buffers = test_buffers();
2201        let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
2202        tree.insert(100, BufferLocation::Added(1), 0, 50, Some(0), &buffers);
2203        assert_eq!(tree.total_bytes(), 150);
2204    }
2205
2206    #[test]
2207    fn test_insert_in_middle() {
2208        let buffers = test_buffers();
2209        let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
2210        tree.insert(50, BufferLocation::Added(2), 0, 25, Some(0), &buffers);
2211        assert_eq!(tree.total_bytes(), 125);
2212        let stats = tree.stats();
2213        assert_eq!(stats.leaf_count, 3); // Original piece split + new piece
2214    }
2215
2216    #[test]
2217    fn test_delete() {
2218        let buffers = test_buffers();
2219        let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
2220        tree.delete(25, 50, &buffers);
2221        assert_eq!(tree.total_bytes(), 50);
2222    }
2223
2224    #[test]
2225    fn test_delete_at_boundaries() {
2226        let buffers = test_buffers();
2227        let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
2228
2229        // Delete from start
2230        tree.delete(0, 10, &buffers);
2231        assert_eq!(tree.total_bytes(), 90);
2232
2233        // Delete from end
2234        tree.delete(80, 10, &buffers);
2235        assert_eq!(tree.total_bytes(), 80);
2236    }
2237
2238    #[test]
2239    fn test_multiple_inserts_and_deletes() {
2240        let buffers = test_buffers();
2241        let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
2242
2243        tree.insert(50, BufferLocation::Added(1), 0, 20, Some(0), &buffers);
2244        assert_eq!(tree.total_bytes(), 120);
2245
2246        tree.delete(40, 30, &buffers);
2247        assert_eq!(tree.total_bytes(), 90);
2248
2249        tree.insert(0, BufferLocation::Added(1), 20, 10, Some(0), &buffers);
2250        assert_eq!(tree.total_bytes(), 100);
2251    }
2252
2253    #[test]
2254    fn test_rebalancing_many_inserts() {
2255        let buffers = test_buffers();
2256        let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
2257
2258        // Insert many times, which could create unbalanced tree
2259        for i in 0..20 {
2260            tree.insert(i * 5, BufferLocation::Added(1), i, 1, Some(0), &buffers);
2261        }
2262
2263        let stats = tree.stats();
2264        assert_eq!(stats.total_bytes, 120);
2265        // Each insert splits pieces, so we expect many leaves
2266        // Exact count depends on implementation details, but should be > 20
2267        assert!(stats.leaf_count > 20);
2268        assert!(stats.leaf_count < 50); // Reasonable upper bound
2269
2270        // Depth should be reasonable due to rebalancing
2271        let max_expected_depth = 2 * (stats.leaf_count as f64).log2().ceil() as usize;
2272        assert!(
2273            stats.depth <= max_expected_depth + 2,
2274            "Tree depth {} exceeds max {} for {} leaves",
2275            stats.depth,
2276            max_expected_depth,
2277            stats.leaf_count
2278        );
2279    }
2280
2281    #[test]
2282    fn test_find_by_offset() {
2283        let tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
2284
2285        let info = tree.find_by_offset(50).unwrap();
2286        assert_eq!(info.location, BufferLocation::Stored(0));
2287        assert_eq!(info.offset_in_piece, Some(50));
2288
2289        // Out of bounds
2290        assert!(tree.find_by_offset(100).is_none());
2291    }
2292
2293    #[test]
2294    fn test_find_after_inserts() {
2295        let buffers = test_buffers();
2296        let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
2297        tree.insert(50, BufferLocation::Added(1), 0, 25, Some(0), &buffers);
2298
2299        // Should find in added section
2300        let info = tree.find_by_offset(50).unwrap();
2301        assert_eq!(info.location, BufferLocation::Added(1));
2302    }
2303
2304    #[test]
2305    fn test_offset_to_position_column_after_modification() {
2306        // This test reproduces a bug where offset_to_position returns incorrect
2307        // column values after buffer modifications.
2308        //
2309        // Initial content: "fn foo(val: i32) {\n    val + 1\n}\n"
2310        // After deleting "val" and inserting "value" twice:
2311        // Buffer becomes: "fn foo(value: i32) {\n    value + 1\n}\n"
2312        //
2313        // Position 25 should be line 1, column 4 (the 'v' in second "value")
2314        // But the bug causes it to return column 0.
2315
2316        // Create buffer with initial content
2317        let initial = b"fn foo(val: i32) {\n    val + 1\n}\n";
2318        let buffer = StringBuffer::new(0, initial.to_vec());
2319        let buffers = vec![buffer.clone()];
2320
2321        let mut tree = PieceTree::new(
2322            BufferLocation::Stored(0),
2323            0,
2324            initial.len(),
2325            Some(initial.iter().filter(|&&b| b == b'\n').count()),
2326        );
2327
2328        // Verify initial position works correctly
2329        // Position 23 = 'v' of second "val" on line 1 (after newline at pos 18)
2330        let pos = tree.offset_to_position(23, &buffers);
2331        assert_eq!(
2332            pos,
2333            Some((1, 4)),
2334            "Initial: position 23 should be line 1, column 4"
2335        );
2336
2337        // Now simulate LSP rename operations:
2338        // 1. Delete "val" at position 23 (3 bytes)
2339        // 2. Insert "value" at position 23 (5 bytes)
2340        // 3. Delete "val" at position 7 (3 bytes)
2341        // 4. Insert "value" at position 7 (5 bytes)
2342
2343        // First modification: delete "val" at position 23
2344        tree.delete(23, 3, &buffers);
2345
2346        // Insert "value" - need a new buffer
2347        let value_buf = StringBuffer::new(1, b"value".to_vec());
2348        let buffers = vec![buffer.clone(), value_buf.clone()];
2349        tree.insert(23, BufferLocation::Added(1), 0, 5, Some(0), &buffers);
2350
2351        // Second modification: delete "val" at position 7
2352        tree.delete(7, 3, &buffers);
2353
2354        // Insert "value" - use another buffer
2355        let value_buf2 = StringBuffer::new(2, b"value".to_vec());
2356        let buffers = vec![buffer.clone(), value_buf.clone(), value_buf2];
2357        tree.insert(7, BufferLocation::Added(2), 0, 5, Some(0), &buffers);
2358
2359        // Buffer is now: "fn foo(value: i32) {\n    value + 1\n}\n"
2360        // Line 0: "fn foo(value: i32) {\n" = 21 bytes (positions 0-20)
2361        // Line 1: "    value + 1\n" starts at position 21
2362        // Position 25 = 21 + 4 = line 1, column 4
2363
2364        // This is where the bug manifests
2365        let pos = tree.offset_to_position(25, &buffers);
2366        assert_eq!(
2367            pos,
2368            Some((1, 4)),
2369            "After modification: position 25 should be line 1, column 4"
2370        );
2371
2372        // Also test position 21 (start of line 1)
2373        let pos = tree.offset_to_position(21, &buffers);
2374        assert_eq!(pos, Some((1, 0)), "Position 21 should be line 1, column 0");
2375    }
2376
2377    // ============== Tests for apply_bulk_edits ==============
2378
2379    // Helper to pre-allocate buffers for bulk edit tests
2380    fn prepare_bulk_edit_buffers(
2381        buffers: &mut Vec<StringBuffer>,
2382        texts: &[&str],
2383    ) -> Vec<(BufferLocation, usize, usize, Option<usize>)> {
2384        let mut infos = Vec::new();
2385        for (i, text) in texts.iter().enumerate() {
2386            let id = buffers.len();
2387            let bytes = text.as_bytes().to_vec();
2388            let lf = bytes.iter().filter(|&&b| b == b'\n').count();
2389            let len = bytes.len();
2390            buffers.push(StringBuffer::new(id, bytes));
2391            infos.push((BufferLocation::Added(id), 0, len, Some(lf)));
2392            let _ = i; // suppress warning
2393        }
2394        infos
2395    }
2396
2397    #[test]
2398    fn test_bulk_edit_single_insert() {
2399        let mut buffers = vec![StringBuffer::new(0, b"hello world".to_vec())];
2400        let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 11, Some(0));
2401
2402        // Pre-allocate buffer for the insert
2403        let infos = prepare_bulk_edit_buffers(&mut buffers, &["!"]);
2404
2405        // Insert "!" at position 11 (end)
2406        let edits: Vec<(usize, usize, &str)> = vec![(11, 0, "!")];
2407        let mut idx = 0;
2408
2409        let delta = tree.apply_bulk_edits(&edits, &buffers, |_text| {
2410            let info = infos[idx];
2411            idx += 1;
2412            info
2413        });
2414
2415        assert_eq!(delta, 1);
2416        assert_eq!(tree.total_bytes(), 12);
2417    }
2418
2419    #[test]
2420    fn test_bulk_edit_single_delete() {
2421        let buffers = vec![StringBuffer::new(0, b"hello world".to_vec())];
2422        let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 11, Some(0));
2423
2424        // Delete "world" (positions 6-11) - no insert, so no buffer needed
2425        let edits: Vec<(usize, usize, &str)> = vec![(6, 5, "")];
2426
2427        let delta = tree.apply_bulk_edits(&edits, &buffers, |_text| {
2428            (BufferLocation::Added(1), 0, 0, Some(0))
2429        });
2430
2431        assert_eq!(delta, -5);
2432        assert_eq!(tree.total_bytes(), 6);
2433    }
2434
2435    #[test]
2436    fn test_bulk_edit_single_replace() {
2437        let mut buffers = vec![StringBuffer::new(0, b"hello world".to_vec())];
2438        let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 11, Some(0));
2439
2440        // Pre-allocate buffer for the replacement
2441        let infos = prepare_bulk_edit_buffers(&mut buffers, &["rust"]);
2442
2443        // Replace "world" with "rust"
2444        let edits: Vec<(usize, usize, &str)> = vec![(6, 5, "rust")];
2445        let mut idx = 0;
2446
2447        let delta = tree.apply_bulk_edits(&edits, &buffers, |_text| {
2448            let info = infos[idx];
2449            idx += 1;
2450            info
2451        });
2452
2453        assert_eq!(delta, -1); // "world" (5) -> "rust" (4)
2454        assert_eq!(tree.total_bytes(), 10);
2455    }
2456
2457    #[test]
2458    fn test_bulk_edit_multiple_inserts_descending() {
2459        // Edits must be sorted descending by position
2460        let mut buffers = vec![StringBuffer::new(0, b"abc".to_vec())];
2461        let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 3, Some(0));
2462
2463        // Pre-allocate buffers for 4 inserts
2464        let infos = prepare_bulk_edit_buffers(&mut buffers, &["X", "X", "X", "X"]);
2465
2466        // Insert "X" at positions 3, 2, 1, 0 (descending order)
2467        let edits: Vec<(usize, usize, &str)> = vec![
2468            (3, 0, "X"), // Insert at end
2469            (2, 0, "X"), // Insert before 'c'
2470            (1, 0, "X"), // Insert before 'b'
2471            (0, 0, "X"), // Insert at start
2472        ];
2473        let mut idx = 0;
2474
2475        let delta = tree.apply_bulk_edits(&edits, &buffers, |_text| {
2476            let info = infos[idx];
2477            idx += 1;
2478            info
2479        });
2480
2481        assert_eq!(delta, 4);
2482        assert_eq!(tree.total_bytes(), 7); // "XaXbXcX"
2483    }
2484
2485    #[test]
2486    fn test_bulk_edit_multiple_deletes_descending() {
2487        let buffers = vec![StringBuffer::new(0, b"abcdefgh".to_vec())];
2488        let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 8, Some(0));
2489
2490        // Delete chars at positions 6, 4, 2, 0 (descending order) - no inserts
2491        let edits: Vec<(usize, usize, &str)> = vec![
2492            (6, 1, ""), // Delete 'g'
2493            (4, 1, ""), // Delete 'e'
2494            (2, 1, ""), // Delete 'c'
2495            (0, 1, ""), // Delete 'a'
2496        ];
2497
2498        let delta = tree.apply_bulk_edits(&edits, &buffers, |_| {
2499            (BufferLocation::Added(1), 0, 0, Some(0))
2500        });
2501
2502        assert_eq!(delta, -4);
2503        assert_eq!(tree.total_bytes(), 4); // "bdfh"
2504    }
2505
2506    #[test]
2507    fn test_bulk_edit_empty_edits() {
2508        let buffers = vec![StringBuffer::new(0, b"hello".to_vec())];
2509        let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 5, Some(0));
2510
2511        let edits: Vec<(usize, usize, &str)> = vec![];
2512
2513        let delta = tree.apply_bulk_edits(&edits, &buffers, |_| {
2514            (BufferLocation::Added(1), 0, 0, Some(0))
2515        });
2516
2517        assert_eq!(delta, 0);
2518        assert_eq!(tree.total_bytes(), 5);
2519    }
2520
2521    #[test]
2522    fn test_bulk_edit_consistency_check() {
2523        // Test that piece sum equals total_bytes after bulk edit
2524        let mut buffers = vec![StringBuffer::new(0, b"0123456789".to_vec())];
2525        let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 10, Some(0));
2526
2527        // Pre-allocate buffers for the inserts
2528        let infos = prepare_bulk_edit_buffers(&mut buffers, &["XX", "Y", "ZZZ"]);
2529
2530        // Multiple mixed operations
2531        let edits: Vec<(usize, usize, &str)> = vec![
2532            (8, 1, "XX"),  // Replace '8' with 'XX'
2533            (5, 2, "Y"),   // Replace '56' with 'Y'
2534            (2, 0, "ZZZ"), // Insert 'ZZZ' at position 2
2535        ];
2536        let mut idx = 0;
2537
2538        tree.apply_bulk_edits(&edits, &buffers, |_text| {
2539            let info = infos[idx];
2540            idx += 1;
2541            info
2542        });
2543
2544        // Verify consistency
2545        let leaves = tree.get_leaves();
2546        let sum: usize = leaves.iter().map(|l| l.bytes).sum();
2547        assert_eq!(
2548            sum,
2549            tree.total_bytes(),
2550            "Piece sum {} != total_bytes {}",
2551            sum,
2552            tree.total_bytes()
2553        );
2554    }
2555
2556    #[test]
2557    fn test_bulk_edit_vs_sequential_equivalence() {
2558        // Verify that bulk edit produces same result as sequential edits
2559        let original_content = b"The quick brown fox";
2560
2561        // Setup for bulk edit
2562        let mut buffers1 = vec![StringBuffer::new(0, original_content.to_vec())];
2563        let mut tree1 = PieceTree::new(BufferLocation::Stored(0), 0, 19, Some(0));
2564
2565        // Pre-allocate buffers for the replacements
2566        let infos = prepare_bulk_edit_buffers(&mut buffers1, &["red", "slow"]);
2567
2568        // Setup for sequential edit
2569        let mut buffers2 = vec![StringBuffer::new(0, original_content.to_vec())];
2570        let mut tree2 = PieceTree::new(BufferLocation::Stored(0), 0, 19, Some(0));
2571        let mut next_id2 = 1;
2572
2573        // Edits: Replace "quick" with "slow", "brown" with "red"
2574        // Positions in original: quick=4-9, brown=10-15
2575        // Must be sorted descending
2576        let edits: Vec<(usize, usize, &str)> = vec![
2577            (10, 5, "red"), // Replace "brown" at 10
2578            (4, 5, "slow"), // Replace "quick" at 4
2579        ];
2580        let mut idx = 0;
2581
2582        // Apply bulk edit
2583        tree1.apply_bulk_edits(&edits, &buffers1, |_text| {
2584            let info = infos[idx];
2585            idx += 1;
2586            info
2587        });
2588
2589        // Apply sequential edits (in descending order to match)
2590        // First: replace "brown" at 10
2591        tree2.delete(10, 5, &buffers2);
2592        buffers2.push(StringBuffer::new(next_id2, b"red".to_vec()));
2593        tree2.insert(
2594            10,
2595            BufferLocation::Added(next_id2),
2596            0,
2597            3,
2598            Some(0),
2599            &buffers2,
2600        );
2601        next_id2 += 1;
2602
2603        // Second: replace "quick" at 4
2604        tree2.delete(4, 5, &buffers2);
2605        buffers2.push(StringBuffer::new(next_id2, b"slow".to_vec()));
2606        tree2.insert(4, BufferLocation::Added(next_id2), 0, 4, Some(0), &buffers2);
2607
2608        assert_eq!(
2609            tree1.total_bytes(),
2610            tree2.total_bytes(),
2611            "Bulk edit total_bytes {} != sequential {}",
2612            tree1.total_bytes(),
2613            tree2.total_bytes()
2614        );
2615    }
2616}
2617
2618#[cfg(test)]
2619mod property_tests {
2620    use super::*;
2621    use proptest::prelude::*;
2622
2623    // Helper to create test buffers - using larger buffers for property tests
2624    fn test_buffers_large() -> Vec<StringBuffer> {
2625        vec![
2626            StringBuffer::new(0, vec![b'a'; 10000]), // Large buffer
2627            StringBuffer::new(1, vec![b'b'; 10000]),
2628        ]
2629    }
2630
2631    // Strategy to generate operations
2632    #[derive(Debug, Clone)]
2633    enum Operation {
2634        Insert { offset: usize, bytes: usize },
2635        Delete { offset: usize, bytes: usize },
2636    }
2637
2638    // Generate a sequence of operations
2639    fn operation_strategy() -> impl Strategy<Value = Vec<Operation>> {
2640        prop::collection::vec(
2641            prop_oneof![
2642                (0usize..200, 1usize..50)
2643                    .prop_map(|(offset, bytes)| { Operation::Insert { offset, bytes } }),
2644                (0usize..200, 1usize..50)
2645                    .prop_map(|(offset, bytes)| { Operation::Delete { offset, bytes } }),
2646            ],
2647            0..50,
2648        )
2649    }
2650
2651    // More aggressive operation strategy that creates more internal nodes
2652    fn aggressive_operation_strategy() -> impl Strategy<Value = Vec<Operation>> {
2653        prop::collection::vec(
2654            prop_oneof![
2655                // More inserts, smaller chunks to create more splits
2656                3 => (0usize..100, 1usize..20).prop_map(|(offset, bytes)| {
2657                    Operation::Insert { offset, bytes }
2658                }),
2659                // Some deletes
2660                1 => (0usize..100, 1usize..30).prop_map(|(offset, bytes)| {
2661                    Operation::Delete { offset, bytes }
2662                }),
2663            ],
2664            10..30, // More operations to force tree growth
2665        )
2666    }
2667
2668    proptest! {
2669        #[test]
2670        fn prop_total_bytes_consistency(operations in operation_strategy()) {
2671            let buffers = test_buffers_large();
2672            let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
2673            let mut expected_bytes = 100;
2674
2675            for op in operations {
2676                match op {
2677                    Operation::Insert { offset, bytes } => {
2678                        let offset = offset.min(tree.total_bytes());
2679                        let buffer_len = buffers[1].get_data().map(|d| d.len()).unwrap_or(0);
2680                        let bytes = bytes.min(buffer_len);
2681                        tree.insert(offset, BufferLocation::Added(1), 0, bytes, Some(0), &buffers);
2682                        expected_bytes += bytes;
2683                    }
2684                    Operation::Delete { offset, bytes } => {
2685                        if offset < tree.total_bytes() {
2686                            let actual_delete = bytes.min(tree.total_bytes() - offset);
2687                            tree.delete(offset, bytes, &buffers);
2688                            expected_bytes -= actual_delete;
2689                        }
2690                    }
2691                }
2692            }
2693
2694            prop_assert_eq!(tree.total_bytes(), expected_bytes);
2695        }
2696
2697        #[test]
2698        fn prop_tree_never_negative_bytes(operations in operation_strategy()) {
2699            let buffers = test_buffers_large();
2700            let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
2701
2702            for op in operations {
2703                match op {
2704                    Operation::Insert { offset, bytes } => {
2705                        let offset = offset.min(tree.total_bytes());
2706                        let buffer_len = buffers[1].get_data().map(|d| d.len()).unwrap_or(0);
2707                        let bytes = bytes.min(buffer_len);
2708                        tree.insert(offset, BufferLocation::Added(1), 0, bytes, Some(0), &buffers);
2709                    }
2710                    Operation::Delete { offset, bytes } => {
2711                        tree.delete(offset, bytes, &buffers);
2712                    }
2713                }
2714
2715                // Tree should never have negative bytes (underflow would wrap to large number)
2716                prop_assert!(tree.total_bytes() < 10_000_000);
2717            }
2718        }
2719
2720        #[test]
2721        fn prop_balanced_after_operations(operations in operation_strategy()) {
2722            let buffers = test_buffers_large();
2723            let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
2724
2725            for op in operations {
2726                match op {
2727                    Operation::Insert { offset, bytes } => {
2728                        let offset = offset.min(tree.total_bytes());
2729                        let buffer_len = buffers[1].get_data().map(|d| d.len()).unwrap_or(0);
2730                        let bytes = bytes.min(buffer_len);
2731                        tree.insert(offset, BufferLocation::Added(1), 0, bytes, Some(0), &buffers);
2732                    }
2733                    Operation::Delete { offset, bytes } => {
2734                        tree.delete(offset, bytes, &buffers);
2735                    }
2736                }
2737            }
2738
2739            let stats = tree.stats();
2740            if stats.leaf_count > 1 {
2741                let max_depth = 2 * (stats.leaf_count as f64).log2().ceil() as usize;
2742                prop_assert!(stats.depth <= max_depth + 2, "Tree depth {} exceeds expected max {} for {} leaves", stats.depth, max_depth, stats.leaf_count);
2743            }
2744        }
2745
2746        #[test]
2747        fn prop_insert_then_delete_equals_original(
2748            insert_offset in 0usize..100,
2749            insert_bytes in 1usize..50
2750        ) {
2751            let buffers = test_buffers_large();
2752            let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
2753            let original_bytes = tree.total_bytes();
2754
2755            let insert_offset = insert_offset.min(tree.total_bytes());
2756            let buffer_len = buffers[1].get_data().map(|d| d.len()).unwrap_or(0);
2757            let insert_bytes = insert_bytes.min(buffer_len);
2758            tree.insert(insert_offset, BufferLocation::Added(1), 0, insert_bytes, Some(0), &buffers);
2759
2760            // Delete what we just inserted
2761            tree.delete(insert_offset, insert_bytes, &buffers);
2762
2763            prop_assert_eq!(tree.total_bytes(), original_bytes);
2764        }
2765
2766        #[test]
2767        fn prop_find_offset_in_bounds(
2768            offset in 0usize..100
2769        ) {
2770            let tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
2771
2772            let result = tree.find_by_offset(offset);
2773            prop_assert!(result.is_some());
2774        }
2775
2776        #[test]
2777        fn prop_find_offset_out_of_bounds(
2778            offset in 100usize..1000
2779        ) {
2780            let tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
2781
2782            let result = tree.find_by_offset(offset);
2783            prop_assert!(result.is_none());
2784        }
2785
2786        #[test]
2787        fn prop_sequential_inserts_maintain_order(
2788            count in 1usize..20,
2789            insert_size in 1usize..10
2790        ) {
2791            let buffers = test_buffers_large();
2792            let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 10, Some(0));
2793
2794            for _i in 0..count {
2795                let buffer_len = buffers[1].get_data().map(|d| d.len()).unwrap_or(0);
2796                let insert_size = insert_size.min(buffer_len);
2797                tree.insert(tree.total_bytes(), BufferLocation::Added(1), 0, insert_size, Some(0), &buffers);
2798            }
2799
2800            let expected_bytes = 10 + (count * insert_size);
2801            prop_assert_eq!(tree.total_bytes(), expected_bytes);
2802        }
2803
2804        #[test]
2805        fn prop_delete_all_reaches_zero(
2806            delete_size in 1usize..10
2807        ) {
2808            let buffers = test_buffers_large();
2809            let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
2810
2811            while tree.total_bytes() > 0 {
2812                let to_delete = delete_size.min(tree.total_bytes());
2813                tree.delete(0, to_delete, &buffers);
2814            }
2815
2816            prop_assert_eq!(tree.total_bytes(), 0);
2817        }
2818    }
2819
2820    #[test]
2821    fn test_empty_delete() {
2822        let buffers = test_buffers_large();
2823        let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
2824        tree.delete(50, 0, &buffers);
2825        assert_eq!(tree.total_bytes(), 100);
2826    }
2827
2828    // ============== Property tests for apply_bulk_edits ==============
2829
2830    // Strategy to generate bulk edit operations
2831    #[derive(Debug, Clone)]
2832    struct BulkEditOp {
2833        position: usize,
2834        delete_len: usize,
2835        insert_text: String,
2836    }
2837
2838    fn bulk_edit_strategy() -> impl Strategy<Value = Vec<BulkEditOp>> {
2839        prop::collection::vec(
2840            (0usize..100, 0usize..20, "[a-zA-Z0-9]{0,10}").prop_map(
2841                |(position, delete_len, insert_text)| BulkEditOp {
2842                    position,
2843                    delete_len,
2844                    insert_text,
2845                },
2846            ),
2847            1..20,
2848        )
2849    }
2850
2851    // Helper to pre-allocate buffers for property tests
2852    fn preallocate_buffers(
2853        buffers: &mut Vec<StringBuffer>,
2854        texts: &[String],
2855    ) -> Vec<(BufferLocation, usize, usize, Option<usize>)> {
2856        let mut infos = Vec::new();
2857        for text in texts {
2858            let id = buffers.len();
2859            let bytes = text.as_bytes().to_vec();
2860            let lf = bytes.iter().filter(|&&b| b == b'\n').count();
2861            let len = bytes.len();
2862            buffers.push(StringBuffer::new(id, bytes));
2863            infos.push((BufferLocation::Added(id), 0, len, Some(lf)));
2864        }
2865        infos
2866    }
2867
2868    proptest! {
2869        /// Property: apply_bulk_edits maintains tree consistency
2870        /// Sum of piece lengths must equal total_bytes after bulk edit
2871        #[test]
2872        fn prop_bulk_edit_tree_consistency(ops in bulk_edit_strategy()) {
2873            let mut buffers = vec![StringBuffer::new(0, b"x".repeat(100).to_vec())];
2874            let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
2875
2876            // Sort edits by position descending (required by apply_bulk_edits)
2877            let mut ops = ops;
2878            ops.sort_by(|a, b| b.position.cmp(&a.position));
2879
2880            // Pre-allocate all buffers
2881            let texts: Vec<String> = ops.iter().map(|op| op.insert_text.clone()).collect();
2882            let infos = preallocate_buffers(&mut buffers, &texts);
2883
2884            // Clamp positions and delete lengths to valid ranges
2885            let edits: Vec<(usize, usize, &str)> = ops.iter()
2886                .map(|op| {
2887                    let pos = op.position.min(tree.total_bytes());
2888                    let del = op.delete_len.min(tree.total_bytes().saturating_sub(pos));
2889                    (pos, del, op.insert_text.as_str())
2890                })
2891                .collect();
2892
2893            let mut idx = 0;
2894            tree.apply_bulk_edits(&edits, &buffers, |_text| {
2895                let info = infos[idx];
2896                idx += 1;
2897                info
2898            });
2899
2900            // INVARIANT: Sum of all piece lengths must equal total_bytes
2901            let leaves = tree.get_leaves();
2902            let sum_of_pieces: usize = leaves.iter().map(|leaf| leaf.bytes).sum();
2903            prop_assert_eq!(
2904                sum_of_pieces,
2905                tree.total_bytes(),
2906                "After bulk edit: sum of pieces ({}) != total_bytes ({})",
2907                sum_of_pieces,
2908                tree.total_bytes()
2909            );
2910        }
2911
2912        /// Property: apply_bulk_edits returns correct delta
2913        #[test]
2914        fn prop_bulk_edit_correct_delta(ops in bulk_edit_strategy()) {
2915            let mut buffers = vec![StringBuffer::new(0, b"x".repeat(100).to_vec())];
2916            let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
2917
2918            let original_bytes = tree.total_bytes();
2919
2920            // Sort edits by position descending
2921            let mut ops = ops;
2922            ops.sort_by(|a, b| b.position.cmp(&a.position));
2923
2924            // Pre-allocate all buffers
2925            let texts: Vec<String> = ops.iter().map(|op| op.insert_text.clone()).collect();
2926            let infos = preallocate_buffers(&mut buffers, &texts);
2927
2928            let edits: Vec<(usize, usize, &str)> = ops.iter()
2929                .map(|op| {
2930                    let pos = op.position.min(tree.total_bytes());
2931                    let del = op.delete_len.min(tree.total_bytes().saturating_sub(pos));
2932                    (pos, del, op.insert_text.as_str())
2933                })
2934                .collect();
2935
2936            let mut idx = 0;
2937            let delta = tree.apply_bulk_edits(&edits, &buffers, |_text| {
2938                let info = infos[idx];
2939                idx += 1;
2940                info
2941            });
2942
2943            let actual_change = tree.total_bytes() as isize - original_bytes as isize;
2944            prop_assert_eq!(
2945                delta,
2946                actual_change,
2947                "Returned delta ({}) != actual change ({})",
2948                delta,
2949                actual_change
2950            );
2951        }
2952
2953        /// Property: bulk edit with only inserts increases size correctly
2954        #[test]
2955        fn prop_bulk_edit_inserts_only(
2956            positions in prop::collection::vec(0usize..50, 1..10),
2957            insert_len in 1usize..10
2958        ) {
2959            let mut buffers = vec![StringBuffer::new(0, b"x".repeat(50).to_vec())];
2960            let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 50, Some(0));
2961
2962            let insert_text = "a".repeat(insert_len);
2963            let original_bytes = tree.total_bytes();
2964
2965            // Sort positions descending
2966            let mut positions = positions;
2967            positions.sort_by(|a, b| b.cmp(a));
2968            positions.dedup();
2969
2970            // Pre-allocate all buffers
2971            let texts: Vec<String> = positions.iter().map(|_| insert_text.clone()).collect();
2972            let infos = preallocate_buffers(&mut buffers, &texts);
2973
2974            let edits: Vec<(usize, usize, &str)> = positions
2975                .iter()
2976                .map(|&pos| (pos.min(tree.total_bytes()), 0, insert_text.as_str()))
2977                .collect();
2978
2979            let mut idx = 0;
2980            tree.apply_bulk_edits(&edits, &buffers, |_text| {
2981                let info = infos[idx];
2982                idx += 1;
2983                info
2984            });
2985
2986            let expected_bytes = original_bytes + edits.len() * insert_len;
2987            prop_assert_eq!(
2988                tree.total_bytes(),
2989                expected_bytes,
2990                "After {} inserts of {} bytes each: expected {} bytes, got {}",
2991                edits.len(),
2992                insert_len,
2993                expected_bytes,
2994                tree.total_bytes()
2995            );
2996        }
2997
2998        /// Property: bulk edit with only deletes decreases size correctly
2999        #[test]
3000        fn prop_bulk_edit_deletes_only(
3001            ops in prop::collection::vec((0usize..80, 1usize..5), 1..10)
3002        ) {
3003            let buffers = vec![StringBuffer::new(0, b"x".repeat(100).to_vec())];
3004            let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
3005
3006            // Sort by position descending
3007            let mut ops = ops;
3008            ops.sort_by(|a, b| b.0.cmp(&a.0));
3009
3010            // Remove overlapping deletes
3011            let mut edits: Vec<(usize, usize, &str)> = Vec::new();
3012            let mut last_affected_pos = tree.total_bytes();
3013            for (pos, del_len) in ops {
3014                if pos < last_affected_pos {
3015                    let actual_del = del_len.min(last_affected_pos - pos);
3016                    if actual_del > 0 {
3017                        edits.push((pos, actual_del, ""));
3018                        last_affected_pos = pos;
3019                    }
3020                }
3021            }
3022
3023            let expected_delete: usize = edits.iter().map(|(_, d, _)| d).sum();
3024
3025            tree.apply_bulk_edits(&edits, &buffers, |_| {
3026                (BufferLocation::Added(1), 0, 0, Some(0))
3027            });
3028
3029            let expected_bytes = 100 - expected_delete;
3030            prop_assert_eq!(
3031                tree.total_bytes(),
3032                expected_bytes,
3033                "After deleting {} bytes: expected {} bytes, got {}",
3034                expected_delete,
3035                expected_bytes,
3036                tree.total_bytes()
3037            );
3038        }
3039    }
3040
3041    proptest! {
3042        /// Property: Sum of all piece lengths must equal total_bytes
3043        /// This catches bugs like duplicate piece insertion
3044        #[test]
3045        fn prop_tree_consistency_piece_sum(operations in operation_strategy()) {
3046            let buffers = test_buffers_large();
3047            let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
3048
3049            for op in operations {
3050                match op {
3051                    Operation::Insert { offset, bytes } => {
3052                        let offset = offset.min(tree.total_bytes());
3053                        let buffer_len = buffers[1].get_data().map(|d| d.len()).unwrap_or(0);
3054                        let bytes = bytes.min(buffer_len);
3055                        tree.insert(offset, BufferLocation::Added(1), 0, bytes, Some(0), &buffers);
3056                    }
3057                    Operation::Delete { offset, bytes } => {
3058                        tree.delete(offset, bytes, &buffers);
3059                    }
3060                }
3061
3062                // INVARIANT: Sum of all piece lengths must equal total_bytes
3063                let leaves = tree.get_leaves();
3064                let sum_of_pieces: usize = leaves.iter().map(|leaf| leaf.bytes).sum();
3065                prop_assert_eq!(
3066                    sum_of_pieces,
3067                    tree.total_bytes(),
3068                    "Tree inconsistency: sum of piece lengths ({}) != total_bytes ({})",
3069                    sum_of_pieces,
3070                    tree.total_bytes()
3071                );
3072            }
3073        }
3074
3075        /// Property: Line feed count consistency
3076        /// Sum of all piece line_feed_cnt must equal tree's total line feeds
3077        #[test]
3078        fn prop_tree_consistency_line_feeds(operations in operation_strategy()) {
3079            let buffers = test_buffers_large();
3080            let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
3081
3082            for op in operations {
3083                match op {
3084                    Operation::Insert { offset, bytes } => {
3085                        let offset = offset.min(tree.total_bytes());
3086                        let buffer_len = buffers[1].get_data().map(|d| d.len()).unwrap_or(0);
3087                        let bytes = bytes.min(buffer_len);
3088                        tree.insert(offset, BufferLocation::Added(1), 0, bytes, Some(0), &buffers);
3089                    }
3090                    Operation::Delete { offset, bytes } => {
3091                        tree.delete(offset, bytes, &buffers);
3092                    }
3093                }
3094
3095                // INVARIANT: Sum of all piece line feeds must equal tree's total
3096                let leaves = tree.get_leaves();
3097                let sum_of_line_feeds: Option<usize> = leaves.iter()
3098                    .try_fold(0, |acc, leaf| leaf.line_feed_cnt.map(|cnt| acc + cnt));
3099                let stats = tree.stats();
3100                prop_assert_eq!(
3101                    sum_of_line_feeds,
3102                    stats.line_feed_count,
3103                    "Line feed inconsistency: sum of piece line feeds ({:?}) != tree total ({:?})",
3104                    sum_of_line_feeds,
3105                    stats.line_feed_count
3106                );
3107            }
3108        }
3109
3110        /// Aggressive consistency test designed to catch the duplicate piece insertion bug
3111        /// Uses more operations with smaller inserts to force internal node creation and splits
3112        #[test]
3113        fn prop_tree_consistency_aggressive(operations in aggressive_operation_strategy()) {
3114            let buffers = test_buffers_large();
3115            let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
3116
3117            // Prime the tree with several inserts to create internal nodes first
3118            // This increases the likelihood of hitting the bug scenario
3119            for i in 0..5 {
3120                let offset = (i * 17) % (tree.total_bytes().max(1));
3121                tree.insert(offset, BufferLocation::Added(1), i * 100, 10, Some(0), &buffers);
3122            }
3123
3124            // Verify we have internal nodes
3125            prop_assert!(tree.stats().depth > 1, "Priming should create internal nodes");
3126
3127            for (i, op) in operations.iter().enumerate() {
3128                match *op {
3129                    Operation::Insert { offset, bytes } => {
3130                        let offset = offset.min(tree.total_bytes());
3131                        let buffer_len = buffers[1].get_data().map(|d| d.len()).unwrap_or(0);
3132                        let bytes = bytes.min(buffer_len);
3133                        tree.insert(offset, BufferLocation::Added(1), 0, bytes, Some(0), &buffers);
3134                    }
3135                    Operation::Delete { offset, bytes } => {
3136                        tree.delete(offset, bytes, &buffers);
3137                    }
3138                }
3139
3140                // CRITICAL INVARIANT: Sum of all piece lengths must equal total_bytes
3141                // This catches the duplicate piece insertion bug
3142                let leaves = tree.get_leaves();
3143                let sum_of_pieces: usize = leaves.iter().map(|leaf| leaf.bytes).sum();
3144                prop_assert_eq!(
3145                    sum_of_pieces,
3146                    tree.total_bytes(),
3147                    "Operation {}: Tree inconsistency after {:?}.\n\
3148                     Sum of piece lengths ({}) != total_bytes ({}).\n\
3149                     Tree depth: {}, leaves: {}.\n\
3150                     Pieces: {:?}",
3151                    i, op, sum_of_pieces, tree.total_bytes(),
3152                    tree.stats().depth, tree.stats().leaf_count,
3153                    leaves
3154                );
3155            }
3156        }
3157    }
3158
3159    #[test]
3160    fn test_delete_beyond_end() {
3161        let buffers = test_buffers_large();
3162        let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
3163        tree.delete(50, 100, &buffers); // Try to delete 100 bytes from offset 50
3164        assert_eq!(tree.total_bytes(), 50); // Should only delete 50 bytes
3165    }
3166
3167    #[test]
3168    fn test_insert_zero_bytes() {
3169        let buffers = test_buffers_large();
3170        let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
3171        tree.insert(50, BufferLocation::Added(1), 0, 0, Some(0), &buffers);
3172        assert_eq!(tree.total_bytes(), 100);
3173    }
3174
3175    #[test]
3176    fn test_tree_consistency_after_insert() {
3177        // Regression test: verify tree consistency after each operation
3178        // This test creates enough inserts to force internal nodes, which is where the bug manifests
3179        let buffers = test_buffers_large();
3180        let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
3181
3182        // Do several inserts to create internal nodes and splits
3183        for i in 0..10 {
3184            let offset = (i * 13) % (tree.total_bytes().max(1)); // Varying offsets
3185            tree.insert(
3186                offset,
3187                BufferLocation::Added(1),
3188                i * 10,
3189                5,
3190                Some(0),
3191                &buffers,
3192            );
3193
3194            // INVARIANT: sum of piece lengths must equal total_bytes
3195            let leaves = tree.get_leaves();
3196            let sum: usize = leaves.iter().map(|l| l.bytes).sum();
3197            assert_eq!(
3198                sum,
3199                tree.total_bytes(),
3200                "After insert {}: sum of pieces ({}) != total_bytes ({}).\nLeaves: {:?}",
3201                i,
3202                sum,
3203                tree.total_bytes(),
3204                leaves
3205            );
3206        }
3207
3208        // Verify we actually created internal nodes
3209        let stats = tree.stats();
3210        assert!(
3211            stats.depth > 1,
3212            "Test should create internal nodes, but depth is {}",
3213            stats.depth
3214        );
3215    }
3216
3217    #[test]
3218    fn test_duplicate_piece_bug_exact_scenario() {
3219        // This replicates the exact scenario that exposed the duplicate insertion bug
3220        let mut buffers = vec![StringBuffer::new(0, b"initial\ntext".to_vec())];
3221        let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 12, Some(1));
3222
3223        // Delete all - creates an empty piece
3224        tree.delete(0, 12, &buffers);
3225
3226        // Check tree consistency after delete
3227        let leaves = tree.get_leaves();
3228        let sum: usize = leaves.iter().map(|l| l.bytes).sum();
3229        assert_eq!(
3230            sum,
3231            tree.total_bytes(),
3232            "After delete: sum={}, total={}",
3233            sum,
3234            tree.total_bytes()
3235        );
3236
3237        // Insert 'a' at position 0
3238        buffers.push(StringBuffer::new(1, b"a".to_vec()));
3239        tree.insert(0, BufferLocation::Added(1), 0, 1, Some(0), &buffers);
3240
3241        // Check consistency
3242        let leaves = tree.get_leaves();
3243        let sum: usize = leaves.iter().map(|l| l.bytes).sum();
3244        assert_eq!(
3245            sum,
3246            tree.total_bytes(),
3247            "After first insert: sum={}, total={}. Leaves: {:?}",
3248            sum,
3249            tree.total_bytes(),
3250            leaves
3251        );
3252
3253        // Insert 'b' at position 0 - this should trigger the bug with buggy code
3254        buffers.push(StringBuffer::new(2, b"b".to_vec()));
3255        tree.insert(0, BufferLocation::Added(2), 0, 1, Some(0), &buffers);
3256
3257        // Check consistency - this will fail with the bug
3258        let leaves = tree.get_leaves();
3259        let sum: usize = leaves.iter().map(|l| l.bytes).sum();
3260        assert_eq!(
3261            sum,
3262            tree.total_bytes(),
3263            "After second insert: sum={}, total={}. Leaves: {:?}",
3264            sum,
3265            tree.total_bytes(),
3266            leaves
3267        );
3268    }
3269
3270    // Property tests for PieceRangeIter
3271    proptest! {
3272        #[test]
3273        fn test_piece_iter_covers_exact_range(
3274            ops in aggressive_operation_strategy(),
3275            start in 0usize..100,
3276            len in 1usize..50
3277        ) {
3278            let mut buffers = vec![StringBuffer::new(0, b"x".repeat(100).to_vec())];
3279            let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
3280
3281            // Apply operations to build up tree
3282            for op in ops.iter() {
3283                match op {
3284                    Operation::Insert { offset, bytes } => {
3285                        let offset = (*offset).min(tree.total_bytes());
3286                        buffers.push(StringBuffer::new(buffers.len(), b"a".repeat(*bytes).to_vec()));
3287                        tree.insert(offset, BufferLocation::Added(buffers.len() - 1), 0, *bytes, Some(0), &buffers);
3288                    }
3289                    Operation::Delete { offset, bytes } => {
3290                        let offset = (*offset).min(tree.total_bytes());
3291                        let bytes = (*bytes).min(tree.total_bytes().saturating_sub(offset));
3292                        if bytes > 0 {
3293                            tree.delete(offset, bytes, &buffers);
3294                        }
3295                    }
3296                }
3297            }
3298
3299            let total_bytes = tree.total_bytes();
3300            if total_bytes == 0 {
3301                return Ok(());
3302            }
3303
3304            let start = start.min(total_bytes.saturating_sub(1));
3305            let end = (start + len).min(total_bytes);
3306
3307            // Collect pieces using iterator
3308            let pieces: Vec<_> = tree.iter_pieces_in_range(start, end).collect();
3309
3310            // Verify coverage: pieces should cover [start, end)
3311            if !pieces.is_empty() {
3312                let first_piece_start = pieces[0].doc_offset;
3313                let last_piece = &pieces[pieces.len() - 1];
3314                let last_piece_end = last_piece.doc_offset + last_piece.bytes;
3315
3316                // First piece should start at or before requested start
3317                prop_assert!(first_piece_start <= start,
3318                    "First piece starts at {}, but requested start is {}", first_piece_start, start);
3319
3320                // Last piece should end at or after requested end
3321                prop_assert!(last_piece_end >= end,
3322                    "Last piece ends at {}, but requested end is {}", last_piece_end, end);
3323            }
3324        }
3325
3326        #[test]
3327        fn test_piece_iter_no_gaps(ops in aggressive_operation_strategy()) {
3328            let mut buffers = vec![StringBuffer::new(0, b"x".repeat(100).to_vec())];
3329            let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
3330
3331            for op in ops {
3332                match op {
3333                    Operation::Insert { offset, bytes } => {
3334                        let offset = offset.min(tree.total_bytes());
3335                        buffers.push(StringBuffer::new(buffers.len(), b"a".repeat(bytes).to_vec()));
3336                        tree.insert(offset, BufferLocation::Added(buffers.len() - 1), 0, bytes, Some(0), &buffers);
3337                    }
3338                    Operation::Delete { offset, bytes } => {
3339                        let offset = offset.min(tree.total_bytes());
3340                        let bytes = bytes.min(tree.total_bytes().saturating_sub(offset));
3341                        if bytes > 0 {
3342                            tree.delete(offset, bytes, &buffers);
3343                        }
3344                    }
3345                }
3346            }
3347
3348            let total_bytes = tree.total_bytes();
3349            if total_bytes == 0 {
3350                return Ok(());
3351            }
3352
3353            // Iterate over entire document
3354            let pieces: Vec<_> = tree.iter_pieces_in_range(0, total_bytes).collect();
3355
3356            // Verify no gaps: each piece should start where previous one ended
3357            for i in 1..pieces.len() {
3358                let prev_end = pieces[i - 1].doc_offset + pieces[i - 1].bytes;
3359                let curr_start = pieces[i].doc_offset;
3360                prop_assert_eq!(prev_end, curr_start,
3361                    "Gap between piece {} (ends at {}) and piece {} (starts at {})",
3362                    i - 1, prev_end, i, curr_start);
3363            }
3364        }
3365
3366        #[test]
3367        fn test_piece_iter_total_bytes_matches(ops in aggressive_operation_strategy()) {
3368            let mut buffers = vec![StringBuffer::new(0, b"x".repeat(100).to_vec())];
3369            let mut tree = PieceTree::new(BufferLocation::Stored(0), 0, 100, Some(0));
3370
3371            for op in ops {
3372                match op {
3373                    Operation::Insert { offset, bytes } => {
3374                        let offset = offset.min(tree.total_bytes());
3375                        buffers.push(StringBuffer::new(buffers.len(), b"a".repeat(bytes).to_vec()));
3376                        tree.insert(offset, BufferLocation::Added(buffers.len() - 1), 0, bytes, Some(0), &buffers);
3377                    }
3378                    Operation::Delete { offset, bytes } => {
3379                        let offset = offset.min(tree.total_bytes());
3380                        let bytes = bytes.min(tree.total_bytes().saturating_sub(offset));
3381                        if bytes > 0 {
3382                            tree.delete(offset, bytes, &buffers);
3383                        }
3384                    }
3385                }
3386            }
3387
3388            let total_bytes = tree.total_bytes();
3389            if total_bytes == 0 {
3390                return Ok(());
3391            }
3392
3393            // Sum of piece bytes should equal total bytes
3394            let pieces: Vec<_> = tree.iter_pieces_in_range(0, total_bytes).collect();
3395            let sum_bytes: usize = pieces.iter().map(|p| p.bytes).sum();
3396            prop_assert_eq!(sum_bytes, total_bytes,
3397                "Sum of piece bytes ({}) doesn't match total_bytes ({})", sum_bytes, total_bytes);
3398        }
3399
3400        /// Property test that verifies offset_to_position returns correct line/column
3401        /// after buffer modifications. This catches the bug where column calculation
3402        /// was incorrect after insertions/deletions.
3403        #[test]
3404        fn prop_offset_to_position_correct_after_modifications(
3405            ops in prop::collection::vec(
3406                prop_oneof![
3407                    // Insert with newlines
3408                    (0usize..50, prop::collection::vec(
3409                        prop_oneof![
3410                            Just(b'a'),
3411                            Just(b'\n'),
3412                        ],
3413                        1..20
3414                    )).prop_map(|(offset, bytes)| (offset, bytes, true)),
3415                    // Delete
3416                    (0usize..50, 1usize..10).prop_map(|(offset, _bytes)| (offset, vec![], false)),
3417                ],
3418                5..20
3419            ),
3420            test_offsets in prop::collection::vec(0usize..100, 3..10)
3421        ) {
3422            // Start with content that has newlines
3423            let initial = b"Hello\nWorld\nTest\n";
3424            let mut content = initial.to_vec();
3425
3426            let mut buffers = vec![StringBuffer::new(0, initial.to_vec())];
3427            let newline_count = initial.iter().filter(|&&b| b == b'\n').count();
3428            let mut tree = PieceTree::new(
3429                BufferLocation::Stored(0),
3430                0,
3431                initial.len(),
3432                Some(newline_count),
3433            );
3434
3435            // Apply operations, tracking actual content
3436            for (offset, bytes, is_insert) in ops {
3437                if is_insert && !bytes.is_empty() {
3438                    let offset = offset.min(content.len());
3439                    let newlines = bytes.iter().filter(|&&b| b == b'\n').count();
3440
3441                    // Add buffer and insert into tree
3442                    buffers.push(StringBuffer::new(buffers.len(), bytes.clone()));
3443                    tree.insert(
3444                        offset,
3445                        BufferLocation::Added(buffers.len() - 1),
3446                        0,
3447                        bytes.len(),
3448                        Some(newlines),
3449                        &buffers,
3450                    );
3451
3452                    // Update actual content
3453                    content.splice(offset..offset, bytes);
3454                } else if !is_insert {
3455                    // Delete operation - offset is first element, bytes length is implied
3456                    let offset = offset.min(content.len());
3457                    let delete_len = 5.min(content.len().saturating_sub(offset)); // Use fixed small delete
3458                    if delete_len > 0 {
3459                        tree.delete(offset, delete_len, &buffers);
3460                        content.drain(offset..offset + delete_len);
3461                    }
3462                }
3463            }
3464
3465            // Helper to compute ground truth line/column from content
3466            let compute_position = |content: &[u8], offset: usize| -> (usize, usize) {
3467                let offset = offset.min(content.len());
3468                let mut line = 0;
3469                let mut col = 0;
3470                for (i, &byte) in content.iter().enumerate() {
3471                    if i == offset {
3472                        break;
3473                    }
3474                    if byte == b'\n' {
3475                        line += 1;
3476                        col = 0;
3477                    } else {
3478                        col += 1;
3479                    }
3480                }
3481                (line, col)
3482            };
3483
3484            // Test various offsets
3485            for offset in test_offsets {
3486                let offset = offset.min(content.len());
3487                if offset == 0 {
3488                    continue; // Skip 0, it's a special case that always works
3489                }
3490
3491                let expected = compute_position(&content, offset);
3492                let actual = tree.offset_to_position(offset, &buffers);
3493
3494                prop_assert_eq!(
3495                    actual,
3496                    Some(expected),
3497                    "offset_to_position({}) returned {:?}, expected {:?}. Content len: {}",
3498                    offset,
3499                    actual,
3500                    expected,
3501                    content.len()
3502                );
3503            }
3504        }
3505    }
3506}