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                        .filter(|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                        .max_by_key(|repo| repo.read(cx).work_directory_abs_path.as_os_str().len())
304                        .cloned()
305                })
306        })
307        .or_else(|| project.active_repository(cx))
308}
309
310pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
311    GitStatusIcon::new(status)
312}
313
314struct RenameBranchModal {
315    current_branch: SharedString,
316    editor: Entity<Editor>,
317    repo: Entity<Repository>,
318}
319
320impl RenameBranchModal {
321    fn new(
322        current_branch: String,
323        repo: Entity<Repository>,
324        window: &mut Window,
325        cx: &mut Context<Self>,
326    ) -> Self {
327        let editor = cx.new(|cx| {
328            let mut editor = Editor::single_line(window, cx);
329            editor.set_text(current_branch.clone(), window, cx);
330            editor
331        });
332        Self {
333            current_branch: current_branch.into(),
334            editor,
335            repo,
336        }
337    }
338
339    fn cancel(&mut self, _: &Cancel, _window: &mut Window, cx: &mut Context<Self>) {
340        cx.emit(DismissEvent);
341    }
342
343    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
344        let new_name = self.editor.read(cx).text(cx);
345        if new_name.is_empty() || new_name == self.current_branch.as_ref() {
346            cx.emit(DismissEvent);
347            return;
348        }
349
350        let repo = self.repo.clone();
351        let current_branch = self.current_branch.to_string();
352        cx.spawn(async move |_, cx| {
353            match repo
354                .update(cx, |repo, _| {
355                    repo.rename_branch(current_branch, new_name.clone())
356                })
357                .await
358            {
359                Ok(Ok(_)) => Ok(()),
360                Ok(Err(error)) => Err(error),
361                Err(_) => Err(anyhow!("Operation was canceled")),
362            }
363        })
364        .detach_and_prompt_err("Failed to rename branch", window, cx, |_, _, _| None);
365        cx.emit(DismissEvent);
366    }
367}
368
369impl EventEmitter<DismissEvent> for RenameBranchModal {}
370impl ModalView for RenameBranchModal {}
371impl Focusable for RenameBranchModal {
372    fn focus_handle(&self, cx: &App) -> FocusHandle {
373        self.editor.focus_handle(cx)
374    }
375}
376
377impl Render for RenameBranchModal {
378    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
379        v_flex()
380            .key_context("RenameBranchModal")
381            .on_action(cx.listener(Self::cancel))
382            .on_action(cx.listener(Self::confirm))
383            .elevation_2(cx)
384            .w(rems(34.))
385            .child(
386                h_flex()
387                    .px_3()
388                    .pt_2()
389                    .pb_1()
390                    .w_full()
391                    .gap_1p5()
392                    .child(Icon::new(IconName::GitBranch).size(IconSize::XSmall))
393                    .child(
394                        Headline::new(format!("Rename Branch ({})", self.current_branch))
395                            .size(HeadlineSize::XSmall),
396                    ),
397            )
398            .child(div().px_3().pb_3().w_full().child(self.editor.clone()))
399    }
400}
401
402fn rename_current_branch(
403    workspace: &mut Workspace,
404    window: &mut Window,
405    cx: &mut Context<Workspace>,
406) {
407    let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
408        return;
409    };
410    let current_branch: Option<String> = panel.update(cx, |panel, cx| {
411        let repo = panel.active_repository.as_ref()?;
412        let repo = repo.read(cx);
413        repo.branch.as_ref().map(|branch| branch.name().to_string())
414    });
415
416    let Some(current_branch_name) = current_branch else {
417        return;
418    };
419
420    let repo = panel.read(cx).active_repository.clone();
421    let Some(repo) = repo else {
422        return;
423    };
424
425    workspace.toggle_modal(window, cx, |window, cx| {
426        RenameBranchModal::new(current_branch_name, repo, window, cx)
427    });
428}
429
430fn render_remote_button(
431    id: impl Into<SharedString>,
432    branch: &Branch,
433    keybinding_target: Option<FocusHandle>,
434    show_fetch_button: bool,
435) -> Option<impl IntoElement> {
436    let id = id.into();
437    let upstream = branch.upstream.as_ref();
438    match upstream {
439        Some(Upstream {
440            tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead, behind }),
441            ..
442        }) => match (*ahead, *behind) {
443            (0, 0) if show_fetch_button => {
444                Some(remote_button::render_fetch_button(keybinding_target, id))
445            }
446            (0, 0) => None,
447            (ahead, 0) => Some(remote_button::render_push_button(
448                keybinding_target,
449                id,
450                ahead,
451            )),
452            (ahead, behind) => Some(remote_button::render_pull_button(
453                keybinding_target,
454                id,
455                ahead,
456                behind,
457            )),
458        },
459        Some(Upstream {
460            tracking: UpstreamTracking::Gone,
461            ..
462        }) => Some(remote_button::render_republish_button(
463            keybinding_target,
464            id,
465        )),
466        None => Some(remote_button::render_publish_button(keybinding_target, id)),
467    }
468}
469
470mod remote_button {
471    use gpui::{Action, AnyView, ClickEvent, Corner, FocusHandle};
472    use ui::{
473        App, ButtonCommon, Clickable, ContextMenu, ElementId, FluentBuilder, Icon, IconName,
474        IconSize, IntoElement, Label, LabelCommon, LabelSize, LineHeightStyle, ParentElement,
475        PopoverMenu, SharedString, SplitButton, Styled, Tooltip, Window, div, h_flex, rems,
476    };
477
478    pub fn render_fetch_button(
479        keybinding_target: Option<FocusHandle>,
480        id: SharedString,
481    ) -> SplitButton {
482        split_button(
483            id,
484            "Fetch",
485            0,
486            0,
487            Some(IconName::ArrowCircle),
488            keybinding_target.clone(),
489            move |_, window, cx| {
490                window.dispatch_action(Box::new(git::Fetch), cx);
491            },
492            move |_window, cx| {
493                git_action_tooltip(
494                    "Fetch updates from remote",
495                    &git::Fetch,
496                    "git fetch",
497                    keybinding_target.clone(),
498                    cx,
499                )
500            },
501        )
502    }
503
504    pub fn render_push_button(
505        keybinding_target: Option<FocusHandle>,
506        id: SharedString,
507        ahead: u32,
508    ) -> SplitButton {
509        split_button(
510            id,
511            "Push",
512            ahead as usize,
513            0,
514            None,
515            keybinding_target.clone(),
516            move |_, window, cx| {
517                window.dispatch_action(Box::new(git::Push), cx);
518            },
519            move |_window, cx| {
520                git_action_tooltip(
521                    "Push committed changes to remote",
522                    &git::Push,
523                    "git push",
524                    keybinding_target.clone(),
525                    cx,
526                )
527            },
528        )
529    }
530
531    pub fn render_pull_button(
532        keybinding_target: Option<FocusHandle>,
533        id: SharedString,
534        ahead: u32,
535        behind: u32,
536    ) -> SplitButton {
537        split_button(
538            id,
539            "Pull",
540            ahead as usize,
541            behind as usize,
542            None,
543            keybinding_target.clone(),
544            move |_, window, cx| {
545                window.dispatch_action(Box::new(git::Pull), cx);
546            },
547            move |_window, cx| {
548                git_action_tooltip(
549                    "Pull",
550                    &git::Pull,
551                    "git pull",
552                    keybinding_target.clone(),
553                    cx,
554                )
555            },
556        )
557    }
558
559    pub fn render_publish_button(
560        keybinding_target: Option<FocusHandle>,
561        id: SharedString,
562    ) -> SplitButton {
563        split_button(
564            id,
565            "Publish",
566            0,
567            0,
568            Some(IconName::ExpandUp),
569            keybinding_target.clone(),
570            move |_, window, cx| {
571                window.dispatch_action(Box::new(git::Push), cx);
572            },
573            move |_window, cx| {
574                git_action_tooltip(
575                    "Publish branch to remote",
576                    &git::Push,
577                    "git push --set-upstream",
578                    keybinding_target.clone(),
579                    cx,
580                )
581            },
582        )
583    }
584
585    pub fn render_republish_button(
586        keybinding_target: Option<FocusHandle>,
587        id: SharedString,
588    ) -> SplitButton {
589        split_button(
590            id,
591            "Republish",
592            0,
593            0,
594            Some(IconName::ExpandUp),
595            keybinding_target.clone(),
596            move |_, window, cx| {
597                window.dispatch_action(Box::new(git::Push), cx);
598            },
599            move |_window, cx| {
600                git_action_tooltip(
601                    "Re-publish branch to remote",
602                    &git::Push,
603                    "git push --set-upstream",
604                    keybinding_target.clone(),
605                    cx,
606                )
607            },
608        )
609    }
610
611    fn git_action_tooltip(
612        label: impl Into<SharedString>,
613        action: &dyn Action,
614        command: impl Into<SharedString>,
615        focus_handle: Option<FocusHandle>,
616        cx: &mut App,
617    ) -> AnyView {
618        let label = label.into();
619        let command = command.into();
620
621        if let Some(handle) = focus_handle {
622            Tooltip::with_meta_in(label, Some(action), command, &handle, cx)
623        } else {
624            Tooltip::with_meta(label, Some(action), command, cx)
625        }
626    }
627
628    fn render_git_action_menu(
629        id: impl Into<ElementId>,
630        keybinding_target: Option<FocusHandle>,
631    ) -> impl IntoElement {
632        PopoverMenu::new(id.into())
633            .trigger(
634                ui::ButtonLike::new_rounded_right("split-button-right")
635                    .layer(ui::ElevationIndex::ModalSurface)
636                    .size(ui::ButtonSize::None)
637                    .child(
638                        div()
639                            .px_1()
640                            .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)),
641                    ),
642            )
643            .menu(move |window, cx| {
644                Some(ContextMenu::build(window, cx, |context_menu, _, _| {
645                    context_menu
646                        .when_some(keybinding_target.clone(), |el, keybinding_target| {
647                            el.context(keybinding_target)
648                        })
649                        .action("Fetch", git::Fetch.boxed_clone())
650                        .action("Fetch From", git::FetchFrom.boxed_clone())
651                        .action("Pull", git::Pull.boxed_clone())
652                        .action("Pull (Rebase)", git::PullRebase.boxed_clone())
653                        .separator()
654                        .action("Push", git::Push.boxed_clone())
655                        .action("Push To", git::PushTo.boxed_clone())
656                        .action("Force Push", git::ForcePush.boxed_clone())
657                }))
658            })
659            .anchor(Corner::TopRight)
660    }
661
662    #[allow(clippy::too_many_arguments)]
663    fn split_button(
664        id: SharedString,
665        left_label: impl Into<SharedString>,
666        ahead_count: usize,
667        behind_count: usize,
668        left_icon: Option<IconName>,
669        keybinding_target: Option<FocusHandle>,
670        left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
671        tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
672    ) -> SplitButton {
673        fn count(count: usize) -> impl IntoElement {
674            h_flex()
675                .ml_neg_px()
676                .h(rems(0.875))
677                .items_center()
678                .overflow_hidden()
679                .px_0p5()
680                .child(
681                    Label::new(count.to_string())
682                        .size(LabelSize::XSmall)
683                        .line_height_style(LineHeightStyle::UiLabel),
684                )
685        }
686
687        let should_render_counts = left_icon.is_none() && (ahead_count > 0 || behind_count > 0);
688
689        let left = ui::ButtonLike::new_rounded_left(ElementId::Name(
690            format!("split-button-left-{}", id).into(),
691        ))
692        .layer(ui::ElevationIndex::ModalSurface)
693        .size(ui::ButtonSize::Compact)
694        .when(should_render_counts, |this| {
695            this.child(
696                h_flex()
697                    .ml_neg_0p5()
698                    .when(behind_count > 0, |this| {
699                        this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall))
700                            .child(count(behind_count))
701                    })
702                    .when(ahead_count > 0, |this| {
703                        this.child(Icon::new(IconName::ArrowUp).size(IconSize::XSmall))
704                            .child(count(ahead_count))
705                    }),
706            )
707        })
708        .when_some(left_icon, |this, left_icon| {
709            this.child(
710                h_flex()
711                    .ml_neg_0p5()
712                    .child(Icon::new(left_icon).size(IconSize::XSmall)),
713            )
714        })
715        .child(
716            div()
717                .child(Label::new(left_label).size(LabelSize::Small))
718                .mr_0p5(),
719        )
720        .on_click(left_on_click)
721        .tooltip(tooltip);
722
723        let right = render_git_action_menu(
724            ElementId::Name(format!("split-button-right-{}", id).into()),
725            keybinding_target,
726        )
727        .into_any_element();
728
729        SplitButton::new(left, right)
730    }
731}
732
733/// A visual representation of a file's Git status.
734#[derive(IntoElement, RegisterComponent)]
735pub struct GitStatusIcon {
736    status: FileStatus,
737}
738
739impl GitStatusIcon {
740    pub fn new(status: FileStatus) -> Self {
741        Self { status }
742    }
743}
744
745impl RenderOnce for GitStatusIcon {
746    fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
747        let status = self.status;
748
749        let (icon_name, color) = if status.is_conflicted() {
750            (
751                IconName::Warning,
752                cx.theme().colors().version_control_conflict,
753            )
754        } else if status.is_deleted() {
755            (
756                IconName::SquareMinus,
757                cx.theme().colors().version_control_deleted,
758            )
759        } else if status.is_modified() {
760            (
761                IconName::SquareDot,
762                cx.theme().colors().version_control_modified,
763            )
764        } else {
765            (
766                IconName::SquarePlus,
767                cx.theme().colors().version_control_added,
768            )
769        };
770
771        Icon::new(icon_name).color(Color::Custom(color))
772    }
773}
774
775// View this component preview using `workspace: open component-preview`
776impl Component for GitStatusIcon {
777    fn scope() -> ComponentScope {
778        ComponentScope::VersionControl
779    }
780
781    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
782        fn tracked_file_status(code: StatusCode) -> FileStatus {
783            FileStatus::Tracked(git::status::TrackedStatus {
784                index_status: code,
785                worktree_status: code,
786            })
787        }
788
789        let modified = tracked_file_status(StatusCode::Modified);
790        let added = tracked_file_status(StatusCode::Added);
791        let deleted = tracked_file_status(StatusCode::Deleted);
792        let conflict = UnmergedStatus {
793            first_head: UnmergedStatusCode::Updated,
794            second_head: UnmergedStatusCode::Updated,
795        }
796        .into();
797
798        Some(
799            v_flex()
800                .gap_6()
801                .children(vec![example_group(vec![
802                    single_example("Modified", GitStatusIcon::new(modified).into_any_element()),
803                    single_example("Added", GitStatusIcon::new(added).into_any_element()),
804                    single_example("Deleted", GitStatusIcon::new(deleted).into_any_element()),
805                    single_example(
806                        "Conflicted",
807                        GitStatusIcon::new(conflict).into_any_element(),
808                    ),
809                ])])
810                .into_any_element(),
811        )
812    }
813}
814
815struct GitCloneModal {
816    panel: Entity<GitPanel>,
817    repo_input: Entity<Editor>,
818    focus_handle: FocusHandle,
819}
820
821impl GitCloneModal {
822    pub fn show(panel: Entity<GitPanel>, window: &mut Window, cx: &mut Context<Self>) -> Self {
823        let repo_input = cx.new(|cx| {
824            let mut editor = Editor::single_line(window, cx);
825            editor.set_placeholder_text("Enter repository URL…", window, cx);
826            editor
827        });
828        let focus_handle = repo_input.focus_handle(cx);
829
830        window.focus(&focus_handle, cx);
831
832        Self {
833            panel,
834            repo_input,
835            focus_handle,
836        }
837    }
838}
839
840impl Focusable for GitCloneModal {
841    fn focus_handle(&self, _: &App) -> FocusHandle {
842        self.focus_handle.clone()
843    }
844}
845
846impl Render for GitCloneModal {
847    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
848        div()
849            .elevation_3(cx)
850            .w(rems(34.))
851            .flex_1()
852            .overflow_hidden()
853            .child(
854                div()
855                    .w_full()
856                    .p_2()
857                    .border_b_1()
858                    .border_color(cx.theme().colors().border_variant)
859                    .child(self.repo_input.clone()),
860            )
861            .child(
862                h_flex()
863                    .w_full()
864                    .p_2()
865                    .gap_0p5()
866                    .rounded_b_sm()
867                    .bg(cx.theme().colors().editor_background)
868                    .child(
869                        Label::new("Clone a repository from GitHub or other sources.")
870                            .color(Color::Muted)
871                            .size(LabelSize::Small),
872                    )
873                    .child(
874                        Button::new("learn-more", "Learn More")
875                            .label_size(LabelSize::Small)
876                            .end_icon(Icon::new(IconName::ArrowUpRight).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 {}