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}