Git keyboard shortcuts (#26374)

Closes #26040

Release Notes:

- git: Add keyboard shortcuts (when the panel is open) for fetch `ctrl-g
ctrl-g`, pull `ctrl-g down`, push `ctrl-g up`, force-push `ctrl-g
shift-up`, open diff `ctrl-g d`
This commit is contained in:
Conrad Irwin 2025-03-10 10:46:53 -06:00 committed by GitHub
parent b91e929086
commit 3891381d3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 160 additions and 67 deletions

View file

@ -102,9 +102,14 @@ enum TrashCancel {
Cancel,
}
fn git_panel_context_menu(window: &mut Window, cx: &mut App) -> Entity<ContextMenu> {
fn git_panel_context_menu(
focus_handle: Option<FocusHandle>,
window: &mut Window,
cx: &mut App,
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |context_menu, _, _| {
context_menu
.when_some(focus_handle, |el, focus_handle| el.context(focus_handle))
.action("Stage All", StageAll.boxed_clone())
.action("Unstage All", UnstageAll.boxed_clone())
.separator()
@ -1232,6 +1237,35 @@ impl GitPanel {
}
}
fn stage_selected(&mut self, _: &git::StageFile, _window: &mut Window, cx: &mut Context<Self>) {
let Some(selected_entry) = self.get_selected_entry() else {
return;
};
let Some(status_entry) = selected_entry.status_entry() else {
return;
};
if status_entry.staging != StageStatus::Staged {
self.change_file_stage(true, vec![status_entry.clone()], cx);
}
}
fn unstage_selected(
&mut self,
_: &git::UnstageFile,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(selected_entry) = self.get_selected_entry() else {
return;
};
let Some(status_entry) = selected_entry.status_entry() else {
return;
};
if status_entry.staging != StageStatus::Unstaged {
self.change_file_stage(false, vec![status_entry.clone()], cx);
}
}
fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
if self
.commit_editor
@ -2425,8 +2459,12 @@ impl GitPanel {
self.panel_header_container(window, cx)
.child(
Button::new("diff", "Open diff")
.tooltip(Tooltip::for_action_title("Open diff", &Diff))
Button::new("diff", "Open Diff")
.tooltip(Tooltip::for_action_title_in(
"Open diff",
&Diff,
&self.focus_handle,
))
.on_click(|_, _, cx| {
cx.defer(|cx| {
cx.dispatch_action(&Diff);
@ -2436,7 +2474,11 @@ impl GitPanel {
.child(div().flex_grow()) // spacer
.child(
Button::new("stage-unstage-all", text)
.tooltip(Tooltip::for_action_title(tooltip, action.as_ref()))
.tooltip(Tooltip::for_action_title_in(
tooltip,
action.as_ref(),
&self.focus_handle,
))
.on_click(move |_, _, cx| {
let action = action.boxed_clone();
cx.defer(move |cx| {
@ -2886,6 +2928,7 @@ impl GitPanel {
};
let context_menu = ContextMenu::build(window, cx, |context_menu, _, _| {
context_menu
.context(self.focus_handle.clone())
.action(stage_title, ToggleStaged.boxed_clone())
.action(restore_title, git::RestoreFile.boxed_clone())
.separator()
@ -2902,7 +2945,7 @@ impl GitPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
let context_menu = git_panel_context_menu(window, cx);
let context_menu = git_panel_context_menu(Some(self.focus_handle.clone()), window, cx);
self.set_context_menu(context_menu, position, window, cx);
}
@ -3169,10 +3212,16 @@ impl Render for GitPanel {
.track_focus(&self.focus_handle)
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
.when(has_write_access && !project.is_read_only(cx), |this| {
this.on_action(cx.listener(|this, &ToggleStaged, window, cx| {
this.toggle_staged_for_selected(&ToggleStaged, window, cx)
}))
.on_action(cx.listener(GitPanel::commit))
this.on_action(cx.listener(Self::toggle_staged_for_selected))
.on_action(cx.listener(GitPanel::commit))
.on_action(cx.listener(Self::stage_all))
.on_action(cx.listener(Self::unstage_all))
.on_action(cx.listener(Self::stage_selected))
.on_action(cx.listener(Self::unstage_selected))
.on_action(cx.listener(Self::restore_tracked_files))
.on_action(cx.listener(Self::revert_selected))
.on_action(cx.listener(Self::clean_all))
.on_action(cx.listener(Self::generate_commit_message_action))
})
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_next))
@ -3181,16 +3230,9 @@ impl Render for GitPanel {
.on_action(cx.listener(Self::close_panel))
.on_action(cx.listener(Self::open_diff))
.on_action(cx.listener(Self::open_file))
.on_action(cx.listener(Self::revert_selected))
.on_action(cx.listener(Self::focus_changes_list))
.on_action(cx.listener(Self::focus_editor))
.on_action(cx.listener(Self::toggle_staged_for_selected))
.on_action(cx.listener(Self::stage_all))
.on_action(cx.listener(Self::unstage_all))
.on_action(cx.listener(Self::restore_tracked_files))
.on_action(cx.listener(Self::clean_all))
.on_action(cx.listener(Self::expand_commit_editor))
.on_action(cx.listener(Self::generate_commit_message_action))
.when(has_write_access && has_co_authors, |git_panel| {
git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
})
@ -3414,14 +3456,18 @@ impl PanelRepoFooter {
}
}
fn render_overflow_menu(&self, id: impl Into<ElementId>) -> impl IntoElement {
fn render_overflow_menu(&self, id: impl Into<ElementId>, cx: &App) -> impl IntoElement {
let focus_handle = self
.git_panel
.as_ref()
.map(|git_panel| git_panel.focus_handle(cx));
PopoverMenu::new(id.into())
.trigger(
IconButton::new("overflow-menu-trigger", IconName::EllipsisVertical)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),
)
.menu(move |window, cx| Some(git_panel_context_menu(window, cx)))
.menu(move |window, cx| Some(git_panel_context_menu(focus_handle.clone(), window, cx)))
.anchor(Corner::TopRight)
}
}
@ -3537,7 +3583,7 @@ impl RenderOnce for PanelRepoFooter {
.gap_1()
.flex_shrink_0()
.children(spinner)
.child(self.render_overflow_menu(overflow_menu_id))
.child(self.render_overflow_menu(overflow_menu_id, cx))
.when_some(branch, |this, branch| {
let mut focus_handle = None;
if let Some(git_panel) = self.git_panel.as_ref() {