use crate::model::buffer::TextBuffer;
pub struct LineIterator<'a> {
buffer: &'a mut TextBuffer,
current_pos: usize,
buffer_len: usize,
estimated_line_length: usize,
}
impl<'a> LineIterator<'a> {
fn find_line_start_backward(
buffer: &mut TextBuffer,
byte_pos: usize,
chunk_size: usize,
) -> usize {
if byte_pos == 0 {
return 0;
}
let mut search_end = byte_pos;
loop {
let scan_start = search_end.saturating_sub(chunk_size);
let scan_len = search_end - scan_start;
if let Ok(chunk) = buffer.get_text_range_mut(scan_start, scan_len) {
for i in (0..chunk.len()).rev() {
if chunk[i] == b'\n' {
return scan_start + i + 1;
}
}
}
if scan_start == 0 {
return 0;
}
search_end = scan_start;
}
}
pub(crate) fn new(
buffer: &'a mut TextBuffer,
byte_pos: usize,
estimated_line_length: usize,
) -> Self {
let buffer_len = buffer.len();
let byte_pos = byte_pos.min(buffer_len);
let line_start = if byte_pos == 0 {
0
} else {
let pos_to_load = if byte_pos >= buffer_len {
buffer_len.saturating_sub(1)
} else {
byte_pos
};
if pos_to_load < buffer_len {
let _ = buffer.get_text_range_mut(pos_to_load, 1);
}
Self::find_line_start_backward(buffer, byte_pos, estimated_line_length)
};
LineIterator {
buffer,
current_pos: line_start,
buffer_len,
estimated_line_length,
}
}
pub fn next(&mut self) -> Option<(usize, String)> {
if self.current_pos >= self.buffer_len {
return None;
}
let line_start = self.current_pos;
let estimated_max_line_length = self.estimated_line_length * 3;
let bytes_to_scan = estimated_max_line_length.min(self.buffer_len - self.current_pos);
let chunk = match self
.buffer
.get_text_range_mut(self.current_pos, bytes_to_scan)
{
Ok(data) => data,
Err(e) => {
tracing::error!(
"LineIterator: Failed to load chunk at offset {}: {}",
self.current_pos,
e
);
return None;
}
};
let mut line_len = 0;
let mut found_newline = false;
for &byte in chunk.iter() {
line_len += 1;
if byte == b'\n' {
found_newline = true;
break;
}
}
if !found_newline && self.current_pos + line_len < self.buffer_len {
let mut extended_chunk = chunk;
while !found_newline && self.current_pos + extended_chunk.len() < self.buffer_len {
let additional_bytes = estimated_max_line_length
.min(self.buffer_len - self.current_pos - extended_chunk.len());
match self
.buffer
.get_text_range_mut(self.current_pos + extended_chunk.len(), additional_bytes)
{
Ok(mut more_data) => {
let start_len = extended_chunk.len();
extended_chunk.append(&mut more_data);
for &byte in extended_chunk[start_len..].iter() {
line_len += 1;
if byte == b'\n' {
found_newline = true;
break;
}
}
}
Err(e) => {
tracing::error!("LineIterator: Failed to extend chunk: {}", e);
break;
}
}
}
let line_bytes = &extended_chunk[..line_len];
self.current_pos += line_len;
let line_string = String::from_utf8_lossy(line_bytes).into_owned();
return Some((line_start, line_string));
}
let line_bytes = &chunk[..line_len];
self.current_pos += line_len;
let line_string = String::from_utf8_lossy(line_bytes).into_owned();
Some((line_start, line_string))
}
pub fn prev(&mut self) -> Option<(usize, String)> {
if self.current_pos == 0 {
return None;
}
if self.current_pos == 0 {
return None;
}
let scan_distance = self.estimated_line_length * 3;
let scan_start = self.current_pos.saturating_sub(scan_distance);
let scan_len = self.current_pos - scan_start;
let chunk = match self.buffer.get_text_range_mut(scan_start, scan_len) {
Ok(data) => data,
Err(e) => {
tracing::error!(
"LineIterator::prev(): Failed to load chunk at {}: {}",
scan_start,
e
);
return None;
}
};
let mut prev_line_end = None;
for i in (0..chunk.len()).rev() {
if chunk[i] == b'\n' {
prev_line_end = Some(scan_start + i);
break;
}
}
let prev_line_end = prev_line_end?;
let prev_line_start = if prev_line_end == 0 {
0
} else {
Self::find_line_start_backward(self.buffer, prev_line_end, scan_distance)
};
let prev_line_len = prev_line_end - prev_line_start + 1; let line_bytes = match self
.buffer
.get_text_range_mut(prev_line_start, prev_line_len)
{
Ok(data) => data,
Err(e) => {
tracing::error!(
"LineIterator::prev(): Failed to load line at {}: {}",
prev_line_start,
e
);
return None;
}
};
let line_string = String::from_utf8_lossy(&line_bytes).into_owned();
self.current_pos = prev_line_start;
Some((prev_line_start, line_string))
}
pub fn current_position(&self) -> usize {
self.current_pos
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_line_iterator_new_at_line_start() {
let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld\nTest".to_vec());
let iter = buffer.line_iterator(0, 80);
assert_eq!(iter.current_position(), 0, "Should be at start of line 0");
let iter = buffer.line_iterator(6, 80);
assert_eq!(iter.current_position(), 6, "Should be at start of line 1");
let iter = buffer.line_iterator(12, 80);
assert_eq!(iter.current_position(), 12, "Should be at start of line 2");
}
#[test]
fn test_line_iterator_new_in_middle_of_line() {
let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld\nTest".to_vec());
let iter = buffer.line_iterator(3, 80);
assert_eq!(iter.current_position(), 0, "Should find start of line 0");
let iter = buffer.line_iterator(9, 80);
assert_eq!(iter.current_position(), 6, "Should find start of line 1");
let iter = buffer.line_iterator(14, 80);
assert_eq!(iter.current_position(), 12, "Should find start of line 2");
}
#[test]
fn test_line_iterator_next() {
let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld\nTest".to_vec());
let mut iter = buffer.line_iterator(0, 80);
let (pos, content) = iter.next().expect("Should have first line");
assert_eq!(pos, 0);
assert_eq!(content, "Hello\n");
let (pos, content) = iter.next().expect("Should have second line");
assert_eq!(pos, 6);
assert_eq!(content, "World\n");
let (pos, content) = iter.next().expect("Should have third line");
assert_eq!(pos, 12);
assert_eq!(content, "Test");
assert!(iter.next().is_none());
}
#[test]
fn test_line_iterator_from_middle_position() {
let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld\nTest".to_vec());
let mut iter = buffer.line_iterator(9, 80);
assert_eq!(
iter.current_position(),
6,
"Should be at start of line containing position 9"
);
let (pos, content) = iter.next().expect("Should have current line");
assert_eq!(pos, 6);
assert_eq!(content, "World\n");
let (pos, content) = iter.next().expect("Should have next line");
assert_eq!(pos, 12);
assert_eq!(content, "Test");
}
#[test]
fn test_line_iterator_offset_to_position_consistency() {
let mut buffer = TextBuffer::from_bytes(b"Hello\nWorld".to_vec());
let expected = vec![
(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), ];
for (offset, expected_line, expected_col) in expected {
let pos = buffer
.offset_to_position(offset)
.expect(&format!("Should have position for offset {}", offset));
assert_eq!(pos.line, expected_line, "Wrong line for offset {}", offset);
assert_eq!(
pos.column, expected_col,
"Wrong column for offset {}",
offset
);
let iter = buffer.line_iterator(offset, 80);
let expected_line_start = if expected_line == 0 { 0 } else { 6 };
assert_eq!(
iter.current_position(),
expected_line_start,
"LineIterator at offset {} should be at line start {}",
offset,
expected_line_start
);
}
}
#[test]
fn test_line_iterator_prev() {
let mut buffer = TextBuffer::from_bytes(b"Line1\nLine2\nLine3".to_vec());
let mut iter = buffer.line_iterator(12, 80);
let (pos, content) = iter.prev().expect("Should have previous line");
assert_eq!(pos, 6);
assert_eq!(content, "Line2\n");
let (pos, content) = iter.prev().expect("Should have previous line");
assert_eq!(pos, 0);
assert_eq!(content, "Line1\n");
assert!(iter.prev().is_none());
}
#[test]
fn test_line_iterator_single_line() {
let mut buffer = TextBuffer::from_bytes(b"Only one line".to_vec());
let mut iter = buffer.line_iterator(0, 80);
let (pos, content) = iter.next().expect("Should have the line");
assert_eq!(pos, 0);
assert_eq!(content, "Only one line");
assert!(iter.next().is_none());
assert!(iter.prev().is_none());
}
#[test]
fn test_line_iterator_empty_lines() {
let mut buffer = TextBuffer::from_bytes(b"Line1\n\nLine3".to_vec());
let mut iter = buffer.line_iterator(0, 80);
let (pos, content) = iter.next().expect("First line");
assert_eq!(pos, 0);
assert_eq!(content, "Line1\n");
let (pos, content) = iter.next().expect("Empty line");
assert_eq!(pos, 6);
assert_eq!(content, "\n");
let (pos, content) = iter.next().expect("Third line");
assert_eq!(pos, 7);
assert_eq!(content, "Line3");
}
#[test]
fn test_line_iterator_long_line_exceeds_estimate() {
let long_line = "x".repeat(200);
let content = format!("{}\n", long_line);
let mut buffer = TextBuffer::from_bytes(content.as_bytes().to_vec());
let estimated_line_length = 50;
let cursor_at_end = 200;
let iter = buffer.line_iterator(cursor_at_end, estimated_line_length);
assert_eq!(
iter.current_position(),
0,
"LineIterator should find actual line start (0), not estimation boundary ({})",
cursor_at_end - estimated_line_length
);
let cursor_in_middle = 100;
let iter = buffer.line_iterator(cursor_in_middle, estimated_line_length);
assert_eq!(
iter.current_position(),
0,
"LineIterator should find line start regardless of cursor position"
);
}
#[test]
fn test_line_iterator_mixed_line_lengths() {
let long_line = "L".repeat(300);
let content = format!("Short1\n{}\nShort2\n", long_line);
let mut buffer = TextBuffer::from_bytes(content.as_bytes().to_vec());
let estimated_line_length = 50;
let cursor_pos = 307;
let iter = buffer.line_iterator(cursor_pos, estimated_line_length);
assert_eq!(
iter.current_position(),
7,
"Should find start of long line at position 7, not estimation boundary"
);
}
}