Fix crash when toggling deleted hunk (#27138)
Release Notes: - Fix rare crash when toggling deleted hunks. --------- Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
parent
11552cc0bd
commit
bbc7fcc54f
4 changed files with 94 additions and 29 deletions
|
@ -7,8 +7,7 @@ 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;
|
use sum_tree::SumTree;
|
||||||
use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point};
|
use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point, ToOffset as _};
|
||||||
use text::{AnchorRangeExt, ToOffset as _};
|
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
pub struct BufferDiff {
|
pub struct BufferDiff {
|
||||||
|
@ -189,7 +188,7 @@ impl BufferDiffSnapshot {
|
||||||
|
|
||||||
impl BufferDiffInner {
|
impl BufferDiffInner {
|
||||||
/// Returns the new index text and new pending hunks.
|
/// Returns the new index text and new pending hunks.
|
||||||
fn stage_or_unstage_hunks(
|
fn stage_or_unstage_hunks_impl(
|
||||||
&mut self,
|
&mut self,
|
||||||
unstaged_diff: &Self,
|
unstaged_diff: &Self,
|
||||||
stage: bool,
|
stage: bool,
|
||||||
|
@ -234,9 +233,6 @@ impl BufferDiffInner {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
|
|
||||||
unstaged_hunk_cursor.next(buffer);
|
|
||||||
|
|
||||||
let mut pending_hunks = SumTree::new(buffer);
|
let mut pending_hunks = SumTree::new(buffer);
|
||||||
let mut old_pending_hunks = unstaged_diff
|
let mut old_pending_hunks = unstaged_diff
|
||||||
.pending_hunks
|
.pending_hunks
|
||||||
|
@ -252,18 +248,16 @@ impl BufferDiffInner {
|
||||||
{
|
{
|
||||||
let preceding_pending_hunks =
|
let preceding_pending_hunks =
|
||||||
old_pending_hunks.slice(&buffer_range.start, Bias::Left, buffer);
|
old_pending_hunks.slice(&buffer_range.start, Bias::Left, buffer);
|
||||||
|
|
||||||
pending_hunks.append(preceding_pending_hunks, buffer);
|
pending_hunks.append(preceding_pending_hunks, buffer);
|
||||||
|
|
||||||
// skip all overlapping old pending hunks
|
// Skip all overlapping or adjacent old pending hunks
|
||||||
while old_pending_hunks
|
while old_pending_hunks.item().is_some_and(|old_hunk| {
|
||||||
.item()
|
old_hunk
|
||||||
.is_some_and(|preceding_pending_hunk_item| {
|
.buffer_range
|
||||||
preceding_pending_hunk_item
|
.start
|
||||||
.buffer_range
|
.cmp(&buffer_range.end, buffer)
|
||||||
.overlaps(&buffer_range, buffer)
|
.is_le()
|
||||||
})
|
}) {
|
||||||
{
|
|
||||||
old_pending_hunks.next(buffer);
|
old_pending_hunks.next(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -291,6 +285,9 @@ impl BufferDiffInner {
|
||||||
// append the remainder
|
// append the remainder
|
||||||
pending_hunks.append(old_pending_hunks.suffix(buffer), buffer);
|
pending_hunks.append(old_pending_hunks.suffix(buffer), buffer);
|
||||||
|
|
||||||
|
let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
|
||||||
|
unstaged_hunk_cursor.next(buffer);
|
||||||
|
|
||||||
let mut prev_unstaged_hunk_buffer_offset = 0;
|
let mut prev_unstaged_hunk_buffer_offset = 0;
|
||||||
let mut prev_unstaged_hunk_base_text_offset = 0;
|
let mut prev_unstaged_hunk_base_text_offset = 0;
|
||||||
let mut edits = Vec::<(Range<usize>, String)>::new();
|
let mut edits = Vec::<(Range<usize>, String)>::new();
|
||||||
|
@ -357,7 +354,13 @@ impl BufferDiffInner {
|
||||||
edits.push((index_range, replacement_text));
|
edits.push((index_range, replacement_text));
|
||||||
}
|
}
|
||||||
|
|
||||||
debug_assert!(edits.iter().is_sorted_by_key(|(range, _)| range.start));
|
#[cfg(debug_assertions)] // invariants: non-overlapping and sorted
|
||||||
|
{
|
||||||
|
for window in edits.windows(2) {
|
||||||
|
let (range_a, range_b) = (&window[0].0, &window[1].0);
|
||||||
|
debug_assert!(range_a.end < range_b.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);
|
||||||
|
@ -854,7 +857,7 @@ impl BufferDiff {
|
||||||
file_exists: bool,
|
file_exists: bool,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Option<Rope> {
|
) -> Option<Rope> {
|
||||||
let (new_index_text, new_pending_hunks) = self.inner.stage_or_unstage_hunks(
|
let (new_index_text, new_pending_hunks) = self.inner.stage_or_unstage_hunks_impl(
|
||||||
&self.secondary_diff.as_ref()?.read(cx).inner,
|
&self.secondary_diff.as_ref()?.read(cx).inner,
|
||||||
stage,
|
stage,
|
||||||
&hunks,
|
&hunks,
|
||||||
|
@ -1240,13 +1243,13 @@ impl DiffHunkStatus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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<ExpectedText, HunkIter>(
|
pub fn assert_hunks<ExpectedText, HunkIter>(
|
||||||
diff_hunks: HunkIter,
|
diff_hunks: HunkIter,
|
||||||
buffer: &text::BufferSnapshot,
|
buffer: &text::BufferSnapshot,
|
||||||
diff_base: &str,
|
diff_base: &str,
|
||||||
|
// Line range, deleted, added, status
|
||||||
expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
|
expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
|
||||||
) where
|
) where
|
||||||
HunkIter: Iterator<Item = DiffHunk>,
|
HunkIter: Iterator<Item = DiffHunk>,
|
||||||
|
@ -1267,11 +1270,11 @@ pub fn assert_hunks<ExpectedText, HunkIter>(
|
||||||
|
|
||||||
let expected_hunks: Vec<_> = expected_hunks
|
let expected_hunks: Vec<_> = expected_hunks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(r, old_text, new_text, status)| {
|
.map(|(line_range, deleted_text, added_text, status)| {
|
||||||
(
|
(
|
||||||
Point::new(r.start, 0)..Point::new(r.end, 0),
|
Point::new(line_range.start, 0)..Point::new(line_range.end, 0),
|
||||||
old_text.as_ref(),
|
deleted_text.as_ref(),
|
||||||
new_text.as_ref().to_string(),
|
added_text.as_ref().to_string(),
|
||||||
*status,
|
*status,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -1286,6 +1289,7 @@ mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
|
use pretty_assertions::{assert_eq, assert_ne};
|
||||||
use rand::{rngs::StdRng, Rng as _};
|
use rand::{rngs::StdRng, Rng as _};
|
||||||
use text::{Buffer, BufferId, Rope};
|
use text::{Buffer, BufferId, Rope};
|
||||||
use unindent::Unindent as _;
|
use unindent::Unindent as _;
|
||||||
|
@ -1705,6 +1709,66 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) {
|
||||||
|
let head_text = "
|
||||||
|
one
|
||||||
|
two
|
||||||
|
three
|
||||||
|
"
|
||||||
|
.unindent();
|
||||||
|
let index_text = head_text.clone();
|
||||||
|
let buffer_text = "
|
||||||
|
one
|
||||||
|
three
|
||||||
|
"
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text.clone());
|
||||||
|
let unstaged = BufferDiff::build_sync(buffer.clone(), index_text, cx);
|
||||||
|
let uncommitted = BufferDiff::build_sync(buffer.clone(), head_text.clone(), cx);
|
||||||
|
let unstaged_diff = cx.new(|cx| {
|
||||||
|
let mut diff = BufferDiff::new(&buffer, cx);
|
||||||
|
diff.set_state(unstaged, &buffer);
|
||||||
|
diff
|
||||||
|
});
|
||||||
|
let uncommitted_diff = cx.new(|cx| {
|
||||||
|
let mut diff = BufferDiff::new(&buffer, cx);
|
||||||
|
diff.set_state(uncommitted, &buffer);
|
||||||
|
diff.set_secondary_diff(unstaged_diff.clone());
|
||||||
|
diff
|
||||||
|
});
|
||||||
|
|
||||||
|
uncommitted_diff.update(cx, |diff, cx| {
|
||||||
|
let hunk = diff.hunks(&buffer, cx).next().unwrap();
|
||||||
|
|
||||||
|
let new_index_text = diff
|
||||||
|
.stage_or_unstage_hunks(true, &[hunk.clone()], &buffer, true, cx)
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
assert_eq!(new_index_text, buffer_text);
|
||||||
|
|
||||||
|
let hunk = diff.hunks(&buffer, &cx).next().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
hunk.secondary_status,
|
||||||
|
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
|
||||||
|
);
|
||||||
|
|
||||||
|
let index_text = diff
|
||||||
|
.stage_or_unstage_hunks(false, &[hunk], &buffer, true, cx)
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
assert_eq!(index_text, head_text);
|
||||||
|
|
||||||
|
let hunk = diff.hunks(&buffer, &cx).next().unwrap();
|
||||||
|
// optimistically unstaged (fine, could also be HasSecondaryHunk)
|
||||||
|
assert_eq!(
|
||||||
|
hunk.secondary_status,
|
||||||
|
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
|
async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
|
||||||
let base_text = "
|
let base_text = "
|
||||||
|
|
|
@ -1176,6 +1176,11 @@ impl FakeFs {
|
||||||
self.state.lock().events_paused = true;
|
self.state.lock().events_paused = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn unpause_events_and_flush(&self) {
|
||||||
|
self.state.lock().events_paused = false;
|
||||||
|
self.flush_events(usize::MAX);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn buffered_event_count(&self) -> usize {
|
pub fn buffered_event_count(&self) -> usize {
|
||||||
self.state.lock().buffered_events.len()
|
self.state.lock().buffered_events.len()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
// #![allow(unused, dead_code)]
|
|
||||||
|
|
||||||
use crate::branch_picker::{self, BranchList};
|
use crate::branch_picker::{self, BranchList};
|
||||||
use crate::git_panel::{commit_message_editor, GitPanel};
|
use crate::git_panel::{commit_message_editor, GitPanel};
|
||||||
use git::{Commit, GenerateCommitMessage};
|
use git::{Commit, GenerateCommitMessage};
|
||||||
|
|
|
@ -24,9 +24,7 @@ use pretty_assertions::{assert_eq, assert_matches};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
use std::os;
|
use std::os;
|
||||||
use std::{str::FromStr, sync::OnceLock};
|
use std::{mem, num::NonZeroU32, ops::Range, str::FromStr, sync::OnceLock, task::Poll};
|
||||||
|
|
||||||
use std::{mem, num::NonZeroU32, ops::Range, task::Poll};
|
|
||||||
use task::{ResolvedTask, TaskContext};
|
use task::{ResolvedTask, TaskContext};
|
||||||
use unindent::Unindent as _;
|
use unindent::Unindent as _;
|
||||||
use util::{
|
use util::{
|
||||||
|
@ -6359,7 +6357,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 10, seeds(340, 472))]
|
#[gpui::test(seeds(340, 472))]
|
||||||
async fn test_staging_hunks_with_delayed_fs_event(cx: &mut gpui::TestAppContext) {
|
async fn test_staging_hunks_with_delayed_fs_event(cx: &mut gpui::TestAppContext) {
|
||||||
use DiffHunkSecondaryStatus::*;
|
use DiffHunkSecondaryStatus::*;
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue