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