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(
234 |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
242 .read(cx)
243 .buffer()
244 .read(cx)
245 .as_singleton()
246 else {
247 return;
248 };
249 let Some(file) = buffer.read(cx).file() else {
250 return;
251 };
252 let worktree_id = file.worktree_id(cx);
253 let project_path = ProjectPath {
254 worktree_id,
255 path: file.path().clone(),
256 };
257 let project = workspace.project();
258 let git_store = project.read(cx).git_store();
259 let Some((repo, repo_path)) = git_store
260 .read(cx)
261 .repository_and_path_for_project_path(&project_path, cx)
262 else {
263 return;
264 };
265 file_history_view::FileHistoryView::open(
266 repo_path,
267 git_store.downgrade(),
268 repo.downgrade(),
269 workspace.weak_handle(),
270 window,
271 cx,
272 );
273 },
274 );
275 })
276 .detach();
277}
278
279fn open_modified_files(
280 workspace: &mut Workspace,
281 window: &mut Window,
282 cx: &mut Context<Workspace>,
283) {
284 let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
285 return;
286 };
287 let modified_paths: Vec<_> = panel.update(cx, |panel, cx| {
288 let Some(repo) = panel.active_repository.as_ref() else {
289 return Vec::new();
290 };
291 let repo = repo.read(cx);
292 repo.cached_status()
293 .filter_map(|entry| {
294 if entry.status.is_modified() {
295 repo.repo_path_to_project_path(&entry.repo_path, cx)
296 } else {
297 None
298 }
299 })
300 .collect()
301 });
302 for path in modified_paths {
303 workspace.open_path(path, None, true, window, cx).detach();
304 }
305}
306
307pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
308 GitStatusIcon::new(status)
309}
310
311struct RenameBranchModal {
312 current_branch: SharedString,
313 editor: Entity<Editor>,
314 repo: Entity<Repository>,
315}
316
317impl RenameBranchModal {
318 fn new(
319 current_branch: String,
320 repo: Entity<Repository>,
321 window: &mut Window,
322 cx: &mut Context<Self>,
323 ) -> Self {
324 let editor = cx.new(|cx| {
325 let mut editor = Editor::single_line(window, cx);
326 editor.set_text(current_branch.clone(), window, cx);
327 editor
328 });
329 Self {
330 current_branch: current_branch.into(),
331 editor,
332 repo,
333 }
334 }
335
336 fn cancel(&mut self, _: &Cancel, _window: &mut Window, cx: &mut Context<Self>) {
337 cx.emit(DismissEvent);
338 }
339
340 fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
341 let new_name = self.editor.read(cx).text(cx);
342 if new_name.is_empty() || new_name == self.current_branch.as_ref() {
343 cx.emit(DismissEvent);
344 return;
345 }
346
347 let repo = self.repo.clone();
348 let current_branch = self.current_branch.to_string();
349 cx.spawn(async move |_, cx| {
350 match repo
351 .update(cx, |repo, _| {
352 repo.rename_branch(current_branch, new_name.clone())
353 })?
354 .await
355 {
356 Ok(Ok(_)) => Ok(()),
357 Ok(Err(error)) => Err(error),
358 Err(_) => Err(anyhow::anyhow!("Operation was canceled")),
359 }
360 })
361 .detach_and_prompt_err("Failed to rename branch", window, cx, |_, _, _| None);
362 cx.emit(DismissEvent);
363 }
364}
365
366impl EventEmitter<DismissEvent> for RenameBranchModal {}
367impl ModalView for RenameBranchModal {}
368impl Focusable for RenameBranchModal {
369 fn focus_handle(&self, cx: &App) -> FocusHandle {
370 self.editor.focus_handle(cx)
371 }
372}
373
374impl Render for RenameBranchModal {
375 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
376 v_flex()
377 .key_context("RenameBranchModal")
378 .on_action(cx.listener(Self::cancel))
379 .on_action(cx.listener(Self::confirm))
380 .elevation_2(cx)
381 .w(rems(34.))
382 .child(
383 h_flex()
384 .px_3()
385 .pt_2()
386 .pb_1()
387 .w_full()
388 .gap_1p5()
389 .child(Icon::new(IconName::GitBranch).size(IconSize::XSmall))
390 .child(
391 Headline::new(format!("Rename Branch ({})", self.current_branch))
392 .size(HeadlineSize::XSmall),
393 ),
394 )
395 .child(div().px_3().pb_3().w_full().child(self.editor.clone()))
396 }
397}
398
399fn rename_current_branch(
400 workspace: &mut Workspace,
401 window: &mut Window,
402 cx: &mut Context<Workspace>,
403) {
404 let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
405 return;
406 };
407 let current_branch: Option<String> = panel.update(cx, |panel, cx| {
408 let repo = panel.active_repository.as_ref()?;
409 let repo = repo.read(cx);
410 repo.branch.as_ref().map(|branch| branch.name().to_string())
411 });
412
413 let Some(current_branch_name) = current_branch else {
414 return;
415 };
416
417 let repo = panel.read(cx).active_repository.clone();
418 let Some(repo) = repo else {
419 return;
420 };
421
422 workspace.toggle_modal(window, cx, |window, cx| {
423 RenameBranchModal::new(current_branch_name, repo, window, cx)
424 });
425}
426
427fn render_remote_button(
428 id: impl Into<SharedString>,
429 branch: &Branch,
430 keybinding_target: Option<FocusHandle>,
431 show_fetch_button: bool,
432) -> Option<impl IntoElement> {
433 let id = id.into();
434 let upstream = branch.upstream.as_ref();
435 match upstream {
436 Some(Upstream {
437 tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead, behind }),
438 ..
439 }) => match (*ahead, *behind) {
440 (0, 0) if show_fetch_button => {
441 Some(remote_button::render_fetch_button(keybinding_target, id))
442 }
443 (0, 0) => None,
444 (ahead, 0) => Some(remote_button::render_push_button(
445 keybinding_target,
446 id,
447 ahead,
448 )),
449 (ahead, behind) => Some(remote_button::render_pull_button(
450 keybinding_target,
451 id,
452 ahead,
453 behind,
454 )),
455 },
456 Some(Upstream {
457 tracking: UpstreamTracking::Gone,
458 ..
459 }) => Some(remote_button::render_republish_button(
460 keybinding_target,
461 id,
462 )),
463 None => Some(remote_button::render_publish_button(keybinding_target, id)),
464 }
465}
466
467mod remote_button {
468 use gpui::{Action, AnyView, ClickEvent, Corner, FocusHandle};
469 use ui::{
470 App, ButtonCommon, Clickable, ContextMenu, ElementId, FluentBuilder, Icon, IconName,
471 IconSize, IntoElement, Label, LabelCommon, LabelSize, LineHeightStyle, ParentElement,
472 PopoverMenu, SharedString, SplitButton, Styled, Tooltip, Window, div, h_flex, rems,
473 };
474
475 pub fn render_fetch_button(
476 keybinding_target: Option<FocusHandle>,
477 id: SharedString,
478 ) -> SplitButton {
479 split_button(
480 id,
481 "Fetch",
482 0,
483 0,
484 Some(IconName::ArrowCircle),
485 keybinding_target.clone(),
486 move |_, window, cx| {
487 window.dispatch_action(Box::new(git::Fetch), cx);
488 },
489 move |_window, cx| {
490 git_action_tooltip(
491 "Fetch updates from remote",
492 &git::Fetch,
493 "git fetch",
494 keybinding_target.clone(),
495 cx,
496 )
497 },
498 )
499 }
500
501 pub fn render_push_button(
502 keybinding_target: Option<FocusHandle>,
503 id: SharedString,
504 ahead: u32,
505 ) -> SplitButton {
506 split_button(
507 id,
508 "Push",
509 ahead as usize,
510 0,
511 None,
512 keybinding_target.clone(),
513 move |_, window, cx| {
514 window.dispatch_action(Box::new(git::Push), cx);
515 },
516 move |_window, cx| {
517 git_action_tooltip(
518 "Push committed changes to remote",
519 &git::Push,
520 "git push",
521 keybinding_target.clone(),
522 cx,
523 )
524 },
525 )
526 }
527
528 pub fn render_pull_button(
529 keybinding_target: Option<FocusHandle>,
530 id: SharedString,
531 ahead: u32,
532 behind: u32,
533 ) -> SplitButton {
534 split_button(
535 id,
536 "Pull",
537 ahead as usize,
538 behind as usize,
539 None,
540 keybinding_target.clone(),
541 move |_, window, cx| {
542 window.dispatch_action(Box::new(git::Pull), cx);
543 },
544 move |_window, cx| {
545 git_action_tooltip(
546 "Pull",
547 &git::Pull,
548 "git pull",
549 keybinding_target.clone(),
550 cx,
551 )
552 },
553 )
554 }
555
556 pub fn render_publish_button(
557 keybinding_target: Option<FocusHandle>,
558 id: SharedString,
559 ) -> SplitButton {
560 split_button(
561 id,
562 "Publish",
563 0,
564 0,
565 Some(IconName::ExpandUp),
566 keybinding_target.clone(),
567 move |_, window, cx| {
568 window.dispatch_action(Box::new(git::Push), cx);
569 },
570 move |_window, cx| {
571 git_action_tooltip(
572 "Publish branch to remote",
573 &git::Push,
574 "git push --set-upstream",
575 keybinding_target.clone(),
576 cx,
577 )
578 },
579 )
580 }
581
582 pub fn render_republish_button(
583 keybinding_target: Option<FocusHandle>,
584 id: SharedString,
585 ) -> SplitButton {
586 split_button(
587 id,
588 "Republish",
589 0,
590 0,
591 Some(IconName::ExpandUp),
592 keybinding_target.clone(),
593 move |_, window, cx| {
594 window.dispatch_action(Box::new(git::Push), cx);
595 },
596 move |_window, cx| {
597 git_action_tooltip(
598 "Re-publish branch to remote",
599 &git::Push,
600 "git push --set-upstream",
601 keybinding_target.clone(),
602 cx,
603 )
604 },
605 )
606 }
607
608 fn git_action_tooltip(
609 label: impl Into<SharedString>,
610 action: &dyn Action,
611 command: impl Into<SharedString>,
612 focus_handle: Option<FocusHandle>,
613 cx: &mut App,
614 ) -> AnyView {
615 let label = label.into();
616 let command = command.into();
617
618 if let Some(handle) = focus_handle {
619 Tooltip::with_meta_in(label, Some(action), command, &handle, cx)
620 } else {
621 Tooltip::with_meta(label, Some(action), command, cx)
622 }
623 }
624
625 fn render_git_action_menu(
626 id: impl Into<ElementId>,
627 keybinding_target: Option<FocusHandle>,
628 ) -> impl IntoElement {
629 PopoverMenu::new(id.into())
630 .trigger(
631 ui::ButtonLike::new_rounded_right("split-button-right")
632 .layer(ui::ElevationIndex::ModalSurface)
633 .size(ui::ButtonSize::None)
634 .child(
635 div()
636 .px_1()
637 .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)),
638 ),
639 )
640 .menu(move |window, cx| {
641 Some(ContextMenu::build(window, cx, |context_menu, _, _| {
642 context_menu
643 .when_some(keybinding_target.clone(), |el, keybinding_target| {
644 el.context(keybinding_target)
645 })
646 .action("Fetch", git::Fetch.boxed_clone())
647 .action("Fetch From", git::FetchFrom.boxed_clone())
648 .action("Pull", git::Pull.boxed_clone())
649 .action("Pull (Rebase)", git::PullRebase.boxed_clone())
650 .separator()
651 .action("Push", git::Push.boxed_clone())
652 .action("Push To", git::PushTo.boxed_clone())
653 .action("Force Push", git::ForcePush.boxed_clone())
654 }))
655 })
656 .anchor(Corner::TopRight)
657 }
658
659 #[allow(clippy::too_many_arguments)]
660 fn split_button(
661 id: SharedString,
662 left_label: impl Into<SharedString>,
663 ahead_count: usize,
664 behind_count: usize,
665 left_icon: Option<IconName>,
666 keybinding_target: Option<FocusHandle>,
667 left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
668 tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
669 ) -> SplitButton {
670 fn count(count: usize) -> impl IntoElement {
671 h_flex()
672 .ml_neg_px()
673 .h(rems(0.875))
674 .items_center()
675 .overflow_hidden()
676 .px_0p5()
677 .child(
678 Label::new(count.to_string())
679 .size(LabelSize::XSmall)
680 .line_height_style(LineHeightStyle::UiLabel),
681 )
682 }
683
684 let should_render_counts = left_icon.is_none() && (ahead_count > 0 || behind_count > 0);
685
686 let left = ui::ButtonLike::new_rounded_left(ElementId::Name(
687 format!("split-button-left-{}", id).into(),
688 ))
689 .layer(ui::ElevationIndex::ModalSurface)
690 .size(ui::ButtonSize::Compact)
691 .when(should_render_counts, |this| {
692 this.child(
693 h_flex()
694 .ml_neg_0p5()
695 .when(behind_count > 0, |this| {
696 this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall))
697 .child(count(behind_count))
698 })
699 .when(ahead_count > 0, |this| {
700 this.child(Icon::new(IconName::ArrowUp).size(IconSize::XSmall))
701 .child(count(ahead_count))
702 }),
703 )
704 })
705 .when_some(left_icon, |this, left_icon| {
706 this.child(
707 h_flex()
708 .ml_neg_0p5()
709 .child(Icon::new(left_icon).size(IconSize::XSmall)),
710 )
711 })
712 .child(
713 div()
714 .child(Label::new(left_label).size(LabelSize::Small))
715 .mr_0p5(),
716 )
717 .on_click(left_on_click)
718 .tooltip(tooltip);
719
720 let right = render_git_action_menu(
721 ElementId::Name(format!("split-button-right-{}", id).into()),
722 keybinding_target,
723 )
724 .into_any_element();
725
726 SplitButton::new(left, right)
727 }
728}
729
730/// A visual representation of a file's Git status.
731#[derive(IntoElement, RegisterComponent)]
732pub struct GitStatusIcon {
733 status: FileStatus,
734}
735
736impl GitStatusIcon {
737 pub fn new(status: FileStatus) -> Self {
738 Self { status }
739 }
740}
741
742impl RenderOnce for GitStatusIcon {
743 fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
744 let status = self.status;
745
746 let (icon_name, color) = if status.is_conflicted() {
747 (
748 IconName::Warning,
749 cx.theme().colors().version_control_conflict,
750 )
751 } else if status.is_deleted() {
752 (
753 IconName::SquareMinus,
754 cx.theme().colors().version_control_deleted,
755 )
756 } else if status.is_modified() {
757 (
758 IconName::SquareDot,
759 cx.theme().colors().version_control_modified,
760 )
761 } else {
762 (
763 IconName::SquarePlus,
764 cx.theme().colors().version_control_added,
765 )
766 };
767
768 Icon::new(icon_name).color(Color::Custom(color))
769 }
770}
771
772// View this component preview using `workspace: open component-preview`
773impl Component for GitStatusIcon {
774 fn scope() -> ComponentScope {
775 ComponentScope::VersionControl
776 }
777
778 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
779 fn tracked_file_status(code: StatusCode) -> FileStatus {
780 FileStatus::Tracked(git::status::TrackedStatus {
781 index_status: code,
782 worktree_status: code,
783 })
784 }
785
786 let modified = tracked_file_status(StatusCode::Modified);
787 let added = tracked_file_status(StatusCode::Added);
788 let deleted = tracked_file_status(StatusCode::Deleted);
789 let conflict = UnmergedStatus {
790 first_head: UnmergedStatusCode::Updated,
791 second_head: UnmergedStatusCode::Updated,
792 }
793 .into();
794
795 Some(
796 v_flex()
797 .gap_6()
798 .children(vec![example_group(vec![
799 single_example("Modified", GitStatusIcon::new(modified).into_any_element()),
800 single_example("Added", GitStatusIcon::new(added).into_any_element()),
801 single_example("Deleted", GitStatusIcon::new(deleted).into_any_element()),
802 single_example(
803 "Conflicted",
804 GitStatusIcon::new(conflict).into_any_element(),
805 ),
806 ])])
807 .into_any_element(),
808 )
809 }
810}
811
812struct GitCloneModal {
813 panel: Entity<GitPanel>,
814 repo_input: Entity<Editor>,
815 focus_handle: FocusHandle,
816}
817
818impl GitCloneModal {
819 pub fn show(panel: Entity<GitPanel>, window: &mut Window, cx: &mut Context<Self>) -> Self {
820 let repo_input = cx.new(|cx| {
821 let mut editor = Editor::single_line(window, cx);
822 editor.set_placeholder_text("Enter repository URL…", window, cx);
823 editor
824 });
825 let focus_handle = repo_input.focus_handle(cx);
826
827 window.focus(&focus_handle);
828
829 Self {
830 panel,
831 repo_input,
832 focus_handle,
833 }
834 }
835}
836
837impl Focusable for GitCloneModal {
838 fn focus_handle(&self, _: &App) -> FocusHandle {
839 self.focus_handle.clone()
840 }
841}
842
843impl Render for GitCloneModal {
844 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
845 div()
846 .elevation_3(cx)
847 .w(rems(34.))
848 .flex_1()
849 .overflow_hidden()
850 .child(
851 div()
852 .w_full()
853 .p_2()
854 .border_b_1()
855 .border_color(cx.theme().colors().border_variant)
856 .child(self.repo_input.clone()),
857 )
858 .child(
859 h_flex()
860 .w_full()
861 .p_2()
862 .gap_0p5()
863 .rounded_b_sm()
864 .bg(cx.theme().colors().editor_background)
865 .child(
866 Label::new("Clone a repository from GitHub or other sources.")
867 .color(Color::Muted)
868 .size(LabelSize::Small),
869 )
870 .child(
871 Button::new("learn-more", "Learn More")
872 .label_size(LabelSize::Small)
873 .icon(IconName::ArrowUpRight)
874 .icon_size(IconSize::XSmall)
875 .on_click(|_, _, cx| {
876 cx.open_url("https://github.com/git-guides/git-clone");
877 }),
878 ),
879 )
880 .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
881 cx.emit(DismissEvent);
882 }))
883 .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
884 let repo = this.repo_input.read(cx).text(cx);
885 this.panel.update(cx, |panel, cx| {
886 panel.git_clone(repo, window, cx);
887 });
888 cx.emit(DismissEvent);
889 }))
890 }
891}
892
893impl EventEmitter<DismissEvent> for GitCloneModal {}
894
895impl ModalView for GitCloneModal {}