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:
+267
-18
@@ -1728,18 +1728,30 @@ fn conflicted_catalog_preserves_symlinks_and_defers_unselected_contents() {
|
||||
let fixture = Fixture::new();
|
||||
let root = fixture.dir("lazy-conflict");
|
||||
init_jj(&root, false);
|
||||
fs::write(root.join("conflict"), "base\n").unwrap();
|
||||
jj(
|
||||
&root,
|
||||
&[
|
||||
"config",
|
||||
"set",
|
||||
"--repo",
|
||||
"snapshot.max-new-file-size",
|
||||
"100MiB",
|
||||
],
|
||||
);
|
||||
let tail_line = "x".repeat(1023);
|
||||
let tail = format!("{tail_line}\n").repeat(17 * 1024 + 1);
|
||||
fs::write(root.join("conflict"), format!("base\n{tail}")).unwrap();
|
||||
fs::write(root.join("unselected"), "old\n").unwrap();
|
||||
symlink("unselected", root.join("unchanged-link")).unwrap();
|
||||
jj(&root, &["describe", "-m", "base"]);
|
||||
let base = jj(&root, &["log", "--no-graph", "-r", "@", "-T", "commit_id"]);
|
||||
jj(&root, &["new", &base]);
|
||||
fs::write(root.join("conflict"), "left\n").unwrap();
|
||||
fs::write(root.join("conflict"), format!("left\n{tail}")).unwrap();
|
||||
fs::write(root.join("unselected"), vec![b'x'; 17 * 1024 * 1024]).unwrap();
|
||||
jj(&root, &["describe", "-m", "left"]);
|
||||
let left = jj(&root, &["log", "--no-graph", "-r", "@", "-T", "commit_id"]);
|
||||
jj(&root, &["new", &base]);
|
||||
fs::write(root.join("conflict"), "right\n").unwrap();
|
||||
fs::write(root.join("conflict"), format!("right\n{tail}")).unwrap();
|
||||
jj(&root, &["describe", "-m", "right"]);
|
||||
let right = jj(&root, &["log", "--no-graph", "-r", "@", "-T", "commit_id"]);
|
||||
jj(&root, &["new", &left, &right, "-m", "conflicted merge"]);
|
||||
@@ -1764,7 +1776,9 @@ fn conflicted_catalog_preserves_symlinks_and_defers_unselected_contents() {
|
||||
[Path::new("conflict"), Path::new("unselected")]
|
||||
);
|
||||
let conflict = repository.load_file(1, 0).unwrap();
|
||||
assert_eq!(conflict.before, ["base"]);
|
||||
assert_eq!(conflict.before[0], "base");
|
||||
assert_eq!(conflict.before.len(), 17 * 1024 + 2);
|
||||
assert!(conflict.before[1..].iter().all(|line| line == &tail_line));
|
||||
let projection = jj(
|
||||
&root,
|
||||
&[
|
||||
@@ -1779,9 +1793,13 @@ fn conflicted_catalog_preserves_symlinks_and_defers_unselected_contents() {
|
||||
"conflict",
|
||||
],
|
||||
);
|
||||
assert!(projection.len() > 16 * 1024 * 1024);
|
||||
assert_eq!(conflict.after, projection.lines().collect::<Vec<_>>());
|
||||
assert!(!conflict.hunks.is_empty());
|
||||
assert!(repository.load_file(1, 1).is_err());
|
||||
let large = repository.load_file(1, 1).unwrap();
|
||||
assert_eq!(large.after.len(), 1);
|
||||
assert_eq!(large.after[0].len(), 17 * 1024 * 1024);
|
||||
assert!(large.after[0].bytes().all(|byte| byte == b'x'));
|
||||
assert_eq!(disk_state(&root), state);
|
||||
|
||||
jj(&root, &["new", &merged]);
|
||||
@@ -1945,6 +1963,220 @@ fn immutable_catalog_skips_unchanged_blobs_and_prepares_only_requested_details()
|
||||
assert!(repository.load_file(100, index).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comparison_size_and_line_ceilings_are_lazy_and_explicit() {
|
||||
let fixture = Fixture::new();
|
||||
let root = fixture.dir("ceilings");
|
||||
init_git(&root);
|
||||
git(&root, &["commit", "--allow-empty", "-m", "before"]);
|
||||
let before = revision(&git(&root, &["rev-parse", "HEAD"]), RepoKind::Git);
|
||||
fs::File::create(root.join("too-many-bytes"))
|
||||
.unwrap()
|
||||
.set_len(64 * 1024 * 1024 + 1)
|
||||
.unwrap();
|
||||
fs::write(root.join("too-many-lines"), vec![b'\n'; 4_000_001]).unwrap();
|
||||
fs::write(root.join("selected"), "still available\n").unwrap();
|
||||
git(&root, &["add", "."]);
|
||||
git(&root, &["commit", "-m", "after"]);
|
||||
let after = revision(&git(&root, &["rev-parse", "HEAD"]), RepoKind::Git);
|
||||
let mut repository = open(&root);
|
||||
let catalog = repository
|
||||
.compare(1, &before, &after, ComparisonMode::Snapshots, &[])
|
||||
.unwrap();
|
||||
assert_eq!(catalog.files.len(), 3);
|
||||
assert!(catalog.files.iter().all(|file| !file.loaded));
|
||||
for (index, entry) in catalog.files.iter().enumerate() {
|
||||
match entry.path.to_str().unwrap() {
|
||||
"selected" => assert_eq!(
|
||||
repository.load_file(1, index).unwrap().after,
|
||||
["still available"]
|
||||
),
|
||||
"too-many-bytes" => assert!(
|
||||
repository
|
||||
.load_file(1, index)
|
||||
.unwrap_err()
|
||||
.contains("64 MiB per-side comparison file limit")
|
||||
),
|
||||
"too-many-lines" => assert!(
|
||||
repository
|
||||
.load_file(1, index)
|
||||
.unwrap_err()
|
||||
.contains("4000000-line per-side comparison limit")
|
||||
),
|
||||
path => panic!("Unexpected file {path}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_git_and_jj_comparisons_load_exact_snapshots_and_frozen_working_overlays() {
|
||||
let fixture = Fixture::new();
|
||||
// Both modified sides exceed 52.3 MB; the Working pair exceeds the old
|
||||
// aggregate 64 MiB budget as well as its old 16 MiB per-file limit.
|
||||
let body_line = "x".repeat(1023);
|
||||
let mut contents = format!(
|
||||
"before\n{}",
|
||||
format!("{body_line}\n").repeat(52_300_000 / 1024 + 1)
|
||||
);
|
||||
let line_count = contents.lines().count();
|
||||
for kind in [RepoKind::Git, RepoKind::Jujutsu] {
|
||||
let root = fixture
|
||||
.dir(if kind == RepoKind::Git { "git" } else { "jj" })
|
||||
.canonicalize()
|
||||
.unwrap();
|
||||
match kind {
|
||||
RepoKind::Git => init_git(&root),
|
||||
RepoKind::Jujutsu => {
|
||||
init_jj(&root, false);
|
||||
jj(
|
||||
&root,
|
||||
&[
|
||||
"config",
|
||||
"set",
|
||||
"--repo",
|
||||
"snapshot.max-new-file-size",
|
||||
"100MiB",
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
let set_header = |contents: &mut String, header: &str| {
|
||||
let end = contents.find('\n').unwrap();
|
||||
contents.replace_range(..end, header);
|
||||
};
|
||||
set_header(&mut contents, "before");
|
||||
fs::write(root.join("modified"), &contents).unwrap();
|
||||
set_header(&mut contents, "removed");
|
||||
fs::write(root.join("removed"), &contents).unwrap();
|
||||
let before = match kind {
|
||||
RepoKind::Git => {
|
||||
git(&root, &["add", "."]);
|
||||
git(&root, &["commit", "-m", "large before"]);
|
||||
git(&root, &["rev-parse", "HEAD"])
|
||||
}
|
||||
RepoKind::Jujutsu => {
|
||||
jj(&root, &["describe", "-m", "large before"]);
|
||||
let id = jj(&root, &["log", "--no-graph", "-r", "@", "-T", "commit_id"]);
|
||||
jj(&root, &["new", &id]);
|
||||
id
|
||||
}
|
||||
};
|
||||
set_header(&mut contents, "after");
|
||||
fs::write(root.join("modified"), &contents).unwrap();
|
||||
fs::remove_file(root.join("removed")).unwrap();
|
||||
set_header(&mut contents, "added");
|
||||
fs::write(root.join("added"), &contents).unwrap();
|
||||
let after = match kind {
|
||||
RepoKind::Git => {
|
||||
git(&root, &["add", "."]);
|
||||
git(&root, &["commit", "-m", "large after"]);
|
||||
git(&root, &["rev-parse", "HEAD"])
|
||||
}
|
||||
RepoKind::Jujutsu => {
|
||||
jj(&root, &["describe", "-m", "large after"]);
|
||||
jj(&root, &["log", "--no-graph", "-r", "@", "-T", "commit_id"])
|
||||
}
|
||||
};
|
||||
let assert_contents = |lines: &[String], header: &str| {
|
||||
assert_eq!(lines.len(), line_count);
|
||||
assert_eq!(lines[0], header);
|
||||
assert!(lines[1..].iter().all(|line| line == &body_line));
|
||||
};
|
||||
let cancelled = Arc::new(AtomicBool::new(false));
|
||||
let mut repository = Repository::open(&root, cancelled.clone()).unwrap();
|
||||
let catalog = repository
|
||||
.compare(
|
||||
1,
|
||||
&revision(&before, kind),
|
||||
&revision(&after, kind),
|
||||
ComparisonMode::Snapshots,
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(catalog.files.len(), 3);
|
||||
assert!(catalog.files.iter().all(|file| !file.loaded
|
||||
&& file.before.is_empty()
|
||||
&& file.after.is_empty()
|
||||
&& file.hunks.is_empty()));
|
||||
for (index, entry) in catalog.files.iter().enumerate() {
|
||||
let file = repository.load_file(1, index).unwrap();
|
||||
assert!(file.loaded);
|
||||
assert_eq!(file.hunks.len(), 1);
|
||||
let hunk = &file.hunks[0];
|
||||
let expected = match entry.path.to_str().unwrap() {
|
||||
"added" => {
|
||||
assert_eq!(file.status, FileStatus::Added);
|
||||
assert!(file.before.is_empty());
|
||||
assert_contents(&file.after, "added");
|
||||
(0, 0, 0, line_count)
|
||||
}
|
||||
"removed" => {
|
||||
assert_eq!(file.status, FileStatus::Removed);
|
||||
assert_contents(&file.before, "removed");
|
||||
assert!(file.after.is_empty());
|
||||
(0, line_count, 0, 0)
|
||||
}
|
||||
"modified" => {
|
||||
assert_eq!(file.status, FileStatus::Modified);
|
||||
assert_contents(&file.before, "before");
|
||||
assert_contents(&file.after, "after");
|
||||
(0, 1, 0, 1)
|
||||
}
|
||||
path => panic!("Unexpected file {path}"),
|
||||
};
|
||||
assert_eq!(
|
||||
(
|
||||
hunk.before_start,
|
||||
hunk.before_len,
|
||||
hunk.after_start,
|
||||
hunk.after_len
|
||||
),
|
||||
expected
|
||||
);
|
||||
assert!(file.before.is_empty() || file.before_eof);
|
||||
assert!(file.after.is_empty() || file.after_eof);
|
||||
}
|
||||
set_header(&mut contents, "overlay");
|
||||
let buffers = [(root.join("modified"), contents)];
|
||||
let working = repository
|
||||
.compare(
|
||||
2,
|
||||
&revision(&after, kind),
|
||||
&working(kind),
|
||||
ComparisonMode::Snapshots,
|
||||
&buffers,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(working.files.len(), 1);
|
||||
assert!(!working.files[0].loaded);
|
||||
assert!(working.editable_paths.contains(Path::new("modified")));
|
||||
fs::write(root.join("modified"), "later disk contents\n").unwrap();
|
||||
cancelled.store(true, Ordering::Release);
|
||||
assert!(
|
||||
repository
|
||||
.load_file(2, 0)
|
||||
.unwrap_err()
|
||||
.contains("cancelled")
|
||||
);
|
||||
cancelled.store(false, Ordering::Release);
|
||||
let file = repository.load_file(2, 0).unwrap();
|
||||
assert_contents(&file.before, "after");
|
||||
assert_contents(&file.after, "overlay");
|
||||
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
|
||||
),
|
||||
(0, 1, 0, 1)
|
||||
);
|
||||
contents = buffers.into_iter().next().unwrap().1;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_file_details_keep_frozen_checkout_and_unsaved_after() {
|
||||
let fixture = Fixture::new();
|
||||
@@ -2112,8 +2344,16 @@ fn working_after_refresh_preserves_frozen_working_before_and_sources_on_failure(
|
||||
assert!(repository.refresh_checkout(10, 11, true, &[]).is_err());
|
||||
cancelled.store(false, Ordering::Release);
|
||||
assert_eq!(repository.load_file(10, 0).unwrap().after, ["frozen after"]);
|
||||
fs::write(root.join("oversized"), vec![b'x'; 17 * 1024 * 1024]).unwrap();
|
||||
assert!(repository.refresh_checkout(10, 11, true, &[]).is_err());
|
||||
fs::File::create(root.join("oversized"))
|
||||
.unwrap()
|
||||
.set_len(64 * 1024 * 1024 + 1)
|
||||
.unwrap();
|
||||
assert!(
|
||||
repository
|
||||
.refresh_checkout(10, 11, true, &[])
|
||||
.unwrap_err()
|
||||
.contains("64 MiB per-side comparison file limit")
|
||||
);
|
||||
let original = repository.load_file(10, 0).unwrap();
|
||||
assert_eq!(original.before, ["frozen before"]);
|
||||
assert_eq!(original.after, ["frozen after"]);
|
||||
@@ -2194,7 +2434,7 @@ fn working_after_refresh_preserves_frozen_working_before_and_sources_on_failure(
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refresh_materialization_failure_restores_prepared_file_sources() {
|
||||
fn refresh_keeps_historical_blob_sources_lazy_even_with_working_files() {
|
||||
let fixture = Fixture::new();
|
||||
let root = fixture.dir("failed-materialization");
|
||||
init_git(&root);
|
||||
@@ -2206,7 +2446,7 @@ fn refresh_materialization_failure_restores_prepared_file_sources() {
|
||||
fs::remove_file(root.join("a-file")).unwrap();
|
||||
fs::remove_file(root.join("z-large")).unwrap();
|
||||
let mut repository = open(&root);
|
||||
let original = repository
|
||||
repository
|
||||
.compare(
|
||||
1,
|
||||
&base,
|
||||
@@ -2215,20 +2455,29 @@ fn refresh_materialization_failure_restores_prepared_file_sources() {
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
let index = original
|
||||
assert_eq!(
|
||||
repository.load_file(1, 0).unwrap().before,
|
||||
["frozen source"]
|
||||
);
|
||||
fs::write(root.join("a-file"), "new checkout\n").unwrap();
|
||||
git(&root, &["add", "."]);
|
||||
git(&root, &["commit", "-m", "descendant"]);
|
||||
let refreshed = repository
|
||||
.refresh_checkout(1, 2, true, &[])
|
||||
.unwrap()
|
||||
.map(checkout_comparison)
|
||||
.unwrap();
|
||||
assert!(refreshed.files.iter().all(|file| !file.loaded));
|
||||
let index = refreshed
|
||||
.files
|
||||
.iter()
|
||||
.position(|file| file.path == Path::new("a-file"))
|
||||
.unwrap();
|
||||
fs::write(root.join("a-file"), "new checkout\n").unwrap();
|
||||
git(&root, &["add", "."]);
|
||||
git(&root, &["commit", "-m", "descendant"]);
|
||||
assert!(repository.refresh_checkout(1, 2, true, &[]).is_err());
|
||||
let file = repository.load_file(1, index).unwrap();
|
||||
let file = repository.load_file(2, index).unwrap();
|
||||
assert_eq!(file.before, ["frozen source"]);
|
||||
assert!(file.after.is_empty());
|
||||
assert_eq!(file.status, FileStatus::Removed);
|
||||
assert!(repository.load_file(2, index).is_err());
|
||||
assert_eq!(file.after, ["new checkout"]);
|
||||
assert_eq!(file.status, FileStatus::Modified);
|
||||
assert!(repository.load_file(1, index).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+256
-3
@@ -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})"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-3
@@ -135,10 +135,21 @@ fn resolver_shares_native_excerpt_search_motions_and_explicit_writable_side() {
|
||||
type_text(editor, "/needle");
|
||||
editor.handle(Input::Enter);
|
||||
assert_eq!(editor.cursor().row, 70);
|
||||
let rows = editor.diff().unwrap().source_rows(Focus::Before).count();
|
||||
let rows = editor
|
||||
.diff()
|
||||
.unwrap()
|
||||
.source_rows_in(Focus::Before, 0, usize::MAX)
|
||||
.count();
|
||||
let cursor = editor.cursor();
|
||||
editor.handle(Input::Char(']'));
|
||||
assert!(editor.diff().unwrap().source_rows(Focus::Before).count() > rows);
|
||||
assert!(
|
||||
editor
|
||||
.diff()
|
||||
.unwrap()
|
||||
.source_rows_in(Focus::Before, 0, usize::MAX)
|
||||
.count()
|
||||
> rows
|
||||
);
|
||||
assert_eq!(editor.cursor(), cursor);
|
||||
type_text(editor, "21g");
|
||||
assert_eq!(editor.cursor().row, 20);
|
||||
@@ -4644,7 +4655,7 @@ fn editable_diff_brackets_expand_caret_excerpt_without_moving_source_anchor() {
|
||||
editor
|
||||
.diff()
|
||||
.unwrap()
|
||||
.source_rows(Focus::After)
|
||||
.source_rows_in(Focus::After, 0, usize::MAX)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(
|
||||
|
||||
@@ -4612,6 +4612,68 @@ fn hiding_diff_explorer_reclaims_workspace_and_restores_its_geometry() {
|
||||
assert_eq!(panels.files.unwrap().0, original[0].0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unavailable_file_documents_show_the_actual_error_in_managed_workspaces() {
|
||||
let mut file = deletion_file();
|
||||
file.loaded = false;
|
||||
let mut diff = diff_comparison(vec![file]);
|
||||
diff.loading = false;
|
||||
diff.error = Some("parser.c exceeds 128 MiB file limit".into());
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 12)).unwrap();
|
||||
let mut panels = WheelPanels {
|
||||
workspace_managed: true,
|
||||
..WheelPanels::default()
|
||||
};
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
panels.draw_documents(frame, &diff, frame.area(), None, None);
|
||||
})
|
||||
.unwrap();
|
||||
let text = screen_text(&terminal);
|
||||
assert!(text.contains("parser.c exceeds 128 MiB file limit"));
|
||||
assert!(!text.contains("select the file to retry"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_generated_additions_render_first_scroll_and_final_rows_without_reparsing() {
|
||||
let after = (0..25_000)
|
||||
.map(|row| format!("state_row_{row:05} = 1234567890123456789012345678901234567890;"))
|
||||
.collect::<Vec<_>>();
|
||||
let hunks = crate::diff::line_hunks(&[], false, &after, true);
|
||||
let mut diff = diff_comparison(vec![ChangedFile {
|
||||
path: "parser.c".into(),
|
||||
old_path: None,
|
||||
status: FileStatus::Added,
|
||||
loaded: true,
|
||||
before: Vec::new(),
|
||||
after,
|
||||
before_eof: false,
|
||||
after_eof: true,
|
||||
hunks,
|
||||
notice: None,
|
||||
}]);
|
||||
diff.files_visible = false;
|
||||
diff.focus = Focus::After;
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 12)).unwrap();
|
||||
let mut panels = WheelPanels::default();
|
||||
let mut counts = None;
|
||||
for row in [0, 3, 12_500, 24_999] {
|
||||
diff.reveal_source_row(Focus::After, row);
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
assert!(screen_text(&terminal).contains(&format!("state_row_{row:05}")));
|
||||
let syntax = panels.diff_syntax.as_ref().unwrap();
|
||||
assert_eq!(
|
||||
*counts.get_or_insert(syntax.after.parse_counts()),
|
||||
syntax.after.parse_counts()
|
||||
);
|
||||
assert!(syntax.after.error().is_some());
|
||||
assert_eq!(
|
||||
diff.comparison.as_ref().unwrap().files[0].after.len(),
|
||||
25_000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_notices_empty_comparisons_and_tiny_layouts_remain_renderable() {
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 12)).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user