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