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