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