git_ui.rs

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