git_ui.rs

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