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 super::*;
327
328 #[test]
329 fn test_line_iterator_new_at_line_start() {
330 let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld\nTest".to_vec());
331
332 let iter = buffer.line_iterator(0, 80);
334 assert_eq!(iter.current_position(), 0, "Should be at start of line 0");
335
336 let iter = buffer.line_iterator(6, 80);
338 assert_eq!(iter.current_position(), 6, "Should be at start of line 1");
339
340 let iter = buffer.line_iterator(12, 80);
342 assert_eq!(iter.current_position(), 12, "Should be at start of line 2");
343 }
344
345 #[test]
346 fn test_line_iterator_new_in_middle_of_line() {
347 let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld\nTest".to_vec());
348
349 let iter = buffer.line_iterator(3, 80);
351 assert_eq!(iter.current_position(), 0, "Should find start of line 0");
352
353 let iter = buffer.line_iterator(9, 80);
355 assert_eq!(iter.current_position(), 6, "Should find start of line 1");
356
357 let iter = buffer.line_iterator(14, 80);
359 assert_eq!(iter.current_position(), 12, "Should find start of line 2");
360 }
361
362 #[test]
363 fn test_line_iterator_next() {
364 let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld\nTest".to_vec());
365 let mut iter = buffer.line_iterator(0, 80);
366
367 let (pos, content) = iter.next_line().expect("Should have first line");
369 assert_eq!(pos, 0);
370 assert_eq!(content, "Hello\n");
371
372 let (pos, content) = iter.next_line().expect("Should have second line");
374 assert_eq!(pos, 6);
375 assert_eq!(content, "World\n");
376
377 let (pos, content) = iter.next_line().expect("Should have third line");
379 assert_eq!(pos, 12);
380 assert_eq!(content, "Test");
381
382 assert!(iter.next_line().is_none());
384 }
385
386 #[test]
387 fn test_line_iterator_from_middle_position() {
388 let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld\nTest".to_vec());
389
390 let mut iter = buffer.line_iterator(9, 80);
392 assert_eq!(
393 iter.current_position(),
394 6,
395 "Should be at start of line containing position 9"
396 );
397
398 let (pos, content) = iter.next_line().expect("Should have current line");
400 assert_eq!(pos, 6);
401 assert_eq!(content, "World\n");
402
403 let (pos, content) = iter.next_line().expect("Should have next line");
405 assert_eq!(pos, 12);
406 assert_eq!(content, "Test");
407 }
408
409 #[test]
410 fn test_line_iterator_offset_to_position_consistency() {
411 let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld".to_vec());
412
413 let expected = vec![
415 (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), ];
427
428 for (offset, expected_line, expected_col) in expected {
429 let pos = buffer
430 .offset_to_position(offset)
431 .unwrap_or_else(|| panic!("Should have position for offset {}", offset));
432 assert_eq!(pos.line, expected_line, "Wrong line for offset {}", offset);
433 assert_eq!(
434 pos.column, expected_col,
435 "Wrong column for offset {}",
436 offset
437 );
438
439 let iter = buffer.line_iterator(offset, 80);
441 let expected_line_start = if expected_line == 0 { 0 } else { 6 };
442 assert_eq!(
443 iter.current_position(),
444 expected_line_start,
445 "LineIterator at offset {} should be at line start {}",
446 offset,
447 expected_line_start
448 );
449 }
450 }
451
452 #[test]
453 fn test_line_iterator_prev() {
454 let mut buffer = TextBuffer::from_bytes(b"Line1\nLine2\nLine3".to_vec());
455
456 let mut iter = buffer.line_iterator(12, 80);
458
459 let (pos, content) = iter.prev().expect("Should have previous line");
461 assert_eq!(pos, 6);
462 assert_eq!(content, "Line2\n");
463
464 let (pos, content) = iter.prev().expect("Should have previous line");
466 assert_eq!(pos, 0);
467 assert_eq!(content, "Line1\n");
468
469 assert!(iter.prev().is_none());
471 }
472
473 #[test]
474 fn test_line_iterator_single_line() {
475 let mut buffer = TextBuffer::from_bytes(b"Only one line".to_vec());
476 let mut iter = buffer.line_iterator(0, 80);
477
478 let (pos, content) = iter.next_line().expect("Should have the line");
479 assert_eq!(pos, 0);
480 assert_eq!(content, "Only one line");
481
482 assert!(iter.next_line().is_none());
483 assert!(iter.prev().is_none());
484 }
485
486 #[test]
487 fn test_line_iterator_empty_lines() {
488 let mut buffer = TextBuffer::from_bytes(b"Line1\n\nLine3".to_vec());
489 let mut iter = buffer.line_iterator(0, 80);
490
491 let (pos, content) = iter.next_line().expect("First line");
492 assert_eq!(pos, 0);
493 assert_eq!(content, "Line1\n");
494
495 let (pos, content) = iter.next_line().expect("Empty line");
496 assert_eq!(pos, 6);
497 assert_eq!(content, "\n");
498
499 let (pos, content) = iter.next_line().expect("Third line");
500 assert_eq!(pos, 7);
501 assert_eq!(content, "Line3");
502 }
503
504 #[test]
505 fn test_line_iterator_trailing_newline_emits_empty_line() {
506 let mut buffer = TextBuffer::from_bytes(b"Hello world\n".to_vec());
507 let mut iter = buffer.line_iterator(0, 80);
508
509 let (pos, content) = iter.next_line().expect("First line");
510 assert_eq!(pos, 0);
511 assert_eq!(content, "Hello world\n");
512
513 let (pos, content) = iter
514 .next_line()
515 .expect("Should emit empty line for trailing newline");
516 assert_eq!(pos, "Hello world\n".len());
517 assert_eq!(content, "");
518
519 assert!(iter.next_line().is_none(), "No more lines expected");
520 }
521
522 #[test]
523 fn test_line_iterator_trailing_newline_starting_at_eof() {
524 let mut buffer = TextBuffer::from_bytes(b"Hello world\n".to_vec());
525 let buffer_len = buffer.len();
526 let mut iter = buffer.line_iterator(buffer_len, 80);
527
528 let (pos, content) = iter
529 .next_line()
530 .expect("Should emit empty line at EOF when starting there");
531 assert_eq!(pos, buffer_len);
532 assert_eq!(content, "");
533
534 assert!(iter.next_line().is_none(), "No more lines expected");
535 }
536
537 #[test]
543 fn test_line_iterator_long_line_exceeds_estimate() {
544 let long_line = "x".repeat(200);
546 let content = format!("{}\n", long_line);
547 let mut buffer = TextBuffer::from_bytes(content.as_bytes().to_vec());
548
549 let estimated_line_length = 50;
551
552 let cursor_at_end = 200;
554
555 let iter = buffer.line_iterator(cursor_at_end, estimated_line_length);
557
558 assert_eq!(
561 iter.current_position(),
562 0,
563 "LineIterator should find actual line start (0), not estimation boundary ({})",
564 cursor_at_end - estimated_line_length
565 );
566
567 let cursor_in_middle = 100;
569 let iter = buffer.line_iterator(cursor_in_middle, estimated_line_length);
570 assert_eq!(
571 iter.current_position(),
572 0,
573 "LineIterator should find line start regardless of cursor position"
574 );
575 }
576
577 #[test]
580 fn test_line_iterator_mixed_line_lengths() {
581 let long_line = "L".repeat(300);
583 let content = format!("Short1\n{}\nShort2\n", long_line);
584 let mut buffer = TextBuffer::from_bytes(content.as_bytes().to_vec());
585
586 let estimated_line_length = 50;
587
588 let cursor_pos = 307;
590
591 let iter = buffer.line_iterator(cursor_pos, estimated_line_length);
592
593 assert_eq!(
595 iter.current_position(),
596 7,
597 "Should find start of long line at position 7, not estimation boundary"
598 );
599 }
600
601 #[test]
604 fn test_line_iterator_crlf() {
605 let content = b"abc\r\ndef\r\nghi\r\n";
608 let buffer_len = content.len();
609 let mut buffer = TextBuffer::from_bytes(content.to_vec());
610
611 let mut iter = buffer.line_iterator(0, 80);
612
613 let (pos, line_content) = iter.next_line().expect("Should have first line");
615 assert_eq!(pos, 0, "First line should start at byte 0");
616 assert_eq!(line_content, "abc\r\n", "First line content");
617
618 let (pos, line_content) = iter.next_line().expect("Should have second line");
620 assert_eq!(pos, 5, "Second line should start at byte 5 (after CRLF)");
621 assert_eq!(line_content, "def\r\n", "Second line content");
622
623 let (pos, line_content) = iter.next_line().expect("Should have third line");
625 assert_eq!(
626 pos, 10,
627 "Third line should start at byte 10 (after two CRLFs)"
628 );
629 assert_eq!(line_content, "ghi\r\n", "Third line content");
630
631 let (pos, line_content) = iter
633 .next_line()
634 .expect("Should emit empty line after trailing CRLF");
635 assert_eq!(pos, buffer_len, "Empty line should start at EOF");
636 assert_eq!(line_content, "", "Empty line content");
637
638 assert!(iter.next_line().is_none(), "Should have no more lines");
639 }
640
641 #[test]
643 fn test_line_iterator_crlf_from_middle() {
644 let content = b"abc\r\ndef\r\nghi";
647 let mut buffer = TextBuffer::from_bytes(content.to_vec());
648
649 let iter = buffer.line_iterator(6, 80);
651 assert_eq!(
652 iter.current_position(),
653 5,
654 "Iterator at byte 6 should find line start at byte 5"
655 );
656
657 let iter = buffer.line_iterator(3, 80);
659 assert_eq!(
660 iter.current_position(),
661 0,
662 "Iterator at byte 3 (\\r) should find line start at byte 0"
663 );
664
665 let iter = buffer.line_iterator(4, 80);
667 assert_eq!(
668 iter.current_position(),
669 0,
670 "Iterator at byte 4 (\\n) should find line start at byte 0"
671 );
672
673 let iter = buffer.line_iterator(10, 80);
675 assert_eq!(
676 iter.current_position(),
677 10,
678 "Iterator at byte 10 should be at line start already"
679 );
680 }
681
682 #[test]
685 fn test_line_iterator_large_single_line_chunked_correctly() {
686 let num_markers = 20_000; let content: String = (1..=num_markers).map(|i| format!("[{:05}]", i)).collect();
690
691 let content_bytes = content.as_bytes().to_vec();
692 let content_len = content_bytes.len();
693 let mut buffer = TextBuffer::from_bytes(content_bytes);
694
695 let mut iter = buffer.line_iterator(0, 200);
697 let mut all_content = String::new();
698 let mut chunk_count = 0;
699 let mut chunk_sizes = Vec::new();
700
701 while let Some((pos, chunk)) = iter.next_line() {
702 assert_eq!(
704 pos,
705 all_content.len(),
706 "Chunk {} should start at byte {}",
707 chunk_count,
708 all_content.len()
709 );
710
711 assert!(
713 chunk.len() <= super::MAX_LINE_BYTES,
714 "Chunk {} exceeds MAX_LINE_BYTES: {} > {}",
715 chunk_count,
716 chunk.len(),
717 super::MAX_LINE_BYTES
718 );
719
720 chunk_sizes.push(chunk.len());
721 all_content.push_str(&chunk);
722 chunk_count += 1;
723 }
724
725 assert_eq!(
727 all_content.len(),
728 content_len,
729 "Total content length should match original"
730 );
731 assert_eq!(
732 all_content, content,
733 "Reconstructed content should match original"
734 );
735
736 assert!(
738 chunk_count >= 2,
739 "Should have multiple chunks for {}KB content (got {})",
740 content_len / 1024,
741 chunk_count
742 );
743
744 for i in 1..=num_markers {
746 let marker = format!("[{:05}]", i);
747 assert!(
748 all_content.contains(&marker),
749 "Missing marker {} in reconstructed content",
750 marker
751 );
752 }
753
754 let pos_1000 = all_content.find("[01000]").unwrap();
756 let pos_2000 = all_content.find("[02000]").unwrap();
757 let pos_10000 = all_content.find("[10000]").unwrap();
758 assert!(
759 pos_1000 < pos_2000 && pos_2000 < pos_10000,
760 "Markers should be in sequential order"
761 );
762 }
763}