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:
parent
18fcdf1d2c
commit
00359271d1
7 changed files with 406 additions and 83 deletions
|
@ -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"] }
|
||||
|
|
|
@ -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<InternalDiffHunk>,
|
||||
pending_hunks: TreeMap<usize, PendingHunk>,
|
||||
pending_hunks: SumTree<PendingHunk>,
|
||||
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<Anchor>,
|
||||
diff_base_byte_range: Range<usize>,
|
||||
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<Rope>, Vec<(usize, PendingHunk)>) {
|
||||
) -> (Option<Rope>, SumTree<PendingHunk>) {
|
||||
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::<DiffHunkSummary>(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::<DiffHunkSummary>(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<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);
|
||||
|
||||
if let Some(secondary_hunk) = skipped_hunks.last() {
|
||||
|
@ -294,22 +353,15 @@ impl BufferDiffInner {
|
|||
.chunks_in_range(diff_base_byte_range.clone())
|
||||
.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));
|
||||
}
|
||||
|
||||
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::<DiffHunkSummary>(buffer);
|
||||
cursor.next(buffer);
|
||||
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();
|
||||
|
@ -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<Self>) {
|
||||
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<Self>,
|
||||
) -> 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,
|
||||
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<Iter>(
|
||||
diff_hunks: Iter,
|
||||
pub fn assert_hunks<ExpectedText, HunkIter>(
|
||||
diff_hunks: HunkIter,
|
||||
buffer: &text::BufferSnapshot,
|
||||
diff_base: &str,
|
||||
expected_hunks: &[(Range<u32>, &str, &str, DiffHunkStatus)],
|
||||
expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
|
||||
) where
|
||||
Iter: Iterator<Item = DiffHunk>,
|
||||
HunkIter: Iterator<Item = DiffHunk>,
|
||||
ExpectedText: AsRef<str>,
|
||||
{
|
||||
let actual_hunks = diff_hunks
|
||||
.map(|hunk| {
|
||||
|
@ -1197,14 +1270,14 @@ pub fn assert_hunks<Iter>(
|
|||
.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::<Vec<_>>();
|
||||
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
|
||||
}
|
||||
|
|
|
@ -278,7 +278,7 @@ impl ProjectDiff {
|
|||
has_staged_hunks = true;
|
||||
has_unstaged_hunks = true;
|
||||
}
|
||||
DiffHunkSecondaryStatus::None
|
||||
DiffHunkSecondaryStatus::NoSecondaryHunk
|
||||
| DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => {
|
||||
has_staged_hunks = true;
|
||||
}
|
||||
|
|
|
@ -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::<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]
|
||||
|
|
|
@ -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<Target>(
|
||||
&mut self,
|
||||
|
|
|
@ -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 {
|
||||
let mut tree = Self::new(cx);
|
||||
tree.push(item, cx);
|
||||
|
|
|
@ -136,6 +136,7 @@ where
|
|||
|
||||
pub trait AnchorRangeExt {
|
||||
fn cmp(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> Ordering;
|
||||
fn overlaps(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> bool;
|
||||
}
|
||||
|
||||
impl AnchorRangeExt for Range<Anchor> {
|
||||
|
@ -145,4 +146,8 @@ impl AnchorRangeExt for Range<Anchor> {
|
|||
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()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue