git_ui.rs

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