diff --git a/crates/core/src/io/progressive_reader.rs b/crates/core/src/io/progressive_reader.rs index 5e62482..62e8ee9 100644 --- a/crates/core/src/io/progressive_reader.rs +++ b/crates/core/src/io/progressive_reader.rs @@ -389,6 +389,11 @@ pub fn spawn_visual_height_rebuild( /// Maximum bytes to scan during initial open for the Sampling state. const INITIAL_SCAN_BYTES: usize = 64 * 1024; +/// Maximum number of additional lines to scan beyond what's already cached +/// in a single `get_line()` call. Prevents O(N) blocking when the user +/// jumps far ahead (e.g. `G` to end-of-file) during the Loading state. +const SCAN_AHEAD_LIMIT: usize = 10_000; + pub struct ProgressiveFileReader { path: PathBuf, pub state: ReaderState, @@ -512,32 +517,23 @@ impl ProgressiveFileReader { let mut newlines = scanned_newlines.borrow_mut(); let mut up_to = scanned_up_to.borrow_mut(); - // We need `idx + 1` newlines to have `idx` lines available. - // Line i starts after the i-th newline (or at byte 0 for line 0) - // and ends at the (i+1)-th newline (or end of file). - // So to return line `idx`, we need at least `idx + 1` newline positions - // (the idx-th newline marks end of line idx-1/start of line idx, - // and the (idx+1)-th newline marks end of line idx). - // Actually: line 0 starts at byte 0, ends at newline[0]. - // line 1 starts at newline[0]+1, ends at newline[1]. - // line i starts at newline[i-1]+1, ends at newline[i]. - // So to serve line idx, we need newline positions up to index idx. + let scan_limit = newlines.len() + SCAN_AHEAD_LIMIT; - // Extend scan if needed - while newlines.len() <= idx && *up_to < mmap_data.len() { + // Extend scan if needed, but stop at scan_limit to avoid O(N) blocking + while newlines.len() <= idx + && newlines.len() < scan_limit + && *up_to < mmap_data.len() + { let remaining = &mmap_data[*up_to..]; if let Some(rel_pos) = memchr::memchr(b'\n', remaining) { newlines.push(*up_to + rel_pos); *up_to += rel_pos + 1; } else { - // No more newlines; rest of file is the last line *up_to = mmap_data.len(); break; } } - // Check if line idx is beyond what's available - // If idx == newlines.len(), it's the last line (after last newline, no trailing \n) if idx > newlines.len() { return None; }