git_panel: Add Git actions in split button (#59608)

# Objective

Improve the Git panel changes header by making the Stage All / Unstage
All control more consistent with other split-button actions in the Git
panel, such as the Fetch/Pull button.

## Solution

- Replaced the standalone Stage All / Unstage All button with a split
button.
- Kept the primary action context-sensitive, switching between Stage All
and Unstage All.
- Moved related Git change actions into the split-button menu: staging,
stashing, discard tracked changes, and trash untracked files.

## Testing

- Ran `cargo fmt --check`.
- Ran `cargo check -p git_ui`.
- Manually verified the Git panel dropdown behavior in the app.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

## Showcase

The Git panel changes header now uses a split button similar to the
Fetch/Pull control. The primary action updates between Stage All and
Unstage All, while the dropdown groups related Git actions.

when nothing is staged
<img width="359" height="245" alt="Screenshot 2026-06-19 at 11 01 44 PM"
src="https://github.com/user-attachments/assets/7fb7601f-8396-49c7-9d90-9f2e1706230a"
/>

when everything is staged already
<img width="360" height="219" alt="Screenshot 2026-06-19 at 11 01 52 PM"
src="https://github.com/user-attachments/assets/38ebebd9-09c2-4d24-aac3-6c4e22786ba5"
/>

rest of the menu options
<img width="385" height="294" alt="Screenshot 2026-06-19 at 11 01 59 PM"
src="https://github.com/user-attachments/assets/47393a17-78af-411c-9cb1-90781cf3a645"
/>


---


Release Notes:

- Improved Git panel change actions with a split-button menu.

---------

Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
This commit is contained in:
Sathwik ChirivelliandChristopher Biscardi authored and GitHub committed 2026-06-21 23:50:08 +00:00
1 parent fdfc80ca83
commit 7e11793875
1 file changed
+98 -108
+98 -108
View File
@@ -3,7 +3,7 @@ use crate::commit_modal::CommitModal;
use crate::commit_tooltip::{CommitAvatar, CommitTooltip};
use crate::commit_view::CommitView;
use crate::git_panel_settings::GitPanelScrollbarAccessor;
use crate::project_diff::{self, BranchDiff, Diff, ProjectDiff};
use crate::project_diff::{BranchDiff, Diff, ProjectDiff};
use crate::remote_output::{self, RemoteAction, SuccessMessage};
use crate::solo_diff_view::SoloDiffView;
use crate::{branch_picker, picker_prompt, render_remote_button};
@@ -163,53 +163,42 @@ enum TrashCancel {
}
struct GitMenuState {
has_tracked_changes: bool,
has_staged_changes: bool,
has_unstaged_changes: bool,
has_new_changes: bool,
sort_by: GitPanelSortBy,
group_by: GitPanelGroupBy,
has_stash_items: bool,
tree_view: bool,
}
fn git_panel_context_menu(
has_tracked_changes: bool,
has_staged_changes: bool,
has_unstaged_changes: bool,
has_new_changes: bool,
has_stash_items: bool,
focus_handle: FocusHandle,
state: GitMenuState,
window: &mut Window,
cx: &mut App,
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, move |context_menu, _, _| {
ContextMenu::build(window, cx, |context_menu, _, _| {
context_menu
.context(focus_handle)
.action_disabled_when(
!state.has_unstaged_changes,
"Stage All",
StageAll.boxed_clone(),
)
.action_disabled_when(
!state.has_staged_changes,
"Unstage All",
UnstageAll.boxed_clone(),
)
.context(focus_handle.clone())
.action_disabled_when(!has_unstaged_changes, "Stage All", StageAll.boxed_clone())
.action_disabled_when(!has_staged_changes, "Unstage All", UnstageAll.boxed_clone())
.separator()
.action_disabled_when(
!(state.has_new_changes || state.has_tracked_changes),
!(has_new_changes || has_tracked_changes),
"Stash All",
StashAll.boxed_clone(),
)
.action_disabled_when(!state.has_stash_items, "Stash Pop", StashPop.boxed_clone())
.action_disabled_when(!has_stash_items, "Stash Pop", StashPop.boxed_clone())
.action("View Stash", zed_actions::git::ViewStash.boxed_clone())
.separator()
.action("Open Diff", project_diff::Diff.boxed_clone())
.separator()
.action_disabled_when(
!state.has_tracked_changes,
!has_tracked_changes,
"Discard Tracked Changes",
RestoreTrackedFiles.boxed_clone(),
)
.action_disabled_when(
!state.has_new_changes,
!has_new_changes,
"Trash Untracked Files",
TrashUntrackedFiles.boxed_clone(),
)
@@ -4410,46 +4399,8 @@ impl GitPanel {
path + file_name + depth * 2
}
fn render_ellipsis_menu(&self, id: impl Into<ElementId>) -> impl IntoElement {
let focus_handle = self.focus_handle.clone();
let has_tracked_changes = self.has_tracked_changes();
let has_staged_changes = self.has_staged_changes();
let has_unstaged_changes = self.has_unstaged_changes();
let has_new_changes = self.new_count > 0;
let has_stash_items = self.stash_entries.entries.len() > 0;
PopoverMenu::new(id.into())
.trigger(
IconButton::new("overflow-menu-trigger", IconName::Ellipsis)
.icon_size(IconSize::Small),
)
.menu(move |window, cx| {
Some(git_panel_context_menu(
focus_handle.clone(),
GitMenuState {
has_tracked_changes,
has_staged_changes,
has_unstaged_changes,
has_new_changes,
sort_by: GitPanelSettings::get_global(cx).sort_by,
group_by: GitPanelSettings::get_global(cx).group_by,
has_stash_items,
tree_view: GitPanelSettings::get_global(cx).tree_view,
},
window,
cx,
))
})
.anchor(Anchor::TopRight)
}
fn render_view_options_menu(&self, id: impl Into<ElementId>) -> impl IntoElement {
let focus_handle = self.focus_handle.clone();
let has_tracked_changes = self.has_tracked_changes();
let has_staged_changes = self.has_staged_changes();
let has_unstaged_changes = self.has_unstaged_changes();
let has_new_changes = self.new_count > 0;
let has_stash_items = self.stash_entries.entries.len() > 0;
PopoverMenu::new(id.into())
.trigger(
@@ -4461,13 +4412,8 @@ impl GitPanel {
Some(git_panel_view_options_menu(
focus_handle.clone(),
GitMenuState {
has_tracked_changes,
has_staged_changes,
has_unstaged_changes,
has_new_changes,
sort_by: GitPanelSettings::get_global(cx).sort_by,
group_by: GitPanelSettings::get_global(cx).group_by,
has_stash_items,
tree_view: GitPanelSettings::get_global(cx).tree_view,
},
window,
@@ -4727,6 +4673,78 @@ impl GitPanel {
})
}
fn render_git_changes_actions_menu(
&self,
id: impl Into<ElementId>,
_cx: &mut Context<Self>,
) -> impl IntoElement {
let has_tracked_changes = self.has_tracked_changes();
let has_staged_changes = self.has_staged_changes();
let has_unstaged_changes = self.has_unstaged_changes();
let has_new_changes = self.new_count > 0;
let has_stash_items = self.stash_entries.entries.len() > 0;
let focus_handle = self.focus_handle.clone();
PopoverMenu::new(id.into())
.trigger(
ui::ButtonLike::new_rounded_right("git-changes-actions-split-button-right")
.layer(ui::ElevationIndex::ModalSurface)
.size(ButtonSize::None)
.child(
div()
.px_1()
.child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)),
),
)
.menu(move |window, cx| {
Some(git_panel_context_menu(
has_tracked_changes,
has_staged_changes,
has_unstaged_changes,
has_new_changes,
has_stash_items,
focus_handle.clone(),
window,
cx,
))
})
.anchor(Anchor::TopRight)
}
fn render_git_changes_actions_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
let (text, action, stage, tooltip) =
if self.total_staged_count() == self.entry_count && self.entry_count > 0 {
("Unstage All", UnstageAll.boxed_clone(), false, "git reset")
} else {
("Stage All", StageAll.boxed_clone(), true, "git add --all")
};
SplitButton::new(
ButtonLike::new_rounded_left("git-changes-actions-split-button-left")
.layer(ElevationIndex::ModalSurface)
.size(ButtonSize::Compact)
.child(Label::new(text).size(LabelSize::Small).mr_0p5())
.tooltip(Tooltip::for_action_title_in(
tooltip,
action.as_ref(),
&self.focus_handle,
))
.disabled(self.entry_count == 0)
.on_click({
let git_panel = cx.weak_entity();
move |_, _, cx| {
git_panel
.update(cx, |git_panel, cx| {
git_panel.change_all_files_stage(stage, cx);
})
.ok();
}
}),
self.render_git_changes_actions_menu("git-changes-actions-split-button-menu", cx)
.into_any_element(),
)
}
fn render_changes_header(
&self,
_window: &mut Window,
@@ -4738,13 +4756,6 @@ impl GitPanel {
self.active_repository.as_ref()?;
let (text, action, stage, tooltip) =
if self.total_staged_count() == self.entry_count && self.entry_count > 0 {
("Unstage All", UnstageAll.boxed_clone(), false, "git reset")
} else {
("Stage All", StageAll.boxed_clone(), true, "git add --all")
};
let diff_stat_total = self.diff_stat_total;
Some(
@@ -4796,29 +4807,7 @@ impl GitPanel {
h_flex()
.gap_1()
.child(self.render_view_options_menu("view_options_menu"))
.child(self.render_ellipsis_menu("overflow_menu"))
.child(
Button::new("stage_unstage_all", text)
.label_size(LabelSize::Small)
.layer(ElevationIndex::ModalSurface)
.style(ButtonStyle::Filled)
.tooltip(Tooltip::for_action_title_in(
tooltip,
action.as_ref(),
&self.focus_handle,
))
.disabled(self.entry_count == 0)
.on_click({
let git_panel = cx.weak_entity();
move |_, _, cx| {
git_panel
.update(cx, |git_panel, cx| {
git_panel.change_all_files_stage(stage, cx);
})
.ok();
}
}),
),
.child(self.render_git_changes_actions_button(cx)),
),
)
}
@@ -6232,18 +6221,19 @@ impl GitPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
let has_tracked_changes = self.has_tracked_changes();
let has_staged_changes = self.has_staged_changes();
let has_unstaged_changes = self.has_unstaged_changes();
let has_new_changes = self.new_count > 0;
let has_stash_items = self.stash_entries.entries.len() > 0;
let context_menu = git_panel_context_menu(
has_tracked_changes,
has_staged_changes,
has_unstaged_changes,
has_new_changes,
has_stash_items,
self.focus_handle.clone(),
GitMenuState {
has_tracked_changes: self.has_tracked_changes(),
has_staged_changes: self.has_staged_changes(),
has_unstaged_changes: self.has_unstaged_changes(),
has_new_changes: self.new_count > 0,
sort_by: GitPanelSettings::get_global(cx).sort_by,
group_by: GitPanelSettings::get_global(cx).group_by,
has_stash_items: self.stash_entries.entries.len() > 0,
tree_view: GitPanelSettings::get_global(cx).tree_view,
},
window,
cx,
);