fix(io): update visual height of last line on append without trailing newline (closes #11)
When a file does not end with a newline, appending content extends the last logical line's text and thus its visual height. The incremental extend path in handle_file_appended only computed heights for newly created logical lines, missing the old last line whose content changed. Add VisualHeightIndex::replace_last_line_height() — an O(1) method that rewrites the final prefix sum entry and total. Called before extend_from_heights so the correct line is targeted. Changes: - progressive_reader.rs: add replace_last_line_height, pub with_params, 7 VHI unit tests - app.rs: save old_reader_line_count before update, recompute last old line height in extend path, 2 integration regression tests
This commit is contained in:
@@ -102,7 +102,7 @@ impl VisualHeightIndex {
|
||||
}
|
||||
}
|
||||
|
||||
fn with_params(mut self, json_format: bool, terminal_width: usize) -> Self {
|
||||
pub fn with_params(mut self, json_format: bool, terminal_width: usize) -> Self {
|
||||
self.json_format = json_format;
|
||||
self.terminal_width = terminal_width;
|
||||
self
|
||||
@@ -159,6 +159,31 @@ impl VisualHeightIndex {
|
||||
self.total_visual_rows += h as u64;
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the visual height of the last logical line. O(1).
|
||||
///
|
||||
/// Must be called **before** `extend_from_heights` so that the last line
|
||||
/// index still refers to the pre-extension line.
|
||||
pub fn replace_last_line_height(&mut self, new_height: usize) {
|
||||
let n = self.prefix_sums.len();
|
||||
if n < 2 {
|
||||
return;
|
||||
}
|
||||
let last_line = n - 2;
|
||||
let old_height = self.prefix_sums[last_line + 1] - self.prefix_sums[last_line];
|
||||
let new_height = new_height as u64;
|
||||
if new_height == old_height {
|
||||
return;
|
||||
}
|
||||
let delta = new_height.abs_diff(old_height);
|
||||
if new_height > old_height {
|
||||
self.prefix_sums[last_line + 1] += delta;
|
||||
self.total_visual_rows += delta;
|
||||
} else {
|
||||
self.prefix_sums[last_line + 1] -= delta;
|
||||
self.total_visual_rows -= delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── VisualHeightRebuildResult ────────────────────────────────────────────────
|
||||
@@ -1339,6 +1364,85 @@ mod tests {
|
||||
assert_eq!(idx.total_visual_rows(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replace_last_line_height_increase() {
|
||||
let heights = [2, 3];
|
||||
let mut idx = VisualHeightIndex::build(&heights);
|
||||
assert_eq!(idx.total_visual_rows(), 5);
|
||||
assert_eq!(idx.visual_height_of_line(1), 3);
|
||||
|
||||
idx.replace_last_line_height(7);
|
||||
|
||||
assert_eq!(idx.visual_height_of_line(0), 2);
|
||||
assert_eq!(idx.visual_height_of_line(1), 7);
|
||||
assert_eq!(idx.total_visual_rows(), 9);
|
||||
assert_eq!(idx.cursor_to_first_visual_row(0), 0);
|
||||
assert_eq!(idx.cursor_to_first_visual_row(1), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replace_last_line_height_decrease() {
|
||||
let heights = [2, 5];
|
||||
let mut idx = VisualHeightIndex::build(&heights);
|
||||
|
||||
idx.replace_last_line_height(1);
|
||||
|
||||
assert_eq!(idx.visual_height_of_line(0), 2);
|
||||
assert_eq!(idx.visual_height_of_line(1), 1);
|
||||
assert_eq!(idx.total_visual_rows(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replace_last_line_height_same_is_noop() {
|
||||
let heights = [2, 3];
|
||||
let mut idx = VisualHeightIndex::build(&heights);
|
||||
let total_before = idx.total_visual_rows();
|
||||
|
||||
idx.replace_last_line_height(3);
|
||||
|
||||
assert_eq!(idx.total_visual_rows(), total_before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replace_last_line_height_then_extend() {
|
||||
let heights = [2, 1];
|
||||
let mut idx = VisualHeightIndex::build(&heights);
|
||||
assert_eq!(idx.total_visual_rows(), 3);
|
||||
|
||||
idx.replace_last_line_height(4);
|
||||
idx.extend_from_heights(&[3]);
|
||||
|
||||
assert_eq!(idx.line_count(), 3);
|
||||
assert_eq!(idx.visual_height_of_line(0), 2);
|
||||
assert_eq!(idx.visual_height_of_line(1), 4);
|
||||
assert_eq!(idx.visual_height_of_line(2), 3);
|
||||
assert_eq!(idx.total_visual_rows(), 9);
|
||||
assert_eq!(idx.cursor_to_first_visual_row(0), 0);
|
||||
assert_eq!(idx.cursor_to_first_visual_row(1), 2);
|
||||
assert_eq!(idx.cursor_to_first_visual_row(2), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replace_last_line_height_single_line() {
|
||||
let heights = [5];
|
||||
let mut idx = VisualHeightIndex::build(&heights);
|
||||
|
||||
idx.replace_last_line_height(2);
|
||||
|
||||
assert_eq!(idx.visual_height_of_line(0), 2);
|
||||
assert_eq!(idx.total_visual_rows(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replace_last_line_height_empty_index() {
|
||||
let heights: [usize; 0] = [];
|
||||
let mut idx = VisualHeightIndex::build(&heights);
|
||||
|
||||
idx.replace_last_line_height(5);
|
||||
|
||||
assert_eq!(idx.total_visual_rows(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spawn_indexer_file_truncated_during_scan() {
|
||||
let mut content = Vec::new();
|
||||
|
||||
Reference in New Issue
Block a user