Skip to main content

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 crate::model::filesystem::StdFileSystem;
327    use std::sync::Arc;
328
329    fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
330        Arc::new(StdFileSystem)
331    }
332    use super::*;
333
334    #[test]
335    fn test_line_iterator_new_at_line_start() {
336        let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld\nTest".to_vec(), test_fs());
337
338        // Test iterator at position 0 (start of line 0)
339        let iter = buffer.line_iterator(0, 80);
340        assert_eq!(iter.current_position(), 0, "Should be at start of line 0");
341
342        // Test iterator at position 6 (start of line 1, after \n)
343        let iter = buffer.line_iterator(6, 80);
344        assert_eq!(iter.current_position(), 6, "Should be at start of line 1");
345
346        // Test iterator at position 12 (start of line 2, after second \n)
347        let iter = buffer.line_iterator(12, 80);
348        assert_eq!(iter.current_position(), 12, "Should be at start of line 2");
349    }
350
351    #[test]
352    fn test_line_iterator_new_in_middle_of_line() {
353        let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld\nTest".to_vec(), test_fs());
354
355        // Test iterator at position 3 (middle of "Hello")
356        let iter = buffer.line_iterator(3, 80);
357        assert_eq!(iter.current_position(), 0, "Should find start of line 0");
358
359        // Test iterator at position 9 (middle of "World")
360        let iter = buffer.line_iterator(9, 80);
361        assert_eq!(iter.current_position(), 6, "Should find start of line 1");
362
363        // Test iterator at position 14 (middle of "Test")
364        let iter = buffer.line_iterator(14, 80);
365        assert_eq!(iter.current_position(), 12, "Should find start of line 2");
366    }
367
368    #[test]
369    fn test_line_iterator_next() {
370        let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld\nTest".to_vec(), test_fs());
371        let mut iter = buffer.line_iterator(0, 80);
372
373        // First line
374        let (pos, content) = iter.next_line().expect("Should have first line");
375        assert_eq!(pos, 0);
376        assert_eq!(content, "Hello\n");
377
378        // Second line
379        let (pos, content) = iter.next_line().expect("Should have second line");
380        assert_eq!(pos, 6);
381        assert_eq!(content, "World\n");
382
383        // Third line
384        let (pos, content) = iter.next_line().expect("Should have third line");
385        assert_eq!(pos, 12);
386        assert_eq!(content, "Test");
387
388        // No more lines
389        assert!(iter.next_line().is_none());
390    }
391
392    #[test]
393    fn test_line_iterator_from_middle_position() {
394        let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld\nTest".to_vec(), test_fs());
395
396        // Start from position 9 (middle of "World")
397        let mut iter = buffer.line_iterator(9, 80);
398        assert_eq!(
399            iter.current_position(),
400            6,
401            "Should be at start of line containing position 9"
402        );
403
404        // First next() should return current line
405        let (pos, content) = iter.next_line().expect("Should have current line");
406        assert_eq!(pos, 6);
407        assert_eq!(content, "World\n");
408
409        // Second next() should return next line
410        let (pos, content) = iter.next_line().expect("Should have next line");
411        assert_eq!(pos, 12);
412        assert_eq!(content, "Test");
413    }
414
415    #[test]
416    fn test_line_iterator_offset_to_position_consistency() {
417        let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld".to_vec(), test_fs());
418
419        // For each position, verify that offset_to_position returns correct values
420        let expected = vec![
421            (0, 0, 0),  // H
422            (1, 0, 1),  // e
423            (2, 0, 2),  // l
424            (3, 0, 3),  // l
425            (4, 0, 4),  // o
426            (5, 0, 5),  // \n
427            (6, 1, 0),  // W
428            (7, 1, 1),  // o
429            (8, 1, 2),  // r
430            (9, 1, 3),  // l
431            (10, 1, 4), // d
432        ];
433
434        for (offset, expected_line, expected_col) in expected {
435            let pos = buffer
436                .offset_to_position(offset)
437                .unwrap_or_else(|| panic!("Should have position for offset {}", offset));
438            assert_eq!(pos.line, expected_line, "Wrong line for offset {}", offset);
439            assert_eq!(
440                pos.column, expected_col,
441                "Wrong column for offset {}",
442                offset
443            );
444
445            // Verify LineIterator uses this correctly
446            let iter = buffer.line_iterator(offset, 80);
447            let expected_line_start = if expected_line == 0 { 0 } else { 6 };
448            assert_eq!(
449                iter.current_position(),
450                expected_line_start,
451                "LineIterator at offset {} should be at line start {}",
452                offset,
453                expected_line_start
454            );
455        }
456    }
457
458    #[test]
459    fn test_line_iterator_prev() {
460        let mut buffer = TextBuffer::from_bytes(b"Line1\nLine2\nLine3".to_vec(), test_fs());
461
462        // Start at line 2
463        let mut iter = buffer.line_iterator(12, 80);
464
465        // Go back to line 1
466        let (pos, content) = iter.prev().expect("Should have previous line");
467        assert_eq!(pos, 6);
468        assert_eq!(content, "Line2\n");
469
470        // Go back to line 0
471        let (pos, content) = iter.prev().expect("Should have previous line");
472        assert_eq!(pos, 0);
473        assert_eq!(content, "Line1\n");
474
475        // No more previous lines
476        assert!(iter.prev().is_none());
477    }
478
479    #[test]
480    fn test_line_iterator_single_line() {
481        let mut buffer = TextBuffer::from_bytes(b"Only one line".to_vec(), test_fs());
482        let mut iter = buffer.line_iterator(0, 80);
483
484        let (pos, content) = iter.next_line().expect("Should have the line");
485        assert_eq!(pos, 0);
486        assert_eq!(content, "Only one line");
487
488        assert!(iter.next_line().is_none());
489        assert!(iter.prev().is_none());
490    }
491
492    #[test]
493    fn test_line_iterator_empty_lines() {
494        let mut buffer = TextBuffer::from_bytes(b"Line1\n\nLine3".to_vec(), test_fs());
495        let mut iter = buffer.line_iterator(0, 80);
496
497        let (pos, content) = iter.next_line().expect("First line");
498        assert_eq!(pos, 0);
499        assert_eq!(content, "Line1\n");
500
501        let (pos, content) = iter.next_line().expect("Empty line");
502        assert_eq!(pos, 6);
503        assert_eq!(content, "\n");
504
505        let (pos, content) = iter.next_line().expect("Third line");
506        assert_eq!(pos, 7);
507        assert_eq!(content, "Line3");
508    }
509
510    #[test]
511    fn test_line_iterator_trailing_newline_emits_empty_line() {
512        let mut buffer = TextBuffer::from_bytes(b"Hello world\n".to_vec(), test_fs());
513        let mut iter = buffer.line_iterator(0, 80);
514
515        let (pos, content) = iter.next_line().expect("First line");
516        assert_eq!(pos, 0);
517        assert_eq!(content, "Hello world\n");
518
519        let (pos, content) = iter
520            .next_line()
521            .expect("Should emit empty line for trailing newline");
522        assert_eq!(pos, "Hello world\n".len());
523        assert_eq!(content, "");
524
525        assert!(iter.next_line().is_none(), "No more lines expected");
526    }
527
528    #[test]
529    fn test_line_iterator_trailing_newline_starting_at_eof() {
530        let mut buffer = TextBuffer::from_bytes(b"Hello world\n".to_vec(), test_fs());
531        let buffer_len = buffer.len();
532        let mut iter = buffer.line_iterator(buffer_len, 80);
533
534        let (pos, content) = iter
535            .next_line()
536            .expect("Should emit empty line at EOF when starting there");
537        assert_eq!(pos, buffer_len);
538        assert_eq!(content, "");
539
540        assert!(iter.next_line().is_none(), "No more lines expected");
541    }
542
543    /// BUG REPRODUCTION: Line longer than estimated_line_length
544    /// When a line is longer than the estimated_line_length passed to line_iterator(),
545    /// the LineIterator::new() constructor fails to find the actual line start.
546    ///
547    /// This causes Home/End key navigation to fail on long lines.
548    #[test]
549    fn test_line_iterator_long_line_exceeds_estimate() {
550        // Create a line that's 200 bytes long (much longer than typical estimate)
551        let long_line = "x".repeat(200);
552        let content = format!("{}\n", long_line);
553        let mut buffer = TextBuffer::from_bytes(content.as_bytes().to_vec(), test_fs());
554
555        // Use a small estimated_line_length (50 bytes) - smaller than actual line
556        let estimated_line_length = 50;
557
558        // Position cursor at the END of the long line (position 200, before the \n)
559        let cursor_at_end = 200;
560
561        // Create iterator from end of line - this should find position 0 as line start
562        let iter = buffer.line_iterator(cursor_at_end, estimated_line_length);
563
564        // BUG: iter.current_position() returns 150 (200 - 50) instead of 0
565        // because find_line_start_backward only scans back 50 bytes
566        assert_eq!(
567            iter.current_position(),
568            0,
569            "LineIterator should find actual line start (0), not estimation boundary ({})",
570            cursor_at_end - estimated_line_length
571        );
572
573        // Test with cursor in the middle too
574        let cursor_in_middle = 100;
575        let iter = buffer.line_iterator(cursor_in_middle, estimated_line_length);
576        assert_eq!(
577            iter.current_position(),
578            0,
579            "LineIterator should find line start regardless of cursor position"
580        );
581    }
582
583    /// BUG REPRODUCTION: Multiple lines where one exceeds estimate
584    /// Tests that line iteration works correctly even when one line is very long
585    #[test]
586    fn test_line_iterator_mixed_line_lengths() {
587        // Short line, very long line, short line
588        let long_line = "L".repeat(300);
589        let content = format!("Short1\n{}\nShort2\n", long_line);
590        let mut buffer = TextBuffer::from_bytes(content.as_bytes().to_vec(), test_fs());
591
592        let estimated_line_length = 50;
593
594        // Position cursor at end of long line (position 7 + 300 = 307)
595        let cursor_pos = 307;
596
597        let iter = buffer.line_iterator(cursor_pos, estimated_line_length);
598
599        // Should find position 7 (start of long line), not 257 (307 - 50)
600        assert_eq!(
601            iter.current_position(),
602            7,
603            "Should find start of long line at position 7, not estimation boundary"
604        );
605    }
606
607    /// Test that LineIterator correctly handles CRLF line endings
608    /// Each line should have the correct byte offset, accounting for 2 bytes per line ending
609    #[test]
610    fn test_line_iterator_crlf() {
611        // CRLF content: "abc\r\ndef\r\nghi\r\n"
612        // 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
613        let content = b"abc\r\ndef\r\nghi\r\n";
614        let buffer_len = content.len();
615        let mut buffer = TextBuffer::from_bytes(content.to_vec(), test_fs());
616
617        let mut iter = buffer.line_iterator(0, 80);
618
619        // First line: starts at 0, content is "abc\r\n"
620        let (pos, line_content) = iter.next_line().expect("Should have first line");
621        assert_eq!(pos, 0, "First line should start at byte 0");
622        assert_eq!(line_content, "abc\r\n", "First line content");
623
624        // Second line: starts at 5 (after "abc\r\n"), content is "def\r\n"
625        let (pos, line_content) = iter.next_line().expect("Should have second line");
626        assert_eq!(pos, 5, "Second line should start at byte 5 (after CRLF)");
627        assert_eq!(line_content, "def\r\n", "Second line content");
628
629        // Third line: starts at 10 (after "abc\r\ndef\r\n"), content is "ghi\r\n"
630        let (pos, line_content) = iter.next_line().expect("Should have third line");
631        assert_eq!(
632            pos, 10,
633            "Third line should start at byte 10 (after two CRLFs)"
634        );
635        assert_eq!(line_content, "ghi\r\n", "Third line content");
636
637        // Trailing CRLF means there's an empty synthetic line at EOF
638        let (pos, line_content) = iter
639            .next_line()
640            .expect("Should emit empty line after trailing CRLF");
641        assert_eq!(pos, buffer_len, "Empty line should start at EOF");
642        assert_eq!(line_content, "", "Empty line content");
643
644        assert!(iter.next_line().is_none(), "Should have no more lines");
645    }
646
647    /// Test that line_start values are correct for CRLF files when starting from middle
648    #[test]
649    fn test_line_iterator_crlf_from_middle() {
650        // CRLF content: "abc\r\ndef\r\nghi"
651        // 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
652        let content = b"abc\r\ndef\r\nghi";
653        let mut buffer = TextBuffer::from_bytes(content.to_vec(), test_fs());
654
655        // Start iterator from middle of second line (byte 6 = 'e')
656        let iter = buffer.line_iterator(6, 80);
657        assert_eq!(
658            iter.current_position(),
659            5,
660            "Iterator at byte 6 should find line start at byte 5"
661        );
662
663        // Start iterator from the \r of first line (byte 3)
664        let iter = buffer.line_iterator(3, 80);
665        assert_eq!(
666            iter.current_position(),
667            0,
668            "Iterator at byte 3 (\\r) should find line start at byte 0"
669        );
670
671        // Start iterator from the \n of first line (byte 4)
672        let iter = buffer.line_iterator(4, 80);
673        assert_eq!(
674            iter.current_position(),
675            0,
676            "Iterator at byte 4 (\\n) should find line start at byte 0"
677        );
678
679        // Start iterator from first char of third line (byte 10 = 'g')
680        let iter = buffer.line_iterator(10, 80);
681        assert_eq!(
682            iter.current_position(),
683            10,
684            "Iterator at byte 10 should be at line start already"
685        );
686    }
687
688    /// Test that large single-line files are chunked correctly and all data is preserved.
689    /// This verifies the MAX_LINE_BYTES limit works correctly with sequential data.
690    #[test]
691    fn test_line_iterator_large_single_line_chunked_correctly() {
692        // Create content with sequential markers: "[00001][00002][00003]..."
693        // Each marker is 7 bytes, so we can verify order and completeness
694        let num_markers = 20_000; // ~140KB of data, spans multiple chunks
695        let content: String = (1..=num_markers).map(|i| format!("[{:05}]", i)).collect();
696
697        let content_bytes = content.as_bytes().to_vec();
698        let content_len = content_bytes.len();
699        let mut buffer = TextBuffer::from_bytes(content_bytes, test_fs());
700
701        // Iterate and collect all chunks
702        let mut iter = buffer.line_iterator(0, 200);
703        let mut all_content = String::new();
704        let mut chunk_count = 0;
705        let mut chunk_sizes = Vec::new();
706
707        while let Some((pos, chunk)) = iter.next_line() {
708            // Verify chunk starts at expected position
709            assert_eq!(
710                pos,
711                all_content.len(),
712                "Chunk {} should start at byte {}",
713                chunk_count,
714                all_content.len()
715            );
716
717            // Verify chunk is within MAX_LINE_BYTES limit
718            assert!(
719                chunk.len() <= super::MAX_LINE_BYTES,
720                "Chunk {} exceeds MAX_LINE_BYTES: {} > {}",
721                chunk_count,
722                chunk.len(),
723                super::MAX_LINE_BYTES
724            );
725
726            chunk_sizes.push(chunk.len());
727            all_content.push_str(&chunk);
728            chunk_count += 1;
729        }
730
731        // Verify all content was retrieved
732        assert_eq!(
733            all_content.len(),
734            content_len,
735            "Total content length should match original"
736        );
737        assert_eq!(
738            all_content, content,
739            "Reconstructed content should match original"
740        );
741
742        // With 140KB of data and 100KB limit, should have 2 chunks
743        assert!(
744            chunk_count >= 2,
745            "Should have multiple chunks for {}KB content (got {})",
746            content_len / 1024,
747            chunk_count
748        );
749
750        // Verify sequential markers are all present and in order
751        for i in 1..=num_markers {
752            let marker = format!("[{:05}]", i);
753            assert!(
754                all_content.contains(&marker),
755                "Missing marker {} in reconstructed content",
756                marker
757            );
758        }
759
760        // Verify markers are in correct order by checking a sample
761        let pos_1000 = all_content.find("[01000]").unwrap();
762        let pos_2000 = all_content.find("[02000]").unwrap();
763        let pos_10000 = all_content.find("[10000]").unwrap();
764        assert!(
765            pos_1000 < pos_2000 && pos_2000 < pos_10000,
766            "Markers should be in sequential order"
767        );
768    }
769}