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