git_ui.rs

  1use std::any::Any;
  2
  3use command_palette_hooks::CommandPaletteFilter;
  4use commit_modal::CommitModal;
  5use editor::{Editor, actions::DiffClipboardWithSelectionData};
  6use project::ProjectPath;
  7use ui::{
  8    Headline, HeadlineSize, Icon, IconName, IconSize, IntoElement, ParentElement, Render, Styled,
  9    StyledExt, div, h_flex, rems, v_flex,
 10};
 11
 12mod blame_ui;
 13
 14use git::{
 15    repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
 16    status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
 17};
 18use gpui::{
 19    Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, SharedString,
 20    Window, actions,
 21};
 22use menu::{Cancel, Confirm};
 23use onboarding::GitOnboardingModal;
 24use project::git_store::Repository;
 25use project_diff::ProjectDiff;
 26use ui::prelude::*;
 27use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr};
 28use zed_actions;
 29
 30use crate::{git_panel::GitPanel, text_diff_view::TextDiffView};
 31
 32mod askpass_modal;
 33pub mod branch_picker;
 34mod commit_modal;
 35pub mod commit_tooltip;
 36pub mod commit_view;
 37mod conflict_view;
 38pub mod file_diff_view;
 39pub mod file_history_view;
 40pub mod git_panel;
 41mod git_panel_settings;
 42pub mod onboarding;
 43pub mod picker_prompt;
 44pub mod project_diff;
 45pub(crate) mod remote_output;
 46pub mod repository_selector;
 47pub mod stash_picker;
 48pub mod text_diff_view;
 49pub mod worktree_picker;
 50
 51actions!(
 52    git,
 53    [
 54        /// Resets the git onboarding state to show the tutorial again.
 55        ResetOnboarding
 56    ]
 57);
 58
 59pub fn init(cx: &mut App) {
 60    editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx);
 61    commit_view::init(cx);
 62    file_history_view::init(cx);
 63
 64    cx.observe_new(|editor: &mut Editor, _, cx| {
 65        conflict_view::register_editor(editor, editor.buffer().clone(), cx);
 66    })
 67    .detach();
 68
 69    cx.observe_new(|workspace: &mut Workspace, _, cx| {
 70        ProjectDiff::register(workspace, cx);
 71        CommitModal::register(workspace);
 72        git_panel::register(workspace);
 73        repository_selector::register(workspace);
 74        branch_picker::register(workspace);
 75        worktree_picker::register(workspace);
 76        stash_picker::register(workspace);
 77
 78        let project = workspace.project().read(cx);
 79        if project.is_read_only(cx) {
 80            return;
 81        }
 82        if !project.is_via_collab() {
 83            workspace.register_action(|workspace, _: &git::Fetch, window, cx| {
 84                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
 85                    return;
 86                };
 87                panel.update(cx, |panel, cx| {
 88                    panel.fetch(true, window, cx);
 89                });
 90            });
 91            workspace.register_action(|workspace, _: &git::FetchFrom, window, cx| {
 92                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
 93                    return;
 94                };
 95                panel.update(cx, |panel, cx| {
 96                    panel.fetch(false, window, cx);
 97                });
 98            });
 99            workspace.register_action(|workspace, _: &git::Push, window, cx| {
100                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
101                    return;
102                };
103                panel.update(cx, |panel, cx| {
104                    panel.push(false, false, window, cx);
105                });
106            });
107            workspace.register_action(|workspace, _: &git::PushTo, window, cx| {
108                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
109                    return;
110                };
111                panel.update(cx, |panel, cx| {
112                    panel.push(false, true, window, cx);
113                });
114            });
115            workspace.register_action(|workspace, _: &git::ForcePush, window, cx| {
116                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
117                    return;
118                };
119                panel.update(cx, |panel, cx| {
120                    panel.push(true, false, window, cx);
121                });
122            });
123            workspace.register_action(|workspace, _: &git::Pull, window, cx| {
124                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
125                    return;
126                };
127                panel.update(cx, |panel, cx| {
128                    panel.pull(false, window, cx);
129                });
130            });
131            workspace.register_action(|workspace, _: &git::PullRebase, window, cx| {
132                let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
133                    return;
134                };
135                panel.update(cx, |panel, cx| {
136                    panel.pull(true, window, cx);
137                });
138            });
139        }
140        workspace.register_action(|workspace, action: &git::StashAll, window, cx| {
141            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
142                return;
143            };
144            panel.update(cx, |panel, cx| {
145                panel.stash_all(action, window, cx);
146            });
147        });
148        workspace.register_action(|workspace, action: &git::StashPop, window, cx| {
149            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
150                return;
151            };
152            panel.update(cx, |panel, cx| {
153                panel.stash_pop(action, window, cx);
154            });
155        });
156        workspace.register_action(|workspace, action: &git::StashApply, window, cx| {
157            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
158                return;
159            };
160            panel.update(cx, |panel, cx| {
161                panel.stash_apply(action, window, cx);
162            });
163        });
164        workspace.register_action(|workspace, action: &git::StageAll, window, cx| {
165            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
166                return;
167            };
168            panel.update(cx, |panel, cx| {
169                panel.stage_all(action, window, cx);
170            });
171        });
172        workspace.register_action(|workspace, action: &git::UnstageAll, window, cx| {
173            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
174                return;
175            };
176            panel.update(cx, |panel, cx| {
177                panel.unstage_all(action, window, cx);
178            });
179        });
180        workspace.register_action(|workspace, _: &git::Uncommit, window, cx| {
181            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
182                return;
183            };
184            panel.update(cx, |panel, cx| {
185                panel.uncommit(window, cx);
186            })
187        });
188        CommandPaletteFilter::update_global(cx, |filter, _cx| {
189            filter.hide_action_types(&[
190                zed_actions::OpenGitIntegrationOnboarding.type_id(),
191                // ResetOnboarding.type_id(),
192            ]);
193        });
194        workspace.register_action(
195            move |workspace, _: &zed_actions::OpenGitIntegrationOnboarding, window, cx| {
196                GitOnboardingModal::toggle(workspace, window, cx)
197            },
198        );
199        workspace.register_action(move |_, _: &ResetOnboarding, window, cx| {
200            window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
201            window.refresh();
202        });
203        workspace.register_action(|workspace, _action: &git::Init, window, cx| {
204            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
205                return;
206            };
207            panel.update(cx, |panel, cx| {
208                panel.git_init(window, cx);
209            });
210        });
211        workspace.register_action(|workspace, _action: &git::Clone, window, cx| {
212            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
213                return;
214            };
215
216            workspace.toggle_modal(window, cx, |window, cx| {
217                GitCloneModal::show(panel, window, cx)
218            });
219        });
220        workspace.register_action(|workspace, _: &git::OpenModifiedFiles, window, cx| {
221            open_modified_files(workspace, window, cx);
222        });
223        workspace.register_action(|workspace, _: &git::RenameBranch, window, cx| {
224            rename_current_branch(workspace, window, cx);
225        });
226        workspace.register_action(
227            |workspace, action: &DiffClipboardWithSelectionData, window, cx| {
228                if let Some(task) = TextDiffView::open(action, workspace, window, cx) {
229                    task.detach();
230                };
231            },
232        );
233        workspace.register_action(|workspace, _: &git::FileHistory, window, cx| {
234            let Some(active_item) = workspace.active_item(cx) else {
235                return;
236            };
237            let Some(editor) = active_item.downcast::<Editor>() else {
238                return;
239            };
240            let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() else {
241                return;
242            };
243            let Some(file) = buffer.read(cx).file() else {
244                return;
245            };
246            let worktree_id = file.worktree_id(cx);
247            let project_path = ProjectPath {
248                worktree_id,
249                path: file.path().clone(),
250            };
251            let project = workspace.project();
252            let git_store = project.read(cx).git_store();
253            let Some((repo, repo_path)) = git_store
254                .read(cx)
255                .repository_and_path_for_project_path(&project_path, cx)
256            else {
257                return;
258            };
259            file_history_view::FileHistoryView::open(
260                repo_path,
261                git_store.downgrade(),
262                repo.downgrade(),
263                workspace.weak_handle(),
264                window,
265                cx,
266            );
267        });
268    })
269    .detach();
270}
271
272fn open_modified_files(
273    workspace: &mut Workspace,
274    window: &mut Window,
275    cx: &mut Context<Workspace>,
276) {
277    let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
278        return;
279    };
280    let modified_paths: Vec<_> = panel.update(cx, |panel, cx| {
281        let Some(repo) = panel.active_repository.as_ref() else {
282            return Vec::new();
283        };
284        let repo = repo.read(cx);
285        repo.cached_status()
286            .filter_map(|entry| {
287                if entry.status.is_modified() {
288                    repo.repo_path_to_project_path(&entry.repo_path, cx)
289                } else {
290                    None
291                }
292            })
293            .collect()
294    });
295    for path in modified_paths {
296        workspace.open_path(path, None, true, window, cx).detach();
297    }
298}
299
300pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
301    GitStatusIcon::new(status)
302}
303
304struct RenameBranchModal {
305    current_branch: SharedString,
306    editor: Entity<Editor>,
307    repo: Entity<Repository>,
308}
309
310impl RenameBranchModal {
311    fn new(
312        current_branch: String,
313        repo: Entity<Repository>,
314        window: &mut Window,
315        cx: &mut Context<Self>,
316    ) -> Self {
317        let editor = cx.new(|cx| {
318            let mut editor = Editor::single_line(window, cx);
319            editor.set_text(current_branch.clone(), window, cx);
320            editor
321        });
322        Self {
323            current_branch: current_branch.into(),
324            editor,
325            repo,
326        }
327    }
328
329    fn cancel(&mut self, _: &Cancel, _window: &mut Window, cx: &mut Context<Self>) {
330        cx.emit(DismissEvent);
331    }
332
333    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
334        let new_name = self.editor.read(cx).text(cx);
335        if new_name.is_empty() || new_name == self.current_branch.as_ref() {
336            cx.emit(DismissEvent);
337            return;
338        }
339
340        let repo = self.repo.clone();
341        let current_branch = self.current_branch.to_string();
342        cx.spawn(async move |_, cx| {
343            match repo
344                .update(cx, |repo, _| {
345                    repo.rename_branch(current_branch, new_name.clone())
346                })?
347                .await
348            {
349                Ok(Ok(_)) => Ok(()),
350                Ok(Err(error)) => Err(error),
351                Err(_) => Err(anyhow::anyhow!("Operation was canceled")),
352            }
353        })
354        .detach_and_prompt_err("Failed to rename branch", window, cx, |_, _, _| None);
355        cx.emit(DismissEvent);
356    }
357}
358
359impl EventEmitter<DismissEvent> for RenameBranchModal {}
360impl ModalView for RenameBranchModal {}
361impl Focusable for RenameBranchModal {
362    fn focus_handle(&self, cx: &App) -> FocusHandle {
363        self.editor.focus_handle(cx)
364    }
365}
366
367impl Render for RenameBranchModal {
368    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
369        v_flex()
370            .key_context("RenameBranchModal")
371            .on_action(cx.listener(Self::cancel))
372            .on_action(cx.listener(Self::confirm))
373            .elevation_2(cx)
374            .w(rems(34.))
375            .child(
376                h_flex()
377                    .px_3()
378                    .pt_2()
379                    .pb_1()
380                    .w_full()
381                    .gap_1p5()
382                    .child(Icon::new(IconName::GitBranch).size(IconSize::XSmall))
383                    .child(
384                        Headline::new(format!("Rename Branch ({})", self.current_branch))
385                            .size(HeadlineSize::XSmall),
386                    ),
387            )
388            .child(div().px_3().pb_3().w_full().child(self.editor.clone()))
389    }
390}
391
392fn rename_current_branch(
393    workspace: &mut Workspace,
394    window: &mut Window,
395    cx: &mut Context<Workspace>,
396) {
397    let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
398        return;
399    };
400    let current_branch: Option<String> = panel.update(cx, |panel, cx| {
401        let repo = panel.active_repository.as_ref()?;
402        let repo = repo.read(cx);
403        repo.branch.as_ref().map(|branch| branch.name().to_string())
404    });
405
406    let Some(current_branch_name) = current_branch else {
407        return;
408    };
409
410    let repo = panel.read(cx).active_repository.clone();
411    let Some(repo) = repo else {
412        return;
413    };
414
415    workspace.toggle_modal(window, cx, |window, cx| {
416        RenameBranchModal::new(current_branch_name, repo, window, cx)
417    });
418}
419
420fn render_remote_button(
421    id: impl Into<SharedString>,
422    branch: &Branch,
423    keybinding_target: Option<FocusHandle>,
424    show_fetch_button: bool,
425) -> Option<impl IntoElement> {
426    let id = id.into();
427    let upstream = branch.upstream.as_ref();
428    match upstream {
429        Some(Upstream {
430            tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead, behind }),
431            ..
432        }) => match (*ahead, *behind) {
433            (0, 0) if show_fetch_button => {
434                Some(remote_button::render_fetch_button(keybinding_target, id))
435            }
436            (0, 0) => None,
437            (ahead, 0) => Some(remote_button::render_push_button(
438                keybinding_target,
439                id,
440                ahead,
441            )),
442            (ahead, behind) => Some(remote_button::render_pull_button(
443                keybinding_target,
444                id,
445                ahead,
446                behind,
447            )),
448        },
449        Some(Upstream {
450            tracking: UpstreamTracking::Gone,
451            ..
452        }) => Some(remote_button::render_republish_button(
453            keybinding_target,
454            id,
455        )),
456        None => Some(remote_button::render_publish_button(keybinding_target, id)),
457    }
458}
459
460mod remote_button {
461    use gpui::{Action, AnyView, ClickEvent, Corner, FocusHandle};
462    use ui::{
463        App, ButtonCommon, Clickable, ContextMenu, ElementId, FluentBuilder, Icon, IconName,
464        IconSize, IntoElement, Label, LabelCommon, LabelSize, LineHeightStyle, ParentElement,
465        PopoverMenu, SharedString, SplitButton, Styled, Tooltip, Window, div, h_flex, rems,
466    };
467
468    pub fn render_fetch_button(
469        keybinding_target: Option<FocusHandle>,
470        id: SharedString,
471    ) -> SplitButton {
472        split_button(
473            id,
474            "Fetch",
475            0,
476            0,
477            Some(IconName::ArrowCircle),
478            keybinding_target.clone(),
479            move |_, window, cx| {
480                window.dispatch_action(Box::new(git::Fetch), cx);
481            },
482            move |_window, cx| {
483                git_action_tooltip(
484                    "Fetch updates from remote",
485                    &git::Fetch,
486                    "git fetch",
487                    keybinding_target.clone(),
488                    cx,
489                )
490            },
491        )
492    }
493
494    pub fn render_push_button(
495        keybinding_target: Option<FocusHandle>,
496        id: SharedString,
497        ahead: u32,
498    ) -> SplitButton {
499        split_button(
500            id,
501            "Push",
502            ahead as usize,
503            0,
504            None,
505            keybinding_target.clone(),
506            move |_, window, cx| {
507                window.dispatch_action(Box::new(git::Push), cx);
508            },
509            move |_window, cx| {
510                git_action_tooltip(
511                    "Push committed changes to remote",
512                    &git::Push,
513                    "git push",
514                    keybinding_target.clone(),
515                    cx,
516                )
517            },
518        )
519    }
520
521    pub fn render_pull_button(
522        keybinding_target: Option<FocusHandle>,
523        id: SharedString,
524        ahead: u32,
525        behind: u32,
526    ) -> SplitButton {
527        split_button(
528            id,
529            "Pull",
530            ahead as usize,
531            behind as usize,
532            None,
533            keybinding_target.clone(),
534            move |_, window, cx| {
535                window.dispatch_action(Box::new(git::Pull), cx);
536            },
537            move |_window, cx| {
538                git_action_tooltip(
539                    "Pull",
540                    &git::Pull,
541                    "git pull",
542                    keybinding_target.clone(),
543                    cx,
544                )
545            },
546        )
547    }
548
549    pub fn render_publish_button(
550        keybinding_target: Option<FocusHandle>,
551        id: SharedString,
552    ) -> SplitButton {
553        split_button(
554            id,
555            "Publish",
556            0,
557            0,
558            Some(IconName::ExpandUp),
559            keybinding_target.clone(),
560            move |_, window, cx| {
561                window.dispatch_action(Box::new(git::Push), cx);
562            },
563            move |_window, cx| {
564                git_action_tooltip(
565                    "Publish branch to remote",
566                    &git::Push,
567                    "git push --set-upstream",
568                    keybinding_target.clone(),
569                    cx,
570                )
571            },
572        )
573    }
574
575    pub fn render_republish_button(
576        keybinding_target: Option<FocusHandle>,
577        id: SharedString,
578    ) -> SplitButton {
579        split_button(
580            id,
581            "Republish",
582            0,
583            0,
584            Some(IconName::ExpandUp),
585            keybinding_target.clone(),
586            move |_, window, cx| {
587                window.dispatch_action(Box::new(git::Push), cx);
588            },
589            move |_window, cx| {
590                git_action_tooltip(
591                    "Re-publish branch to remote",
592                    &git::Push,
593                    "git push --set-upstream",
594                    keybinding_target.clone(),
595                    cx,
596                )
597            },
598        )
599    }
600
601    fn git_action_tooltip(
602        label: impl Into<SharedString>,
603        action: &dyn Action,
604        command: impl Into<SharedString>,
605        focus_handle: Option<FocusHandle>,
606        cx: &mut App,
607    ) -> AnyView {
608        let label = label.into();
609        let command = command.into();
610
611        if let Some(handle) = focus_handle {
612            Tooltip::with_meta_in(label, Some(action), command, &handle, cx)
613        } else {
614            Tooltip::with_meta(label, Some(action), command, cx)
615        }
616    }
617
618    fn render_git_action_menu(
619        id: impl Into<ElementId>,
620        keybinding_target: Option<FocusHandle>,
621    ) -> impl IntoElement {
622        PopoverMenu::new(id.into())
623            .trigger(
624                ui::ButtonLike::new_rounded_right("split-button-right")
625                    .layer(ui::ElevationIndex::ModalSurface)
626                    .size(ui::ButtonSize::None)
627                    .child(
628                        div()
629                            .px_1()
630                            .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)),
631                    ),
632            )
633            .menu(move |window, cx| {
634                Some(ContextMenu::build(window, cx, |context_menu, _, _| {
635                    context_menu
636                        .when_some(keybinding_target.clone(), |el, keybinding_target| {
637                            el.context(keybinding_target)
638                        })
639                        .action("Fetch", git::Fetch.boxed_clone())
640                        .action("Fetch From", git::FetchFrom.boxed_clone())
641                        .action("Pull", git::Pull.boxed_clone())
642                        .action("Pull (Rebase)", git::PullRebase.boxed_clone())
643                        .separator()
644                        .action("Push", git::Push.boxed_clone())
645                        .action("Push To", git::PushTo.boxed_clone())
646                        .action("Force Push", git::ForcePush.boxed_clone())
647                }))
648            })
649            .anchor(Corner::TopRight)
650    }
651
652    #[allow(clippy::too_many_arguments)]
653    fn split_button(
654        id: SharedString,
655        left_label: impl Into<SharedString>,
656        ahead_count: usize,
657        behind_count: usize,
658        left_icon: Option<IconName>,
659        keybinding_target: Option<FocusHandle>,
660        left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
661        tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
662    ) -> SplitButton {
663        fn count(count: usize) -> impl IntoElement {
664            h_flex()
665                .ml_neg_px()
666                .h(rems(0.875))
667                .items_center()
668                .overflow_hidden()
669                .px_0p5()
670                .child(
671                    Label::new(count.to_string())
672                        .size(LabelSize::XSmall)
673                        .line_height_style(LineHeightStyle::UiLabel),
674                )
675        }
676
677        let should_render_counts = left_icon.is_none() && (ahead_count > 0 || behind_count > 0);
678
679        let left = ui::ButtonLike::new_rounded_left(ElementId::Name(
680            format!("split-button-left-{}", id).into(),
681        ))
682        .layer(ui::ElevationIndex::ModalSurface)
683        .size(ui::ButtonSize::Compact)
684        .when(should_render_counts, |this| {
685            this.child(
686                h_flex()
687                    .ml_neg_0p5()
688                    .when(behind_count > 0, |this| {
689                        this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall))
690                            .child(count(behind_count))
691                    })
692                    .when(ahead_count > 0, |this| {
693                        this.child(Icon::new(IconName::ArrowUp).size(IconSize::XSmall))
694                            .child(count(ahead_count))
695                    }),
696            )
697        })
698        .when_some(left_icon, |this, left_icon| {
699            this.child(
700                h_flex()
701                    .ml_neg_0p5()
702                    .child(Icon::new(left_icon).size(IconSize::XSmall)),
703            )
704        })
705        .child(
706            div()
707                .child(Label::new(left_label).size(LabelSize::Small))
708                .mr_0p5(),
709        )
710        .on_click(left_on_click)
711        .tooltip(tooltip);
712
713        let right = render_git_action_menu(
714            ElementId::Name(format!("split-button-right-{}", id).into()),
715            keybinding_target,
716        )
717        .into_any_element();
718
719        SplitButton::new(left, right)
720    }
721}
722
723/// A visual representation of a file's Git status.
724#[derive(IntoElement, RegisterComponent)]
725pub struct GitStatusIcon {
726    status: FileStatus,
727}
728
729impl GitStatusIcon {
730    pub fn new(status: FileStatus) -> Self {
731        Self { status }
732    }
733}
734
735impl RenderOnce for GitStatusIcon {
736    fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
737        let status = self.status;
738
739        let (icon_name, color) = if status.is_conflicted() {
740            (
741                IconName::Warning,
742                cx.theme().colors().version_control_conflict,
743            )
744        } else if status.is_deleted() {
745            (
746                IconName::SquareMinus,
747                cx.theme().colors().version_control_deleted,
748            )
749        } else if status.is_modified() {
750            (
751                IconName::SquareDot,
752                cx.theme().colors().version_control_modified,
753            )
754        } else {
755            (
756                IconName::SquarePlus,
757                cx.theme().colors().version_control_added,
758            )
759        };
760
761        Icon::new(icon_name).color(Color::Custom(color))
762    }
763}
764
765// View this component preview using `workspace: open component-preview`
766impl Component for GitStatusIcon {
767    fn scope() -> ComponentScope {
768        ComponentScope::VersionControl
769    }
770
771    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
772        fn tracked_file_status(code: StatusCode) -> FileStatus {
773            FileStatus::Tracked(git::status::TrackedStatus {
774                index_status: code,
775                worktree_status: code,
776            })
777        }
778
779        let modified = tracked_file_status(StatusCode::Modified);
780        let added = tracked_file_status(StatusCode::Added);
781        let deleted = tracked_file_status(StatusCode::Deleted);
782        let conflict = UnmergedStatus {
783            first_head: UnmergedStatusCode::Updated,
784            second_head: UnmergedStatusCode::Updated,
785        }
786        .into();
787
788        Some(
789            v_flex()
790                .gap_6()
791                .children(vec![example_group(vec![
792                    single_example("Modified", GitStatusIcon::new(modified).into_any_element()),
793                    single_example("Added", GitStatusIcon::new(added).into_any_element()),
794                    single_example("Deleted", GitStatusIcon::new(deleted).into_any_element()),
795                    single_example(
796                        "Conflicted",
797                        GitStatusIcon::new(conflict).into_any_element(),
798                    ),
799                ])])
800                .into_any_element(),
801        )
802    }
803}
804
805struct GitCloneModal {
806    panel: Entity<GitPanel>,
807    repo_input: Entity<Editor>,
808    focus_handle: FocusHandle,
809}
810
811impl GitCloneModal {
812    pub fn show(panel: Entity<GitPanel>, window: &mut Window, cx: &mut Context<Self>) -> Self {
813        let repo_input = cx.new(|cx| {
814            let mut editor = Editor::single_line(window, cx);
815            editor.set_placeholder_text("Enter repository URL…", window, cx);
816            editor
817        });
818        let focus_handle = repo_input.focus_handle(cx);
819
820        window.focus(&focus_handle);
821
822        Self {
823            panel,
824            repo_input,
825            focus_handle,
826        }
827    }
828}
829
830impl Focusable for GitCloneModal {
831    fn focus_handle(&self, _: &App) -> FocusHandle {
832        self.focus_handle.clone()
833    }
834}
835
836impl Render for GitCloneModal {
837    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
838        div()
839            .elevation_3(cx)
840            .w(rems(34.))
841            .flex_1()
842            .overflow_hidden()
843            .child(
844                div()
845                    .w_full()
846                    .p_2()
847                    .border_b_1()
848                    .border_color(cx.theme().colors().border_variant)
849                    .child(self.repo_input.clone()),
850            )
851            .child(
852                h_flex()
853                    .w_full()
854                    .p_2()
855                    .gap_0p5()
856                    .rounded_b_sm()
857                    .bg(cx.theme().colors().editor_background)
858                    .child(
859                        Label::new("Clone a repository from GitHub or other sources.")
860                            .color(Color::Muted)
861                            .size(LabelSize::Small),
862                    )
863                    .child(
864                        Button::new("learn-more", "Learn More")
865                            .label_size(LabelSize::Small)
866                            .icon(IconName::ArrowUpRight)
867                            .icon_size(IconSize::XSmall)
868                            .on_click(|_, _, cx| {
869                                cx.open_url("https://github.com/git-guides/git-clone");
870                            }),
871                    ),
872            )
873            .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
874                cx.emit(DismissEvent);
875            }))
876            .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
877                let repo = this.repo_input.read(cx).text(cx);
878                this.panel.update(cx, |panel, cx| {
879                    panel.git_clone(repo, window, cx);
880                });
881                cx.emit(DismissEvent);
882            }))
883    }
884}
885
886impl EventEmitter<DismissEvent> for GitCloneModal {}
887
888impl ModalView for GitCloneModal {}