From b9de700b0c035d7c0cd08de77c94d9adb3340649 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:48:57 -0300 Subject: [PATCH] git_ui: Unify branch, worktree, and stash pickers in one (#46621) This PR creates a unified picker for all the Git-related pickers: branch, worktree and stash. The main motivation for this is to bring awareness and ease of access for the worktree and stash pickers that were previously (and arguably) hidden amidst the sea of commands in the command palette. As worktrees in particular become more relevant for AI-related flows, having an easier way to reach for its picker will be beneficial. Note that the actions/commands remain the same; you can still look for `git: worktree` or `git: view stash`. The difference is that these actions will take you to the unified picker with the correct corresponding active view. https://github.com/user-attachments/assets/99d1cd6f-a19d-47d3-9bca-d7257e7ed5b8 Release Notes: - Git: Unify the branch, worktree, and stash pickers into one, making it easier to find and access them from one another. --- assets/keymaps/default-linux.json | 8 + assets/keymaps/default-macos.json | 8 + assets/keymaps/default-windows.json | 8 + crates/git_ui/src/branch_picker.rs | 89 +++-- crates/git_ui/src/git_picker.rs | 578 +++++++++++++++++++++++++++ crates/git_ui/src/git_ui.rs | 5 +- crates/git_ui/src/stash_picker.rs | 57 ++- crates/git_ui/src/worktree_picker.rs | 69 +++- crates/zed/src/zed.rs | 1 + 9 files changed, 768 insertions(+), 55 deletions(-) create mode 100644 crates/git_ui/src/git_picker.rs diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 73ddf48f91213352dc3cc5b79f8b3ef4a7217dca..1693f3262d1dc900a0b2746c08cd1744e1001404 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1340,4 +1340,12 @@ "ctrl-shift-i": "branch_picker::FilterRemotes", }, }, + { + "context": "GitPicker", + "bindings": { + "alt-1": "git_picker::ActivateBranchesTab", + "alt-2": "git_picker::ActivateWorktreesTab", + "alt-3": "git_picker::ActivateStashTab", + }, + }, ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index d4249adfb1db4d678af7e7b3ae2960cbab1772c1..cfe13134ab282cb60db9e8bcde43227ff384be6f 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1442,4 +1442,12 @@ "cmd-shift-i": "branch_picker::FilterRemotes", }, }, + { + "context": "GitPicker", + "bindings": { + "cmd-1": "git_picker::ActivateBranchesTab", + "cmd-2": "git_picker::ActivateWorktreesTab", + "cmd-3": "git_picker::ActivateStashTab", + }, + }, ] diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 672ad3741358ba3f1ecba54988f28836b7ca083b..2fbf8c51f03048dfa89bb90b282dbd32dfc52366 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -1361,4 +1361,12 @@ "ctrl-shift-i": "branch_picker::FilterRemotes", }, }, + { + "context": "GitPicker", + "bindings": { + "alt-1": "git_picker::ActivateBranchesTab", + "alt-2": "git_picker::ActivateWorktreesTab", + "alt-3": "git_picker::ActivateStashTab", + }, + }, ] diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index befcf83d1f93432ef8e05aa8d15bf77c6ea7d0e0..423431b2232ccd8cdd2ffadd921456d49623de4e 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -36,14 +36,6 @@ actions!( ] ); -pub fn register(workspace: &mut Workspace) { - workspace.register_action(|workspace, branch: &zed_actions::git::Branch, window, cx| { - open(workspace, branch, window, cx); - }); - workspace.register_action(switch); - workspace.register_action(checkout_branch); -} - pub fn checkout_branch( workspace: &mut Workspace, _: &zed_actions::git::CheckoutBranch, @@ -103,6 +95,16 @@ pub fn popover( }) } +pub fn create_embedded( + workspace: WeakEntity, + repository: Option>, + width: Rems, + window: &mut Window, + cx: &mut Context, +) -> BranchList { + BranchList::new_embedded(workspace, repository, width, window, cx) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum BranchListStyle { Modal, @@ -113,7 +115,8 @@ pub struct BranchList { width: Rems, pub picker: Entity>, picker_focus_handle: FocusHandle, - _subscription: Subscription, + _subscription: Option, + embedded: bool, } impl BranchList { @@ -124,10 +127,27 @@ impl BranchList { width: Rems, window: &mut Window, cx: &mut Context, + ) -> Self { + let mut this = Self::new_inner(workspace, repository, style, width, false, window, cx); + this._subscription = Some(cx.subscribe(&this.picker, |_, _, _, cx| { + cx.emit(DismissEvent); + })); + this + } + + fn new_inner( + workspace: WeakEntity, + repository: Option>, + style: BranchListStyle, + width: Rems, + embedded: bool, + window: &mut Window, + cx: &mut Context, ) -> Self { let all_branches_request = repository .clone() .map(|repository| repository.update(cx, |repository, _| repository.branches())); + let default_branch_request = repository.clone().map(|repository| { repository.update(cx, |repository, _| repository.default_branch(false)) }); @@ -186,25 +206,45 @@ impl BranchList { .detach_and_log_err(cx); let delegate = BranchListDelegate::new(workspace, repository, style, cx); - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(!embedded)); let picker_focus_handle = picker.focus_handle(cx); + picker.update(cx, |picker, _| { picker.delegate.focus_handle = picker_focus_handle.clone(); }); - let _subscription = cx.subscribe(&picker, |_, _, _, cx| { - cx.emit(DismissEvent); - }); - Self { picker, picker_focus_handle, width, - _subscription, + _subscription: None, + embedded, } } - fn handle_modifiers_changed( + fn new_embedded( + workspace: WeakEntity, + repository: Option>, + width: Rems, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let mut this = Self::new_inner( + workspace, + repository, + BranchListStyle::Modal, + width, + true, + window, + cx, + ); + this._subscription = Some(cx.subscribe(&this.picker, |_, _, _, cx| { + cx.emit(DismissEvent); + })); + this + } + + pub fn handle_modifiers_changed( &mut self, ev: &ModifiersChangedEvent, _: &mut Window, @@ -214,7 +254,7 @@ impl BranchList { .update(cx, |picker, _| picker.delegate.modifiers = ev.modifiers) } - fn handle_delete( + pub fn handle_delete( &mut self, _: &branch_picker::DeleteBranch, window: &mut Window, @@ -227,7 +267,7 @@ impl BranchList { }) } - fn handle_filter( + pub fn handle_filter( &mut self, _: &branch_picker::FilterRemotes, window: &mut Window, @@ -259,10 +299,12 @@ impl Render for BranchList { .on_action(cx.listener(Self::handle_delete)) .on_action(cx.listener(Self::handle_filter)) .child(self.picker.clone()) - .on_mouse_down_out({ - cx.listener(move |this, _, window, cx| { - this.picker.update(cx, |this, cx| { - this.cancel(&Default::default(), window, cx); + .when(!self.embedded, |this| { + this.on_mouse_down_out({ + cx.listener(move |this, _, window, cx| { + this.picker.update(cx, |this, cx| { + this.cancel(&Default::default(), window, cx); + }) }) }) }) @@ -1324,7 +1366,8 @@ mod tests { picker, picker_focus_handle, width: rems(34.), - _subscription, + _subscription: Some(_subscription), + embedded: false, } }) }) diff --git a/crates/git_ui/src/git_picker.rs b/crates/git_ui/src/git_picker.rs new file mode 100644 index 0000000000000000000000000000000000000000..44fe62f5a02249005dc6518c7ec50b940faae972 --- /dev/null +++ b/crates/git_ui/src/git_picker.rs @@ -0,0 +1,578 @@ +use std::fmt::Display; + +use gpui::{ + App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, + KeyContext, ModifiersChangedEvent, MouseButton, ParentElement, Render, Styled, Subscription, + WeakEntity, Window, actions, rems, +}; +use project::git_store::Repository; +use ui::{ + FluentBuilder, ToggleButtonGroup, ToggleButtonGroupStyle, ToggleButtonSimple, Tooltip, + prelude::*, +}; +use workspace::{ModalView, Workspace, pane}; + +use crate::branch_picker::{self, BranchList, DeleteBranch, FilterRemotes}; +use crate::stash_picker::{self, DropStashItem, ShowStashItem, StashList}; +use crate::worktree_picker::{ + self, WorktreeFromDefault, WorktreeFromDefaultOnWindow, WorktreeList, +}; + +actions!( + git_picker, + [ActivateBranchesTab, ActivateWorktreesTab, ActivateStashTab,] +); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum GitPickerTab { + Branches, + Worktrees, + Stash, +} + +impl Display for GitPickerTab { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let label = match self { + GitPickerTab::Branches => "Branches", + GitPickerTab::Worktrees => "Worktrees", + GitPickerTab::Stash => "Stash", + }; + write!(f, "{}", label) + } +} + +pub struct GitPicker { + tab: GitPickerTab, + workspace: WeakEntity, + repository: Option>, + width: Rems, + branch_list: Option>, + worktree_list: Option>, + stash_list: Option>, + _subscriptions: Vec, +} + +impl GitPicker { + pub fn new( + workspace: WeakEntity, + repository: Option>, + initial_tab: GitPickerTab, + width: Rems, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let mut this = Self { + tab: initial_tab, + workspace, + repository, + width, + branch_list: None, + worktree_list: None, + stash_list: None, + _subscriptions: Vec::new(), + }; + + this.ensure_active_picker(window, cx); + this + } + + fn ensure_active_picker(&mut self, window: &mut Window, cx: &mut Context) { + match self.tab { + GitPickerTab::Branches => { + self.ensure_branch_list(window, cx); + } + GitPickerTab::Worktrees => { + self.ensure_worktree_list(window, cx); + } + GitPickerTab::Stash => { + self.ensure_stash_list(window, cx); + } + } + } + + fn ensure_branch_list( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + if self.branch_list.is_none() { + let branch_list = cx.new(|cx| { + branch_picker::create_embedded( + self.workspace.clone(), + self.repository.clone(), + self.width, + window, + cx, + ) + }); + + let subscription = cx.subscribe(&branch_list, |this, _, _: &DismissEvent, cx| { + if this.tab == GitPickerTab::Branches { + cx.emit(DismissEvent); + } + }); + + self._subscriptions.push(subscription); + self.branch_list = Some(branch_list); + } + self.branch_list.clone().unwrap() + } + + fn ensure_worktree_list( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + if self.worktree_list.is_none() { + let worktree_list = cx.new(|cx| { + worktree_picker::create_embedded( + self.repository.clone(), + self.workspace.clone(), + self.width, + window, + cx, + ) + }); + + let subscription = cx.subscribe(&worktree_list, |this, _, _: &DismissEvent, cx| { + if this.tab == GitPickerTab::Worktrees { + cx.emit(DismissEvent); + } + }); + + self._subscriptions.push(subscription); + self.worktree_list = Some(worktree_list); + } + self.worktree_list.clone().unwrap() + } + + fn ensure_stash_list( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + if self.stash_list.is_none() { + let stash_list = cx.new(|cx| { + stash_picker::create_embedded( + self.repository.clone(), + self.workspace.clone(), + self.width, + window, + cx, + ) + }); + + let subscription = cx.subscribe(&stash_list, |this, _, _: &DismissEvent, cx| { + if this.tab == GitPickerTab::Stash { + cx.emit(DismissEvent); + } + }); + + self._subscriptions.push(subscription); + self.stash_list = Some(stash_list); + } + self.stash_list.clone().unwrap() + } + + fn activate_next_tab(&mut self, window: &mut Window, cx: &mut Context) { + self.tab = match self.tab { + GitPickerTab::Branches => GitPickerTab::Worktrees, + GitPickerTab::Worktrees => GitPickerTab::Stash, + GitPickerTab::Stash => GitPickerTab::Branches, + }; + self.ensure_active_picker(window, cx); + self.focus_active_picker(window, cx); + cx.notify(); + } + + fn activate_previous_tab(&mut self, window: &mut Window, cx: &mut Context) { + self.tab = match self.tab { + GitPickerTab::Branches => GitPickerTab::Stash, + GitPickerTab::Worktrees => GitPickerTab::Branches, + GitPickerTab::Stash => GitPickerTab::Worktrees, + }; + self.ensure_active_picker(window, cx); + self.focus_active_picker(window, cx); + cx.notify(); + } + + fn focus_active_picker(&self, window: &mut Window, cx: &mut App) { + match self.tab { + GitPickerTab::Branches => { + if let Some(branch_list) = &self.branch_list { + branch_list.focus_handle(cx).focus(window, cx); + } + } + GitPickerTab::Worktrees => { + if let Some(worktree_list) = &self.worktree_list { + worktree_list.focus_handle(cx).focus(window, cx); + } + } + GitPickerTab::Stash => { + if let Some(stash_list) = &self.stash_list { + stash_list.focus_handle(cx).focus(window, cx); + } + } + } + } + + fn render_tab_bar(&self, cx: &mut Context) -> impl IntoElement { + let focus_handle = self.focus_handle(cx); + let branches_focus_handle = focus_handle.clone(); + let worktrees_focus_handle = focus_handle.clone(); + let stash_focus_handle = focus_handle; + + h_flex().p_2().pb_0p5().w_full().child( + ToggleButtonGroup::single_row( + "git-picker-tabs", + [ + ToggleButtonSimple::new( + GitPickerTab::Branches.to_string(), + cx.listener(|this, _, window, cx| { + this.tab = GitPickerTab::Branches; + this.ensure_active_picker(window, cx); + this.focus_active_picker(window, cx); + cx.notify(); + }), + ) + .tooltip(move |_, cx| { + Tooltip::for_action_in( + "Toggle Branch Picker", + &ActivateBranchesTab, + &branches_focus_handle, + cx, + ) + }), + ToggleButtonSimple::new( + GitPickerTab::Worktrees.to_string(), + cx.listener(|this, _, window, cx| { + this.tab = GitPickerTab::Worktrees; + this.ensure_active_picker(window, cx); + this.focus_active_picker(window, cx); + cx.notify(); + }), + ) + .tooltip(move |_, cx| { + Tooltip::for_action_in( + "Toggle Worktree Picker", + &ActivateWorktreesTab, + &worktrees_focus_handle, + cx, + ) + }), + ToggleButtonSimple::new( + GitPickerTab::Stash.to_string(), + cx.listener(|this, _, window, cx| { + this.tab = GitPickerTab::Stash; + this.ensure_active_picker(window, cx); + this.focus_active_picker(window, cx); + cx.notify(); + }), + ) + .tooltip(move |_, cx| { + Tooltip::for_action_in( + "Toggle Stash Picker", + &ActivateStashTab, + &stash_focus_handle, + cx, + ) + }), + ], + ) + .label_size(LabelSize::Default) + .style(ToggleButtonGroupStyle::Outlined) + .auto_width() + .selected_index(match self.tab { + GitPickerTab::Branches => 0, + GitPickerTab::Worktrees => 1, + GitPickerTab::Stash => 2, + }), + ) + } + + fn render_active_picker( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { + match self.tab { + GitPickerTab::Branches => { + let branch_list = self.ensure_branch_list(window, cx); + branch_list.into_any_element() + } + GitPickerTab::Worktrees => { + let worktree_list = self.ensure_worktree_list(window, cx); + worktree_list.into_any_element() + } + GitPickerTab::Stash => { + let stash_list = self.ensure_stash_list(window, cx); + stash_list.into_any_element() + } + } + } + + fn handle_modifiers_changed( + &mut self, + ev: &ModifiersChangedEvent, + window: &mut Window, + cx: &mut Context, + ) { + match self.tab { + GitPickerTab::Branches => { + if let Some(branch_list) = &self.branch_list { + branch_list.update(cx, |list, cx| { + list.handle_modifiers_changed(ev, window, cx); + }); + } + } + GitPickerTab::Worktrees => { + if let Some(worktree_list) = &self.worktree_list { + worktree_list.update(cx, |list, cx| { + list.handle_modifiers_changed(ev, window, cx); + }); + } + } + GitPickerTab::Stash => { + if let Some(stash_list) = &self.stash_list { + stash_list.update(cx, |list, cx| { + list.handle_modifiers_changed(ev, window, cx); + }); + } + } + } + } + + fn handle_delete_branch( + &mut self, + _: &DeleteBranch, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(branch_list) = &self.branch_list { + branch_list.update(cx, |list, cx| { + list.handle_delete(&DeleteBranch, window, cx); + }); + } + } + + fn handle_filter_remotes( + &mut self, + _: &FilterRemotes, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(branch_list) = &self.branch_list { + branch_list.update(cx, |list, cx| { + list.handle_filter(&FilterRemotes, window, cx); + }); + } + } + + fn handle_worktree_from_default( + &mut self, + _: &WorktreeFromDefault, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(worktree_list) = &self.worktree_list { + worktree_list.update(cx, |list, cx| { + list.handle_new_worktree(false, window, cx); + }); + } + } + + fn handle_worktree_from_default_on_window( + &mut self, + _: &WorktreeFromDefaultOnWindow, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(worktree_list) = &self.worktree_list { + worktree_list.update(cx, |list, cx| { + list.handle_new_worktree(true, window, cx); + }); + } + } + + fn handle_drop_stash( + &mut self, + _: &DropStashItem, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(stash_list) = &self.stash_list { + stash_list.update(cx, |list, cx| { + list.handle_drop_stash(&DropStashItem, window, cx); + }); + } + } + + fn handle_show_stash( + &mut self, + _: &ShowStashItem, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(stash_list) = &self.stash_list { + stash_list.update(cx, |list, cx| { + list.handle_show_stash(&ShowStashItem, window, cx); + }); + } + } +} + +impl ModalView for GitPicker {} +impl EventEmitter for GitPicker {} + +impl Focusable for GitPicker { + fn focus_handle(&self, cx: &App) -> FocusHandle { + match self.tab { + GitPickerTab::Branches => { + if let Some(branch_list) = &self.branch_list { + return branch_list.focus_handle(cx); + } + } + GitPickerTab::Worktrees => { + if let Some(worktree_list) = &self.worktree_list { + return worktree_list.focus_handle(cx); + } + } + GitPickerTab::Stash => { + if let Some(stash_list) = &self.stash_list { + return stash_list.focus_handle(cx); + } + } + } + cx.focus_handle() + } +} + +impl Render for GitPicker { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .occlude() + .w(self.width) + .elevation_3(cx) + .overflow_hidden() + .key_context({ + let mut key_context = KeyContext::new_with_defaults(); + key_context.add("Pane"); + key_context.add("GitPicker"); + match self.tab { + GitPickerTab::Branches => key_context.add("GitBranchSelector"), + GitPickerTab::Worktrees => key_context.add("GitWorktreeSelector"), + GitPickerTab::Stash => key_context.add("StashList"), + } + key_context + }) + .on_mouse_down(MouseButton::Left, |_, _, cx| { + cx.stop_propagation(); + }) + .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| { + cx.emit(DismissEvent); + })) + .on_action(cx.listener(|this, _: &pane::ActivateNextItem, window, cx| { + this.activate_next_tab(window, cx); + })) + .on_action( + cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| { + this.activate_previous_tab(window, cx); + }), + ) + .on_action(cx.listener(|this, _: &ActivateBranchesTab, window, cx| { + this.tab = GitPickerTab::Branches; + this.ensure_active_picker(window, cx); + this.focus_active_picker(window, cx); + cx.notify(); + })) + .on_action(cx.listener(|this, _: &ActivateWorktreesTab, window, cx| { + this.tab = GitPickerTab::Worktrees; + this.ensure_active_picker(window, cx); + this.focus_active_picker(window, cx); + cx.notify(); + })) + .on_action(cx.listener(|this, _: &ActivateStashTab, window, cx| { + this.tab = GitPickerTab::Stash; + this.ensure_active_picker(window, cx); + this.focus_active_picker(window, cx); + cx.notify(); + })) + .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) + .when(self.tab == GitPickerTab::Branches, |el| { + el.on_action(cx.listener(Self::handle_delete_branch)) + .on_action(cx.listener(Self::handle_filter_remotes)) + }) + .when(self.tab == GitPickerTab::Worktrees, |el| { + el.on_action(cx.listener(Self::handle_worktree_from_default)) + .on_action(cx.listener(Self::handle_worktree_from_default_on_window)) + }) + .when(self.tab == GitPickerTab::Stash, |el| { + el.on_action(cx.listener(Self::handle_drop_stash)) + .on_action(cx.listener(Self::handle_show_stash)) + }) + .child(self.render_tab_bar(cx)) + .child(self.render_active_picker(window, cx)) + } +} + +pub fn open_branches( + workspace: &mut Workspace, + _: &zed_actions::git::Branch, + window: &mut Window, + cx: &mut Context, +) { + open_with_tab(workspace, GitPickerTab::Branches, window, cx); +} + +pub fn open_worktrees( + workspace: &mut Workspace, + _: &zed_actions::git::Worktree, + window: &mut Window, + cx: &mut Context, +) { + open_with_tab(workspace, GitPickerTab::Worktrees, window, cx); +} + +pub fn open_stash( + workspace: &mut Workspace, + _: &zed_actions::git::ViewStash, + window: &mut Window, + cx: &mut Context, +) { + open_with_tab(workspace, GitPickerTab::Stash, window, cx); +} + +fn open_with_tab( + workspace: &mut Workspace, + tab: GitPickerTab, + window: &mut Window, + cx: &mut Context, +) { + let workspace_handle = workspace.weak_handle(); + let repository = workspace.project().read(cx).active_repository(cx); + + workspace.toggle_modal(window, cx, |window, cx| { + GitPicker::new(workspace_handle, repository, tab, rems(34.), window, cx) + }) +} + +/// Register all git picker actions with the workspace. +pub fn register(workspace: &mut Workspace) { + workspace.register_action(|workspace, _: &zed_actions::git::Branch, window, cx| { + open_with_tab(workspace, GitPickerTab::Branches, window, cx); + }); + workspace.register_action(|workspace, _: &zed_actions::git::Switch, window, cx| { + open_with_tab(workspace, GitPickerTab::Branches, window, cx); + }); + workspace.register_action( + |workspace, _: &zed_actions::git::CheckoutBranch, window, cx| { + open_with_tab(workspace, GitPickerTab::Branches, window, cx); + }, + ); + workspace.register_action(|workspace, _: &zed_actions::git::Worktree, window, cx| { + open_with_tab(workspace, GitPickerTab::Worktrees, window, cx); + }); + workspace.register_action(|workspace, _: &zed_actions::git::ViewStash, window, cx| { + open_with_tab(workspace, GitPickerTab::Stash, window, cx); + }); +} diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 5cb99689e0c23304ec21c1b4fe935ac543299a75..0c7da7e90e10260e7ce645716ce31da98f902252 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -41,6 +41,7 @@ pub mod file_diff_view; pub mod file_history_view; pub mod git_panel; mod git_panel_settings; +pub mod git_picker; pub mod onboarding; pub mod picker_prompt; pub mod project_diff; @@ -73,9 +74,7 @@ pub fn init(cx: &mut App) { CommitModal::register(workspace); git_panel::register(workspace); repository_selector::register(workspace); - branch_picker::register(workspace); - worktree_picker::register(workspace); - stash_picker::register(workspace); + git_picker::register(workspace); let project = workspace.project().read(cx); if project.is_read_only(cx) { diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index a688c250a63e09deab03677efda92e34a38aebc4..c0e077262eda9bfc12581f9b23108c2f74928f76 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -29,10 +29,6 @@ actions!( ] ); -pub fn register(workspace: &mut Workspace) { - workspace.register_action(open); -} - pub fn open( workspace: &mut Workspace, _: &zed_actions::git::ViewStash, @@ -46,6 +42,16 @@ pub fn open( }) } +pub fn create_embedded( + repository: Option>, + workspace: WeakEntity, + width: Rems, + window: &mut Window, + cx: &mut Context, +) -> StashList { + StashList::new_embedded(repository, workspace, width, window, cx) +} + pub struct StashList { width: Rems, pub picker: Entity>, @@ -60,6 +66,22 @@ impl StashList { width: Rems, window: &mut Window, cx: &mut Context, + ) -> Self { + let mut this = Self::new_inner(repository, workspace, width, false, window, cx); + this._subscriptions + .push(cx.subscribe(&this.picker, |_, _, _, cx| { + cx.emit(DismissEvent); + })); + this + } + + fn new_inner( + repository: Option>, + workspace: WeakEntity, + width: Rems, + embedded: bool, + window: &mut Window, + cx: &mut Context, ) -> Self { let mut _subscriptions = Vec::new(); let stash_request = repository @@ -103,16 +125,12 @@ impl StashList { .detach_and_log_err(cx); let delegate = StashListDelegate::new(repository, workspace, window, cx); - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(!embedded)); let picker_focus_handle = picker.focus_handle(cx); picker.update(cx, |picker, _| { picker.delegate.focus_handle = picker_focus_handle.clone(); }); - _subscriptions.push(cx.subscribe(&picker, |_, _, _, cx| { - cx.emit(DismissEvent); - })); - Self { picker, picker_focus_handle, @@ -121,7 +139,22 @@ impl StashList { } } - fn handle_drop_stash( + fn new_embedded( + repository: Option>, + workspace: WeakEntity, + width: Rems, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let mut this = Self::new_inner(repository, workspace, width, true, window, cx); + this._subscriptions + .push(cx.subscribe(&this.picker, |_, _, _, cx| { + cx.emit(DismissEvent); + })); + this + } + + pub fn handle_drop_stash( &mut self, _: &DropStashItem, window: &mut Window, @@ -135,7 +168,7 @@ impl StashList { cx.notify(); } - fn handle_show_stash( + pub fn handle_show_stash( &mut self, _: &ShowStashItem, window: &mut Window, @@ -149,7 +182,7 @@ impl StashList { cx.notify(); } - fn handle_modifiers_changed( + pub fn handle_modifiers_changed( &mut self, ev: &ModifiersChangedEvent, _: &mut Window, diff --git a/crates/git_ui/src/worktree_picker.rs b/crates/git_ui/src/worktree_picker.rs index 3d71ff4abc254a689f72e81d388b30f2aa2b3873..a66c640305ffa0efff0c99d81a0b78b32baa3fd6 100644 --- a/crates/git_ui/src/worktree_picker.rs +++ b/crates/git_ui/src/worktree_picker.rs @@ -24,10 +24,6 @@ use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr}; actions!(git, [WorktreeFromDefault, WorktreeFromDefaultOnWindow]); -pub fn register(workspace: &mut Workspace) { - workspace.register_action(open); -} - pub fn open( workspace: &mut Workspace, _: &zed_actions::git::Worktree, @@ -41,11 +37,22 @@ pub fn open( }) } +pub fn create_embedded( + repository: Option>, + workspace: WeakEntity, + width: Rems, + window: &mut Window, + cx: &mut Context, +) -> WorktreeList { + WorktreeList::new_embedded(repository, workspace, width, window, cx) +} + pub struct WorktreeList { width: Rems, pub picker: Entity>, picker_focus_handle: FocusHandle, - _subscription: Subscription, + _subscription: Option, + embedded: bool, } impl WorktreeList { @@ -55,6 +62,21 @@ impl WorktreeList { width: Rems, window: &mut Window, cx: &mut Context, + ) -> Self { + let mut this = Self::new_inner(repository, workspace, width, false, window, cx); + this._subscription = Some(cx.subscribe(&this.picker, |_, _, _, cx| { + cx.emit(DismissEvent); + })); + this + } + + fn new_inner( + repository: Option>, + workspace: WeakEntity, + width: Rems, + embedded: bool, + window: &mut Window, + cx: &mut Context, ) -> Self { let all_worktrees_request = repository .clone() @@ -90,25 +112,36 @@ impl WorktreeList { .detach_and_log_err(cx); let delegate = WorktreeListDelegate::new(workspace, repository, window, cx); - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(!embedded)); let picker_focus_handle = picker.focus_handle(cx); picker.update(cx, |picker, _| { picker.delegate.focus_handle = picker_focus_handle.clone(); }); - let _subscription = cx.subscribe(&picker, |_, _, _, cx| { - cx.emit(DismissEvent); - }); - Self { picker, picker_focus_handle, width, - _subscription, + _subscription: None, + embedded, } } - fn handle_modifiers_changed( + fn new_embedded( + repository: Option>, + workspace: WeakEntity, + width: Rems, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let mut this = Self::new_inner(repository, workspace, width, true, window, cx); + this._subscription = Some(cx.subscribe(&this.picker, |_, _, _, cx| { + cx.emit(DismissEvent); + })); + this + } + + pub fn handle_modifiers_changed( &mut self, ev: &ModifiersChangedEvent, _: &mut Window, @@ -118,7 +151,7 @@ impl WorktreeList { .update(cx, |picker, _| picker.delegate.modifiers = ev.modifiers) } - fn handle_new_worktree( + pub fn handle_new_worktree( &mut self, replace_current_window: bool, window: &mut Window, @@ -167,10 +200,12 @@ impl Render for WorktreeList { this.handle_new_worktree(true, w, cx) })) .child(self.picker.clone()) - .on_mouse_down_out({ - cx.listener(move |this, _, window, cx| { - this.picker.update(cx, |this, cx| { - this.cancel(&Default::default(), window, cx); + .when(!self.embedded, |el| { + el.on_mouse_down_out({ + cx.listener(move |this, _, window, cx| { + this.picker.update(cx, |this, cx| { + this.cancel(&Default::default(), window, cx); + }) }) }) }) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 3f789fbaaaeec49ec29b9a2c102550f92b16c027..783742d54a23649716237eefd105358aa90ffee1 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4736,6 +4736,7 @@ mod tests { "git", "git_onboarding", "git_panel", + "git_picker", "go_to_line", "icon_theme_selector", "inline_assistant",