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}