git_ui.rs

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