git_ui.rs

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