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