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