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