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