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