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}