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