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}