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