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