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