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 let _ = buffer.get_text_range_mut(pos_to_load, 1);
107 }
108
109 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 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 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 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 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 !found_newline && self.current_pos + line_len < self.buffer_len {
185 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()); 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 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 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 line_len = line_len.min(MAX_LINE_BYTES).min(extended_chunk.len());
224
225 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 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 pub fn prev(&mut self) -> Option<(usize, String)> {
244 if self.current_pos == 0 {
245 return None;
246 }
247
248 if self.current_pos == 0 {
251 return None;
252 }
253
254 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 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 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 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 let prev_line_len = prev_line_end - prev_line_start + 1; 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 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 let iter = buffer.line_iterator(0, 80);
340 assert_eq!(iter.current_position(), 0, "Should be at start of line 0");
341
342 let iter = buffer.line_iterator(6, 80);
344 assert_eq!(iter.current_position(), 6, "Should be at start of line 1");
345
346 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 let iter = buffer.line_iterator(3, 80);
357 assert_eq!(iter.current_position(), 0, "Should find start of line 0");
358
359 let iter = buffer.line_iterator(9, 80);
361 assert_eq!(iter.current_position(), 6, "Should find start of line 1");
362
363 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 let (pos, content) = iter.next_line().expect("Should have first line");
375 assert_eq!(pos, 0);
376 assert_eq!(content, "Hello\n");
377
378 let (pos, content) = iter.next_line().expect("Should have second line");
380 assert_eq!(pos, 6);
381 assert_eq!(content, "World\n");
382
383 let (pos, content) = iter.next_line().expect("Should have third line");
385 assert_eq!(pos, 12);
386 assert_eq!(content, "Test");
387
388 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 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 let (pos, content) = iter.next_line().expect("Should have current line");
406 assert_eq!(pos, 6);
407 assert_eq!(content, "World\n");
408
409 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 let expected = vec![
421 (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), ];
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 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 let mut iter = buffer.line_iterator(12, 80);
464
465 let (pos, content) = iter.prev().expect("Should have previous line");
467 assert_eq!(pos, 6);
468 assert_eq!(content, "Line2\n");
469
470 let (pos, content) = iter.prev().expect("Should have previous line");
472 assert_eq!(pos, 0);
473 assert_eq!(content, "Line1\n");
474
475 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 #[test]
549 fn test_line_iterator_long_line_exceeds_estimate() {
550 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 let estimated_line_length = 50;
557
558 let cursor_at_end = 200;
560
561 let iter = buffer.line_iterator(cursor_at_end, estimated_line_length);
563
564 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 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 #[test]
586 fn test_line_iterator_mixed_line_lengths() {
587 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 let cursor_pos = 307;
596
597 let iter = buffer.line_iterator(cursor_pos, estimated_line_length);
598
599 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]
610 fn test_line_iterator_crlf() {
611 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 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 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 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 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]
649 fn test_line_iterator_crlf_from_middle() {
650 let content = b"abc\r\ndef\r\nghi";
653 let mut buffer = TextBuffer::from_bytes(content.to_vec(), test_fs());
654
655 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 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 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 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]
691 fn test_line_iterator_large_single_line_chunked_correctly() {
692 let num_markers = 20_000; 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 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 assert_eq!(
710 pos,
711 all_content.len(),
712 "Chunk {} should start at byte {}",
713 chunk_count,
714 all_content.len()
715 );
716
717 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 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 assert!(
744 chunk_count >= 2,
745 "Should have multiple chunks for {}KB content (got {})",
746 content_len / 1024,
747 chunk_count
748 );
749
750 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 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}