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};
 17
 18actions!(git_picker, [ActivateBranchesTab, ActivateStashTab,]);
 19
 20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 21pub enum GitPickerTab {
 22    Branches,
 23    Stash,
 24}
 25
 26impl Display for GitPickerTab {
 27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 28        let label = match self {
 29            GitPickerTab::Branches => "Branches",
 30            GitPickerTab::Stash => "Stash",
 31        };
 32        write!(f, "{}", label)
 33    }
 34}
 35
 36pub struct GitPicker {
 37    tab: GitPickerTab,
 38    workspace: WeakEntity<Workspace>,
 39    repository: Option<Entity<Repository>>,
 40    width: Rems,
 41    branch_list: Option<Entity<BranchList>>,
 42    stash_list: Option<Entity<StashList>>,
 43    _subscriptions: Vec<Subscription>,
 44    popover_style: bool,
 45}
 46
 47impl GitPicker {
 48    pub fn new(
 49        workspace: WeakEntity<Workspace>,
 50        repository: Option<Entity<Repository>>,
 51        initial_tab: GitPickerTab,
 52        width: Rems,
 53        window: &mut Window,
 54        cx: &mut Context<Self>,
 55    ) -> Self {
 56        Self::new_internal(workspace, repository, initial_tab, width, false, window, cx)
 57    }
 58
 59    fn new_internal(
 60        workspace: WeakEntity<Workspace>,
 61        repository: Option<Entity<Repository>>,
 62        initial_tab: GitPickerTab,
 63        width: Rems,
 64        popover_style: bool,
 65        window: &mut Window,
 66        cx: &mut Context<Self>,
 67    ) -> Self {
 68        let mut this = Self {
 69            tab: initial_tab,
 70            workspace,
 71            repository,
 72            width,
 73            branch_list: None,
 74            stash_list: None,
 75            _subscriptions: Vec::new(),
 76            popover_style,
 77        };
 78
 79        this.ensure_active_picker(window, cx);
 80        this
 81    }
 82
 83    fn ensure_active_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 84        match self.tab {
 85            GitPickerTab::Branches => {
 86                self.ensure_branch_list(window, cx);
 87            }
 88            GitPickerTab::Stash => {
 89                self.ensure_stash_list(window, cx);
 90            }
 91        }
 92    }
 93
 94    fn ensure_branch_list(
 95        &mut self,
 96        window: &mut Window,
 97        cx: &mut Context<Self>,
 98    ) -> Entity<BranchList> {
 99        if self.branch_list.is_none() {
100            let show_footer = !self.popover_style;
101            let branch_list = cx.new(|cx| {
102                branch_picker::create_embedded(
103                    self.workspace.clone(),
104                    self.repository.clone(),
105                    self.width,
106                    show_footer,
107                    window,
108                    cx,
109                )
110            });
111
112            let subscription = cx.subscribe(&branch_list, |this, _, _: &DismissEvent, cx| {
113                if this.tab == GitPickerTab::Branches {
114                    cx.emit(DismissEvent);
115                }
116            });
117
118            self._subscriptions.push(subscription);
119            self.branch_list = Some(branch_list);
120        }
121        self.branch_list.clone().unwrap()
122    }
123
124    fn ensure_stash_list(
125        &mut self,
126        window: &mut Window,
127        cx: &mut Context<Self>,
128    ) -> Entity<StashList> {
129        if self.stash_list.is_none() {
130            let show_footer = !self.popover_style;
131            let stash_list = cx.new(|cx| {
132                stash_picker::create_embedded(
133                    self.repository.clone(),
134                    self.workspace.clone(),
135                    self.width,
136                    show_footer,
137                    window,
138                    cx,
139                )
140            });
141
142            let subscription = cx.subscribe(&stash_list, |this, _, _: &DismissEvent, cx| {
143                if this.tab == GitPickerTab::Stash {
144                    cx.emit(DismissEvent);
145                }
146            });
147
148            self._subscriptions.push(subscription);
149            self.stash_list = Some(stash_list);
150        }
151        self.stash_list.clone().unwrap()
152    }
153
154    fn activate_next_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
155        self.tab = match self.tab {
156            GitPickerTab::Branches => GitPickerTab::Stash,
157            GitPickerTab::Stash => GitPickerTab::Branches,
158        };
159        self.ensure_active_picker(window, cx);
160        self.focus_active_picker(window, cx);
161        cx.notify();
162    }
163
164    fn activate_previous_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
165        self.tab = match self.tab {
166            GitPickerTab::Branches => GitPickerTab::Stash,
167            GitPickerTab::Stash => GitPickerTab::Branches,
168        };
169        self.ensure_active_picker(window, cx);
170        self.focus_active_picker(window, cx);
171        cx.notify();
172    }
173
174    fn focus_active_picker(&self, window: &mut Window, cx: &mut App) {
175        match self.tab {
176            GitPickerTab::Branches => {
177                if let Some(branch_list) = &self.branch_list {
178                    branch_list.focus_handle(cx).focus(window, cx);
179                }
180            }
181            GitPickerTab::Stash => {
182                if let Some(stash_list) = &self.stash_list {
183                    stash_list.focus_handle(cx).focus(window, cx);
184                }
185            }
186        }
187    }
188
189    fn render_tab_bar(&self, cx: &mut Context<Self>) -> impl IntoElement {
190        let focus_handle = self.focus_handle(cx);
191        let branches_focus_handle = focus_handle.clone();
192        let stash_focus_handle = focus_handle;
193
194        h_flex().p_2().pb_0p5().w_full().child(
195            ToggleButtonGroup::single_row(
196                "git-picker-tabs",
197                [
198                    ToggleButtonSimple::new(
199                        GitPickerTab::Branches.to_string(),
200                        cx.listener(|this, _, window, cx| {
201                            this.tab = GitPickerTab::Branches;
202                            this.ensure_active_picker(window, cx);
203                            this.focus_active_picker(window, cx);
204                            cx.notify();
205                        }),
206                    )
207                    .tooltip(move |_, cx| {
208                        Tooltip::for_action_in(
209                            "Toggle Branch Picker",
210                            &ActivateBranchesTab,
211                            &branches_focus_handle,
212                            cx,
213                        )
214                    }),
215                    ToggleButtonSimple::new(
216                        GitPickerTab::Stash.to_string(),
217                        cx.listener(|this, _, window, cx| {
218                            this.tab = GitPickerTab::Stash;
219                            this.ensure_active_picker(window, cx);
220                            this.focus_active_picker(window, cx);
221                            cx.notify();
222                        }),
223                    )
224                    .tooltip(move |_, cx| {
225                        Tooltip::for_action_in(
226                            "Toggle Stash Picker",
227                            &ActivateStashTab,
228                            &stash_focus_handle,
229                            cx,
230                        )
231                    }),
232                ],
233            )
234            .label_size(LabelSize::Default)
235            .style(ToggleButtonGroupStyle::Outlined)
236            .auto_width()
237            .selected_index(match self.tab {
238                GitPickerTab::Branches => 0,
239                GitPickerTab::Stash => 1,
240            }),
241        )
242    }
243
244    fn render_active_picker(
245        &mut self,
246        window: &mut Window,
247        cx: &mut Context<Self>,
248    ) -> impl IntoElement {
249        match self.tab {
250            GitPickerTab::Branches => {
251                let branch_list = self.ensure_branch_list(window, cx);
252                branch_list.into_any_element()
253            }
254            GitPickerTab::Stash => {
255                let stash_list = self.ensure_stash_list(window, cx);
256                stash_list.into_any_element()
257            }
258        }
259    }
260
261    fn handle_modifiers_changed(
262        &mut self,
263        ev: &ModifiersChangedEvent,
264        window: &mut Window,
265        cx: &mut Context<Self>,
266    ) {
267        match self.tab {
268            GitPickerTab::Branches => {
269                if let Some(branch_list) = &self.branch_list {
270                    branch_list.update(cx, |list, cx| {
271                        list.handle_modifiers_changed(ev, window, cx);
272                    });
273                }
274            }
275            GitPickerTab::Stash => {
276                if let Some(stash_list) = &self.stash_list {
277                    stash_list.update(cx, |list, cx| {
278                        list.handle_modifiers_changed(ev, window, cx);
279                    });
280                }
281            }
282        }
283    }
284
285    fn handle_delete_branch(
286        &mut self,
287        _: &DeleteBranch,
288        window: &mut Window,
289        cx: &mut Context<Self>,
290    ) {
291        if let Some(branch_list) = &self.branch_list {
292            branch_list.update(cx, |list, cx| {
293                list.handle_delete(&DeleteBranch, window, cx);
294            });
295        }
296    }
297
298    fn handle_filter_remotes(
299        &mut self,
300        _: &FilterRemotes,
301        window: &mut Window,
302        cx: &mut Context<Self>,
303    ) {
304        if let Some(branch_list) = &self.branch_list {
305            branch_list.update(cx, |list, cx| {
306                list.handle_filter(&FilterRemotes, window, cx);
307            });
308        }
309    }
310
311    fn handle_drop_stash(
312        &mut self,
313        _: &DropStashItem,
314        window: &mut Window,
315        cx: &mut Context<Self>,
316    ) {
317        if let Some(stash_list) = &self.stash_list {
318            stash_list.update(cx, |list, cx| {
319                list.handle_drop_stash(&DropStashItem, window, cx);
320            });
321        }
322    }
323
324    fn handle_show_stash(
325        &mut self,
326        _: &ShowStashItem,
327        window: &mut Window,
328        cx: &mut Context<Self>,
329    ) {
330        if let Some(stash_list) = &self.stash_list {
331            stash_list.update(cx, |list, cx| {
332                list.handle_show_stash(&ShowStashItem, window, cx);
333            });
334        }
335    }
336}
337
338impl ModalView for GitPicker {}
339impl EventEmitter<DismissEvent> for GitPicker {}
340
341impl Focusable for GitPicker {
342    fn focus_handle(&self, cx: &App) -> FocusHandle {
343        match self.tab {
344            GitPickerTab::Branches => {
345                if let Some(branch_list) = &self.branch_list {
346                    return branch_list.focus_handle(cx);
347                }
348            }
349            GitPickerTab::Stash => {
350                if let Some(stash_list) = &self.stash_list {
351                    return stash_list.focus_handle(cx);
352                }
353            }
354        }
355        cx.focus_handle()
356    }
357}
358
359impl Render for GitPicker {
360    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
361        v_flex()
362            .occlude()
363            .w(self.width)
364            .elevation_3(cx)
365            .overflow_hidden()
366            .when(self.popover_style, |el| {
367                el.on_mouse_down_out(cx.listener(|_, _, _, cx| {
368                    cx.emit(DismissEvent);
369                }))
370            })
371            .key_context({
372                let mut key_context = KeyContext::new_with_defaults();
373                key_context.add("Pane");
374                key_context.add("GitPicker");
375                match self.tab {
376                    GitPickerTab::Branches => key_context.add("GitBranchSelector"),
377                    GitPickerTab::Stash => key_context.add("StashList"),
378                }
379                key_context
380            })
381            .on_mouse_down(MouseButton::Left, |_, _, cx| {
382                cx.stop_propagation();
383            })
384            .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
385                cx.emit(DismissEvent);
386            }))
387            .on_action(cx.listener(|this, _: &pane::ActivateNextItem, window, cx| {
388                this.activate_next_tab(window, cx);
389            }))
390            .on_action(
391                cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| {
392                    this.activate_previous_tab(window, cx);
393                }),
394            )
395            .on_action(cx.listener(|this, _: &ActivateBranchesTab, window, cx| {
396                this.tab = GitPickerTab::Branches;
397                this.ensure_active_picker(window, cx);
398                this.focus_active_picker(window, cx);
399                cx.notify();
400            }))
401            .on_action(cx.listener(|this, _: &ActivateStashTab, window, cx| {
402                this.tab = GitPickerTab::Stash;
403                this.ensure_active_picker(window, cx);
404                this.focus_active_picker(window, cx);
405                cx.notify();
406            }))
407            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
408            .when(self.tab == GitPickerTab::Branches, |el| {
409                el.on_action(cx.listener(Self::handle_delete_branch))
410                    .on_action(cx.listener(Self::handle_filter_remotes))
411            })
412            .when(self.tab == GitPickerTab::Stash, |el| {
413                el.on_action(cx.listener(Self::handle_drop_stash))
414                    .on_action(cx.listener(Self::handle_show_stash))
415            })
416            .child(self.render_tab_bar(cx))
417            .child(self.render_active_picker(window, cx))
418    }
419}
420
421pub fn open_branches(
422    workspace: &mut Workspace,
423    _: &zed_actions::git::Branch,
424    window: &mut Window,
425    cx: &mut Context<Workspace>,
426) {
427    open_with_tab(workspace, GitPickerTab::Branches, window, cx);
428}
429
430pub fn open_stash(
431    workspace: &mut Workspace,
432    _: &zed_actions::git::ViewStash,
433    window: &mut Window,
434    cx: &mut Context<Workspace>,
435) {
436    open_with_tab(workspace, GitPickerTab::Stash, window, cx);
437}
438
439fn open_with_tab(
440    workspace: &mut Workspace,
441    tab: GitPickerTab,
442    window: &mut Window,
443    cx: &mut Context<Workspace>,
444) {
445    let workspace_handle = workspace.weak_handle();
446    let repository = workspace.project().read(cx).active_repository(cx);
447
448    workspace.toggle_modal(window, cx, |window, cx| {
449        GitPicker::new(workspace_handle, repository, tab, rems(34.), window, cx)
450    })
451}
452
453pub fn popover(
454    workspace: WeakEntity<Workspace>,
455    repository: Option<Entity<Repository>>,
456    initial_tab: GitPickerTab,
457    width: Rems,
458    window: &mut Window,
459    cx: &mut App,
460) -> Entity<GitPicker> {
461    cx.new(|cx| {
462        let picker =
463            GitPicker::new_internal(workspace, repository, initial_tab, width, true, window, cx);
464        picker.focus_handle(cx).focus(window, cx);
465        picker
466    })
467}
468
469pub fn register(workspace: &mut Workspace) {
470    workspace.register_action(|workspace, _: &zed_actions::git::Branch, window, cx| {
471        open_with_tab(workspace, GitPickerTab::Branches, window, cx);
472    });
473    workspace.register_action(|workspace, _: &zed_actions::git::Switch, window, cx| {
474        open_with_tab(workspace, GitPickerTab::Branches, window, cx);
475    });
476    workspace.register_action(
477        |workspace, _: &zed_actions::git::CheckoutBranch, window, cx| {
478            open_with_tab(workspace, GitPickerTab::Branches, window, cx);
479        },
480    );
481    workspace.register_action(|workspace, _: &zed_actions::git::ViewStash, window, cx| {
482        open_with_tab(workspace, GitPickerTab::Stash, window, cx);
483    });
484}