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