Git actions v2 (#25197)

Closes #ISSUE

Release Notes:

- Rename `editor::RevertSelectedHunks` and `editor::RevertFile` to
`git::Restore` and `git::RestoreFile` for consistency with git
This commit is contained in:
Conrad Irwin 2025-02-19 21:22:31 -07:00 committed by GitHub
parent e3836712d6
commit d0f7dede79
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 195 additions and 1688 deletions

View file

@ -116,7 +116,7 @@
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
"ctrl-alt-space": "editor::ShowCharacterPalette",
"ctrl-;": "editor::ToggleLineNumbers",
"ctrl-k ctrl-r": "editor::RevertSelectedHunks",
"ctrl-k ctrl-r": "git::Restore",
"ctrl-'": "editor::ToggleSelectedDiffHunks",
"ctrl-\"": "editor::ExpandAllHunkDiffs",
"ctrl-i": "editor::ShowSignatureHelp",

View file

@ -126,7 +126,10 @@
"ctrl-shift-v": ["editor::MovePageUp", { "center_cursor": true }],
"ctrl-cmd-space": "editor::ShowCharacterPalette",
"cmd-;": "editor::ToggleLineNumbers",
"cmd-alt-z": "editor::RevertSelectedHunks",
"cmd-alt-z": "git::Restore",
"cmd-alt-y": "git::ToggleStaged",
"cmd-y": "git::StageAndNext",
"cmd-shift-y": "git::UnstageAndNext",
"cmd-'": "editor::ToggleSelectedDiffHunks",
"cmd-\"": "editor::ExpandAllHunkDiffs",
"cmd-alt-g b": "editor::ToggleGitBlame",

View file

@ -44,7 +44,7 @@
"shift-f2": "editor::GoToPrevDiagnostic",
"ctrl-alt-shift-down": "editor::GoToHunk",
"ctrl-alt-shift-up": "editor::GoToPrevHunk",
"ctrl-alt-z": "editor::RevertSelectedHunks",
"ctrl-alt-z": "git::Restore",
"ctrl-home": "editor::MoveToBeginning",
"ctrl-end": "editor::MoveToEnd",
"ctrl-shift-home": "editor::SelectToBeginning",

View file

@ -446,7 +446,7 @@
"d": "vim::CurrentLine",
"s": "vim::PushDeleteSurrounds",
"o": "editor::ToggleSelectedDiffHunks", // "d o"
"p": "editor::RevertSelectedHunks" // "d p"
"p": "git::Restore" // "d p"
}
},
{

View file

@ -357,7 +357,6 @@ gpui::actions!(
ReverseLines,
RevertFile,
ReloadFile,
RevertSelectedHunks,
Rewrap,
ScrollCursorBottom,
ScrollCursorCenter,
@ -400,7 +399,6 @@ gpui::actions!(
ToggleInlayHints,
ToggleEditPrediction,
ToggleLineNumbers,
ToggleStagedSelectedDiffHunks,
SwapSelectionEnds,
SetMark,
ToggleRelativeLineNumbers,

View file

@ -73,6 +73,7 @@ use futures::{
};
use fuzzy::StringMatchCandidate;
use ::git::Restore;
use code_context_menus::{
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
CompletionsMenu, ContextMenuOrigin,
@ -7025,21 +7026,6 @@ impl Editor {
})
}
pub fn revert_file(&mut self, _: &RevertFile, window: &mut Window, cx: &mut Context<Self>) {
let mut revert_changes = HashMap::default();
let snapshot = self.snapshot(window, cx);
for hunk in snapshot
.hunks_for_ranges(Some(Point::zero()..snapshot.buffer_snapshot.max_point()).into_iter())
{
self.prepare_revert_change(&mut revert_changes, &hunk, cx);
}
if !revert_changes.is_empty() {
self.transact(window, cx, |editor, window, cx| {
editor.revert(revert_changes, window, cx);
});
}
}
pub fn reload_file(&mut self, _: &ReloadFile, window: &mut Window, cx: &mut Context<Self>) {
let Some(project) = self.project.clone() else {
return;
@ -7048,27 +7034,62 @@ impl Editor {
.detach_and_notify_err(window, cx);
}
pub fn revert_selected_hunks(
pub fn restore_file(
&mut self,
_: &RevertSelectedHunks,
_: &::git::RestoreFile,
window: &mut Window,
cx: &mut Context<Self>,
) {
let selections = self.selections.all(cx).into_iter().map(|s| s.range());
self.revert_hunks_in_ranges(selections, window, cx);
let mut buffer_ids = HashSet::default();
let snapshot = self.buffer().read(cx).snapshot(cx);
for selection in self.selections.all::<usize>(cx) {
buffer_ids.extend(snapshot.buffer_ids_for_range(selection.range()))
}
let buffer = self.buffer().read(cx);
let ranges = buffer_ids
.into_iter()
.flat_map(|buffer_id| buffer.excerpt_ranges_for_buffer(buffer_id, cx))
.collect::<Vec<_>>();
self.restore_hunks_in_ranges(ranges, window, cx);
}
fn revert_hunks_in_ranges(
pub fn git_restore(&mut self, _: &Restore, window: &mut Window, cx: &mut Context<Self>) {
let selections = self
.selections
.all(cx)
.into_iter()
.map(|s| s.range())
.collect();
self.restore_hunks_in_ranges(selections, window, cx);
}
fn restore_hunks_in_ranges(
&mut self,
ranges: impl Iterator<Item = Range<Point>>,
ranges: Vec<Range<Point>>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let mut revert_changes = HashMap::default();
let snapshot = self.snapshot(window, cx);
for hunk in &snapshot.hunks_for_ranges(ranges) {
self.prepare_revert_change(&mut revert_changes, &hunk, cx);
let snapshot = self.buffer.read(cx).snapshot(cx);
let Some(project) = &self.project else {
return;
};
let chunk_by = self
.snapshot(window, cx)
.hunks_for_ranges(ranges.into_iter())
.into_iter()
.chunk_by(|hunk| hunk.buffer_id);
for (buffer_id, hunks) in &chunk_by {
let hunks = hunks.collect::<Vec<_>>();
for hunk in &hunks {
self.prepare_restore_change(&mut revert_changes, hunk, cx);
}
Self::do_stage_or_unstage(project, false, buffer_id, hunks.into_iter(), &snapshot, cx);
}
drop(chunk_by);
if !revert_changes.is_empty() {
self.transact(window, cx, |editor, window, cx| {
editor.revert(revert_changes, window, cx);
@ -7098,7 +7119,7 @@ impl Editor {
}
}
pub fn prepare_revert_change(
pub fn prepare_restore_change(
&self,
revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
hunk: &MultiBufferDiffHunk,
@ -12576,89 +12597,134 @@ impl Editor {
pub fn toggle_staged_selected_diff_hunks(
&mut self,
_: &ToggleStagedSelectedDiffHunks,
_: &::git::ToggleStaged,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect();
self.stage_or_unstage_diff_hunks(&ranges, cx);
let stage = self.has_stageable_diff_hunks_in_ranges(&ranges, &snapshot);
self.stage_or_unstage_diff_hunks(stage, &ranges, cx);
}
pub fn stage_and_next(
&mut self,
_: &::git::StageAndNext,
window: &mut Window,
cx: &mut Context<Self>,
) {
let head = self.selections.newest_anchor().head();
self.stage_or_unstage_diff_hunks(true, &[head..head], cx);
self.go_to_next_hunk(&Default::default(), window, cx);
}
pub fn unstage_and_next(
&mut self,
_: &::git::UnstageAndNext,
window: &mut Window,
cx: &mut Context<Self>,
) {
let head = self.selections.newest_anchor().head();
self.stage_or_unstage_diff_hunks(false, &[head..head], cx);
self.go_to_next_hunk(&Default::default(), window, cx);
}
pub fn stage_or_unstage_diff_hunks(
&mut self,
stage: bool,
ranges: &[Range<Anchor>],
cx: &mut Context<Self>,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
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);
Self::do_stage_or_unstage(project, stage, buffer_id, hunks, &snapshot, cx);
}
}
fn do_stage_or_unstage(
project: &Entity<Project>,
stage: bool,
buffer_id: BufferId,
hunks: impl Iterator<Item = MultiBufferDiffHunk>,
snapshot: &MultiBufferSnapshot,
cx: &mut Context<Self>,
) {
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else {
log::debug!("no buffer for id");
return;
};
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");
return;
};
let Some(diff) = snapshot.diff_for_buffer_id(buffer_id) else {
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(
stage,
hunks.filter_map(|hunk| {
if stage && hunk.secondary_status == DiffHunkSecondaryStatus::None {
return None;
} else if !stage
&& hunk.secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk
{
return None;
}
Some((
hunk.diff_base_byte_range.clone(),
hunk.secondary_diff_base_byte_range.clone(),
hunk.buffer_range.clone(),
))
}),
&buffer,
);
let Some(index_base) = secondary_diff
.base_text()
.map(|snapshot| snapshot.text.as_rope().clone())
else {
log::debug!("no index base");
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()
&& (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

View file

@ -12718,7 +12718,10 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
multibuffer
});
let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [path!("/").as_ref()], cx).await;
let (editor, cx) = cx
.add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
editor.update_in(cx, |editor, _window, cx| {
for (buffer, diff_base) in [
(buffer_1.clone(), base_text_1),
@ -12736,7 +12739,7 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
editor.update_in(cx, |editor, window, cx| {
assert_eq!(editor.text(cx), "Xaaa\nXbbb\nXccc\n\nXfff\nXggg\n\nXjjj\nXlll\nXmmm\nXnnn\n\nXqqq\nXrrr\n\nXuuu\nXvvv\nXwww\nXxxx\n\nX{{{\nX|||\n\nX\u{7f}\u{7f}\u{7f}");
editor.select_all(&SelectAll, window, cx);
editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
editor.git_restore(&Default::default(), window, cx);
});
cx.executor().run_until_parked();
@ -12762,7 +12765,7 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
editor.change_selections(None, window, cx, |s| {
s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
});
editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
editor.git_restore(&Default::default(), window, cx);
});
// Now, when all ranges selected belong to buffer_1, the revert should succeed,
@ -14157,7 +14160,7 @@ async fn test_stage_and_unstage_added_file_hunk(
cx.assert_index_text(None);
cx.update_editor(|editor, window, cx| {
editor.toggle_staged_selected_diff_hunks(&ToggleStagedSelectedDiffHunks, window, cx);
editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
});
executor.run_until_parked();
cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
@ -14171,7 +14174,7 @@ async fn test_stage_and_unstage_added_file_hunk(
);
cx.update_editor(|editor, window, cx| {
editor.toggle_staged_selected_diff_hunks(&ToggleStagedSelectedDiffHunks, window, cx);
editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
});
executor.run_until_parked();
cx.assert_index_text(None);
@ -16103,7 +16106,7 @@ fn assert_hunk_revert(
.map(|hunk| hunk.status())
.collect::<Vec<_>>();
editor.revert_selected_hunks(&RevertSelectedHunks, window, cx);
editor.git_restore(&Default::default(), window, cx);
reverted_hunk_statuses
});
cx.executor().run_until_parked();

View file

@ -19,11 +19,10 @@ use crate::{
DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GoToHunk,
GoToPrevHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
InlineCompletion, JumpData, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point,
RevertSelectedHunks, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection,
SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS,
CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
InlineCompletion, JumpData, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt,
RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap, StickyHeaderExcerpt,
ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT,
GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
};
use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
use client::ParticipantIndex;
@ -418,6 +417,8 @@ impl EditorElement {
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::stage_and_next);
register_action(editor, window, Editor::unstage_and_next);
register_action(editor, window, Editor::expand_all_diff_hunks);
register_action(editor, window, |editor, action, window, cx| {
@ -491,8 +492,8 @@ impl EditorElement {
register_action(editor, window, Editor::unique_lines_case_sensitive);
register_action(editor, window, Editor::accept_partial_inline_completion);
register_action(editor, window, Editor::accept_edit_prediction);
register_action(editor, window, Editor::revert_file);
register_action(editor, window, Editor::revert_selected_hunks);
register_action(editor, window, Editor::restore_file);
register_action(editor, window, Editor::git_restore);
register_action(editor, window, Editor::apply_all_diff_hunks);
register_action(editor, window, Editor::apply_selected_diff_hunks);
register_action(editor, window, Editor::open_active_item_in_terminal);
@ -9004,8 +9005,8 @@ fn diff_hunk_controls(
let focus_handle = editor.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"Discard Hunk",
&RevertSelectedHunks,
"Restore Hunk",
&::git::Restore,
&focus_handle,
window,
cx,
@ -9018,7 +9019,7 @@ fn diff_hunk_controls(
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
let point = hunk_range.start.to_point(&snapshot.buffer_snapshot);
editor.revert_hunks_in_ranges([point..point].into_iter(), window, cx);
editor.restore_hunks_in_ranges(vec![point..point], window, cx);
});
}
}),

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,7 @@ pub mod repository;
pub mod status;
use anyhow::{anyhow, Context as _, Result};
use gpui::action_with_deprecated_aliases;
use gpui::actions;
use serde::{Deserialize, Serialize};
use std::ffi::OsStr;
@ -29,21 +30,24 @@ pub static INDEX_LOCK: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("i
actions!(
git,
[
// per-hunk
ToggleStaged,
StageAndNext,
UnstageAndNext,
// per-file
StageFile,
UnstageFile,
ToggleStaged,
// Revert actions are currently in the editor crate:
// editor::RevertFile,
// editor::RevertSelectedHunks
// repo-wide
StageAll,
UnstageAll,
DiscardTrackedChanges,
RestoreTrackedFiles,
TrashUntrackedFiles,
Uncommit,
Commit,
ClearCommitMessage
]
);
action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);
action_with_deprecated_aliases!(git, Restore, ["editor::RevertSelectedHunks"]);
/// The length of a Git short SHA.
pub const SHORT_SHA_LENGTH: usize = 7;

View file

@ -13,7 +13,7 @@ use editor::{
};
use git::repository::{CommitDetails, ResetMode};
use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
use git::{DiscardTrackedChanges, StageAll, TrashUntrackedFiles, UnstageAll};
use git::{RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
use gpui::*;
use itertools::Itertools;
use language::{Buffer, File};
@ -625,7 +625,7 @@ impl GitPanel {
fn revert_selected(
&mut self,
_: &editor::actions::RevertFile,
_: &git::RestoreFile,
window: &mut Window,
cx: &mut Context<Self>,
) {
@ -763,7 +763,7 @@ impl GitPanel {
fn discard_tracked_changes(
&mut self,
_: &DiscardTrackedChanges,
_: &RestoreTrackedFiles,
window: &mut Window,
cx: &mut Context<Self>,
) {
@ -2019,7 +2019,7 @@ impl GitPanel {
let context_menu = ContextMenu::build(window, cx, |context_menu, _, _| {
context_menu
.action("Stage File", ToggleStaged.boxed_clone())
.action(revert_title, editor::actions::RevertFile.boxed_clone())
.action(revert_title, git::RestoreFile.boxed_clone())
.separator()
.action("Open Diff", Confirm.boxed_clone())
.action("Open File", SecondaryConfirm.boxed_clone())
@ -2040,10 +2040,7 @@ impl GitPanel {
.action("Unstage All", UnstageAll.boxed_clone())
.action("Open Diff", project_diff::Diff.boxed_clone())
.separator()
.action(
"Discard Tracked Changes",
DiscardTrackedChanges.boxed_clone(),
)
.action("Discard Tracked Changes", RestoreTrackedFiles.boxed_clone())
.action("Trash Untracked Files", TrashUntrackedFiles.boxed_clone())
});
self.set_context_menu(context_menu, position, window, cx);

View file

@ -841,7 +841,7 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
.range(act_on_range),
VimCommand::new(("dif", "fupdate"), editor::actions::ToggleSelectedDiffHunks)
.range(act_on_range),
VimCommand::new(("rev", "ert"), editor::actions::RevertSelectedHunks).range(act_on_range),
VimCommand::str(("rev", "ert"), "git::Restore").range(act_on_range),
VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range),
VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| {
Some(