From 00359271d11b6ba865fe81f04f675bf588a17335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Marcos?= Date: Thu, 13 Mar 2025 14:41:04 -0300 Subject: [PATCH] git: Fix race condition when [un]staging hunks in quick succession (#26422) - [x] Fix `[un]stage` hunk operations cancelling pending ones - [x] Add test - [ ] bugs I stumbled upon (try to repro again before merging) - [x] holding `git::StageAndNext` skips hunks randomly - [x] Add test - [x] restoring a file keeps it in the git panel - [x] Double clicking on `toggle staged` fast makes Zed disagree with `git` CLI - [x] checkbox shows :heavy_check_mark: (fully staged) after a single stage Release Notes: - N/A --------- Co-authored-by: Cole Co-authored-by: Max --- crates/buffer_diff/Cargo.toml | 2 +- crates/buffer_diff/src/buffer_diff.rs | 228 ++++++++++++++++-------- crates/git_ui/src/project_diff.rs | 2 +- crates/project/src/project_tests.rs | 242 +++++++++++++++++++++++++- crates/sum_tree/src/cursor.rs | 1 + crates/sum_tree/src/sum_tree.rs | 9 + crates/text/src/anchor.rs | 5 + 7 files changed, 406 insertions(+), 83 deletions(-) diff --git a/crates/buffer_diff/Cargo.toml b/crates/buffer_diff/Cargo.toml index c92688698a..c2b11f4791 100644 --- a/crates/buffer_diff/Cargo.toml +++ b/crates/buffer_diff/Cargo.toml @@ -22,6 +22,7 @@ git2.workspace = true gpui.workspace = true language.workspace = true log.workspace = true +pretty_assertions.workspace = true rope.workspace = true sum_tree.workspace = true text.workspace = true @@ -31,7 +32,6 @@ util.workspace = true ctor.workspace = true env_logger.workspace = true gpui = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true rand.workspace = true serde_json.workspace = true text = { workspace = true, features = ["test-support"] } diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 2a6c31744e..62f46e4c19 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -6,9 +6,9 @@ use rope::Rope; use std::cmp::Ordering; use std::mem; use std::{future::Future, iter, ops::Range, sync::Arc}; -use sum_tree::{SumTree, TreeMap}; -use text::ToOffset as _; +use sum_tree::SumTree; use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point}; +use text::{AnchorRangeExt, ToOffset as _}; use util::ResultExt; pub struct BufferDiff { @@ -26,7 +26,7 @@ pub struct BufferDiffSnapshot { #[derive(Clone)] struct BufferDiffInner { hunks: SumTree, - pending_hunks: TreeMap, + pending_hunks: SumTree, base_text: language::BufferSnapshot, base_text_exists: bool, } @@ -48,7 +48,7 @@ pub enum DiffHunkStatusKind { pub enum DiffHunkSecondaryStatus { HasSecondaryHunk, OverlapsWithSecondaryHunk, - None, + NoSecondaryHunk, SecondaryHunkAdditionPending, SecondaryHunkRemovalPending, } @@ -74,6 +74,8 @@ struct InternalDiffHunk { #[derive(Debug, Clone, PartialEq, Eq)] struct PendingHunk { + buffer_range: Range, + diff_base_byte_range: Range, buffer_version: clock::Global, new_status: DiffHunkSecondaryStatus, } @@ -93,6 +95,16 @@ impl sum_tree::Item for InternalDiffHunk { } } +impl sum_tree::Item for PendingHunk { + type Summary = DiffHunkSummary; + + fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary { + DiffHunkSummary { + buffer_range: self.buffer_range.clone(), + } + } +} + impl sum_tree::Summary for DiffHunkSummary { type Context = text::BufferSnapshot; @@ -176,6 +188,7 @@ impl BufferDiffSnapshot { } impl BufferDiffInner { + /// Returns the new index text and new pending hunks. fn stage_or_unstage_hunks( &mut self, unstaged_diff: &Self, @@ -183,7 +196,7 @@ impl BufferDiffInner { hunks: &[DiffHunk], buffer: &text::BufferSnapshot, file_exists: bool, - ) -> (Option, Vec<(usize, PendingHunk)>) { + ) -> (Option, SumTree) { let head_text = self .base_text_exists .then(|| self.base_text.as_rope().clone()); @@ -195,41 +208,41 @@ impl BufferDiffInner { // entire file must be either created or deleted in the index. let (index_text, head_text) = match (index_text, head_text) { (Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text), - (_, head_text @ _) => { - if stage { + (index_text, head_text) => { + let (rope, new_status) = if stage { log::debug!("stage all"); - return ( + ( file_exists.then(|| buffer.as_rope().clone()), - vec![( - 0, - PendingHunk { - buffer_version: buffer.version().clone(), - new_status: DiffHunkSecondaryStatus::SecondaryHunkRemovalPending, - }, - )], - ); + DiffHunkSecondaryStatus::SecondaryHunkRemovalPending, + ) } else { log::debug!("unstage all"); - return ( + ( head_text, - vec![( - 0, - PendingHunk { - buffer_version: buffer.version().clone(), - new_status: DiffHunkSecondaryStatus::SecondaryHunkAdditionPending, - }, - )], - ); - } + DiffHunkSecondaryStatus::SecondaryHunkAdditionPending, + ) + }; + + let hunk = PendingHunk { + buffer_range: Anchor::MIN..Anchor::MAX, + diff_base_byte_range: 0..index_text.map_or(0, |rope| rope.len()), + buffer_version: buffer.version().clone(), + new_status, + }; + let tree = SumTree::from_item(hunk, buffer); + return (rope, tree); } }; let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::(buffer); unstaged_hunk_cursor.next(buffer); - let mut edits = Vec::new(); - let mut pending_hunks = Vec::new(); - let mut prev_unstaged_hunk_buffer_offset = 0; - let mut prev_unstaged_hunk_base_text_offset = 0; + + let mut pending_hunks = SumTree::new(buffer); + let mut old_pending_hunks = unstaged_diff + .pending_hunks + .cursor::(buffer); + + // first, merge new hunks into pending_hunks for DiffHunk { buffer_range, diff_base_byte_range, @@ -237,12 +250,58 @@ impl BufferDiffInner { .. } in hunks.iter().cloned() { - if (stage && secondary_status == DiffHunkSecondaryStatus::None) + let preceding_pending_hunks = + old_pending_hunks.slice(&buffer_range.start, Bias::Left, buffer); + + pending_hunks.append(preceding_pending_hunks, buffer); + + // skip all overlapping old pending hunks + while old_pending_hunks + .item() + .is_some_and(|preceding_pending_hunk_item| { + preceding_pending_hunk_item + .buffer_range + .overlaps(&buffer_range, buffer) + }) + { + old_pending_hunks.next(buffer); + } + + // merge into pending hunks + if (stage && secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk) || (!stage && secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk) { continue; } + pending_hunks.push( + PendingHunk { + buffer_range, + diff_base_byte_range, + buffer_version: buffer.version().clone(), + new_status: if stage { + DiffHunkSecondaryStatus::SecondaryHunkRemovalPending + } else { + DiffHunkSecondaryStatus::SecondaryHunkAdditionPending + }, + }, + buffer, + ); + } + // append the remainder + pending_hunks.append(old_pending_hunks.suffix(buffer), buffer); + + let mut prev_unstaged_hunk_buffer_offset = 0; + let mut prev_unstaged_hunk_base_text_offset = 0; + let mut edits = Vec::<(Range, String)>::new(); + + // then, iterate over all pending hunks (both new ones and the existing ones) and compute the edits + for PendingHunk { + buffer_range, + diff_base_byte_range, + .. + } in pending_hunks.iter().cloned() + { let skipped_hunks = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left, buffer); if let Some(secondary_hunk) = skipped_hunks.last() { @@ -294,22 +353,15 @@ impl BufferDiffInner { .chunks_in_range(diff_base_byte_range.clone()) .collect::() }; - pending_hunks.push(( - diff_base_byte_range.start, - PendingHunk { - buffer_version: buffer.version().clone(), - new_status: if stage { - DiffHunkSecondaryStatus::SecondaryHunkRemovalPending - } else { - DiffHunkSecondaryStatus::SecondaryHunkAdditionPending - }, - }, - )); + edits.push((index_range, replacement_text)); } + debug_assert!(edits.iter().is_sorted_by_key(|(range, _)| range.start)); + let mut new_index_text = Rope::new(); let mut index_cursor = index_text.cursor(0); + for (old_range, replacement_text) in edits { new_index_text.append(index_cursor.slice(old_range.start)); index_cursor.seek_forward(old_range.end); @@ -354,12 +406,14 @@ impl BufferDiffInner { }); let mut secondary_cursor = None; - let mut pending_hunks = TreeMap::default(); + let mut pending_hunks_cursor = None; if let Some(secondary) = secondary.as_ref() { let mut cursor = secondary.hunks.cursor::(buffer); cursor.next(buffer); secondary_cursor = Some(cursor); - pending_hunks = secondary.pending_hunks.clone(); + let mut cursor = secondary.pending_hunks.cursor::(buffer); + cursor.next(buffer); + pending_hunks_cursor = Some(cursor); } let max_point = buffer.max_point(); @@ -378,16 +432,33 @@ impl BufferDiffInner { end_anchor = buffer.anchor_before(end_point); } - let mut secondary_status = DiffHunkSecondaryStatus::None; + let mut secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk; let mut has_pending = false; - if let Some(pending_hunk) = pending_hunks.get(&start_base) { - if !buffer.has_edits_since_in_range( - &pending_hunk.buffer_version, - start_anchor..end_anchor, - ) { - has_pending = true; - secondary_status = pending_hunk.new_status; + if let Some(pending_cursor) = pending_hunks_cursor.as_mut() { + if start_anchor + .cmp(&pending_cursor.start().buffer_range.start, buffer) + .is_gt() + { + pending_cursor.seek_forward(&start_anchor, Bias::Left, buffer); + } + + if let Some(pending_hunk) = pending_cursor.item() { + let mut pending_range = pending_hunk.buffer_range.to_point(buffer); + if pending_range.end.column > 0 { + pending_range.end.row += 1; + pending_range.end.column = 0; + } + + if pending_range == (start_point..end_point) { + if !buffer.has_edits_since_in_range( + &pending_hunk.buffer_version, + start_anchor..end_anchor, + ) { + has_pending = true; + secondary_status = pending_hunk.new_status; + } + } } } @@ -449,7 +520,7 @@ impl BufferDiffInner { diff_base_byte_range: hunk.diff_base_byte_range.clone(), buffer_range: hunk.buffer_range.clone(), // The secondary status is not used by callers of this method. - secondary_status: DiffHunkSecondaryStatus::None, + secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk, }) }) } @@ -724,7 +795,7 @@ impl BufferDiff { base_text, hunks, base_text_exists, - pending_hunks: TreeMap::default(), + pending_hunks: SumTree::new(&buffer), } } } @@ -740,8 +811,8 @@ impl BufferDiff { cx.background_spawn(async move { BufferDiffInner { base_text: base_text_snapshot, + pending_hunks: SumTree::new(&buffer), hunks: compute_hunks(base_text_pair, buffer), - pending_hunks: TreeMap::default(), base_text_exists, } }) @@ -751,7 +822,7 @@ impl BufferDiff { BufferDiffInner { base_text: language::Buffer::build_empty_snapshot(cx), hunks: SumTree::new(buffer), - pending_hunks: TreeMap::default(), + pending_hunks: SumTree::new(buffer), base_text_exists: false, } } @@ -767,7 +838,7 @@ impl BufferDiff { pub fn clear_pending_hunks(&mut self, cx: &mut Context) { if let Some(secondary_diff) = &self.secondary_diff { secondary_diff.update(cx, |diff, _| { - diff.inner.pending_hunks.clear(); + diff.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary::default()); }); cx.emit(BufferDiffEvent::DiffChanged { changed_range: Some(Anchor::MIN..Anchor::MAX), @@ -783,18 +854,17 @@ impl BufferDiff { file_exists: bool, cx: &mut Context, ) -> Option { - let (new_index_text, pending_hunks) = self.inner.stage_or_unstage_hunks( + let (new_index_text, new_pending_hunks) = self.inner.stage_or_unstage_hunks( &self.secondary_diff.as_ref()?.read(cx).inner, stage, &hunks, buffer, file_exists, ); + if let Some(unstaged_diff) = &self.secondary_diff { unstaged_diff.update(cx, |diff, _| { - for (offset, pending_hunk) in pending_hunks { - diff.inner.pending_hunks.insert(offset, pending_hunk); - } + diff.inner.pending_hunks = new_pending_hunks; }); } cx.emit(BufferDiffEvent::HunksStagedOrUnstaged( @@ -916,7 +986,9 @@ impl BufferDiff { } _ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)), }; - let pending_hunks = mem::take(&mut self.inner.pending_hunks); + + let pending_hunks = mem::replace(&mut self.inner.pending_hunks, SumTree::new(buffer)); + self.inner = new_state; if !base_text_changed { self.inner.pending_hunks = pending_hunks; @@ -1149,21 +1221,21 @@ impl DiffHunkStatus { pub fn deleted_none() -> Self { Self { kind: DiffHunkStatusKind::Deleted, - secondary: DiffHunkSecondaryStatus::None, + secondary: DiffHunkSecondaryStatus::NoSecondaryHunk, } } pub fn added_none() -> Self { Self { kind: DiffHunkStatusKind::Added, - secondary: DiffHunkSecondaryStatus::None, + secondary: DiffHunkSecondaryStatus::NoSecondaryHunk, } } pub fn modified_none() -> Self { Self { kind: DiffHunkStatusKind::Modified, - secondary: DiffHunkSecondaryStatus::None, + secondary: DiffHunkSecondaryStatus::NoSecondaryHunk, } } } @@ -1171,13 +1243,14 @@ impl DiffHunkStatus { /// Range (crossing new lines), old, new #[cfg(any(test, feature = "test-support"))] #[track_caller] -pub fn assert_hunks( - diff_hunks: Iter, +pub fn assert_hunks( + diff_hunks: HunkIter, buffer: &text::BufferSnapshot, diff_base: &str, - expected_hunks: &[(Range, &str, &str, DiffHunkStatus)], + expected_hunks: &[(Range, ExpectedText, ExpectedText, DiffHunkStatus)], ) where - Iter: Iterator, + HunkIter: Iterator, + ExpectedText: AsRef, { let actual_hunks = diff_hunks .map(|hunk| { @@ -1197,14 +1270,14 @@ pub fn assert_hunks( .map(|(r, old_text, new_text, status)| { ( Point::new(r.start, 0)..Point::new(r.end, 0), - *old_text, - new_text.to_string(), + old_text.as_ref(), + new_text.as_ref().to_string(), *status, ) }) .collect(); - assert_eq!(actual_hunks, expected_hunks); + pretty_assertions::assert_eq!(actual_hunks, expected_hunks); } #[cfg(test)] @@ -1263,7 +1336,7 @@ mod tests { ); diff = cx.update(|cx| BufferDiff::build_empty(&buffer, cx)); - assert_hunks( + assert_hunks::<&str, _>( diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None), &buffer, &diff_base, @@ -1601,7 +1674,10 @@ mod tests { .hunks_intersecting_range(hunk_range.clone(), &buffer, &cx) .collect::>(); for hunk in &hunks { - assert_ne!(hunk.secondary_status, DiffHunkSecondaryStatus::None) + assert_ne!( + hunk.secondary_status, + DiffHunkSecondaryStatus::NoSecondaryHunk + ) } let new_index_text = diff @@ -1880,10 +1956,10 @@ mod tests { let hunk_to_change = hunk.clone(); let stage = match hunk.secondary_status { DiffHunkSecondaryStatus::HasSecondaryHunk => { - hunk.secondary_status = DiffHunkSecondaryStatus::None; + hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk; true } - DiffHunkSecondaryStatus::None => { + DiffHunkSecondaryStatus::NoSecondaryHunk => { hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk; false } diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 0e279ea0fe..f0fcd488e0 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -278,7 +278,7 @@ impl ProjectDiff { has_staged_hunks = true; has_unstaged_hunks = true; } - DiffHunkSecondaryStatus::None + DiffHunkSecondaryStatus::NoSecondaryHunk | DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => { has_staged_hunks = true; } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index fed349736b..45ae65132b 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -941,7 +941,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon cx.executor().run_until_parked(); // Start the language server by opening a buffer with a compatible file extension. - let _ = project + project .update(cx, |project, cx| { project.open_local_buffer_with_lsp(path!("/the-root/src/a.rs"), cx) }) @@ -6008,7 +6008,7 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) { 0..0, "// the-deleted-contents\n", "", - DiffHunkStatus::deleted(DiffHunkSecondaryStatus::None), + DiffHunkStatus::deleted(DiffHunkSecondaryStatus::NoSecondaryHunk), )], ); }); @@ -6168,7 +6168,12 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { "", DiffHunkStatus::deleted(HasSecondaryHunk), ), - (1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)), + ( + 1..2, + "two\n", + "TWO\n", + DiffHunkStatus::modified(NoSecondaryHunk), + ), ( 3..4, "four\n", @@ -6217,7 +6222,12 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { "", DiffHunkStatus::deleted(HasSecondaryHunk), ), - (1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)), + ( + 1..2, + "two\n", + "TWO\n", + DiffHunkStatus::modified(NoSecondaryHunk), + ), ( 3..4, "four\n", @@ -6256,7 +6266,12 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { "", DiffHunkStatus::deleted(HasSecondaryHunk), ), - (1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)), + ( + 1..2, + "two\n", + "TWO\n", + DiffHunkStatus::modified(NoSecondaryHunk), + ), ( 3..4, "four\n", @@ -6277,6 +6292,223 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { } else { panic!("Unexpected event {event:?}"); } + + // Allow writing to the git index to succeed again. + fs.set_error_message_for_index_write("/dir/.git".as_ref(), None); + + // Stage two hunks with separate operations. + uncommitted_diff.update(cx, |diff, cx| { + let hunks = diff.hunks(&snapshot, cx).collect::>(); + diff.stage_or_unstage_hunks(true, &hunks[0..1], &snapshot, true, cx); + diff.stage_or_unstage_hunks(true, &hunks[2..3], &snapshot, true, cx); + }); + + // Both staged hunks appear as pending. + uncommitted_diff.update(cx, |diff, cx| { + assert_hunks( + diff.hunks(&snapshot, cx), + &snapshot, + &diff.base_text_string().unwrap(), + &[ + ( + 0..0, + "zero\n", + "", + DiffHunkStatus::deleted(SecondaryHunkRemovalPending), + ), + ( + 1..2, + "two\n", + "TWO\n", + DiffHunkStatus::modified(NoSecondaryHunk), + ), + ( + 3..4, + "four\n", + "FOUR\n", + DiffHunkStatus::modified(SecondaryHunkRemovalPending), + ), + ], + ); + }); + + // Both staging operations take effect. + cx.run_until_parked(); + uncommitted_diff.update(cx, |diff, cx| { + assert_hunks( + diff.hunks(&snapshot, cx), + &snapshot, + &diff.base_text_string().unwrap(), + &[ + (0..0, "zero\n", "", DiffHunkStatus::deleted(NoSecondaryHunk)), + ( + 1..2, + "two\n", + "TWO\n", + DiffHunkStatus::modified(NoSecondaryHunk), + ), + ( + 3..4, + "four\n", + "FOUR\n", + DiffHunkStatus::modified(NoSecondaryHunk), + ), + ], + ); + }); +} + +#[allow(clippy::format_collect)] +#[gpui::test] +async fn test_staging_lots_of_hunks_fast(cx: &mut gpui::TestAppContext) { + use DiffHunkSecondaryStatus::*; + init_test(cx); + + let different_lines = (0..500) + .step_by(5) + .map(|i| format!("diff {}\n", i)) + .collect::>(); + let committed_contents = (0..500).map(|i| format!("{}\n", i)).collect::(); + let file_contents = (0..500) + .map(|i| { + if i % 5 == 0 { + different_lines[i / 5].clone() + } else { + format!("{}\n", i) + } + }) + .collect::(); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/dir", + json!({ + ".git": {}, + "file.txt": file_contents.clone() + }), + ) + .await; + + fs.set_head_for_repo( + "/dir/.git".as_ref(), + &[("file.txt".into(), committed_contents.clone())], + ); + fs.set_index_for_repo( + "/dir/.git".as_ref(), + &[("file.txt".into(), committed_contents.clone())], + ); + + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/dir/file.txt", cx) + }) + .await + .unwrap(); + let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); + let uncommitted_diff = project + .update(cx, |project, cx| { + project.open_uncommitted_diff(buffer.clone(), cx) + }) + .await + .unwrap(); + + let range = Anchor::MIN..snapshot.anchor_after(snapshot.max_point()); + + let mut expected_hunks: Vec<(Range, 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(); + + // 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 (_, _, _, 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_intersecting_range(range.clone(), &snapshot, cx) + .collect::>(); + for hunk in hunks { + diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx); + } + + 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_intersecting_range(range, &snapshot, cx) + .collect::>(); + for hunk in hunks { + diff.stage_or_unstage_hunks(false, &[hunk], &snapshot, true, cx); + } + + 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); + } + + uncommitted_diff.update(cx, |diff, cx| { + assert_hunks( + diff.hunks(&snapshot, cx), + &snapshot, + &diff.base_text_string().unwrap(), + &expected_hunks, + ); + }); } #[gpui::test] diff --git a/crates/sum_tree/src/cursor.rs b/crates/sum_tree/src/cursor.rs index 3257976df4..8edd04afce 100644 --- a/crates/sum_tree/src/cursor.rs +++ b/crates/sum_tree/src/cursor.rs @@ -406,6 +406,7 @@ where self.seek_internal(pos, bias, &mut (), cx) } + /// Advances the cursor and returns traversed items as a tree. #[track_caller] pub fn slice( &mut self, diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index 237e314c14..ace47cc4b2 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -225,6 +225,15 @@ impl SumTree { })) } + /// Useful in cases where the item type has a non-trivial context type, but the zero value of the summary type doesn't depend on that context. + pub fn from_summary(summary: T::Summary) -> Self { + SumTree(Arc::new(Node::Leaf { + summary, + items: ArrayVec::new(), + item_summaries: ArrayVec::new(), + })) + } + pub fn from_item(item: T, cx: &::Context) -> Self { let mut tree = Self::new(cx); tree.push(item, cx); diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index 3bc5889cae..d29dfc0bb5 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -136,6 +136,7 @@ where pub trait AnchorRangeExt { fn cmp(&self, b: &Range, buffer: &BufferSnapshot) -> Ordering; + fn overlaps(&self, b: &Range, buffer: &BufferSnapshot) -> bool; } impl AnchorRangeExt for Range { @@ -145,4 +146,8 @@ impl AnchorRangeExt for Range { ord => ord, } } + + fn overlaps(&self, other: &Range, buffer: &BufferSnapshot) -> bool { + self.start.cmp(&other.end, buffer).is_lt() && other.start.cmp(&self.end, buffer).is_lt() + } }