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