1use crate::model::buffer::TextBuffer;
2
3const MAX_LINE_BYTES: usize = 100_000;
29
30pub struct LineIterator<'a> {
31 buffer: &'a mut TextBuffer,
32 current_pos: usize,
34 buffer_len: usize,
35 estimated_line_length: usize,
37 pending_trailing_empty_line: bool,
41}
42
43impl<'a> LineIterator<'a> {
44 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 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 if let Ok(chunk) = buffer.get_text_range_mut(scan_start, scan_len) {
65 for i in (0..chunk.len()).rev() {
67 if chunk[i] == b'\n' {
68 return scan_start + i + 1;
70 }
71 }
72 }
73
74 if scan_start == 0 {
76 return 0;
78 }
79
80 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 let line_start = if byte_pos == 0 {
95 0
96 } else {
97 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 #[allow(clippy::let_underscore_must_use)]
108 let _ = buffer.get_text_range_mut(pos_to_load, 1);
109 }
110
111 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 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 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 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 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 !found_newline && self.current_pos + line_len < self.buffer_len {
187 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()); 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 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 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 line_len = line_len.min(MAX_LINE_BYTES).min(extended_chunk.len());
226
227 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 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 pub fn prev(&mut self) -> Option<(usize, String)> {
246 if self.current_pos == 0 {
247 return None;
248 }
249
250 if self.current_pos == 0 {
253 return None;
254 }
255
256 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 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 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 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 let prev_line_len = prev_line_end - prev_line_start + 1; 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 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 let iter = buffer.line_iterator(0, 80);
342 assert_eq!(iter.current_position(), 0, "Should be at start of line 0");
343
344 let iter = buffer.line_iterator(6, 80);
346 assert_eq!(iter.current_position(), 6, "Should be at start of line 1");
347
348 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 let iter = buffer.line_iterator(3, 80);
359 assert_eq!(iter.current_position(), 0, "Should find start of line 0");
360
361 let iter = buffer.line_iterator(9, 80);
363 assert_eq!(iter.current_position(), 6, "Should find start of line 1");
364
365 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 let (pos, content) = iter.next_line().expect("Should have first line");
377 assert_eq!(pos, 0);
378 assert_eq!(content, "Hello\n");
379
380 let (pos, content) = iter.next_line().expect("Should have second line");
382 assert_eq!(pos, 6);
383 assert_eq!(content, "World\n");
384
385 let (pos, content) = iter.next_line().expect("Should have third line");
387 assert_eq!(pos, 12);
388 assert_eq!(content, "Test");
389
390 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 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 let (pos, content) = iter.next_line().expect("Should have current line");
408 assert_eq!(pos, 6);
409 assert_eq!(content, "World\n");
410
411 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 let expected = vec![
423 (0, 0, 0), (1, 0, 1), (2, 0, 2), (3, 0, 3), (4, 0, 4), (5, 0, 5), (6, 1, 0), (7, 1, 1), (8, 1, 2), (9, 1, 3), (10, 1, 4), ];
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 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 let mut iter = buffer.line_iterator(12, 80);
466
467 let (pos, content) = iter.prev().expect("Should have previous line");
469 assert_eq!(pos, 6);
470 assert_eq!(content, "Line2\n");
471
472 let (pos, content) = iter.prev().expect("Should have previous line");
474 assert_eq!(pos, 0);
475 assert_eq!(content, "Line1\n");
476
477 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 #[test]
551 fn test_line_iterator_long_line_exceeds_estimate() {
552 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 let estimated_line_length = 50;
559
560 let cursor_at_end = 200;
562
563 let iter = buffer.line_iterator(cursor_at_end, estimated_line_length);
565
566 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 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 #[test]
588 fn test_line_iterator_mixed_line_lengths() {
589 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 let cursor_pos = 307;
598
599 let iter = buffer.line_iterator(cursor_pos, estimated_line_length);
600
601 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]
612 fn test_line_iterator_crlf() {
613 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 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 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 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 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]
651 fn test_line_iterator_crlf_from_middle() {
652 let content = b"abc\r\ndef\r\nghi";
655 let mut buffer = TextBuffer::from_bytes(content.to_vec(), test_fs());
656
657 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 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 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 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]
693 fn test_line_iterator_large_single_line_chunked_correctly() {
694 let num_markers = 20_000; 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 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 assert_eq!(
712 pos,
713 all_content.len(),
714 "Chunk {} should start at byte {}",
715 chunk_count,
716 all_content.len()
717 );
718
719 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 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 assert!(
746 chunk_count >= 2,
747 "Should have multiple chunks for {}KB content (got {})",
748 content_len / 1024,
749 chunk_count
750 );
751
752 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 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}