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