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