fix: support large files in comparisons

Replace the 16 MiB comparison rejection with independent bounded per-side loading and frozen Working blobs. Bound diff computation, defer large inline highlighting, index source-row navigation, and expose actual load failures. Verified the real 52.3 MB parser through repository/session APIs and terminal rendering with EOF/start navigation.
This commit is contained in:
2026-10-17 00:03:43 +02:00
parent adbd2c3f81
commit b9f29d258c
11 changed files with 986 additions and 199 deletions
+256 -3
View File
@@ -2240,7 +2240,7 @@ fn unified_groups_changed_runs_and_roundtrips_side_specific_source_rows() {
);
assert!(state.after_editable());
for focus in [Focus::Before, Focus::After] {
for source in state.source_rows(focus) {
for source in state.source_rows_in(focus, 0, usize::MAX) {
let display = state.source_display_row(focus, source).unwrap();
assert_eq!(state.display_source_row(focus, display), Some(source));
}
@@ -2271,10 +2271,15 @@ fn unified_live_reflow_preserves_removed_top_and_expanded_source_excerpts() {
assert_eq!(state.display_source_row(Focus::Before, state.top), Some(20));
assert!(state.source_display_row(Focus::After, 17).is_some());
state.reveal_source_row(Focus::After, 35);
let visible: Vec<_> = state.source_rows(Focus::After).collect();
let visible: Vec<_> = state.source_rows_in(Focus::After, 0, usize::MAX).collect();
state.set_view_mode(ViewMode::Split);
state.set_view_mode(ViewMode::Unified);
assert_eq!(state.source_rows(Focus::After).collect::<Vec<_>>(), visible);
assert_eq!(
state
.source_rows_in(Focus::After, 0, usize::MAX)
.collect::<Vec<_>>(),
visible
);
assert_eq!(state.display_source_row(Focus::After, state.top), Some(35));
}
@@ -2750,3 +2755,251 @@ fn review_anchors_match_frozen_context_and_exact_live_eof_without_allocating() {
state.comparison.as_mut().unwrap().revisions[0] = "other".into();
assert!(!state.review_anchor_current(&before));
}
#[test]
fn million_row_additions_and_removals_keep_complete_sources_and_compact_navigation() {
let lines = vec!["static const unsigned int parser_state = 1234567890;".to_owned(); 1_000_000];
let mut state = live_state(Vec::new(), lines, true);
state.comparison.as_mut().unwrap().files[0].status = FileStatus::Added;
state.align_file();
assert_eq!(state.rows.len(), 1_000_000);
assert!(state.rows.iter().all(|row| row.changes.is_empty()));
assert_eq!(state.source_ranges[1], [(0..1_000_000, 0)]);
assert_eq!(state.after_display_row(999_999), Some(999_999));
assert_eq!(
state.clamp_source_row(Focus::After, usize::MAX),
Some(999_999)
);
let mut visible = state.source_rows_in(Focus::After, 999_990, 999_999);
assert_eq!(visible.next(), Some(999_990));
assert_eq!(visible.next_back(), Some(999_998));
drop(visible);
let ColumnRow::Text(last) = state
.column_view(Focus::After)
.unwrap()
.row(999_999)
.unwrap()
else {
panic!("the final added source line must remain available");
};
assert_eq!(
last.text,
Some("static const unsigned int parser_state = 1234567890;")
);
assert_eq!(last.source_row, 999_999);
let file = &mut state.comparison.as_mut().unwrap().files[0];
std::mem::swap(&mut file.before, &mut file.after);
file.status = FileStatus::Removed;
file.hunks = line_hunks(&file.before, true, &file.after, false);
assert_eq!(
(file.hunks[0].before_len, file.hunks[0].after_len),
(1_000_000, 0)
);
state.align_file();
assert_eq!(state.source_ranges[0], [(0..1_000_000, 0)]);
assert_eq!(
state.source_display_row(Focus::Before, 999_999),
Some(999_999)
);
assert_eq!(state.source_display_row(Focus::After, 0), None);
}
#[test]
fn large_localized_modifications_keep_exact_hunks_and_only_diff_visible_characters() {
let before = vec!["unchanged generated parser body".to_owned(); 25_000];
let mut after = before.clone();
after[20_000] = "unchanged generated parser body!".into();
let state = live_state(before, after, true);
let file = &state.comparison.as_ref().unwrap().files[0];
assert_eq!(file.hunks.len(), 1);
let hunk = &file.hunks[0];
assert_eq!(
(
hunk.before_start,
hunk.before_len,
hunk.after_start,
hunk.after_len
),
(20_000, 1, 20_000, 1)
);
assert!(state.rows.iter().all(|row| row.changes.is_empty()));
let display = state.after_display_row(20_000).unwrap();
let ColumnRow::Text(line) = state
.column_view(Focus::After)
.unwrap()
.row(display)
.unwrap()
else {
panic!("the modified source row must remain visible");
};
assert_eq!(line.changes().cloned().collect::<Vec<_>>(), [31..32]);
}
#[test]
fn pathological_modifications_preserve_every_line_when_diff_budget_is_exhausted() {
let before = (0..25_000)
.map(|row| format!("old {row}"))
.collect::<Vec<_>>();
let after = (0..25_000)
.map(|row| format!("new {row}"))
.collect::<Vec<_>>();
let state = live_state(before, after, false);
let file = &state.comparison.as_ref().unwrap().files[0];
let (mut old, mut new) = (0, 0);
for hunk in &file.hunks {
assert_eq!(
&file.before[old..hunk.before_start],
&file.after[new..hunk.after_start]
);
old = hunk.before_start + hunk.before_len;
new = hunk.after_start + hunk.after_len;
}
assert_eq!(&file.before[old..], &file.after[new..]);
assert_eq!(state.rows.len(), 25_000);
assert!(
state
.rows
.iter()
.all(|row| row.kind == RowKind::Changed && row.changes.is_empty())
);
assert_eq!(state.after_display_row(24_999), Some(24_999));
let giant = "界".repeat(64 * 1024);
assert_eq!(
character_changes(&giant, "replacement"),
[[0..usize::MAX, 0..usize::MAX]]
);
}
#[test]
fn indexed_source_ranges_match_projection_with_folds_gaps_and_unified_runs() {
let before = (0..60).map(|row| format!("row {row}")).collect::<Vec<_>>();
let mut after = before.clone();
after.splice(10..14, ["inserted".into()]);
after.splice(35..35, ["extra 1".into(), "extra 2".into()]);
let mut state = live_state(before, after, false);
state.set_context_options(2, 5);
for mode in [ViewMode::Split, ViewMode::Unified] {
state.set_view_mode(mode);
for focus in [Focus::Before, Focus::After] {
let expected = (0..state.display_rows.len())
.filter_map(|display| {
state
.display_source_row(focus, display)
.map(|row| (row, display))
})
.collect::<Vec<_>>();
for source in 0..65 {
assert_eq!(
state.source_display_row(focus, source),
expected
.iter()
.find(|(row, _)| *row == source)
.map(|(_, display)| *display)
);
assert_eq!(
state.clamp_source_row(focus, source),
expected
.iter()
.map(|(row, _)| *row)
.min_by_key(|row| (row.abs_diff(source), *row))
);
let target = if focus == Focus::Before {
Focus::After
} else {
Focus::Before
};
let aligned = state.rows.iter().position(|row| match focus {
Focus::Before => row.before == Some(source),
_ => row.after == Some(source),
});
let nearest = aligned.and_then(|aligned| {
(0..state.display_rows.len())
.filter_map(|display| {
let DisplayRow::Line(index) = state.display_rows[display] else {
return None;
};
state
.display_source_row(target, display)
.map(|row| (index.abs_diff(aligned), row))
})
.min()
.map(|(_, row)| row)
});
assert_eq!(state.corresponding_row(focus, target, source), nearest);
}
assert_eq!(
state.source_rows_in(focus, 9, 40).collect::<Vec<_>>(),
expected
.iter()
.map(|(row, _)| *row)
.filter(|row| (9..40).contains(row))
.collect::<Vec<_>>()
);
}
}
}
#[test]
fn ordinary_hunks_keep_similar_anchoring_for_repeated_lines_and_eof_changes() {
let sources = (0..=3)
.flat_map(|len| {
(0..1 << len).map(move |bits| {
(0..len)
.map(|bit| {
if bits & (1 << bit) == 0 {
"a".into()
} else {
"b".into()
}
})
.collect::<Vec<String>>()
})
})
.collect::<Vec<_>>();
for before in &sources {
for after in &sources {
for before_eof in [false, true] {
for after_eof in [false, true] {
let tokens = |lines: &[String], eof| {
lines
.iter()
.enumerate()
.map(|(index, line)| (line.clone(), index + 1 < lines.len() || eof))
.collect::<Vec<_>>()
};
let expected = similar::capture_diff_slices(
similar::Algorithm::Myers,
&tokens(before, before_eof),
&tokens(after, after_eof),
)
.into_iter()
.filter_map(|op| {
let (tag, old, new) = op.as_tag_tuple();
(tag != similar::DiffTag::Equal).then_some((
old.start,
old.len(),
new.start,
new.len(),
))
})
.collect::<Vec<_>>();
let actual = line_hunks(before, before_eof, after, after_eof)
.iter()
.map(|hunk| {
(
hunk.before_start,
hunk.before_len,
hunk.after_start,
hunk.after_len,
)
})
.collect::<Vec<_>>();
assert_eq!(
actual, expected,
"{before:?} ({before_eof}) -> {after:?} ({after_eof})"
);
}
}
}
}
}