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