Implement staging of partially-staged hunks (#25520)

Closes: #25475 

This PR makes it possible to stage uncommitted hunks that overlap but do
not coincide with an unstaged hunk.

Release Notes:

- Made it possible to stage hunks that are already partially staged

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Max <max@zed.dev>
This commit is contained in:
Cole Miller 2025-02-24 23:13:13 -05:00 committed by GitHub
parent bcbb19e06e
commit 45146b6f30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 432 additions and 179 deletions

View file

@ -13329,7 +13329,7 @@ impl Editor {
snapshot: &MultiBufferSnapshot,
) -> bool {
let mut hunks = self.diff_hunks_in_ranges(ranges, &snapshot);
hunks.any(|hunk| hunk.secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk)
hunks.any(|hunk| hunk.secondary_status != DiffHunkSecondaryStatus::None)
}
pub fn toggle_staged_selected_diff_hunks(
@ -13474,12 +13474,8 @@ impl Editor {
log::debug!("no diff for buffer id");
return;
};
let Some(secondary_diff) = diff.secondary_diff() else {
log::debug!("no secondary diff for buffer id");
return;
};
let edits = diff.secondary_edits_for_stage_or_unstage(
let Some(new_index_text) = diff.new_secondary_text_for_stage_or_unstage(
stage,
hunks.filter_map(|hunk| {
if stage && hunk.secondary_status == DiffHunkSecondaryStatus::None {
@ -13489,29 +13485,14 @@ impl Editor {
{
return None;
}
Some((
hunk.diff_base_byte_range.clone(),
hunk.secondary_diff_base_byte_range.clone(),
hunk.buffer_range.clone(),
))
Some((hunk.buffer_range.clone(), hunk.diff_base_byte_range.clone()))
}),
&buffer_snapshot,
);
let Some(index_base) = secondary_diff
.base_text()
.map(|snapshot| snapshot.text.as_rope().clone())
else {
log::debug!("no index base");
cx,
) else {
log::debug!("missing secondary diff or index text");
return;
};
let index_buffer = cx.new(|cx| {
Buffer::local_normalized(index_base.clone(), text::LineEnding::default(), cx)
});
let new_index_text = index_buffer.update(cx, |index_buffer, cx| {
index_buffer.edit(edits, None, cx);
index_buffer.snapshot().as_rope().to_string()
});
let new_index_text = if new_index_text.is_empty()
&& !stage
&& (diff.is_single_insertion
@ -13531,7 +13512,7 @@ impl Editor {
cx.background_spawn(
repo.read(cx)
.set_index_text(&path, new_index_text)
.set_index_text(&path, new_index_text.map(|rope| rope.to_string()))
.log_err(),
)
.detach();

View file

@ -7,7 +7,7 @@ use crate::{
},
JoinLines,
};
use buffer_diff::{BufferDiff, DiffHunkStatus};
use buffer_diff::{BufferDiff, DiffHunkStatus, DiffHunkStatusKind};
use futures::StreamExt;
use gpui::{
div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
@ -3389,7 +3389,7 @@ async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &m
.unindent(),
);
cx.set_diff_base(&diff_base);
cx.set_head_text(&diff_base);
executor.run_until_parked();
// Join lines
@ -3429,7 +3429,7 @@ async fn test_custom_newlines_cause_no_false_positive_diffs(
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
cx.set_diff_base("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
@ -5811,7 +5811,7 @@ async fn test_fold_function_bodies(cx: &mut TestAppContext) {
let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
cx.set_state(&text);
cx.set_diff_base(&base_text);
cx.set_head_text(&base_text);
cx.update_editor(|editor, window, cx| {
editor.expand_all_diff_hunks(&Default::default(), window, cx);
});
@ -11039,7 +11039,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
.unindent(),
);
cx.set_diff_base(&diff_base);
cx.set_head_text(&diff_base);
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
@ -12531,7 +12531,7 @@ async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
three
"#};
cx.set_diff_base(base_text);
cx.set_head_text(base_text);
cx.set_state("\nˇ\n");
cx.executor().run_until_parked();
cx.update_editor(|editor, _window, cx| {
@ -13168,7 +13168,7 @@ async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut
.unindent(),
);
cx.set_diff_base(&diff_base);
cx.set_head_text(&diff_base);
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
@ -13302,7 +13302,7 @@ async fn test_diff_base_change_with_expanded_diff_hunks(
.unindent(),
);
cx.set_diff_base(&diff_base);
cx.set_head_text(&diff_base);
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
@ -13330,7 +13330,7 @@ async fn test_diff_base_change_with_expanded_diff_hunks(
.unindent(),
);
cx.set_diff_base("new diff base!");
cx.set_head_text("new diff base!");
executor.run_until_parked();
cx.assert_state_with_diff(
r#"
@ -13630,7 +13630,7 @@ async fn test_edits_around_expanded_insertion_hunks(
.unindent(),
);
cx.set_diff_base(&diff_base);
cx.set_head_text(&diff_base);
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
@ -13778,7 +13778,7 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.set_diff_base(indoc! { "
cx.set_head_text(indoc! { "
one
two
three
@ -13901,7 +13901,7 @@ async fn test_edits_around_expanded_deletion_hunks(
.unindent(),
);
cx.set_diff_base(&diff_base);
cx.set_head_text(&diff_base);
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
@ -14024,7 +14024,7 @@ async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &m
.unindent(),
);
cx.set_diff_base(&base_text);
cx.set_head_text(&base_text);
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
@ -14106,7 +14106,7 @@ async fn test_edit_after_expanded_modification_hunk(
.unindent(),
);
cx.set_diff_base(&diff_base);
cx.set_head_text(&diff_base);
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
@ -14841,7 +14841,7 @@ async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestApp
"#
.unindent(),
);
cx.set_diff_base(&diff_base);
cx.set_head_text(&diff_base);
cx.update_editor(|editor, window, cx| {
editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
});
@ -14978,6 +14978,80 @@ async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestApp
);
}
#[gpui::test]
async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.set_head_text(indoc! { "
one
two
three
four
five
"
});
cx.set_index_text(indoc! { "
one
two
three
four
five
"
});
cx.set_state(indoc! {"
one
TWO
ˇTHREE
FOUR
five
"});
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
});
cx.run_until_parked();
cx.assert_index_text(Some(indoc! {"
one
TWO
THREE
FOUR
five
"}));
cx.set_state(indoc! { "
one
TWO
ˇTHREE-HUNDRED
FOUR
five
"});
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
let snapshot = editor.snapshot(window, cx);
let hunks = editor
.diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
.collect::<Vec<_>>();
assert_eq!(hunks.len(), 1);
assert_eq!(
hunks[0].status(),
DiffHunkStatus {
kind: DiffHunkStatusKind::Modified,
secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
}
);
editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
});
cx.run_until_parked();
cx.assert_index_text(Some(indoc! {"
one
TWO
THREE-HUNDRED
FOUR
five
"}));
}
#[gpui::test]
fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@ -16341,7 +16415,7 @@ fn assert_hunk_revert(
cx: &mut EditorLspTestContext,
) {
cx.set_state(not_reverted_text_with_selections);
cx.set_diff_base(base_text);
cx.set_head_text(base_text);
cx.executor().run_until_parked();
let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {

View file

@ -285,7 +285,7 @@ impl EditorTestContext {
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
}
pub fn set_diff_base(&mut self, diff_base: &str) {
pub fn set_head_text(&mut self, diff_base: &str) {
self.cx.run_until_parked();
let fs = self.update_editor(|editor, _, cx| {
editor.project.as_ref().unwrap().read(cx).fs().as_fake()
@ -298,6 +298,19 @@ impl EditorTestContext {
self.cx.run_until_parked();
}
pub fn set_index_text(&mut self, diff_base: &str) {
self.cx.run_until_parked();
let fs = self.update_editor(|editor, _, cx| {
editor.project.as_ref().unwrap().read(cx).fs().as_fake()
});
let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
fs.set_index_for_repo(
&Self::root_path().join(".git"),
&[(path.into(), diff_base.to_string())],
);
self.cx.run_until_parked();
}
#[track_caller]
pub fn assert_index_text(&mut self, expected: Option<&str>) {
let fs = self.update_editor(|editor, _, cx| {