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