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, DeleteWorktree, 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_worktree_delete(
412        &mut self,
413        _: &DeleteWorktree,
414        window: &mut Window,
415        cx: &mut Context<Self>,
416    ) {
417        if let Some(worktree_list) = &self.worktree_list {
418            worktree_list.update(cx, |list, cx| {
419                list.handle_delete(&DeleteWorktree, window, cx);
420            });
421        }
422    }
423
424    fn handle_drop_stash(
425        &mut self,
426        _: &DropStashItem,
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_drop_stash(&DropStashItem, window, cx);
433            });
434        }
435    }
436
437    fn handle_show_stash(
438        &mut self,
439        _: &ShowStashItem,
440        window: &mut Window,
441        cx: &mut Context<Self>,
442    ) {
443        if let Some(stash_list) = &self.stash_list {
444            stash_list.update(cx, |list, cx| {
445                list.handle_show_stash(&ShowStashItem, window, cx);
446            });
447        }
448    }
449}
450
451impl ModalView for GitPicker {}
452impl EventEmitter<DismissEvent> for GitPicker {}
453
454impl Focusable for GitPicker {
455    fn focus_handle(&self, cx: &App) -> FocusHandle {
456        match self.tab {
457            GitPickerTab::Branches => {
458                if let Some(branch_list) = &self.branch_list {
459                    return branch_list.focus_handle(cx);
460                }
461            }
462            GitPickerTab::Worktrees => {
463                if let Some(worktree_list) = &self.worktree_list {
464                    return worktree_list.focus_handle(cx);
465                }
466            }
467            GitPickerTab::Stash => {
468                if let Some(stash_list) = &self.stash_list {
469                    return stash_list.focus_handle(cx);
470                }
471            }
472        }
473        cx.focus_handle()
474    }
475}
476
477impl Render for GitPicker {
478    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
479        v_flex()
480            .occlude()
481            .w(self.width)
482            .elevation_3(cx)
483            .overflow_hidden()
484            .when(self.popover_style, |el| {
485                el.on_mouse_down_out(cx.listener(|_, _, _, cx| {
486                    cx.emit(DismissEvent);
487                }))
488            })
489            .key_context({
490                let mut key_context = KeyContext::new_with_defaults();
491                key_context.add("Pane");
492                key_context.add("GitPicker");
493                match self.tab {
494                    GitPickerTab::Branches => key_context.add("GitBranchSelector"),
495                    GitPickerTab::Worktrees => key_context.add("GitWorktreeSelector"),
496                    GitPickerTab::Stash => key_context.add("StashList"),
497                }
498                key_context
499            })
500            .on_mouse_down(MouseButton::Left, |_, _, cx| {
501                cx.stop_propagation();
502            })
503            .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
504                cx.emit(DismissEvent);
505            }))
506            .on_action(cx.listener(|this, _: &pane::ActivateNextItem, window, cx| {
507                this.activate_next_tab(window, cx);
508            }))
509            .on_action(
510                cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| {
511                    this.activate_previous_tab(window, cx);
512                }),
513            )
514            .on_action(cx.listener(|this, _: &ActivateBranchesTab, window, cx| {
515                this.tab = GitPickerTab::Branches;
516                this.ensure_active_picker(window, cx);
517                this.focus_active_picker(window, cx);
518                cx.notify();
519            }))
520            .on_action(cx.listener(|this, _: &ActivateWorktreesTab, window, cx| {
521                this.tab = GitPickerTab::Worktrees;
522                this.ensure_active_picker(window, cx);
523                this.focus_active_picker(window, cx);
524                cx.notify();
525            }))
526            .on_action(cx.listener(|this, _: &ActivateStashTab, window, cx| {
527                this.tab = GitPickerTab::Stash;
528                this.ensure_active_picker(window, cx);
529                this.focus_active_picker(window, cx);
530                cx.notify();
531            }))
532            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
533            .when(self.tab == GitPickerTab::Branches, |el| {
534                el.on_action(cx.listener(Self::handle_delete_branch))
535                    .on_action(cx.listener(Self::handle_filter_remotes))
536            })
537            .when(self.tab == GitPickerTab::Worktrees, |el| {
538                el.on_action(cx.listener(Self::handle_worktree_from_default))
539                    .on_action(cx.listener(Self::handle_worktree_from_default_on_window))
540                    .on_action(cx.listener(Self::handle_worktree_delete))
541            })
542            .when(self.tab == GitPickerTab::Stash, |el| {
543                el.on_action(cx.listener(Self::handle_drop_stash))
544                    .on_action(cx.listener(Self::handle_show_stash))
545            })
546            .child(self.render_tab_bar(cx))
547            .child(self.render_active_picker(window, cx))
548    }
549}
550
551pub fn open_branches(
552    workspace: &mut Workspace,
553    _: &zed_actions::git::Branch,
554    window: &mut Window,
555    cx: &mut Context<Workspace>,
556) {
557    open_with_tab(workspace, GitPickerTab::Branches, window, cx);
558}
559
560pub fn open_worktrees(
561    workspace: &mut Workspace,
562    _: &zed_actions::git::Worktree,
563    window: &mut Window,
564    cx: &mut Context<Workspace>,
565) {
566    open_with_tab(workspace, GitPickerTab::Worktrees, window, cx);
567}
568
569pub fn open_stash(
570    workspace: &mut Workspace,
571    _: &zed_actions::git::ViewStash,
572    window: &mut Window,
573    cx: &mut Context<Workspace>,
574) {
575    open_with_tab(workspace, GitPickerTab::Stash, window, cx);
576}
577
578fn open_with_tab(
579    workspace: &mut Workspace,
580    tab: GitPickerTab,
581    window: &mut Window,
582    cx: &mut Context<Workspace>,
583) {
584    let workspace_handle = workspace.weak_handle();
585    let repository = crate::resolve_active_repository(workspace, cx);
586
587    workspace.toggle_modal(window, cx, |window, cx| {
588        GitPicker::new(workspace_handle, repository, tab, rems(34.), window, cx)
589    })
590}
591
592pub fn popover(
593    workspace: WeakEntity<Workspace>,
594    repository: Option<Entity<Repository>>,
595    initial_tab: GitPickerTab,
596    width: Rems,
597    window: &mut Window,
598    cx: &mut App,
599) -> Entity<GitPicker> {
600    cx.new(|cx| {
601        let picker =
602            GitPicker::new_internal(workspace, repository, initial_tab, width, true, window, cx);
603        picker.focus_handle(cx).focus(window, cx);
604        picker
605    })
606}
607
608pub fn register(workspace: &mut Workspace) {
609    workspace.register_action(|workspace, _: &zed_actions::git::Branch, window, cx| {
610        open_with_tab(workspace, GitPickerTab::Branches, window, cx);
611    });
612    workspace.register_action(|workspace, _: &zed_actions::git::Switch, window, cx| {
613        open_with_tab(workspace, GitPickerTab::Branches, window, cx);
614    });
615    workspace.register_action(
616        |workspace, _: &zed_actions::git::CheckoutBranch, window, cx| {
617            open_with_tab(workspace, GitPickerTab::Branches, window, cx);
618        },
619    );
620    workspace.register_action(|workspace, _: &zed_actions::git::Worktree, window, cx| {
621        open_with_tab(workspace, GitPickerTab::Worktrees, window, cx);
622    });
623    workspace.register_action(|workspace, _: &zed_actions::git::ViewStash, window, cx| {
624        open_with_tab(workspace, GitPickerTab::Stash, window, cx);
625    });
626}