git_picker.rs

  1use std::fmt::Display;
  2
  3use gpui::{
  4    App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
  5    KeyContext, ModifiersChangedEvent, MouseButton, ParentElement, Rems, Render, Styled,
  6    Subscription, WeakEntity, Window, actions, rems,
  7};
  8use project::git_store::Repository;
  9use ui::{
 10    FluentBuilder, ToggleButtonGroup, ToggleButtonGroupStyle, ToggleButtonSimple, Tooltip,
 11    prelude::*,
 12};
 13use workspace::{ModalView, Workspace, pane};
 14
 15use crate::branch_picker::{self, BranchList, DeleteBranch, FilterRemotes};
 16use crate::stash_picker::{self, DropStashItem, ShowStashItem, StashList};
 17use crate::worktree_picker::{
 18    self, WorktreeFromDefault, WorktreeFromDefaultOnWindow, WorktreeList,
 19};
 20
 21actions!(
 22    git_picker,
 23    [ActivateBranchesTab, ActivateWorktreesTab, ActivateStashTab,]
 24);
 25
 26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 27pub enum GitPickerTab {
 28    Branches,
 29    Worktrees,
 30    Stash,
 31}
 32
 33impl Display for GitPickerTab {
 34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 35        let label = match self {
 36            GitPickerTab::Branches => "Branches",
 37            GitPickerTab::Worktrees => "Worktrees",
 38            GitPickerTab::Stash => "Stash",
 39        };
 40        write!(f, "{}", label)
 41    }
 42}
 43
 44pub struct GitPicker {
 45    tab: GitPickerTab,
 46    workspace: WeakEntity<Workspace>,
 47    repository: Option<Entity<Repository>>,
 48    width: Rems,
 49    branch_list: Option<Entity<BranchList>>,
 50    worktree_list: Option<Entity<WorktreeList>>,
 51    stash_list: Option<Entity<StashList>>,
 52    _subscriptions: Vec<Subscription>,
 53    popover_style: bool,
 54}
 55
 56impl GitPicker {
 57    pub fn new(
 58        workspace: WeakEntity<Workspace>,
 59        repository: Option<Entity<Repository>>,
 60        initial_tab: GitPickerTab,
 61        width: Rems,
 62        window: &mut Window,
 63        cx: &mut Context<Self>,
 64    ) -> Self {
 65        Self::new_internal(workspace, repository, initial_tab, width, false, window, cx)
 66    }
 67
 68    fn new_internal(
 69        workspace: WeakEntity<Workspace>,
 70        repository: Option<Entity<Repository>>,
 71        initial_tab: GitPickerTab,
 72        width: Rems,
 73        popover_style: bool,
 74        window: &mut Window,
 75        cx: &mut Context<Self>,
 76    ) -> Self {
 77        let mut this = Self {
 78            tab: initial_tab,
 79            workspace,
 80            repository,
 81            width,
 82            branch_list: None,
 83            worktree_list: None,
 84            stash_list: None,
 85            _subscriptions: Vec::new(),
 86            popover_style,
 87        };
 88
 89        this.ensure_active_picker(window, cx);
 90        this
 91    }
 92
 93    fn ensure_active_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 94        match self.tab {
 95            GitPickerTab::Branches => {
 96                self.ensure_branch_list(window, cx);
 97            }
 98            GitPickerTab::Worktrees => {
 99                self.ensure_worktree_list(window, cx);
100            }
101            GitPickerTab::Stash => {
102                self.ensure_stash_list(window, cx);
103            }
104        }
105    }
106
107    fn ensure_branch_list(
108        &mut self,
109        window: &mut Window,
110        cx: &mut Context<Self>,
111    ) -> Entity<BranchList> {
112        if self.branch_list.is_none() {
113            let branch_list = cx.new(|cx| {
114                branch_picker::create_embedded(
115                    self.workspace.clone(),
116                    self.repository.clone(),
117                    self.width,
118                    window,
119                    cx,
120                )
121            });
122
123            let subscription = cx.subscribe(&branch_list, |this, _, _: &DismissEvent, cx| {
124                if this.tab == GitPickerTab::Branches {
125                    cx.emit(DismissEvent);
126                }
127            });
128
129            self._subscriptions.push(subscription);
130            self.branch_list = Some(branch_list);
131        }
132        self.branch_list.clone().unwrap()
133    }
134
135    fn ensure_worktree_list(
136        &mut self,
137        window: &mut Window,
138        cx: &mut Context<Self>,
139    ) -> Entity<WorktreeList> {
140        if self.worktree_list.is_none() {
141            let worktree_list = cx.new(|cx| {
142                worktree_picker::create_embedded(
143                    self.repository.clone(),
144                    self.workspace.clone(),
145                    self.width,
146                    window,
147                    cx,
148                )
149            });
150
151            let subscription = cx.subscribe(&worktree_list, |this, _, _: &DismissEvent, cx| {
152                if this.tab == GitPickerTab::Worktrees {
153                    cx.emit(DismissEvent);
154                }
155            });
156
157            self._subscriptions.push(subscription);
158            self.worktree_list = Some(worktree_list);
159        }
160        self.worktree_list.clone().unwrap()
161    }
162
163    fn ensure_stash_list(
164        &mut self,
165        window: &mut Window,
166        cx: &mut Context<Self>,
167    ) -> Entity<StashList> {
168        if self.stash_list.is_none() {
169            let stash_list = cx.new(|cx| {
170                stash_picker::create_embedded(
171                    self.repository.clone(),
172                    self.workspace.clone(),
173                    self.width,
174                    window,
175                    cx,
176                )
177            });
178
179            let subscription = cx.subscribe(&stash_list, |this, _, _: &DismissEvent, cx| {
180                if this.tab == GitPickerTab::Stash {
181                    cx.emit(DismissEvent);
182                }
183            });
184
185            self._subscriptions.push(subscription);
186            self.stash_list = Some(stash_list);
187        }
188        self.stash_list.clone().unwrap()
189    }
190
191    fn activate_next_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
192        self.tab = match self.tab {
193            GitPickerTab::Branches => GitPickerTab::Worktrees,
194            GitPickerTab::Worktrees => GitPickerTab::Stash,
195            GitPickerTab::Stash => GitPickerTab::Branches,
196        };
197        self.ensure_active_picker(window, cx);
198        self.focus_active_picker(window, cx);
199        cx.notify();
200    }
201
202    fn activate_previous_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
203        self.tab = match self.tab {
204            GitPickerTab::Branches => GitPickerTab::Stash,
205            GitPickerTab::Worktrees => GitPickerTab::Branches,
206            GitPickerTab::Stash => GitPickerTab::Worktrees,
207        };
208        self.ensure_active_picker(window, cx);
209        self.focus_active_picker(window, cx);
210        cx.notify();
211    }
212
213    fn focus_active_picker(&self, window: &mut Window, cx: &mut App) {
214        match self.tab {
215            GitPickerTab::Branches => {
216                if let Some(branch_list) = &self.branch_list {
217                    branch_list.focus_handle(cx).focus(window, cx);
218                }
219            }
220            GitPickerTab::Worktrees => {
221                if let Some(worktree_list) = &self.worktree_list {
222                    worktree_list.focus_handle(cx).focus(window, cx);
223                }
224            }
225            GitPickerTab::Stash => {
226                if let Some(stash_list) = &self.stash_list {
227                    stash_list.focus_handle(cx).focus(window, cx);
228                }
229            }
230        }
231    }
232
233    fn render_tab_bar(&self, cx: &mut Context<Self>) -> impl IntoElement {
234        let focus_handle = self.focus_handle(cx);
235        let branches_focus_handle = focus_handle.clone();
236        let worktrees_focus_handle = focus_handle.clone();
237        let stash_focus_handle = focus_handle;
238
239        h_flex().p_2().pb_0p5().w_full().child(
240            ToggleButtonGroup::single_row(
241                "git-picker-tabs",
242                [
243                    ToggleButtonSimple::new(
244                        GitPickerTab::Branches.to_string(),
245                        cx.listener(|this, _, window, cx| {
246                            this.tab = GitPickerTab::Branches;
247                            this.ensure_active_picker(window, cx);
248                            this.focus_active_picker(window, cx);
249                            cx.notify();
250                        }),
251                    )
252                    .tooltip(move |_, cx| {
253                        Tooltip::for_action_in(
254                            "Toggle Branch Picker",
255                            &ActivateBranchesTab,
256                            &branches_focus_handle,
257                            cx,
258                        )
259                    }),
260                    ToggleButtonSimple::new(
261                        GitPickerTab::Worktrees.to_string(),
262                        cx.listener(|this, _, window, cx| {
263                            this.tab = GitPickerTab::Worktrees;
264                            this.ensure_active_picker(window, cx);
265                            this.focus_active_picker(window, cx);
266                            cx.notify();
267                        }),
268                    )
269                    .tooltip(move |_, cx| {
270                        Tooltip::for_action_in(
271                            "Toggle Worktree Picker",
272                            &ActivateWorktreesTab,
273                            &worktrees_focus_handle,
274                            cx,
275                        )
276                    }),
277                    ToggleButtonSimple::new(
278                        GitPickerTab::Stash.to_string(),
279                        cx.listener(|this, _, window, cx| {
280                            this.tab = GitPickerTab::Stash;
281                            this.ensure_active_picker(window, cx);
282                            this.focus_active_picker(window, cx);
283                            cx.notify();
284                        }),
285                    )
286                    .tooltip(move |_, cx| {
287                        Tooltip::for_action_in(
288                            "Toggle Stash Picker",
289                            &ActivateStashTab,
290                            &stash_focus_handle,
291                            cx,
292                        )
293                    }),
294                ],
295            )
296            .label_size(LabelSize::Default)
297            .style(ToggleButtonGroupStyle::Outlined)
298            .auto_width()
299            .selected_index(match self.tab {
300                GitPickerTab::Branches => 0,
301                GitPickerTab::Worktrees => 1,
302                GitPickerTab::Stash => 2,
303            }),
304        )
305    }
306
307    fn render_active_picker(
308        &mut self,
309        window: &mut Window,
310        cx: &mut Context<Self>,
311    ) -> impl IntoElement {
312        match self.tab {
313            GitPickerTab::Branches => {
314                let branch_list = self.ensure_branch_list(window, cx);
315                branch_list.into_any_element()
316            }
317            GitPickerTab::Worktrees => {
318                let worktree_list = self.ensure_worktree_list(window, cx);
319                worktree_list.into_any_element()
320            }
321            GitPickerTab::Stash => {
322                let stash_list = self.ensure_stash_list(window, cx);
323                stash_list.into_any_element()
324            }
325        }
326    }
327
328    fn handle_modifiers_changed(
329        &mut self,
330        ev: &ModifiersChangedEvent,
331        window: &mut Window,
332        cx: &mut Context<Self>,
333    ) {
334        match self.tab {
335            GitPickerTab::Branches => {
336                if let Some(branch_list) = &self.branch_list {
337                    branch_list.update(cx, |list, cx| {
338                        list.handle_modifiers_changed(ev, window, cx);
339                    });
340                }
341            }
342            GitPickerTab::Worktrees => {
343                if let Some(worktree_list) = &self.worktree_list {
344                    worktree_list.update(cx, |list, cx| {
345                        list.handle_modifiers_changed(ev, window, cx);
346                    });
347                }
348            }
349            GitPickerTab::Stash => {
350                if let Some(stash_list) = &self.stash_list {
351                    stash_list.update(cx, |list, cx| {
352                        list.handle_modifiers_changed(ev, window, cx);
353                    });
354                }
355            }
356        }
357    }
358
359    fn handle_delete_branch(
360        &mut self,
361        _: &DeleteBranch,
362        window: &mut Window,
363        cx: &mut Context<Self>,
364    ) {
365        if let Some(branch_list) = &self.branch_list {
366            branch_list.update(cx, |list, cx| {
367                list.handle_delete(&DeleteBranch, window, cx);
368            });
369        }
370    }
371
372    fn handle_filter_remotes(
373        &mut self,
374        _: &FilterRemotes,
375        window: &mut Window,
376        cx: &mut Context<Self>,
377    ) {
378        if let Some(branch_list) = &self.branch_list {
379            branch_list.update(cx, |list, cx| {
380                list.handle_filter(&FilterRemotes, window, cx);
381            });
382        }
383    }
384
385    fn handle_worktree_from_default(
386        &mut self,
387        _: &WorktreeFromDefault,
388        window: &mut Window,
389        cx: &mut Context<Self>,
390    ) {
391        if let Some(worktree_list) = &self.worktree_list {
392            worktree_list.update(cx, |list, cx| {
393                list.handle_new_worktree(false, window, cx);
394            });
395        }
396    }
397
398    fn handle_worktree_from_default_on_window(
399        &mut self,
400        _: &WorktreeFromDefaultOnWindow,
401        window: &mut Window,
402        cx: &mut Context<Self>,
403    ) {
404        if let Some(worktree_list) = &self.worktree_list {
405            worktree_list.update(cx, |list, cx| {
406                list.handle_new_worktree(true, window, cx);
407            });
408        }
409    }
410
411    fn handle_drop_stash(
412        &mut self,
413        _: &DropStashItem,
414        window: &mut Window,
415        cx: &mut Context<Self>,
416    ) {
417        if let Some(stash_list) = &self.stash_list {
418            stash_list.update(cx, |list, cx| {
419                list.handle_drop_stash(&DropStashItem, window, cx);
420            });
421        }
422    }
423
424    fn handle_show_stash(
425        &mut self,
426        _: &ShowStashItem,
427        window: &mut Window,
428        cx: &mut Context<Self>,
429    ) {
430        if let Some(stash_list) = &self.stash_list {
431            stash_list.update(cx, |list, cx| {
432                list.handle_show_stash(&ShowStashItem, window, cx);
433            });
434        }
435    }
436}
437
438impl ModalView for GitPicker {}
439impl EventEmitter<DismissEvent> for GitPicker {}
440
441impl Focusable for GitPicker {
442    fn focus_handle(&self, cx: &App) -> FocusHandle {
443        match self.tab {
444            GitPickerTab::Branches => {
445                if let Some(branch_list) = &self.branch_list {
446                    return branch_list.focus_handle(cx);
447                }
448            }
449            GitPickerTab::Worktrees => {
450                if let Some(worktree_list) = &self.worktree_list {
451                    return worktree_list.focus_handle(cx);
452                }
453            }
454            GitPickerTab::Stash => {
455                if let Some(stash_list) = &self.stash_list {
456                    return stash_list.focus_handle(cx);
457                }
458            }
459        }
460        cx.focus_handle()
461    }
462}
463
464impl Render for GitPicker {
465    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
466        v_flex()
467            .occlude()
468            .w(self.width)
469            .elevation_3(cx)
470            .overflow_hidden()
471            .when(self.popover_style, |el| {
472                el.on_mouse_down_out(cx.listener(|_, _, _, cx| {
473                    cx.emit(DismissEvent);
474                }))
475            })
476            .key_context({
477                let mut key_context = KeyContext::new_with_defaults();
478                key_context.add("Pane");
479                key_context.add("GitPicker");
480                match self.tab {
481                    GitPickerTab::Branches => key_context.add("GitBranchSelector"),
482                    GitPickerTab::Worktrees => key_context.add("GitWorktreeSelector"),
483                    GitPickerTab::Stash => key_context.add("StashList"),
484                }
485                key_context
486            })
487            .on_mouse_down(MouseButton::Left, |_, _, cx| {
488                cx.stop_propagation();
489            })
490            .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
491                cx.emit(DismissEvent);
492            }))
493            .on_action(cx.listener(|this, _: &pane::ActivateNextItem, window, cx| {
494                this.activate_next_tab(window, cx);
495            }))
496            .on_action(
497                cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| {
498                    this.activate_previous_tab(window, cx);
499                }),
500            )
501            .on_action(cx.listener(|this, _: &ActivateBranchesTab, window, cx| {
502                this.tab = GitPickerTab::Branches;
503                this.ensure_active_picker(window, cx);
504                this.focus_active_picker(window, cx);
505                cx.notify();
506            }))
507            .on_action(cx.listener(|this, _: &ActivateWorktreesTab, window, cx| {
508                this.tab = GitPickerTab::Worktrees;
509                this.ensure_active_picker(window, cx);
510                this.focus_active_picker(window, cx);
511                cx.notify();
512            }))
513            .on_action(cx.listener(|this, _: &ActivateStashTab, window, cx| {
514                this.tab = GitPickerTab::Stash;
515                this.ensure_active_picker(window, cx);
516                this.focus_active_picker(window, cx);
517                cx.notify();
518            }))
519            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
520            .when(self.tab == GitPickerTab::Branches, |el| {
521                el.on_action(cx.listener(Self::handle_delete_branch))
522                    .on_action(cx.listener(Self::handle_filter_remotes))
523            })
524            .when(self.tab == GitPickerTab::Worktrees, |el| {
525                el.on_action(cx.listener(Self::handle_worktree_from_default))
526                    .on_action(cx.listener(Self::handle_worktree_from_default_on_window))
527            })
528            .when(self.tab == GitPickerTab::Stash, |el| {
529                el.on_action(cx.listener(Self::handle_drop_stash))
530                    .on_action(cx.listener(Self::handle_show_stash))
531            })
532            .child(self.render_tab_bar(cx))
533            .child(self.render_active_picker(window, cx))
534    }
535}
536
537pub fn open_branches(
538    workspace: &mut Workspace,
539    _: &zed_actions::git::Branch,
540    window: &mut Window,
541    cx: &mut Context<Workspace>,
542) {
543    open_with_tab(workspace, GitPickerTab::Branches, window, cx);
544}
545
546pub fn open_worktrees(
547    workspace: &mut Workspace,
548    _: &zed_actions::git::Worktree,
549    window: &mut Window,
550    cx: &mut Context<Workspace>,
551) {
552    open_with_tab(workspace, GitPickerTab::Worktrees, window, cx);
553}
554
555pub fn open_stash(
556    workspace: &mut Workspace,
557    _: &zed_actions::git::ViewStash,
558    window: &mut Window,
559    cx: &mut Context<Workspace>,
560) {
561    open_with_tab(workspace, GitPickerTab::Stash, window, cx);
562}
563
564fn open_with_tab(
565    workspace: &mut Workspace,
566    tab: GitPickerTab,
567    window: &mut Window,
568    cx: &mut Context<Workspace>,
569) {
570    let workspace_handle = workspace.weak_handle();
571    let project = workspace.project().clone();
572
573    // Check if there's a worktree override from the project dropdown.
574    // This ensures the git picker shows info for the project the user
575    // explicitly selected in the title bar, not just the focused file's project.
576    // This is only relevant if for multi-projects workspaces.
577    let repository = workspace
578        .active_worktree_override()
579        .and_then(|override_id| {
580            let project_ref = project.read(cx);
581            project_ref
582                .worktree_for_id(override_id, cx)
583                .and_then(|worktree| {
584                    let worktree_abs_path = worktree.read(cx).abs_path();
585                    let git_store = project_ref.git_store().read(cx);
586                    git_store
587                        .repositories()
588                        .values()
589                        .find(|repo| {
590                            let repo_path = &repo.read(cx).work_directory_abs_path;
591                            *repo_path == worktree_abs_path
592                                || worktree_abs_path.starts_with(repo_path.as_ref())
593                        })
594                        .cloned()
595                })
596        })
597        .or_else(|| project.read(cx).active_repository(cx));
598
599    workspace.toggle_modal(window, cx, |window, cx| {
600        GitPicker::new(workspace_handle, repository, tab, rems(34.), window, cx)
601    })
602}
603
604pub fn popover(
605    workspace: WeakEntity<Workspace>,
606    repository: Option<Entity<Repository>>,
607    initial_tab: GitPickerTab,
608    width: Rems,
609    window: &mut Window,
610    cx: &mut App,
611) -> Entity<GitPicker> {
612    cx.new(|cx| {
613        let picker =
614            GitPicker::new_internal(workspace, repository, initial_tab, width, true, window, cx);
615        picker.focus_handle(cx).focus(window, cx);
616        picker
617    })
618}
619
620pub fn register(workspace: &mut Workspace) {
621    workspace.register_action(|workspace, _: &zed_actions::git::Branch, window, cx| {
622        open_with_tab(workspace, GitPickerTab::Branches, window, cx);
623    });
624    workspace.register_action(|workspace, _: &zed_actions::git::Switch, window, cx| {
625        open_with_tab(workspace, GitPickerTab::Branches, window, cx);
626    });
627    workspace.register_action(
628        |workspace, _: &zed_actions::git::CheckoutBranch, window, cx| {
629            open_with_tab(workspace, GitPickerTab::Branches, window, cx);
630        },
631    );
632    workspace.register_action(|workspace, _: &zed_actions::git::Worktree, window, cx| {
633        open_with_tab(workspace, GitPickerTab::Worktrees, window, cx);
634    });
635    workspace.register_action(|workspace, _: &zed_actions::git::ViewStash, window, cx| {
636        open_with_tab(workspace, GitPickerTab::Stash, window, cx);
637    });
638}