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