git_picker.rs

  1use std::fmt::Display;
  2
  3use gpui::{
  4    App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
  5    KeyContext, ModifiersChangedEvent, MouseButton, ParentElement, Render, Styled, Subscription,
  6    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}
 54
 55impl GitPicker {
 56    pub fn new(
 57        workspace: WeakEntity<Workspace>,
 58        repository: Option<Entity<Repository>>,
 59        initial_tab: GitPickerTab,
 60        width: Rems,
 61        window: &mut Window,
 62        cx: &mut Context<Self>,
 63    ) -> Self {
 64        let mut this = Self {
 65            tab: initial_tab,
 66            workspace,
 67            repository,
 68            width,
 69            branch_list: None,
 70            worktree_list: None,
 71            stash_list: None,
 72            _subscriptions: Vec::new(),
 73        };
 74
 75        this.ensure_active_picker(window, cx);
 76        this
 77    }
 78
 79    fn ensure_active_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 80        match self.tab {
 81            GitPickerTab::Branches => {
 82                self.ensure_branch_list(window, cx);
 83            }
 84            GitPickerTab::Worktrees => {
 85                self.ensure_worktree_list(window, cx);
 86            }
 87            GitPickerTab::Stash => {
 88                self.ensure_stash_list(window, cx);
 89            }
 90        }
 91    }
 92
 93    fn ensure_branch_list(
 94        &mut self,
 95        window: &mut Window,
 96        cx: &mut Context<Self>,
 97    ) -> Entity<BranchList> {
 98        if self.branch_list.is_none() {
 99            let branch_list = cx.new(|cx| {
100                branch_picker::create_embedded(
101                    self.workspace.clone(),
102                    self.repository.clone(),
103                    self.width,
104                    window,
105                    cx,
106                )
107            });
108
109            let subscription = cx.subscribe(&branch_list, |this, _, _: &DismissEvent, cx| {
110                if this.tab == GitPickerTab::Branches {
111                    cx.emit(DismissEvent);
112                }
113            });
114
115            self._subscriptions.push(subscription);
116            self.branch_list = Some(branch_list);
117        }
118        self.branch_list.clone().unwrap()
119    }
120
121    fn ensure_worktree_list(
122        &mut self,
123        window: &mut Window,
124        cx: &mut Context<Self>,
125    ) -> Entity<WorktreeList> {
126        if self.worktree_list.is_none() {
127            let worktree_list = cx.new(|cx| {
128                worktree_picker::create_embedded(
129                    self.repository.clone(),
130                    self.workspace.clone(),
131                    self.width,
132                    window,
133                    cx,
134                )
135            });
136
137            let subscription = cx.subscribe(&worktree_list, |this, _, _: &DismissEvent, cx| {
138                if this.tab == GitPickerTab::Worktrees {
139                    cx.emit(DismissEvent);
140                }
141            });
142
143            self._subscriptions.push(subscription);
144            self.worktree_list = Some(worktree_list);
145        }
146        self.worktree_list.clone().unwrap()
147    }
148
149    fn ensure_stash_list(
150        &mut self,
151        window: &mut Window,
152        cx: &mut Context<Self>,
153    ) -> Entity<StashList> {
154        if self.stash_list.is_none() {
155            let stash_list = cx.new(|cx| {
156                stash_picker::create_embedded(
157                    self.repository.clone(),
158                    self.workspace.clone(),
159                    self.width,
160                    window,
161                    cx,
162                )
163            });
164
165            let subscription = cx.subscribe(&stash_list, |this, _, _: &DismissEvent, cx| {
166                if this.tab == GitPickerTab::Stash {
167                    cx.emit(DismissEvent);
168                }
169            });
170
171            self._subscriptions.push(subscription);
172            self.stash_list = Some(stash_list);
173        }
174        self.stash_list.clone().unwrap()
175    }
176
177    fn activate_next_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
178        self.tab = match self.tab {
179            GitPickerTab::Branches => GitPickerTab::Worktrees,
180            GitPickerTab::Worktrees => GitPickerTab::Stash,
181            GitPickerTab::Stash => GitPickerTab::Branches,
182        };
183        self.ensure_active_picker(window, cx);
184        self.focus_active_picker(window, cx);
185        cx.notify();
186    }
187
188    fn activate_previous_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
189        self.tab = match self.tab {
190            GitPickerTab::Branches => GitPickerTab::Stash,
191            GitPickerTab::Worktrees => GitPickerTab::Branches,
192            GitPickerTab::Stash => GitPickerTab::Worktrees,
193        };
194        self.ensure_active_picker(window, cx);
195        self.focus_active_picker(window, cx);
196        cx.notify();
197    }
198
199    fn focus_active_picker(&self, window: &mut Window, cx: &mut App) {
200        match self.tab {
201            GitPickerTab::Branches => {
202                if let Some(branch_list) = &self.branch_list {
203                    branch_list.focus_handle(cx).focus(window, cx);
204                }
205            }
206            GitPickerTab::Worktrees => {
207                if let Some(worktree_list) = &self.worktree_list {
208                    worktree_list.focus_handle(cx).focus(window, cx);
209                }
210            }
211            GitPickerTab::Stash => {
212                if let Some(stash_list) = &self.stash_list {
213                    stash_list.focus_handle(cx).focus(window, cx);
214                }
215            }
216        }
217    }
218
219    fn render_tab_bar(&self, cx: &mut Context<Self>) -> impl IntoElement {
220        let focus_handle = self.focus_handle(cx);
221        let branches_focus_handle = focus_handle.clone();
222        let worktrees_focus_handle = focus_handle.clone();
223        let stash_focus_handle = focus_handle;
224
225        h_flex().p_2().pb_0p5().w_full().child(
226            ToggleButtonGroup::single_row(
227                "git-picker-tabs",
228                [
229                    ToggleButtonSimple::new(
230                        GitPickerTab::Branches.to_string(),
231                        cx.listener(|this, _, window, cx| {
232                            this.tab = GitPickerTab::Branches;
233                            this.ensure_active_picker(window, cx);
234                            this.focus_active_picker(window, cx);
235                            cx.notify();
236                        }),
237                    )
238                    .tooltip(move |_, cx| {
239                        Tooltip::for_action_in(
240                            "Toggle Branch Picker",
241                            &ActivateBranchesTab,
242                            &branches_focus_handle,
243                            cx,
244                        )
245                    }),
246                    ToggleButtonSimple::new(
247                        GitPickerTab::Worktrees.to_string(),
248                        cx.listener(|this, _, window, cx| {
249                            this.tab = GitPickerTab::Worktrees;
250                            this.ensure_active_picker(window, cx);
251                            this.focus_active_picker(window, cx);
252                            cx.notify();
253                        }),
254                    )
255                    .tooltip(move |_, cx| {
256                        Tooltip::for_action_in(
257                            "Toggle Worktree Picker",
258                            &ActivateWorktreesTab,
259                            &worktrees_focus_handle,
260                            cx,
261                        )
262                    }),
263                    ToggleButtonSimple::new(
264                        GitPickerTab::Stash.to_string(),
265                        cx.listener(|this, _, window, cx| {
266                            this.tab = GitPickerTab::Stash;
267                            this.ensure_active_picker(window, cx);
268                            this.focus_active_picker(window, cx);
269                            cx.notify();
270                        }),
271                    )
272                    .tooltip(move |_, cx| {
273                        Tooltip::for_action_in(
274                            "Toggle Stash Picker",
275                            &ActivateStashTab,
276                            &stash_focus_handle,
277                            cx,
278                        )
279                    }),
280                ],
281            )
282            .label_size(LabelSize::Default)
283            .style(ToggleButtonGroupStyle::Outlined)
284            .auto_width()
285            .selected_index(match self.tab {
286                GitPickerTab::Branches => 0,
287                GitPickerTab::Worktrees => 1,
288                GitPickerTab::Stash => 2,
289            }),
290        )
291    }
292
293    fn render_active_picker(
294        &mut self,
295        window: &mut Window,
296        cx: &mut Context<Self>,
297    ) -> impl IntoElement {
298        match self.tab {
299            GitPickerTab::Branches => {
300                let branch_list = self.ensure_branch_list(window, cx);
301                branch_list.into_any_element()
302            }
303            GitPickerTab::Worktrees => {
304                let worktree_list = self.ensure_worktree_list(window, cx);
305                worktree_list.into_any_element()
306            }
307            GitPickerTab::Stash => {
308                let stash_list = self.ensure_stash_list(window, cx);
309                stash_list.into_any_element()
310            }
311        }
312    }
313
314    fn handle_modifiers_changed(
315        &mut self,
316        ev: &ModifiersChangedEvent,
317        window: &mut Window,
318        cx: &mut Context<Self>,
319    ) {
320        match self.tab {
321            GitPickerTab::Branches => {
322                if let Some(branch_list) = &self.branch_list {
323                    branch_list.update(cx, |list, cx| {
324                        list.handle_modifiers_changed(ev, window, cx);
325                    });
326                }
327            }
328            GitPickerTab::Worktrees => {
329                if let Some(worktree_list) = &self.worktree_list {
330                    worktree_list.update(cx, |list, cx| {
331                        list.handle_modifiers_changed(ev, window, cx);
332                    });
333                }
334            }
335            GitPickerTab::Stash => {
336                if let Some(stash_list) = &self.stash_list {
337                    stash_list.update(cx, |list, cx| {
338                        list.handle_modifiers_changed(ev, window, cx);
339                    });
340                }
341            }
342        }
343    }
344
345    fn handle_delete_branch(
346        &mut self,
347        _: &DeleteBranch,
348        window: &mut Window,
349        cx: &mut Context<Self>,
350    ) {
351        if let Some(branch_list) = &self.branch_list {
352            branch_list.update(cx, |list, cx| {
353                list.handle_delete(&DeleteBranch, window, cx);
354            });
355        }
356    }
357
358    fn handle_filter_remotes(
359        &mut self,
360        _: &FilterRemotes,
361        window: &mut Window,
362        cx: &mut Context<Self>,
363    ) {
364        if let Some(branch_list) = &self.branch_list {
365            branch_list.update(cx, |list, cx| {
366                list.handle_filter(&FilterRemotes, window, cx);
367            });
368        }
369    }
370
371    fn handle_worktree_from_default(
372        &mut self,
373        _: &WorktreeFromDefault,
374        window: &mut Window,
375        cx: &mut Context<Self>,
376    ) {
377        if let Some(worktree_list) = &self.worktree_list {
378            worktree_list.update(cx, |list, cx| {
379                list.handle_new_worktree(false, window, cx);
380            });
381        }
382    }
383
384    fn handle_worktree_from_default_on_window(
385        &mut self,
386        _: &WorktreeFromDefaultOnWindow,
387        window: &mut Window,
388        cx: &mut Context<Self>,
389    ) {
390        if let Some(worktree_list) = &self.worktree_list {
391            worktree_list.update(cx, |list, cx| {
392                list.handle_new_worktree(true, window, cx);
393            });
394        }
395    }
396
397    fn handle_drop_stash(
398        &mut self,
399        _: &DropStashItem,
400        window: &mut Window,
401        cx: &mut Context<Self>,
402    ) {
403        if let Some(stash_list) = &self.stash_list {
404            stash_list.update(cx, |list, cx| {
405                list.handle_drop_stash(&DropStashItem, window, cx);
406            });
407        }
408    }
409
410    fn handle_show_stash(
411        &mut self,
412        _: &ShowStashItem,
413        window: &mut Window,
414        cx: &mut Context<Self>,
415    ) {
416        if let Some(stash_list) = &self.stash_list {
417            stash_list.update(cx, |list, cx| {
418                list.handle_show_stash(&ShowStashItem, window, cx);
419            });
420        }
421    }
422}
423
424impl ModalView for GitPicker {}
425impl EventEmitter<DismissEvent> for GitPicker {}
426
427impl Focusable for GitPicker {
428    fn focus_handle(&self, cx: &App) -> FocusHandle {
429        match self.tab {
430            GitPickerTab::Branches => {
431                if let Some(branch_list) = &self.branch_list {
432                    return branch_list.focus_handle(cx);
433                }
434            }
435            GitPickerTab::Worktrees => {
436                if let Some(worktree_list) = &self.worktree_list {
437                    return worktree_list.focus_handle(cx);
438                }
439            }
440            GitPickerTab::Stash => {
441                if let Some(stash_list) = &self.stash_list {
442                    return stash_list.focus_handle(cx);
443                }
444            }
445        }
446        cx.focus_handle()
447    }
448}
449
450impl Render for GitPicker {
451    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
452        v_flex()
453            .occlude()
454            .w(self.width)
455            .elevation_3(cx)
456            .overflow_hidden()
457            .key_context({
458                let mut key_context = KeyContext::new_with_defaults();
459                key_context.add("Pane");
460                key_context.add("GitPicker");
461                match self.tab {
462                    GitPickerTab::Branches => key_context.add("GitBranchSelector"),
463                    GitPickerTab::Worktrees => key_context.add("GitWorktreeSelector"),
464                    GitPickerTab::Stash => key_context.add("StashList"),
465                }
466                key_context
467            })
468            .on_mouse_down(MouseButton::Left, |_, _, cx| {
469                cx.stop_propagation();
470            })
471            .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
472                cx.emit(DismissEvent);
473            }))
474            .on_action(cx.listener(|this, _: &pane::ActivateNextItem, window, cx| {
475                this.activate_next_tab(window, cx);
476            }))
477            .on_action(
478                cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| {
479                    this.activate_previous_tab(window, cx);
480                }),
481            )
482            .on_action(cx.listener(|this, _: &ActivateBranchesTab, window, cx| {
483                this.tab = GitPickerTab::Branches;
484                this.ensure_active_picker(window, cx);
485                this.focus_active_picker(window, cx);
486                cx.notify();
487            }))
488            .on_action(cx.listener(|this, _: &ActivateWorktreesTab, window, cx| {
489                this.tab = GitPickerTab::Worktrees;
490                this.ensure_active_picker(window, cx);
491                this.focus_active_picker(window, cx);
492                cx.notify();
493            }))
494            .on_action(cx.listener(|this, _: &ActivateStashTab, window, cx| {
495                this.tab = GitPickerTab::Stash;
496                this.ensure_active_picker(window, cx);
497                this.focus_active_picker(window, cx);
498                cx.notify();
499            }))
500            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
501            .when(self.tab == GitPickerTab::Branches, |el| {
502                el.on_action(cx.listener(Self::handle_delete_branch))
503                    .on_action(cx.listener(Self::handle_filter_remotes))
504            })
505            .when(self.tab == GitPickerTab::Worktrees, |el| {
506                el.on_action(cx.listener(Self::handle_worktree_from_default))
507                    .on_action(cx.listener(Self::handle_worktree_from_default_on_window))
508            })
509            .when(self.tab == GitPickerTab::Stash, |el| {
510                el.on_action(cx.listener(Self::handle_drop_stash))
511                    .on_action(cx.listener(Self::handle_show_stash))
512            })
513            .child(self.render_tab_bar(cx))
514            .child(self.render_active_picker(window, cx))
515    }
516}
517
518pub fn open_branches(
519    workspace: &mut Workspace,
520    _: &zed_actions::git::Branch,
521    window: &mut Window,
522    cx: &mut Context<Workspace>,
523) {
524    open_with_tab(workspace, GitPickerTab::Branches, window, cx);
525}
526
527pub fn open_worktrees(
528    workspace: &mut Workspace,
529    _: &zed_actions::git::Worktree,
530    window: &mut Window,
531    cx: &mut Context<Workspace>,
532) {
533    open_with_tab(workspace, GitPickerTab::Worktrees, window, cx);
534}
535
536pub fn open_stash(
537    workspace: &mut Workspace,
538    _: &zed_actions::git::ViewStash,
539    window: &mut Window,
540    cx: &mut Context<Workspace>,
541) {
542    open_with_tab(workspace, GitPickerTab::Stash, window, cx);
543}
544
545fn open_with_tab(
546    workspace: &mut Workspace,
547    tab: GitPickerTab,
548    window: &mut Window,
549    cx: &mut Context<Workspace>,
550) {
551    let workspace_handle = workspace.weak_handle();
552    let repository = workspace.project().read(cx).active_repository(cx);
553
554    workspace.toggle_modal(window, cx, |window, cx| {
555        GitPicker::new(workspace_handle, repository, tab, rems(34.), window, cx)
556    })
557}
558
559/// Register all git picker actions with the workspace.
560pub fn register(workspace: &mut Workspace) {
561    workspace.register_action(|workspace, _: &zed_actions::git::Branch, window, cx| {
562        open_with_tab(workspace, GitPickerTab::Branches, window, cx);
563    });
564    workspace.register_action(|workspace, _: &zed_actions::git::Switch, window, cx| {
565        open_with_tab(workspace, GitPickerTab::Branches, window, cx);
566    });
567    workspace.register_action(
568        |workspace, _: &zed_actions::git::CheckoutBranch, window, cx| {
569            open_with_tab(workspace, GitPickerTab::Branches, window, cx);
570        },
571    );
572    workspace.register_action(|workspace, _: &zed_actions::git::Worktree, window, cx| {
573        open_with_tab(workspace, GitPickerTab::Worktrees, window, cx);
574    });
575    workspace.register_action(|workspace, _: &zed_actions::git::ViewStash, window, cx| {
576        open_with_tab(workspace, GitPickerTab::Stash, window, cx);
577    });
578}