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