fresh/primitives/
line_iterator.rs

1use crate::model::buffer::TextBuffer;
2
3/// Iterator over lines in a TextBuffer with bidirectional support
4/// Uses piece iterator for efficient sequential scanning (ONE O(log n) initialization)
5///
6/// # Performance Characteristics
7///
8/// Line tracking is now always computed when chunks are loaded:
9/// - **All loaded chunks**: `line_starts = Vec<usize>` → exact line metadata available
10/// - **Unloaded chunks**: Only metadata unavailable until first access
11///
12/// ## Current Performance:
13/// - **Forward iteration (`next()`)**: ✅ Efficient O(1) amortized per line using piece iterator
14/// - **Backward iteration (`prev()`)**: ✅ O(log n) using piece tree line indexing
15/// - **Initialization (`new()`)**: ✅ O(log n) using offset_to_position
16///
17/// ## Design:
18/// - Loaded chunks are always indexed (10% memory overhead per chunk)
19/// - Cursor vicinity is always loaded and indexed → 100% accurate navigation
20/// - Forward scanning with lazy loading handles long lines efficiently
21/// - Backward navigation uses piece tree's line_range() lookup
22///
23/// The `estimated_line_length` parameter is still used for forward scanning to estimate
24/// initial chunk sizes, but line boundaries are always accurate after data is loaded.
25/// Maximum bytes to return per "line" to prevent memory exhaustion from huge single-line files.
26/// Lines longer than this are split into multiple chunks, each treated as a separate "line".
27/// This is generous enough for any practical line while preventing OOM from 10MB+ lines.
28const MAX_LINE_BYTES: usize = 100_000;
29
30pub struct LineIterator<'a> {
31    buffer: &'a mut TextBuffer,
32    /// Current byte position in the document (points to start of current line)
33    current_pos: usize,
34    buffer_len: usize,
35    /// Estimated average line length in bytes (for large file estimation)
36    estimated_line_length: usize,
37    /// Whether we still need to emit a synthetic empty line at EOF
38    /// (set when starting at EOF after a trailing newline or when a newline-ending
39    /// line exhausts the buffer during forward iteration)
40    pending_trailing_empty_line: bool,
41}
42
43impl<'a> LineIterator<'a> {
44    /// Scan backward from byte_pos to find the start of the line
45    /// chunk_size: suggested chunk size for loading (used as performance hint only)
46    fn find_line_start_backward(
47        buffer: &mut TextBuffer,
48        byte_pos: usize,
49        chunk_size: usize,
50    ) -> usize {
51        if byte_pos == 0 {
52            return 0;
53        }
54
55        // Scan backward in chunks until we find a newline or reach position 0
56        // The chunk_size is just a hint for performance - we MUST find the actual line start
57        let mut search_end = byte_pos;
58
59        loop {
60            let scan_start = search_end.saturating_sub(chunk_size);
61            let scan_len = search_end - scan_start;
62
63            // Load the chunk we need to scan
64            if let Ok(chunk) = buffer.get_text_range_mut(scan_start, scan_len) {
65                // Scan backward through the chunk to find the last newline
66                for i in (0..chunk.len()).rev() {
67                    if chunk[i] == b'\n' {
68                        // Found newline - line starts at the next byte
69                        return scan_start + i + 1;
70                    }
71                }
72            }
73
74            // No newline found in this chunk
75            if scan_start == 0 {
76                // Reached the start of the buffer - line starts at 0
77                return 0;
78            }
79
80            // Continue searching from earlier position
81            search_end = scan_start;
82        }
83    }
84
85    pub(crate) fn new(
86        buffer: &'a mut TextBuffer,
87        byte_pos: usize,
88        estimated_line_length: usize,
89    ) -> Self {
90        let buffer_len = buffer.len();
91        let byte_pos = byte_pos.min(buffer_len);
92
93        // Find the start of the line containing byte_pos
94        let line_start = if byte_pos == 0 {
95            0
96        } else {
97            // CRITICAL: Pre-load the chunk containing byte_pos to ensure offset_to_position works
98            // Handle EOF case where byte_pos might equal buffer_len
99            let pos_to_load = if byte_pos >= buffer_len {
100                buffer_len.saturating_sub(1)
101            } else {
102                byte_pos
103            };
104
105            if pos_to_load < buffer_len {
106                let _ = buffer.get_text_range_mut(pos_to_load, 1);
107            }
108
109            // Scan backward from byte_pos to find the start of the line
110            // We scan backward looking for a newline character
111            // NOTE: We previously tried to use offset_to_position() but it has bugs with column calculation
112            Self::find_line_start_backward(buffer, byte_pos, estimated_line_length)
113        };
114
115        let mut pending_trailing_empty_line = false;
116        if buffer_len > 0 && byte_pos == buffer_len {
117            if let Ok(bytes) = buffer.get_text_range_mut(buffer_len - 1, 1) {
118                if bytes.first() == Some(&b'\n') {
119                    pending_trailing_empty_line = true;
120                }
121            }
122        }
123
124        LineIterator {
125            buffer,
126            current_pos: line_start,
127            buffer_len,
128            estimated_line_length,
129            pending_trailing_empty_line,
130        }
131    }
132
133    /// Get the next line (moving forward)
134    /// Uses lazy loading to handle unloaded buffers transparently
135    pub fn next_line(&mut self) -> Option<(usize, String)> {
136        if self.pending_trailing_empty_line {
137            self.pending_trailing_empty_line = false;
138            let line_start = self.buffer_len;
139            return Some((line_start, String::new()));
140        }
141
142        if self.current_pos >= self.buffer_len {
143            return None;
144        }
145
146        let line_start = self.current_pos;
147
148        // Estimate line length for chunk loading (typically lines are < 200 bytes)
149        // We load more than average to handle long lines without multiple loads
150        let estimated_max_line_length = self.estimated_line_length * 3;
151        let bytes_to_scan = estimated_max_line_length.min(self.buffer_len - self.current_pos);
152
153        // Use get_text_range_mut() which handles lazy loading automatically
154        // This never scans the entire file - only loads the chunk needed for this line
155        let chunk = match self
156            .buffer
157            .get_text_range_mut(self.current_pos, bytes_to_scan)
158        {
159            Ok(data) => data,
160            Err(e) => {
161                tracing::error!(
162                    "LineIterator: Failed to load chunk at offset {}: {}",
163                    self.current_pos,
164                    e
165                );
166                return None;
167            }
168        };
169
170        // Scan for newline in the loaded chunk
171        let mut line_len = 0;
172        let mut found_newline = false;
173        for &byte in chunk.iter() {
174            line_len += 1;
175            if byte == b'\n' {
176                found_newline = true;
177                break;
178            }
179        }
180
181        // If we didn't find a newline and didn't reach EOF, the line is longer than our estimate
182        // Load more data iteratively (rare case for very long lines)
183        // BUT: limit to MAX_LINE_BYTES to prevent memory exhaustion from huge lines
184        if !found_newline && self.current_pos + line_len < self.buffer_len {
185            // Line is longer than expected, keep loading until we find newline, EOF, or hit limit
186            let mut extended_chunk = chunk;
187            while !found_newline
188                && self.current_pos + extended_chunk.len() < self.buffer_len
189                && extended_chunk.len() < MAX_LINE_BYTES
190            {
191                let additional_bytes = estimated_max_line_length
192                    .min(self.buffer_len - self.current_pos - extended_chunk.len())
193                    .min(MAX_LINE_BYTES - extended_chunk.len()); // Don't exceed limit
194                match self
195                    .buffer
196                    .get_text_range_mut(self.current_pos + extended_chunk.len(), additional_bytes)
197                {
198                    Ok(mut more_data) => {
199                        let start_len = extended_chunk.len();
200                        extended_chunk.append(&mut more_data);
201
202                        // Scan the newly added portion
203                        for &byte in extended_chunk[start_len..].iter() {
204                            line_len += 1;
205                            if byte == b'\n' {
206                                found_newline = true;
207                                break;
208                            }
209                            // Also stop if we've hit the limit
210                            if line_len >= MAX_LINE_BYTES {
211                                break;
212                            }
213                        }
214                    }
215                    Err(e) => {
216                        tracing::error!("LineIterator: Failed to extend chunk: {}", e);
217                        break;
218                    }
219                }
220            }
221
222            // Clamp line_len to MAX_LINE_BYTES (safety limit for huge single-line files)
223            line_len = line_len.min(MAX_LINE_BYTES).min(extended_chunk.len());
224
225            // Use the extended chunk
226            let line_bytes = &extended_chunk[..line_len];
227            self.current_pos += line_len;
228            self.schedule_trailing_empty_line(line_bytes);
229            let line_string = String::from_utf8_lossy(line_bytes).into_owned();
230            return Some((line_start, line_string));
231        }
232
233        // Normal case: found newline or reached EOF within initial chunk
234        let line_bytes = &chunk[..line_len];
235        self.current_pos += line_len;
236        self.schedule_trailing_empty_line(line_bytes);
237        let line_string = String::from_utf8_lossy(line_bytes).into_owned();
238        Some((line_start, line_string))
239    }
240
241    /// Get the previous line (moving backward)
242    /// Uses direct byte scanning which works even with unloaded chunks
243    pub fn prev(&mut self) -> Option<(usize, String)> {
244        if self.current_pos == 0 {
245            return None;
246        }
247
248        // current_pos is the start of the current line
249        // Scan backward from current_pos-1 to find the end of the previous line
250        if self.current_pos == 0 {
251            return None;
252        }
253
254        // Load a reasonable chunk backward for scanning
255        let scan_distance = self.estimated_line_length * 3;
256        let scan_start = self.current_pos.saturating_sub(scan_distance);
257        let scan_len = self.current_pos - scan_start;
258
259        // Load the data we need to scan
260        let chunk = match self.buffer.get_text_range_mut(scan_start, scan_len) {
261            Ok(data) => data,
262            Err(e) => {
263                tracing::error!(
264                    "LineIterator::prev(): Failed to load chunk at {}: {}",
265                    scan_start,
266                    e
267                );
268                return None;
269            }
270        };
271
272        // Scan backward to find the last newline (end of previous line)
273        let mut prev_line_end = None;
274        for i in (0..chunk.len()).rev() {
275            if chunk[i] == b'\n' {
276                prev_line_end = Some(scan_start + i);
277                break;
278            }
279        }
280
281        let prev_line_end = prev_line_end?;
282
283        // Now find the start of the previous line by scanning backward from prev_line_end
284        let prev_line_start = if prev_line_end == 0 {
285            0
286        } else {
287            Self::find_line_start_backward(self.buffer, prev_line_end, scan_distance)
288        };
289
290        // Load the previous line content
291        let prev_line_len = prev_line_end - prev_line_start + 1; // +1 to include the newline
292        let line_bytes = match self
293            .buffer
294            .get_text_range_mut(prev_line_start, prev_line_len)
295        {
296            Ok(data) => data,
297            Err(e) => {
298                tracing::error!(
299                    "LineIterator::prev(): Failed to load line at {}: {}",
300                    prev_line_start,
301                    e
302                );
303                return None;
304            }
305        };
306
307        let line_string = String::from_utf8_lossy(&line_bytes).into_owned();
308        self.current_pos = prev_line_start;
309        Some((prev_line_start, line_string))
310    }
311
312    /// Get the current position in the buffer (byte offset of current line start)
313    pub fn current_position(&self) -> usize {
314        self.current_pos
315    }
316
317    fn schedule_trailing_empty_line(&mut self, line_bytes: &[u8]) {
318        if line_bytes.ends_with(b"\n") && self.current_pos == self.buffer_len {
319            self.pending_trailing_empty_line = true;
320        }
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn test_line_iterator_new_at_line_start() {
330        let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld\nTest".to_vec());
331
332        // Test iterator at position 0 (start of line 0)
333        let iter = buffer.line_iterator(0, 80);
334        assert_eq!(iter.current_position(), 0, "Should be at start of line 0");
335
336        // Test iterator at position 6 (start of line 1, after \n)
337        let iter = buffer.line_iterator(6, 80);
338        assert_eq!(iter.current_position(), 6, "Should be at start of line 1");
339
340        // Test iterator at position 12 (start of line 2, after second \n)
341        let iter = buffer.line_iterator(12, 80);
342        assert_eq!(iter.current_position(), 12, "Should be at start of line 2");
343    }
344
345    #[test]
346    fn test_line_iterator_new_in_middle_of_line() {
347        let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld\nTest".to_vec());
348
349        // Test iterator at position 3 (middle of "Hello")
350        let iter = buffer.line_iterator(3, 80);
351        assert_eq!(iter.current_position(), 0, "Should find start of line 0");
352
353        // Test iterator at position 9 (middle of "World")
354        let iter = buffer.line_iterator(9, 80);
355        assert_eq!(iter.current_position(), 6, "Should find start of line 1");
356
357        // Test iterator at position 14 (middle of "Test")
358        let iter = buffer.line_iterator(14, 80);
359        assert_eq!(iter.current_position(), 12, "Should find start of line 2");
360    }
361
362    #[test]
363    fn test_line_iterator_next() {
364        let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld\nTest".to_vec());
365        let mut iter = buffer.line_iterator(0, 80);
366
367        // First line
368        let (pos, content) = iter.next_line().expect("Should have first line");
369        assert_eq!(pos, 0);
370        assert_eq!(content, "Hello\n");
371
372        // Second line
373        let (pos, content) = iter.next_line().expect("Should have second line");
374        assert_eq!(pos, 6);
375        assert_eq!(content, "World\n");
376
377        // Third line
378        let (pos, content) = iter.next_line().expect("Should have third line");
379        assert_eq!(pos, 12);
380        assert_eq!(content, "Test");
381
382        // No more lines
383        assert!(iter.next_line().is_none());
384    }
385
386    #[test]
387    fn test_line_iterator_from_middle_position() {
388        let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld\nTest".to_vec());
389
390        // Start from position 9 (middle of "World")
391        let mut iter = buffer.line_iterator(9, 80);
392        assert_eq!(
393            iter.current_position(),
394            6,
395            "Should be at start of line containing position 9"
396        );
397
398        // First next() should return current line
399        let (pos, content) = iter.next_line().expect("Should have current line");
400        assert_eq!(pos, 6);
401        assert_eq!(content, "World\n");
402
403        // Second next() should return next line
404        let (pos, content) = iter.next_line().expect("Should have next line");
405        assert_eq!(pos, 12);
406        assert_eq!(content, "Test");
407    }
408
409    #[test]
410    fn test_line_iterator_offset_to_position_consistency() {
411        let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld".to_vec());
412
413        // For each position, verify that offset_to_position returns correct values
414        let expected = vec![
415            (0, 0, 0),  // H
416            (1, 0, 1),  // e
417            (2, 0, 2),  // l
418            (3, 0, 3),  // l
419            (4, 0, 4),  // o
420            (5, 0, 5),  // \n
421            (6, 1, 0),  // W
422            (7, 1, 1),  // o
423            (8, 1, 2),  // r
424            (9, 1, 3),  // l
425            (10, 1, 4), // d
426        ];
427
428        for (offset, expected_line, expected_col) in expected {
429            let pos = buffer
430                .offset_to_position(offset)
431                .unwrap_or_else(|| panic!("Should have position for offset {}", offset));
432            assert_eq!(pos.line, expected_line, "Wrong line for offset {}", offset);
433            assert_eq!(
434                pos.column, expected_col,
435                "Wrong column for offset {}",
436                offset
437            );
438
439            // Verify LineIterator uses this correctly
440            let iter = buffer.line_iterator(offset, 80);
441            let expected_line_start = if expected_line == 0 { 0 } else { 6 };
442            assert_eq!(
443                iter.current_position(),
444                expected_line_start,
445                "LineIterator at offset {} should be at line start {}",
446                offset,
447                expected_line_start
448            );
449        }
450    }
451
452    #[test]
453    fn test_line_iterator_prev() {
454        let mut buffer = TextBuffer::from_bytes(b"Line1\nLine2\nLine3".to_vec());
455
456        // Start at line 2
457        let mut iter = buffer.line_iterator(12, 80);
458
459        // Go back to line 1
460        let (pos, content) = iter.prev().expect("Should have previous line");
461        assert_eq!(pos, 6);
462        assert_eq!(content, "Line2\n");
463
464        // Go back to line 0
465        let (pos, content) = iter.prev().expect("Should have previous line");
466        assert_eq!(pos, 0);
467        assert_eq!(content, "Line1\n");
468
469        // No more previous lines
470        assert!(iter.prev().is_none());
471    }
472
473    #[test]
474    fn test_line_iterator_single_line() {
475        let mut buffer = TextBuffer::from_bytes(b"Only one line".to_vec());
476        let mut iter = buffer.line_iterator(0, 80);
477
478        let (pos, content) = iter.next_line().expect("Should have the line");
479        assert_eq!(pos, 0);
480        assert_eq!(content, "Only one line");
481
482        assert!(iter.next_line().is_none());
483        assert!(iter.prev().is_none());
484    }
485
486    #[test]
487    fn test_line_iterator_empty_lines() {
488        let mut buffer = TextBuffer::from_bytes(b"Line1\n\nLine3".to_vec());
489        let mut iter = buffer.line_iterator(0, 80);
490
491        let (pos, content) = iter.next_line().expect("First line");
492        assert_eq!(pos, 0);
493        assert_eq!(content, "Line1\n");
494
495        let (pos, content) = iter.next_line().expect("Empty line");
496        assert_eq!(pos, 6);
497        assert_eq!(content, "\n");
498
499        let (pos, content) = iter.next_line().expect("Third line");
500        assert_eq!(pos, 7);
501        assert_eq!(content, "Line3");
502    }
503
504    #[test]
505    fn test_line_iterator_trailing_newline_emits_empty_line() {
506        let mut buffer = TextBuffer::from_bytes(b"Hello world\n".to_vec());
507        let mut iter = buffer.line_iterator(0, 80);
508
509        let (pos, content) = iter.next_line().expect("First line");
510        assert_eq!(pos, 0);
511        assert_eq!(content, "Hello world\n");
512
513        let (pos, content) = iter
514            .next_line()
515            .expect("Should emit empty line for trailing newline");
516        assert_eq!(pos, "Hello world\n".len());
517        assert_eq!(content, "");
518
519        assert!(iter.next_line().is_none(), "No more lines expected");
520    }
521
522    #[test]
523    fn test_line_iterator_trailing_newline_starting_at_eof() {
524        let mut buffer = TextBuffer::from_bytes(b"Hello world\n".to_vec());
525        let buffer_len = buffer.len();
526        let mut iter = buffer.line_iterator(buffer_len, 80);
527
528        let (pos, content) = iter
529            .next_line()
530            .expect("Should emit empty line at EOF when starting there");
531        assert_eq!(pos, buffer_len);
532        assert_eq!(content, "");
533
534        assert!(iter.next_line().is_none(), "No more lines expected");
535    }
536
537    /// BUG REPRODUCTION: Line longer than estimated_line_length
538    /// When a line is longer than the estimated_line_length passed to line_iterator(),
539    /// the LineIterator::new() constructor fails to find the actual line start.
540    ///
541    /// This causes Home/End key navigation to fail on long lines.
542    #[test]
543    fn test_line_iterator_long_line_exceeds_estimate() {
544        // Create a line that's 200 bytes long (much longer than typical estimate)
545        let long_line = "x".repeat(200);
546        let content = format!("{}\n", long_line);
547        let mut buffer = TextBuffer::from_bytes(content.as_bytes().to_vec());
548
549        // Use a small estimated_line_length (50 bytes) - smaller than actual line
550        let estimated_line_length = 50;
551
552        // Position cursor at the END of the long line (position 200, before the \n)
553        let cursor_at_end = 200;
554
555        // Create iterator from end of line - this should find position 0 as line start
556        let iter = buffer.line_iterator(cursor_at_end, estimated_line_length);
557
558        // BUG: iter.current_position() returns 150 (200 - 50) instead of 0
559        // because find_line_start_backward only scans back 50 bytes
560        assert_eq!(
561            iter.current_position(),
562            0,
563            "LineIterator should find actual line start (0), not estimation boundary ({})",
564            cursor_at_end - estimated_line_length
565        );
566
567        // Test with cursor in the middle too
568        let cursor_in_middle = 100;
569        let iter = buffer.line_iterator(cursor_in_middle, estimated_line_length);
570        assert_eq!(
571            iter.current_position(),
572            0,
573            "LineIterator should find line start regardless of cursor position"
574        );
575    }
576
577    /// BUG REPRODUCTION: Multiple lines where one exceeds estimate
578    /// Tests that line iteration works correctly even when one line is very long
579    #[test]
580    fn test_line_iterator_mixed_line_lengths() {
581        // Short line, very long line, short line
582        let long_line = "L".repeat(300);
583        let content = format!("Short1\n{}\nShort2\n", long_line);
584        let mut buffer = TextBuffer::from_bytes(content.as_bytes().to_vec());
585
586        let estimated_line_length = 50;
587
588        // Position cursor at end of long line (position 7 + 300 = 307)
589        let cursor_pos = 307;
590
591        let iter = buffer.line_iterator(cursor_pos, estimated_line_length);
592
593        // Should find position 7 (start of long line), not 257 (307 - 50)
594        assert_eq!(
595            iter.current_position(),
596            7,
597            "Should find start of long line at position 7, not estimation boundary"
598        );
599    }
600
601    /// Test that LineIterator correctly handles CRLF line endings
602    /// Each line should have the correct byte offset, accounting for 2 bytes per line ending
603    #[test]
604    fn test_line_iterator_crlf() {
605        // CRLF content: "abc\r\ndef\r\nghi\r\n"
606        // Bytes: a=0, b=1, c=2, \r=3, \n=4, d=5, e=6, f=7, \r=8, \n=9, g=10, h=11, i=12, \r=13, \n=14
607        let content = b"abc\r\ndef\r\nghi\r\n";
608        let buffer_len = content.len();
609        let mut buffer = TextBuffer::from_bytes(content.to_vec());
610
611        let mut iter = buffer.line_iterator(0, 80);
612
613        // First line: starts at 0, content is "abc\r\n"
614        let (pos, line_content) = iter.next_line().expect("Should have first line");
615        assert_eq!(pos, 0, "First line should start at byte 0");
616        assert_eq!(line_content, "abc\r\n", "First line content");
617
618        // Second line: starts at 5 (after "abc\r\n"), content is "def\r\n"
619        let (pos, line_content) = iter.next_line().expect("Should have second line");
620        assert_eq!(pos, 5, "Second line should start at byte 5 (after CRLF)");
621        assert_eq!(line_content, "def\r\n", "Second line content");
622
623        // Third line: starts at 10 (after "abc\r\ndef\r\n"), content is "ghi\r\n"
624        let (pos, line_content) = iter.next_line().expect("Should have third line");
625        assert_eq!(
626            pos, 10,
627            "Third line should start at byte 10 (after two CRLFs)"
628        );
629        assert_eq!(line_content, "ghi\r\n", "Third line content");
630
631        // Trailing CRLF means there's an empty synthetic line at EOF
632        let (pos, line_content) = iter
633            .next_line()
634            .expect("Should emit empty line after trailing CRLF");
635        assert_eq!(pos, buffer_len, "Empty line should start at EOF");
636        assert_eq!(line_content, "", "Empty line content");
637
638        assert!(iter.next_line().is_none(), "Should have no more lines");
639    }
640
641    /// Test that line_start values are correct for CRLF files when starting from middle
642    #[test]
643    fn test_line_iterator_crlf_from_middle() {
644        // CRLF content: "abc\r\ndef\r\nghi"
645        // Bytes: a=0, b=1, c=2, \r=3, \n=4, d=5, e=6, f=7, \r=8, \n=9, g=10, h=11, i=12
646        let content = b"abc\r\ndef\r\nghi";
647        let mut buffer = TextBuffer::from_bytes(content.to_vec());
648
649        // Start iterator from middle of second line (byte 6 = 'e')
650        let iter = buffer.line_iterator(6, 80);
651        assert_eq!(
652            iter.current_position(),
653            5,
654            "Iterator at byte 6 should find line start at byte 5"
655        );
656
657        // Start iterator from the \r of first line (byte 3)
658        let iter = buffer.line_iterator(3, 80);
659        assert_eq!(
660            iter.current_position(),
661            0,
662            "Iterator at byte 3 (\\r) should find line start at byte 0"
663        );
664
665        // Start iterator from the \n of first line (byte 4)
666        let iter = buffer.line_iterator(4, 80);
667        assert_eq!(
668            iter.current_position(),
669            0,
670            "Iterator at byte 4 (\\n) should find line start at byte 0"
671        );
672
673        // Start iterator from first char of third line (byte 10 = 'g')
674        let iter = buffer.line_iterator(10, 80);
675        assert_eq!(
676            iter.current_position(),
677            10,
678            "Iterator at byte 10 should be at line start already"
679        );
680    }
681
682    /// Test that large single-line files are chunked correctly and all data is preserved.
683    /// This verifies the MAX_LINE_BYTES limit works correctly with sequential data.
684    #[test]
685    fn test_line_iterator_large_single_line_chunked_correctly() {
686        // Create content with sequential markers: "[00001][00002][00003]..."
687        // Each marker is 7 bytes, so we can verify order and completeness
688        let num_markers = 20_000; // ~140KB of data, spans multiple chunks
689        let content: String = (1..=num_markers).map(|i| format!("[{:05}]", i)).collect();
690
691        let content_bytes = content.as_bytes().to_vec();
692        let content_len = content_bytes.len();
693        let mut buffer = TextBuffer::from_bytes(content_bytes);
694
695        // Iterate and collect all chunks
696        let mut iter = buffer.line_iterator(0, 200);
697        let mut all_content = String::new();
698        let mut chunk_count = 0;
699        let mut chunk_sizes = Vec::new();
700
701        while let Some((pos, chunk)) = iter.next_line() {
702            // Verify chunk starts at expected position
703            assert_eq!(
704                pos,
705                all_content.len(),
706                "Chunk {} should start at byte {}",
707                chunk_count,
708                all_content.len()
709            );
710
711            // Verify chunk is within MAX_LINE_BYTES limit
712            assert!(
713                chunk.len() <= super::MAX_LINE_BYTES,
714                "Chunk {} exceeds MAX_LINE_BYTES: {} > {}",
715                chunk_count,
716                chunk.len(),
717                super::MAX_LINE_BYTES
718            );
719
720            chunk_sizes.push(chunk.len());
721            all_content.push_str(&chunk);
722            chunk_count += 1;
723        }
724
725        // Verify all content was retrieved
726        assert_eq!(
727            all_content.len(),
728            content_len,
729            "Total content length should match original"
730        );
731        assert_eq!(
732            all_content, content,
733            "Reconstructed content should match original"
734        );
735
736        // With 140KB of data and 100KB limit, should have 2 chunks
737        assert!(
738            chunk_count >= 2,
739            "Should have multiple chunks for {}KB content (got {})",
740            content_len / 1024,
741            chunk_count
742        );
743
744        // Verify sequential markers are all present and in order
745        for i in 1..=num_markers {
746            let marker = format!("[{:05}]", i);
747            assert!(
748                all_content.contains(&marker),
749                "Missing marker {} in reconstructed content",
750                marker
751            );
752        }
753
754        // Verify markers are in correct order by checking a sample
755        let pos_1000 = all_content.find("[01000]").unwrap();
756        let pos_2000 = all_content.find("[02000]").unwrap();
757        let pos_10000 = all_content.find("[10000]").unwrap();
758        assert!(
759            pos_1000 < pos_2000 && pos_2000 < pos_10000,
760            "Markers should be in sequential order"
761        );
762    }
763}