diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 92324b01bb..3bb4b1a565 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -731,14 +731,16 @@ "up": "menu::SelectPrevious", "down": "menu::SelectNext", "enter": "menu::Confirm", + "alt-y": "git::StageFile", + "alt-shift-y": "git::UnstageFile", + "ctrl-alt-y": "git::ToggleStaged", "space": "git::ToggleStaged", - "ctrl-space": "git::StageAll", - "ctrl-shift-space": "git::UnstageAll", "tab": "git_panel::FocusEditor", "shift-tab": "git_panel::FocusEditor", "escape": "git_panel::ToggleFocus", "ctrl-enter": "git::Commit", - "alt-enter": "menu::SecondaryConfirm" + "alt-enter": "menu::SecondaryConfirm", + "backspace": "git::RestoreFile" } }, { @@ -749,10 +751,27 @@ "alt-l": "git::GenerateCommitMessage" } }, + { + "context": "GitPanel", + "use_key_equivalents": true, + "bindings": { + "ctrl-g ctrl-g": "git::Fetch", + "ctrl-g up": "git::Push", + "ctrl-g down": "git::Pull", + "ctrl-g shift-up": "git::ForcePush", + "ctrl-g d": "git::Diff", + "ctrl-g backspace": "git::RestoreTrackedFiles", + "ctrl-g shift-backspace": "git::TrashUntrackedFiles", + "ctrl-space": "git::StageAll", + "ctrl-shift-space": "git::UnstageAll" + } + }, { "context": "GitDiff > Editor", "bindings": { - "ctrl-enter": "git::Commit" + "ctrl-enter": "git::Commit", + "ctrl-space": "git::StageAll", + "ctrl-shift-space": "git::UnstageAll" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 294521f75f..654e04cac2 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -763,28 +763,25 @@ "cmd-up": "menu::SelectFirst", "cmd-down": "menu::SelectLast", "enter": "menu::Confirm", + "cmd-alt-y": "git::ToggleStaged", "space": "git::ToggleStaged", - "cmd-shift-space": "git::StageAll", - "ctrl-shift-space": "git::UnstageAll", + "cmd-y": "git::StageFile", + "cmd-shift-y": "git::UnstageFile", "alt-down": "git_panel::FocusEditor", "tab": "git_panel::FocusEditor", "shift-tab": "git_panel::FocusEditor", "escape": "git_panel::ToggleFocus", - "cmd-enter": "git::Commit" + "cmd-enter": "git::Commit", + "backspace": "git::RestoreFile" } }, { "context": "GitDiff > Editor", "use_key_equivalents": true, "bindings": { - "cmd-enter": "git::Commit" - } - }, - { - "context": "AskPass > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "menu::Confirm" + "cmd-enter": "git::Commit", + "cmd-ctrl-y": "git::StageAll", + "cmd-ctrl-shift-y": "git::UnstageAll" } }, { @@ -800,6 +797,21 @@ "alt-tab": "git::GenerateCommitMessage" } }, + { + "context": "GitPanel", + "use_key_equivalents": true, + "bindings": { + "ctrl-g ctrl-g": "git::Fetch", + "ctrl-g up": "git::Push", + "ctrl-g down": "git::Pull", + "ctrl-g shift-up": "git::ForcePush", + "ctrl-g d": "git::Diff", + "ctrl-g backspace": "git::RestoreTrackedFiles", + "ctrl-g shift-backspace": "git::TrashUntrackedFiles", + "cmd-ctrl-y": "git::StageAll", + "cmd-ctrl-shift-y": "git::UnstageAll" + } + }, { "context": "GitCommit > Editor", "use_key_equivalents": true, diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index e813614a6a..1216766bcb 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -102,9 +102,14 @@ enum TrashCancel { Cancel, } -fn git_panel_context_menu(window: &mut Window, cx: &mut App) -> Entity { +fn git_panel_context_menu( + focus_handle: Option, + window: &mut Window, + cx: &mut App, +) -> Entity { 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) { + 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, + ) { + 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) { 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, ) { - 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) -> impl IntoElement { + fn render_overflow_menu(&self, id: impl Into, 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() { diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 2036deca29..1e543c9a25 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -29,41 +29,43 @@ pub fn init(cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _, cx| { let project = workspace.project().read(cx); - if project.is_via_collab() { + if project.is_read_only(cx) { return; } - workspace.register_action(|workspace, _: &git::Fetch, window, cx| { - let Some(panel) = workspace.panel::(cx) else { - return; - }; - panel.update(cx, |panel, cx| { - panel.fetch(window, cx); + if !project.is_via_collab() { + workspace.register_action(|workspace, _: &git::Fetch, window, cx| { + let Some(panel) = workspace.panel::(cx) else { + return; + }; + panel.update(cx, |panel, cx| { + panel.fetch(window, cx); + }); }); - }); - workspace.register_action(|workspace, _: &git::Push, window, cx| { - let Some(panel) = workspace.panel::(cx) else { - return; - }; - panel.update(cx, |panel, cx| { - panel.push(false, window, cx); + workspace.register_action(|workspace, _: &git::Push, window, cx| { + let Some(panel) = workspace.panel::(cx) else { + return; + }; + panel.update(cx, |panel, cx| { + panel.push(false, window, cx); + }); }); - }); - workspace.register_action(|workspace, _: &git::ForcePush, window, cx| { - let Some(panel) = workspace.panel::(cx) else { - return; - }; - panel.update(cx, |panel, cx| { - panel.push(true, window, cx); + workspace.register_action(|workspace, _: &git::ForcePush, window, cx| { + let Some(panel) = workspace.panel::(cx) else { + return; + }; + panel.update(cx, |panel, cx| { + panel.push(true, window, cx); + }); }); - }); - workspace.register_action(|workspace, _: &git::Pull, window, cx| { - let Some(panel) = workspace.panel::(cx) else { - return; - }; - panel.update(cx, |panel, cx| { - panel.pull(window, cx); + workspace.register_action(|workspace, _: &git::Pull, window, cx| { + let Some(panel) = workspace.panel::(cx) else { + return; + }; + panel.update(cx, |panel, cx| { + panel.pull(window, cx); + }); }); - }); + } workspace.register_action(|workspace, action: &git::StageAll, window, cx| { let Some(panel) = workspace.panel::(cx) else { return; @@ -173,6 +175,7 @@ mod remote_button { 0, 0, Some(IconName::ArrowCircle), + keybinding_target.clone(), move |_, window, cx| { window.dispatch_action(Box::new(git::Fetch), cx); }, @@ -200,6 +203,7 @@ mod remote_button { ahead as usize, 0, None, + keybinding_target.clone(), move |_, window, cx| { window.dispatch_action(Box::new(git::Push), cx); }, @@ -228,6 +232,7 @@ mod remote_button { ahead as usize, behind as usize, None, + keybinding_target.clone(), move |_, window, cx| { window.dispatch_action(Box::new(git::Pull), cx); }, @@ -254,6 +259,7 @@ mod remote_button { 0, 0, Some(IconName::ArrowUpFromLine), + keybinding_target.clone(), move |_, window, cx| { window.dispatch_action(Box::new(git::Push), cx); }, @@ -280,6 +286,7 @@ mod remote_button { 0, 0, Some(IconName::ArrowUpFromLine), + keybinding_target.clone(), move |_, window, cx| { window.dispatch_action(Box::new(git::Push), cx); }, @@ -321,7 +328,10 @@ mod remote_button { } } - fn render_git_action_menu(id: impl Into) -> impl IntoElement { + fn render_git_action_menu( + id: impl Into, + keybinding_target: Option, + ) -> impl IntoElement { PopoverMenu::new(id.into()) .trigger( ui::ButtonLike::new_rounded_right("split-button-right") @@ -336,6 +346,9 @@ mod remote_button { .menu(move |window, cx| { Some(ContextMenu::build(window, cx, |context_menu, _, _| { context_menu + .when_some(keybinding_target.clone(), |el, keybinding_target| { + el.context(keybinding_target.clone()) + }) .action("Fetch", git::Fetch.boxed_clone()) .action("Pull", git::Pull.boxed_clone()) .separator() @@ -353,12 +366,14 @@ mod remote_button { } impl SplitButton { + #[allow(clippy::too_many_arguments)] fn new( id: impl Into, left_label: impl Into, ahead_count: usize, behind_count: usize, left_icon: Option, + keybinding_target: Option, left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, ) -> Self { @@ -416,9 +431,10 @@ mod remote_button { .on_click(left_on_click) .tooltip(tooltip); - let right = render_git_action_menu(ElementId::Name( - format!("split-button-right-{}", id).into(), - )) + let right = render_git_action_menu( + ElementId::Name(format!("split-button-right-{}", id).into()), + keybinding_target, + ) .into_any_element(); Self { left, right }