Implement staging and unstaging hunks (#24606)
- [x] Staging hunks - [x] Unstaging hunks - [x] Write a randomized test - [x] Get test passing - [x] Fix existing bug in diff_base_byte_range computation - [x] Remote project support - [ ] ~~Improve performance of buffer_range_to_unchanged_diff_base_range~~ - [ ] ~~Bug: project diff editor scrolls to top when staging/unstaging hunk~~ existing issue - [ ] ~~UI~~ deferred - [x] Tricky cases - [x] Correctly handle acting on multiple hunks for a single file - [x] Remove path from index when unstaging the last staged hunk, if it's absent from HEAD, or staging the only hunk, if it's deleted in the working copy Release Notes: - Add `ToggleStagedSelectedDiffHunks` action for staging and unstaging individual diff hunks
This commit is contained in:
parent
ea8da43c6b
commit
eea6b526dc
18 changed files with 768 additions and 70 deletions
|
@ -402,6 +402,7 @@ gpui::actions!(
|
|||
ToggleInlayHints,
|
||||
ToggleEditPrediction,
|
||||
ToggleLineNumbers,
|
||||
ToggleStagedSelectedDiffHunks,
|
||||
SwapSelectionEnds,
|
||||
SetMark,
|
||||
ToggleRelativeLineNumbers,
|
||||
|
|
|
@ -52,6 +52,7 @@ pub use actions::{AcceptEditPrediction, OpenExcerpts, OpenExcerptsSplit};
|
|||
use aho_corasick::AhoCorasick;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use blink_manager::BlinkManager;
|
||||
use buffer_diff::DiffHunkSecondaryStatus;
|
||||
use client::{Collaborator, ParticipantIndex};
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
||||
|
@ -95,7 +96,7 @@ use itertools::Itertools;
|
|||
use language::{
|
||||
language_settings::{self, all_language_settings, language_settings, InlayHintSettings},
|
||||
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel,
|
||||
CompletionDocumentation, CursorShape, Diagnostic, EditPredictionsMode, EditPreview,
|
||||
CompletionDocumentation, CursorShape, Diagnostic, DiskState, EditPredictionsMode, EditPreview,
|
||||
HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection,
|
||||
SelectionGoal, TextObject, TransactionId, TreeSitterOptions,
|
||||
};
|
||||
|
@ -12431,6 +12432,121 @@ impl Editor {
|
|||
self.toggle_diff_hunks_in_ranges(ranges, cx);
|
||||
}
|
||||
|
||||
fn diff_hunks_in_ranges<'a>(
|
||||
&'a self,
|
||||
ranges: &'a [Range<Anchor>],
|
||||
buffer: &'a MultiBufferSnapshot,
|
||||
) -> impl 'a + Iterator<Item = MultiBufferDiffHunk> {
|
||||
ranges.iter().flat_map(move |range| {
|
||||
let end_excerpt_id = range.end.excerpt_id;
|
||||
let range = range.to_point(buffer);
|
||||
let mut peek_end = range.end;
|
||||
if range.end.row < buffer.max_row().0 {
|
||||
peek_end = Point::new(range.end.row + 1, 0);
|
||||
}
|
||||
buffer
|
||||
.diff_hunks_in_range(range.start..peek_end)
|
||||
.filter(move |hunk| hunk.excerpt_id.cmp(&end_excerpt_id, buffer).is_le())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn has_stageable_diff_hunks_in_ranges(
|
||||
&self,
|
||||
ranges: &[Range<Anchor>],
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
) -> bool {
|
||||
let mut hunks = self.diff_hunks_in_ranges(ranges, &snapshot);
|
||||
hunks.any(|hunk| {
|
||||
log::debug!("considering {hunk:?}");
|
||||
hunk.secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk
|
||||
})
|
||||
}
|
||||
|
||||
pub fn toggle_staged_selected_diff_hunks(
|
||||
&mut self,
|
||||
_: &ToggleStagedSelectedDiffHunks,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect();
|
||||
self.stage_or_unstage_diff_hunks(&ranges, cx);
|
||||
}
|
||||
|
||||
pub fn stage_or_unstage_diff_hunks(
|
||||
&mut self,
|
||||
ranges: &[Range<Anchor>],
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(project) = &self.project else {
|
||||
return;
|
||||
};
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let stage = self.has_stageable_diff_hunks_in_ranges(ranges, &snapshot);
|
||||
|
||||
let chunk_by = self
|
||||
.diff_hunks_in_ranges(&ranges, &snapshot)
|
||||
.chunk_by(|hunk| hunk.buffer_id);
|
||||
for (buffer_id, hunks) in &chunk_by {
|
||||
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else {
|
||||
log::debug!("no buffer for id");
|
||||
continue;
|
||||
};
|
||||
let buffer = buffer.read(cx).snapshot();
|
||||
let Some((repo, path)) = project
|
||||
.read(cx)
|
||||
.repository_and_path_for_buffer_id(buffer_id, cx)
|
||||
else {
|
||||
log::debug!("no git repo for buffer id");
|
||||
continue;
|
||||
};
|
||||
let Some(diff) = snapshot.diff_for_buffer_id(buffer_id) else {
|
||||
log::debug!("no diff for buffer id");
|
||||
continue;
|
||||
};
|
||||
let Some(secondary_diff) = diff.secondary_diff() else {
|
||||
log::debug!("no secondary diff for buffer id");
|
||||
continue;
|
||||
};
|
||||
|
||||
let edits = diff.secondary_edits_for_stage_or_unstage(
|
||||
stage,
|
||||
hunks.map(|hunk| {
|
||||
(
|
||||
hunk.diff_base_byte_range.clone(),
|
||||
hunk.secondary_diff_base_byte_range.clone(),
|
||||
hunk.buffer_range.clone(),
|
||||
)
|
||||
}),
|
||||
&buffer,
|
||||
);
|
||||
|
||||
let index_base = secondary_diff.base_text().map_or_else(
|
||||
|| Rope::from(""),
|
||||
|snapshot| snapshot.text.as_rope().clone(),
|
||||
);
|
||||
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()
|
||||
&& (diff.is_single_insertion
|
||||
|| buffer
|
||||
.file()
|
||||
.map_or(false, |file| file.disk_state() == DiskState::New))
|
||||
{
|
||||
log::debug!("removing from index");
|
||||
None
|
||||
} else {
|
||||
Some(new_index_text)
|
||||
};
|
||||
|
||||
let _ = repo.read(cx).set_index_text(&path, new_index_text);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context<Self>) {
|
||||
let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect();
|
||||
self.buffer
|
||||
|
|
|
@ -14047,6 +14047,59 @@ async fn test_edit_after_expanded_modification_hunk(
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_stage_and_unstage_added_file_hunk(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
editor.set_expand_all_diff_hunks(cx);
|
||||
});
|
||||
|
||||
let working_copy = r#"
|
||||
ˇfn main() {
|
||||
println!("hello, world!");
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
cx.set_state(&working_copy);
|
||||
executor.run_until_parked();
|
||||
|
||||
cx.assert_state_with_diff(
|
||||
r#"
|
||||
+ ˇfn main() {
|
||||
+ println!("hello, world!");
|
||||
+ }
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
cx.assert_index_text(None);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.toggle_staged_selected_diff_hunks(&ToggleStagedSelectedDiffHunks, window, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
|
||||
cx.assert_state_with_diff(
|
||||
r#"
|
||||
+ ˇfn main() {
|
||||
+ println!("hello, world!");
|
||||
+ }
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.toggle_staged_selected_diff_hunks(&ToggleStagedSelectedDiffHunks, window, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
cx.assert_index_text(None);
|
||||
}
|
||||
|
||||
async fn setup_indent_guides_editor(
|
||||
text: &str,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
|
|
|
@ -417,7 +417,9 @@ impl EditorElement {
|
|||
register_action(editor, window, Editor::toggle_git_blame);
|
||||
register_action(editor, window, Editor::toggle_git_blame_inline);
|
||||
register_action(editor, window, Editor::toggle_selected_diff_hunks);
|
||||
register_action(editor, window, Editor::toggle_staged_selected_diff_hunks);
|
||||
register_action(editor, window, Editor::expand_all_diff_hunks);
|
||||
|
||||
register_action(editor, window, |editor, action, window, cx| {
|
||||
if let Some(task) = editor.format(action, window, cx) {
|
||||
task.detach_and_notify_err(window, cx);
|
||||
|
|
|
@ -298,6 +298,18 @@ impl EditorTestContext {
|
|||
self.cx.run_until_parked();
|
||||
}
|
||||
|
||||
pub fn assert_index_text(&mut self, expected: Option<&str>) {
|
||||
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());
|
||||
let mut found = None;
|
||||
fs.with_git_state(&Self::root_path().join(".git"), false, |git_state| {
|
||||
found = git_state.index_contents.get(path.as_ref()).cloned();
|
||||
});
|
||||
assert_eq!(expected, found.as_deref());
|
||||
}
|
||||
|
||||
/// Change the editor's text and selections using a string containing
|
||||
/// embedded range markers that represent the ranges and directions of
|
||||
/// each selection.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue