1use std::sync::Arc;
2
3use crate::ZedPredictUpsell;
4use ai_onboarding::EditPredictionOnboarding;
5use client::{Client, UserStore};
6use db::kvp::Dismissable;
7use fs::Fs;
8use gpui::{
9 ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
10 linear_color_stop, linear_gradient,
11};
12use language::language_settings::EditPredictionProvider;
13use settings::update_settings_file;
14use ui::{Vector, VectorName, prelude::*};
15use workspace::{ModalView, Workspace};
16
17#[macro_export]
18macro_rules! onboarding_event {
19 ($name:expr) => {
20 telemetry::event!($name, source = "Edit Prediction Onboarding");
21 };
22 ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
23 telemetry::event!($name, source = "Edit Prediction Onboarding", $($key $(= $value)?),+);
24 };
25}
26
27/// Introduces user to Zed's Edit Prediction feature
28pub struct ZedPredictModal {
29 onboarding: Entity<EditPredictionOnboarding>,
30 focus_handle: FocusHandle,
31}
32
33pub(crate) fn set_edit_prediction_provider(provider: EditPredictionProvider, cx: &mut App) {
34 let fs = <dyn Fs>::global(cx);
35 update_settings_file(fs, cx, move |settings, _| {
36 settings
37 .project
38 .all_languages
39 .features
40 .get_or_insert(Default::default())
41 .edit_prediction_provider = Some(provider);
42 });
43}
44
45impl ZedPredictModal {
46 pub fn toggle(
47 workspace: &mut Workspace,
48 user_store: Entity<UserStore>,
49 client: Arc<Client>,
50 window: &mut Window,
51 cx: &mut Context<Workspace>,
52 ) {
53 workspace.toggle_modal(window, cx, |_window, cx| {
54 let weak_entity = cx.weak_entity();
55 Self {
56 onboarding: cx.new(|cx| {
57 EditPredictionOnboarding::new(
58 user_store.clone(),
59 client.clone(),
60 copilot::Copilot::global(cx)
61 .is_some_and(|copilot| copilot.read(cx).status().is_configured()),
62 Arc::new({
63 let this = weak_entity.clone();
64 move |_window, cx| {
65 ZedPredictUpsell::set_dismissed(true, cx);
66 set_edit_prediction_provider(EditPredictionProvider::Zed, cx);
67 this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
68 }
69 }),
70 Arc::new({
71 let this = weak_entity.clone();
72 move |window, cx| {
73 ZedPredictUpsell::set_dismissed(true, cx);
74 set_edit_prediction_provider(EditPredictionProvider::Copilot, cx);
75 this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
76 copilot::initiate_sign_in(window, cx);
77 }
78 }),
79 cx,
80 )
81 }),
82 focus_handle: cx.focus_handle(),
83 }
84 });
85 }
86
87 fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
88 ZedPredictUpsell::set_dismissed(true, cx);
89 cx.emit(DismissEvent);
90 }
91}
92
93impl EventEmitter<DismissEvent> for ZedPredictModal {}
94
95impl Focusable for ZedPredictModal {
96 fn focus_handle(&self, _cx: &App) -> FocusHandle {
97 self.focus_handle.clone()
98 }
99}
100
101impl ModalView for ZedPredictModal {
102 fn on_before_dismiss(
103 &mut self,
104 _window: &mut Window,
105 cx: &mut Context<Self>,
106 ) -> workspace::DismissDecision {
107 ZedPredictUpsell::set_dismissed(true, cx);
108 workspace::DismissDecision::Dismiss(true)
109 }
110}
111
112impl Render for ZedPredictModal {
113 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
114 let window_height = window.viewport_size().height;
115 let max_height = window_height - px(200.);
116
117 v_flex()
118 .id("edit-prediction-onboarding")
119 .key_context("ZedPredictModal")
120 .relative()
121 .w(px(550.))
122 .h_full()
123 .max_h(max_height)
124 .p_4()
125 .gap_2()
126 .elevation_3(cx)
127 .track_focus(&self.focus_handle(cx))
128 .overflow_hidden()
129 .on_action(cx.listener(Self::cancel))
130 .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
131 onboarding_event!("Cancelled", trigger = "Action");
132 cx.emit(DismissEvent);
133 }))
134 .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
135 this.focus_handle.focus(window);
136 }))
137 .child(
138 div()
139 .opacity(0.5)
140 .absolute()
141 .top(px(-8.0))
142 .right_0()
143 .w(px(400.))
144 .h(px(92.))
145 .child(
146 Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.))
147 .color(Color::Custom(cx.theme().colors().text.alpha(0.32))),
148 ),
149 )
150 .child(
151 div()
152 .absolute()
153 .top_0()
154 .right_0()
155 .w(px(660.))
156 .h(px(401.))
157 .overflow_hidden()
158 .bg(linear_gradient(
159 75.,
160 linear_color_stop(cx.theme().colors().panel_background.alpha(0.01), 1.0),
161 linear_color_stop(cx.theme().colors().panel_background, 0.45),
162 )),
163 )
164 .child(h_flex().absolute().top_2().right_2().child(
165 IconButton::new("cancel", IconName::Close).on_click(cx.listener(
166 |_, _: &ClickEvent, _window, cx| {
167 onboarding_event!("Cancelled", trigger = "X click");
168 cx.emit(DismissEvent);
169 },
170 )),
171 ))
172 .child(self.onboarding.clone())
173 }
174}