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
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue