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 ✔️ (fully staged) after a single
stage

Release Notes:

- N/A

---------

Co-authored-by: Cole <cole@zed.dev>
Co-authored-by: Max <max@zed.dev>
This commit is contained in:
João Marcos 2025-03-13 14:41:04 -03:00 committed by GitHub
parent 18fcdf1d2c
commit 00359271d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 406 additions and 83 deletions

View file

@ -22,6 +22,7 @@ git2.workspace = true
gpui.workspace = true gpui.workspace = true
language.workspace = true language.workspace = true
log.workspace = true log.workspace = true
pretty_assertions.workspace = true
rope.workspace = true rope.workspace = true
sum_tree.workspace = true sum_tree.workspace = true
text.workspace = true text.workspace = true
@ -31,7 +32,6 @@ util.workspace = true
ctor.workspace = true ctor.workspace = true
env_logger.workspace = true env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
rand.workspace = true rand.workspace = true
serde_json.workspace = true serde_json.workspace = true
text = { workspace = true, features = ["test-support"] } text = { workspace = true, features = ["test-support"] }

View file

@ -6,9 +6,9 @@ use rope::Rope;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::mem; use std::mem;
use std::{future::Future, iter, ops::Range, sync::Arc}; use std::{future::Future, iter, ops::Range, sync::Arc};
use sum_tree::{SumTree, TreeMap}; use sum_tree::SumTree;
use text::ToOffset as _;
use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point}; use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point};
use text::{AnchorRangeExt, ToOffset as _};
use util::ResultExt; use util::ResultExt;
pub struct BufferDiff { pub struct BufferDiff {
@ -26,7 +26,7 @@ pub struct BufferDiffSnapshot {
#[derive(Clone)] #[derive(Clone)]
struct BufferDiffInner { struct BufferDiffInner {
hunks: SumTree<InternalDiffHunk>, hunks: SumTree<InternalDiffHunk>,
pending_hunks: TreeMap<usize, PendingHunk>, pending_hunks: SumTree<PendingHunk>,
base_text: language::BufferSnapshot, base_text: language::BufferSnapshot,
base_text_exists: bool, base_text_exists: bool,
} }
@ -48,7 +48,7 @@ pub enum DiffHunkStatusKind {
pub enum DiffHunkSecondaryStatus { pub enum DiffHunkSecondaryStatus {
HasSecondaryHunk, HasSecondaryHunk,
OverlapsWithSecondaryHunk, OverlapsWithSecondaryHunk,
None, NoSecondaryHunk,
SecondaryHunkAdditionPending, SecondaryHunkAdditionPending,
SecondaryHunkRemovalPending, SecondaryHunkRemovalPending,
} }
@ -74,6 +74,8 @@ struct InternalDiffHunk {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
struct PendingHunk { struct PendingHunk {
buffer_range: Range<Anchor>,
diff_base_byte_range: Range<usize>,
buffer_version: clock::Global, buffer_version: clock::Global,
new_status: DiffHunkSecondaryStatus, 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 { impl sum_tree::Summary for DiffHunkSummary {
type Context = text::BufferSnapshot; type Context = text::BufferSnapshot;
@ -176,6 +188,7 @@ impl BufferDiffSnapshot {
} }
impl BufferDiffInner { impl BufferDiffInner {
/// Returns the new index text and new pending hunks.
fn stage_or_unstage_hunks( fn stage_or_unstage_hunks(
&mut self, &mut self,
unstaged_diff: &Self, unstaged_diff: &Self,
@ -183,7 +196,7 @@ impl BufferDiffInner {
hunks: &[DiffHunk], hunks: &[DiffHunk],
buffer: &text::BufferSnapshot, buffer: &text::BufferSnapshot,
file_exists: bool, file_exists: bool,
) -> (Option<Rope>, Vec<(usize, PendingHunk)>) { ) -> (Option<Rope>, SumTree<PendingHunk>) {
let head_text = self let head_text = self
.base_text_exists .base_text_exists
.then(|| self.base_text.as_rope().clone()); .then(|| self.base_text.as_rope().clone());
@ -195,41 +208,41 @@ impl BufferDiffInner {
// entire file must be either created or deleted in the index. // entire file must be either created or deleted in the index.
let (index_text, head_text) = match (index_text, head_text) { let (index_text, head_text) = match (index_text, head_text) {
(Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text), (Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text),
(_, head_text @ _) => { (index_text, head_text) => {
if stage { let (rope, new_status) = if stage {
log::debug!("stage all"); log::debug!("stage all");
return ( (
file_exists.then(|| buffer.as_rope().clone()), file_exists.then(|| buffer.as_rope().clone()),
vec![( DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
0, )
PendingHunk {
buffer_version: buffer.version().clone(),
new_status: DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
},
)],
);
} else { } else {
log::debug!("unstage all"); log::debug!("unstage all");
return ( (
head_text, head_text,
vec![( DiffHunkSecondaryStatus::SecondaryHunkAdditionPending,
0, )
PendingHunk { };
buffer_version: buffer.version().clone(),
new_status: 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::<DiffHunkSummary>(buffer); let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
unstaged_hunk_cursor.next(buffer); unstaged_hunk_cursor.next(buffer);
let mut edits = Vec::new();
let mut pending_hunks = Vec::new(); let mut pending_hunks = SumTree::new(buffer);
let mut prev_unstaged_hunk_buffer_offset = 0; let mut old_pending_hunks = unstaged_diff
let mut prev_unstaged_hunk_base_text_offset = 0; .pending_hunks
.cursor::<DiffHunkSummary>(buffer);
// first, merge new hunks into pending_hunks
for DiffHunk { for DiffHunk {
buffer_range, buffer_range,
diff_base_byte_range, diff_base_byte_range,
@ -237,12 +250,58 @@ impl BufferDiffInner {
.. ..
} in hunks.iter().cloned() } 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) || (!stage && secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk)
{ {
continue; 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<usize>, 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); let skipped_hunks = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left, buffer);
if let Some(secondary_hunk) = skipped_hunks.last() { if let Some(secondary_hunk) = skipped_hunks.last() {
@ -294,22 +353,15 @@ impl BufferDiffInner {
.chunks_in_range(diff_base_byte_range.clone()) .chunks_in_range(diff_base_byte_range.clone())
.collect::<String>() .collect::<String>()
}; };
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)); 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 new_index_text = Rope::new();
let mut index_cursor = index_text.cursor(0); let mut index_cursor = index_text.cursor(0);
for (old_range, replacement_text) in edits { for (old_range, replacement_text) in edits {
new_index_text.append(index_cursor.slice(old_range.start)); new_index_text.append(index_cursor.slice(old_range.start));
index_cursor.seek_forward(old_range.end); index_cursor.seek_forward(old_range.end);
@ -354,12 +406,14 @@ impl BufferDiffInner {
}); });
let mut secondary_cursor = None; let mut secondary_cursor = None;
let mut pending_hunks = TreeMap::default(); let mut pending_hunks_cursor = None;
if let Some(secondary) = secondary.as_ref() { if let Some(secondary) = secondary.as_ref() {
let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer); let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer);
cursor.next(buffer); cursor.next(buffer);
secondary_cursor = Some(cursor); secondary_cursor = Some(cursor);
pending_hunks = secondary.pending_hunks.clone(); let mut cursor = secondary.pending_hunks.cursor::<DiffHunkSummary>(buffer);
cursor.next(buffer);
pending_hunks_cursor = Some(cursor);
} }
let max_point = buffer.max_point(); let max_point = buffer.max_point();
@ -378,16 +432,33 @@ impl BufferDiffInner {
end_anchor = buffer.anchor_before(end_point); end_anchor = buffer.anchor_before(end_point);
} }
let mut secondary_status = DiffHunkSecondaryStatus::None; let mut secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
let mut has_pending = false; let mut has_pending = false;
if let Some(pending_hunk) = pending_hunks.get(&start_base) { if let Some(pending_cursor) = pending_hunks_cursor.as_mut() {
if !buffer.has_edits_since_in_range( if start_anchor
&pending_hunk.buffer_version, .cmp(&pending_cursor.start().buffer_range.start, buffer)
start_anchor..end_anchor, .is_gt()
) { {
has_pending = true; pending_cursor.seek_forward(&start_anchor, Bias::Left, buffer);
secondary_status = pending_hunk.new_status; }
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(), diff_base_byte_range: hunk.diff_base_byte_range.clone(),
buffer_range: hunk.buffer_range.clone(), buffer_range: hunk.buffer_range.clone(),
// The secondary status is not used by callers of this method. // 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, base_text,
hunks, hunks,
base_text_exists, base_text_exists,
pending_hunks: TreeMap::default(), pending_hunks: SumTree::new(&buffer),
} }
} }
} }
@ -740,8 +811,8 @@ impl BufferDiff {
cx.background_spawn(async move { cx.background_spawn(async move {
BufferDiffInner { BufferDiffInner {
base_text: base_text_snapshot, base_text: base_text_snapshot,
pending_hunks: SumTree::new(&buffer),
hunks: compute_hunks(base_text_pair, buffer), hunks: compute_hunks(base_text_pair, buffer),
pending_hunks: TreeMap::default(),
base_text_exists, base_text_exists,
} }
}) })
@ -751,7 +822,7 @@ impl BufferDiff {
BufferDiffInner { BufferDiffInner {
base_text: language::Buffer::build_empty_snapshot(cx), base_text: language::Buffer::build_empty_snapshot(cx),
hunks: SumTree::new(buffer), hunks: SumTree::new(buffer),
pending_hunks: TreeMap::default(), pending_hunks: SumTree::new(buffer),
base_text_exists: false, base_text_exists: false,
} }
} }
@ -767,7 +838,7 @@ impl BufferDiff {
pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) { pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
if let Some(secondary_diff) = &self.secondary_diff { if let Some(secondary_diff) = &self.secondary_diff {
secondary_diff.update(cx, |diff, _| { secondary_diff.update(cx, |diff, _| {
diff.inner.pending_hunks.clear(); diff.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary::default());
}); });
cx.emit(BufferDiffEvent::DiffChanged { cx.emit(BufferDiffEvent::DiffChanged {
changed_range: Some(Anchor::MIN..Anchor::MAX), changed_range: Some(Anchor::MIN..Anchor::MAX),
@ -783,18 +854,17 @@ impl BufferDiff {
file_exists: bool, file_exists: bool,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Option<Rope> { ) -> Option<Rope> {
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, &self.secondary_diff.as_ref()?.read(cx).inner,
stage, stage,
&hunks, &hunks,
buffer, buffer,
file_exists, file_exists,
); );
if let Some(unstaged_diff) = &self.secondary_diff { if let Some(unstaged_diff) = &self.secondary_diff {
unstaged_diff.update(cx, |diff, _| { unstaged_diff.update(cx, |diff, _| {
for (offset, pending_hunk) in pending_hunks { diff.inner.pending_hunks = new_pending_hunks;
diff.inner.pending_hunks.insert(offset, pending_hunk);
}
}); });
} }
cx.emit(BufferDiffEvent::HunksStagedOrUnstaged( cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
@ -916,7 +986,9 @@ impl BufferDiff {
} }
_ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)), _ => (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; self.inner = new_state;
if !base_text_changed { if !base_text_changed {
self.inner.pending_hunks = pending_hunks; self.inner.pending_hunks = pending_hunks;
@ -1149,21 +1221,21 @@ impl DiffHunkStatus {
pub fn deleted_none() -> Self { pub fn deleted_none() -> Self {
Self { Self {
kind: DiffHunkStatusKind::Deleted, kind: DiffHunkStatusKind::Deleted,
secondary: DiffHunkSecondaryStatus::None, secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
} }
} }
pub fn added_none() -> Self { pub fn added_none() -> Self {
Self { Self {
kind: DiffHunkStatusKind::Added, kind: DiffHunkStatusKind::Added,
secondary: DiffHunkSecondaryStatus::None, secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
} }
} }
pub fn modified_none() -> Self { pub fn modified_none() -> Self {
Self { Self {
kind: DiffHunkStatusKind::Modified, kind: DiffHunkStatusKind::Modified,
secondary: DiffHunkSecondaryStatus::None, secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
} }
} }
} }
@ -1171,13 +1243,14 @@ impl DiffHunkStatus {
/// Range (crossing new lines), old, new /// Range (crossing new lines), old, new
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
#[track_caller] #[track_caller]
pub fn assert_hunks<Iter>( pub fn assert_hunks<ExpectedText, HunkIter>(
diff_hunks: Iter, diff_hunks: HunkIter,
buffer: &text::BufferSnapshot, buffer: &text::BufferSnapshot,
diff_base: &str, diff_base: &str,
expected_hunks: &[(Range<u32>, &str, &str, DiffHunkStatus)], expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
) where ) where
Iter: Iterator<Item = DiffHunk>, HunkIter: Iterator<Item = DiffHunk>,
ExpectedText: AsRef<str>,
{ {
let actual_hunks = diff_hunks let actual_hunks = diff_hunks
.map(|hunk| { .map(|hunk| {
@ -1197,14 +1270,14 @@ pub fn assert_hunks<Iter>(
.map(|(r, old_text, new_text, status)| { .map(|(r, old_text, new_text, status)| {
( (
Point::new(r.start, 0)..Point::new(r.end, 0), Point::new(r.start, 0)..Point::new(r.end, 0),
*old_text, old_text.as_ref(),
new_text.to_string(), new_text.as_ref().to_string(),
*status, *status,
) )
}) })
.collect(); .collect();
assert_eq!(actual_hunks, expected_hunks); pretty_assertions::assert_eq!(actual_hunks, expected_hunks);
} }
#[cfg(test)] #[cfg(test)]
@ -1263,7 +1336,7 @@ mod tests {
); );
diff = cx.update(|cx| BufferDiff::build_empty(&buffer, cx)); diff = cx.update(|cx| BufferDiff::build_empty(&buffer, cx));
assert_hunks( assert_hunks::<&str, _>(
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None), diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
&buffer, &buffer,
&diff_base, &diff_base,
@ -1601,7 +1674,10 @@ mod tests {
.hunks_intersecting_range(hunk_range.clone(), &buffer, &cx) .hunks_intersecting_range(hunk_range.clone(), &buffer, &cx)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for hunk in &hunks { for hunk in &hunks {
assert_ne!(hunk.secondary_status, DiffHunkSecondaryStatus::None) assert_ne!(
hunk.secondary_status,
DiffHunkSecondaryStatus::NoSecondaryHunk
)
} }
let new_index_text = diff let new_index_text = diff
@ -1880,10 +1956,10 @@ mod tests {
let hunk_to_change = hunk.clone(); let hunk_to_change = hunk.clone();
let stage = match hunk.secondary_status { let stage = match hunk.secondary_status {
DiffHunkSecondaryStatus::HasSecondaryHunk => { DiffHunkSecondaryStatus::HasSecondaryHunk => {
hunk.secondary_status = DiffHunkSecondaryStatus::None; hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
true true
} }
DiffHunkSecondaryStatus::None => { DiffHunkSecondaryStatus::NoSecondaryHunk => {
hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk; hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
false false
} }

View file

@ -278,7 +278,7 @@ impl ProjectDiff {
has_staged_hunks = true; has_staged_hunks = true;
has_unstaged_hunks = true; has_unstaged_hunks = true;
} }
DiffHunkSecondaryStatus::None DiffHunkSecondaryStatus::NoSecondaryHunk
| DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => { | DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => {
has_staged_hunks = true; has_staged_hunks = true;
} }

View file

@ -941,7 +941,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
cx.executor().run_until_parked(); cx.executor().run_until_parked();
// Start the language server by opening a buffer with a compatible file extension. // Start the language server by opening a buffer with a compatible file extension.
let _ = project project
.update(cx, |project, cx| { .update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/src/a.rs"), 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, 0..0,
"// the-deleted-contents\n", "// 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), DiffHunkStatus::deleted(HasSecondaryHunk),
), ),
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)), (
1..2,
"two\n",
"TWO\n",
DiffHunkStatus::modified(NoSecondaryHunk),
),
( (
3..4, 3..4,
"four\n", "four\n",
@ -6217,7 +6222,12 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
"", "",
DiffHunkStatus::deleted(HasSecondaryHunk), DiffHunkStatus::deleted(HasSecondaryHunk),
), ),
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)), (
1..2,
"two\n",
"TWO\n",
DiffHunkStatus::modified(NoSecondaryHunk),
),
( (
3..4, 3..4,
"four\n", "four\n",
@ -6256,7 +6266,12 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
"", "",
DiffHunkStatus::deleted(HasSecondaryHunk), DiffHunkStatus::deleted(HasSecondaryHunk),
), ),
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)), (
1..2,
"two\n",
"TWO\n",
DiffHunkStatus::modified(NoSecondaryHunk),
),
( (
3..4, 3..4,
"four\n", "four\n",
@ -6277,6 +6292,223 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
} else { } else {
panic!("Unexpected event {event:?}"); 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::<Vec<_>>();
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::<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)
}
})
.collect::<String>();
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<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();
// 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::<Vec<_>>();
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::<Vec<_>>();
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] #[gpui::test]

View file

@ -406,6 +406,7 @@ where
self.seek_internal(pos, bias, &mut (), cx) self.seek_internal(pos, bias, &mut (), cx)
} }
/// Advances the cursor and returns traversed items as a tree.
#[track_caller] #[track_caller]
pub fn slice<Target>( pub fn slice<Target>(
&mut self, &mut self,

View file

@ -225,6 +225,15 @@ impl<T: Item> SumTree<T> {
})) }))
} }
/// 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: &<T::Summary as Summary>::Context) -> Self { pub fn from_item(item: T, cx: &<T::Summary as Summary>::Context) -> Self {
let mut tree = Self::new(cx); let mut tree = Self::new(cx);
tree.push(item, cx); tree.push(item, cx);

View file

@ -136,6 +136,7 @@ where
pub trait AnchorRangeExt { pub trait AnchorRangeExt {
fn cmp(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> Ordering; fn cmp(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> Ordering;
fn overlaps(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> bool;
} }
impl AnchorRangeExt for Range<Anchor> { impl AnchorRangeExt for Range<Anchor> {
@ -145,4 +146,8 @@ impl AnchorRangeExt for Range<Anchor> {
ord => ord, ord => ord,
} }
} }
fn overlaps(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> bool {
self.start.cmp(&other.end, buffer).is_lt() && other.start.cmp(&self.end, buffer).is_lt()
}
} }