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:
@@ -865,6 +865,7 @@ impl App {
|
||||
let width = self.get_content_width();
|
||||
match &mut self.loading_state {
|
||||
AppLoadingState::Ready { reader } => {
|
||||
let old_reader_line_count = reader.line_count();
|
||||
let status = reader.update_for_append();
|
||||
match status {
|
||||
Ok(
|
||||
@@ -883,13 +884,24 @@ impl App {
|
||||
};
|
||||
let new_line_count = reader.line_count();
|
||||
|
||||
if can_extend && new_line_count > old_line_count {
|
||||
if can_extend && old_line_count == old_reader_line_count {
|
||||
if let log_viewer_core::io::progressive_reader::ReaderState::Ready {
|
||||
visual_height_index: Some(index),
|
||||
reader: fr,
|
||||
} = &mut reader.state
|
||||
{
|
||||
let mut new_heights = Vec::with_capacity(new_line_count - old_line_count);
|
||||
if old_line_count > 0 {
|
||||
let last_old_line_text =
|
||||
fr.get_line(old_line_count - 1).unwrap_or("");
|
||||
let new_h = compute_line_visual_height(
|
||||
last_old_line_text,
|
||||
width,
|
||||
self.json_format,
|
||||
);
|
||||
index.replace_last_line_height(new_h);
|
||||
}
|
||||
let mut new_heights =
|
||||
Vec::with_capacity(new_line_count.saturating_sub(old_line_count));
|
||||
for i in old_line_count..new_line_count {
|
||||
let line_text = fr.get_line(i).unwrap_or("");
|
||||
new_heights.push(compute_line_visual_height(
|
||||
@@ -2473,7 +2485,9 @@ plain text line
|
||||
}
|
||||
|
||||
fn install_vhi(app: &mut App, heights: &[usize]) {
|
||||
let vhi = VisualHeightIndex::build(heights);
|
||||
let width = app.get_content_width();
|
||||
let json_format = app.json_format;
|
||||
let vhi = VisualHeightIndex::build(heights).with_params(json_format, width);
|
||||
if let AppLoadingState::Ready { reader } = &mut app.loading_state {
|
||||
if let log_viewer_core::io::progressive_reader::ReaderState::Ready {
|
||||
visual_height_index,
|
||||
@@ -2800,4 +2814,88 @@ plain text line
|
||||
cleanup(&path);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_append_no_trailing_newline_updates_last_line_height() {
|
||||
let path = make_temp_file("abc");
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
let mut app = App::new();
|
||||
load_file_ready(&mut app, &path);
|
||||
assert_eq!(app.total_lines(), 1);
|
||||
|
||||
app.content_width = 5;
|
||||
install_vhi(&mut app, &[1usize]);
|
||||
|
||||
{
|
||||
let vhi = app.get_visual_height_index().unwrap();
|
||||
assert_eq!(vhi.visual_height_of_line(0), 1);
|
||||
assert_eq!(vhi.total_visual_rows(), 1);
|
||||
}
|
||||
|
||||
// Append content that extends line 0 (no trailing newline before)
|
||||
// "abc" + "defgh\n" = "abcdefgh\n" → 8 chars in width 5 → wraps to 2 visual rows
|
||||
// old_total=1, old_had_trailing=false → starts_new_line=false
|
||||
// new_has_trailing=true, new_newlines=1 → added = 1-1 = 0
|
||||
// Still 1 logical line, but line 0 text changed from "abc" to "abcdefgh"
|
||||
{
|
||||
use std::io::Write;
|
||||
let mut f = std::fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.unwrap();
|
||||
f.write_all(b"defgh\n").unwrap();
|
||||
}
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
app.poll_file_watcher();
|
||||
|
||||
assert_eq!(app.total_lines(), 1,
|
||||
"\"abcdefgh\\n\" has trailing newline → 1 logical line");
|
||||
|
||||
let vhi = app.get_visual_height_index().expect("VHI should still exist after append");
|
||||
assert_eq!(vhi.visual_height_of_line(0), 2,
|
||||
"line 0 height should be updated from 1 to 2 after extending 'abcdefgh' in width 5");
|
||||
assert_eq!(vhi.total_visual_rows(), 2);
|
||||
assert_eq!(vhi.cursor_to_first_visual_row(0), 0);
|
||||
cleanup(&path);
|
||||
});
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_append_no_trailing_newline_no_new_lines_only_height_change() {
|
||||
let path = make_temp_file("abc");
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
let mut app = App::new();
|
||||
load_file_ready(&mut app, &path);
|
||||
assert_eq!(app.total_lines(), 1);
|
||||
|
||||
app.content_width = 5;
|
||||
install_vhi(&mut app, &[1usize]);
|
||||
|
||||
// Append without adding any new line — just extends line 0
|
||||
// "abc" + "def" = "abcdef" → 6 chars in width 5 → wraps to 2 visual rows, still 1 logical line
|
||||
{
|
||||
use std::io::Write;
|
||||
let mut f = std::fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.unwrap();
|
||||
f.write_all(b"def").unwrap();
|
||||
}
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
app.poll_file_watcher();
|
||||
|
||||
assert_eq!(app.total_lines(), 1,
|
||||
"no new logical line should be added");
|
||||
|
||||
let vhi = app.get_visual_height_index().expect("VHI should still exist");
|
||||
assert_eq!(vhi.visual_height_of_line(0), 2,
|
||||
"line 0 height should update even when no new lines added");
|
||||
assert_eq!(vhi.total_visual_rows(), 2);
|
||||
cleanup(&path);
|
||||
});
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user