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