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