git_ui.rs

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