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