Fix diff recalculation hang (#28377)
Fixes https://github.com/zed-industries/zed/issues/26039 Release Notes: - Fixed an issue where diffs stopped updating closing and reopening them after staging hunks. - Fixed a bug where staging a hunk while the cursor was in a deleted line would move the cursor erroneously. --------- Co-authored-by: Cole Miller <m@cole-miller.net> Co-authored-by: João Marcos <marcospb19@hotmail.com>
This commit is contained in:
parent
ffdf725f32
commit
294a1b63c0
6 changed files with 544 additions and 441 deletions
|
@ -5,7 +5,8 @@ use crate::{
|
|||
*,
|
||||
};
|
||||
use buffer_diff::{
|
||||
BufferDiffEvent, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind, assert_hunks,
|
||||
BufferDiffEvent, CALCULATE_DIFF_TASK, DiffHunkSecondaryStatus, DiffHunkStatus,
|
||||
DiffHunkStatusKind, assert_hunks,
|
||||
};
|
||||
use fs::FakeFs;
|
||||
use futures::{StreamExt, future};
|
||||
|
@ -30,10 +31,11 @@ use parking_lot::Mutex;
|
|||
use paths::{config_dir, tasks_file};
|
||||
use postage::stream::Stream as _;
|
||||
use pretty_assertions::{assert_eq, assert_matches};
|
||||
use rand::{Rng as _, rngs::StdRng};
|
||||
use serde_json::json;
|
||||
#[cfg(not(windows))]
|
||||
use std::os;
|
||||
use std::{mem, num::NonZeroU32, ops::Range, str::FromStr, sync::OnceLock, task::Poll};
|
||||
use std::{env, mem, num::NonZeroU32, ops::Range, str::FromStr, sync::OnceLock, task::Poll};
|
||||
use task::{ResolvedTask, TaskContext};
|
||||
use unindent::Unindent as _;
|
||||
use util::{
|
||||
|
@ -6605,7 +6607,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
|||
} = event
|
||||
{
|
||||
let changed_range = changed_range.to_point(&snapshot);
|
||||
assert_eq!(changed_range, Point::new(0, 0)..Point::new(5, 0));
|
||||
assert_eq!(changed_range, Point::new(0, 0)..Point::new(4, 0));
|
||||
} else {
|
||||
panic!("Unexpected event {event:?}");
|
||||
}
|
||||
|
@ -6966,49 +6968,55 @@ async fn test_staging_hunks_with_delayed_fs_event(cx: &mut gpui::TestAppContext)
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_staging_lots_of_hunks_fast(cx: &mut gpui::TestAppContext) {
|
||||
async fn test_staging_random_hunks(
|
||||
mut rng: StdRng,
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let operations = env::var("OPERATIONS")
|
||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
.unwrap_or(20);
|
||||
|
||||
// Try to induce races between diff recalculation and index writes.
|
||||
if rng.gen_bool(0.5) {
|
||||
executor.deprioritize(*CALCULATE_DIFF_TASK);
|
||||
}
|
||||
|
||||
use DiffHunkSecondaryStatus::*;
|
||||
init_test(cx);
|
||||
|
||||
let different_lines = (0..500)
|
||||
.step_by(5)
|
||||
.map(|i| format!("diff {}\n", i))
|
||||
.collect::<Vec<String>>();
|
||||
let committed_contents = (0..500).map(|i| format!("{}\n", i)).collect::<String>();
|
||||
let file_contents = (0..500)
|
||||
.map(|i| {
|
||||
if i % 5 == 0 {
|
||||
different_lines[i / 5].clone()
|
||||
} else {
|
||||
format!("{}\n", i)
|
||||
}
|
||||
let committed_text = (0..30).map(|i| format!("line {i}\n")).collect::<String>();
|
||||
let index_text = committed_text.clone();
|
||||
let buffer_text = (0..30)
|
||||
.map(|i| match i % 5 {
|
||||
0 => format!("line {i} (modified)\n"),
|
||||
_ => format!("line {i}\n"),
|
||||
})
|
||||
.collect::<String>();
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
path!("/dir"),
|
||||
json!({
|
||||
".git": {},
|
||||
"file.txt": file_contents.clone()
|
||||
"file.txt": buffer_text.clone()
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.set_head_for_repo(
|
||||
"/dir/.git".as_ref(),
|
||||
&[("file.txt".into(), committed_contents.clone())],
|
||||
path!("/dir/.git").as_ref(),
|
||||
&[("file.txt".into(), committed_text.clone())],
|
||||
);
|
||||
fs.set_index_for_repo(
|
||||
"/dir/.git".as_ref(),
|
||||
&[("file.txt".into(), committed_contents.clone())],
|
||||
path!("/dir/.git").as_ref(),
|
||||
&[("file.txt".into(), index_text.clone())],
|
||||
);
|
||||
let repo = fs.open_repo(path!("/dir/.git").as_ref()).unwrap();
|
||||
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/dir/file.txt", cx)
|
||||
project.open_local_buffer(path!("/dir/file.txt"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -7020,94 +7028,60 @@ async fn test_staging_lots_of_hunks_fast(cx: &mut gpui::TestAppContext) {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut expected_hunks: Vec<(Range<u32>, String, String, DiffHunkStatus)> = (0..500)
|
||||
.step_by(5)
|
||||
.map(|i| {
|
||||
(
|
||||
i as u32..i as u32 + 1,
|
||||
format!("{}\n", i),
|
||||
different_lines[i / 5].clone(),
|
||||
DiffHunkStatus::modified(HasSecondaryHunk),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut hunks =
|
||||
uncommitted_diff.update(cx, |diff, cx| diff.hunks(&snapshot, cx).collect::<Vec<_>>());
|
||||
assert_eq!(hunks.len(), 6);
|
||||
|
||||
// The hunks are initially unstaged
|
||||
uncommitted_diff.read_with(cx, |diff, cx| {
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&expected_hunks,
|
||||
);
|
||||
});
|
||||
for _i in 0..operations {
|
||||
let hunk_ix = rng.gen_range(0..hunks.len());
|
||||
let hunk = &mut hunks[hunk_ix];
|
||||
let row = hunk.range.start.row;
|
||||
|
||||
for (_, _, _, status) in expected_hunks.iter_mut() {
|
||||
*status = DiffHunkStatus::modified(SecondaryHunkRemovalPending);
|
||||
}
|
||||
|
||||
// Stage every hunk with a different call
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
let hunks = diff.hunks(&snapshot, cx).collect::<Vec<_>>();
|
||||
for hunk in hunks {
|
||||
diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
|
||||
if hunk.status().has_secondary_hunk() {
|
||||
log::info!("staging hunk at {row}");
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
diff.stage_or_unstage_hunks(true, &[hunk.clone()], &snapshot, true, cx);
|
||||
});
|
||||
hunk.secondary_status = SecondaryHunkRemovalPending;
|
||||
} else {
|
||||
log::info!("unstaging hunk at {row}");
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
diff.stage_or_unstage_hunks(false, &[hunk.clone()], &snapshot, true, cx);
|
||||
});
|
||||
hunk.secondary_status = SecondaryHunkAdditionPending;
|
||||
}
|
||||
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&expected_hunks,
|
||||
);
|
||||
});
|
||||
|
||||
// If we wait, we'll have no pending hunks
|
||||
cx.run_until_parked();
|
||||
for (_, _, _, status) in expected_hunks.iter_mut() {
|
||||
*status = DiffHunkStatus::modified(NoSecondaryHunk);
|
||||
}
|
||||
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&expected_hunks,
|
||||
);
|
||||
});
|
||||
|
||||
for (_, _, _, status) in expected_hunks.iter_mut() {
|
||||
*status = DiffHunkStatus::modified(SecondaryHunkAdditionPending);
|
||||
}
|
||||
|
||||
// Unstage every hunk with a different call
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
let hunks = diff.hunks(&snapshot, cx).collect::<Vec<_>>();
|
||||
for hunk in hunks {
|
||||
diff.stage_or_unstage_hunks(false, &[hunk], &snapshot, true, cx);
|
||||
for _ in 0..rng.gen_range(0..10) {
|
||||
log::info!("yielding");
|
||||
cx.executor().simulate_random_delay().await;
|
||||
}
|
||||
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&expected_hunks,
|
||||
);
|
||||
});
|
||||
|
||||
// If we wait, we'll have no pending hunks, again
|
||||
cx.run_until_parked();
|
||||
for (_, _, _, status) in expected_hunks.iter_mut() {
|
||||
*status = DiffHunkStatus::modified(HasSecondaryHunk);
|
||||
}
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
for hunk in &mut hunks {
|
||||
if hunk.secondary_status == SecondaryHunkRemovalPending {
|
||||
hunk.secondary_status = NoSecondaryHunk;
|
||||
} else if hunk.secondary_status == SecondaryHunkAdditionPending {
|
||||
hunk.secondary_status = HasSecondaryHunk;
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"index text:\n{}",
|
||||
repo.load_index_text("file.txt".into()).await.unwrap()
|
||||
);
|
||||
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&expected_hunks,
|
||||
);
|
||||
let expected_hunks = hunks
|
||||
.iter()
|
||||
.map(|hunk| (hunk.range.start.row, hunk.secondary_status))
|
||||
.collect::<Vec<_>>();
|
||||
let actual_hunks = diff
|
||||
.hunks(&snapshot, cx)
|
||||
.map(|hunk| (hunk.range.start.row, hunk.secondary_status))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(actual_hunks, expected_hunks);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue