git_ui.rs

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