1use std::sync::Arc;
2
3use crate::{EditPredictionStore, 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 let project = workspace.project().clone();
54 workspace.toggle_modal(window, cx, |_window, cx| {
55 let weak_entity = cx.weak_entity();
56 let copilot = EditPredictionStore::try_global(cx)
57 .and_then(|store| store.read(cx).copilot_for_project(&project));
58 Self {
59 onboarding: cx.new(|cx| {
60 EditPredictionOnboarding::new(
61 user_store.clone(),
62 client.clone(),
63 copilot.is_some_and(|copilot| copilot.read(cx).status().is_configured()),
64 Arc::new({
65 let this = weak_entity.clone();
66 move |_window, cx| {
67 ZedPredictUpsell::set_dismissed(true, cx);
68 set_edit_prediction_provider(EditPredictionProvider::Zed, cx);
69 this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
70 }
71 }),
72 Arc::new({
73 let this = weak_entity.clone();
74 move |window, cx| {
75 ZedPredictUpsell::set_dismissed(true, cx);
76 set_edit_prediction_provider(EditPredictionProvider::Copilot, cx);
77 this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
78 copilot_ui::initiate_sign_in(window, cx);
79 }
80 }),
81 cx,
82 )
83 }),
84 focus_handle: cx.focus_handle(),
85 }
86 });
87 }
88
89 fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
90 ZedPredictUpsell::set_dismissed(true, cx);
91 cx.emit(DismissEvent);
92 }
93}
94
95impl EventEmitter<DismissEvent> for ZedPredictModal {}
96
97impl Focusable for ZedPredictModal {
98 fn focus_handle(&self, _cx: &App) -> FocusHandle {
99 self.focus_handle.clone()
100 }
101}
102
103impl ModalView for ZedPredictModal {
104 fn on_before_dismiss(
105 &mut self,
106 _window: &mut Window,
107 cx: &mut Context<Self>,
108 ) -> workspace::DismissDecision {
109 ZedPredictUpsell::set_dismissed(true, cx);
110 workspace::DismissDecision::Dismiss(true)
111 }
112}
113
114impl Render for ZedPredictModal {
115 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
116 let window_height = window.viewport_size().height;
117 let max_height = window_height - px(200.);
118
119 v_flex()
120 .id("edit-prediction-onboarding")
121 .key_context("ZedPredictModal")
122 .relative()
123 .w(px(550.))
124 .h_full()
125 .max_h(max_height)
126 .p_4()
127 .gap_2()
128 .elevation_3(cx)
129 .track_focus(&self.focus_handle(cx))
130 .overflow_hidden()
131 .on_action(cx.listener(Self::cancel))
132 .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
133 onboarding_event!("Cancelled", trigger = "Action");
134 cx.emit(DismissEvent);
135 }))
136 .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
137 this.focus_handle.focus(window, cx);
138 }))
139 .child(
140 div()
141 .opacity(0.5)
142 .absolute()
143 .top(px(-8.0))
144 .right_0()
145 .w(px(400.))
146 .h(px(92.))
147 .child(
148 Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.))
149 .color(Color::Custom(cx.theme().colors().text.alpha(0.32))),
150 ),
151 )
152 .child(
153 div()
154 .absolute()
155 .top_0()
156 .right_0()
157 .w(px(660.))
158 .h(px(401.))
159 .overflow_hidden()
160 .bg(linear_gradient(
161 75.,
162 linear_color_stop(cx.theme().colors().panel_background.alpha(0.01), 1.0),
163 linear_color_stop(cx.theme().colors().panel_background, 0.45),
164 )),
165 )
166 .child(h_flex().absolute().top_2().right_2().child(
167 IconButton::new("cancel", IconName::Close).on_click(cx.listener(
168 |_, _: &ClickEvent, _window, cx| {
169 onboarding_event!("Cancelled", trigger = "X click");
170 cx.emit(DismissEvent);
171 },
172 )),
173 ))
174 .child(self.onboarding.clone())
175 }
176}