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
|
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"] }
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue