git_ui.rs

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