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