1use crate::{request::PromptUserDeviceFlow, Copilot, Status};
2use gpui::{
3 elements::*,
4 geometry::rect::RectF,
5 impl_internal_actions,
6 platform::{WindowBounds, WindowKind, WindowOptions},
7 AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
8 ViewHandle,
9};
10use settings::Settings;
11use theme::ui::modal;
12
13#[derive(PartialEq, Eq, Debug, Clone)]
14struct ClickedConnect;
15
16impl_internal_actions!(copilot_verification, [ClickedConnect]);
17
18#[derive(PartialEq, Eq, Debug, Clone)]
19struct CopyUserCode;
20
21#[derive(PartialEq, Eq, Debug, Clone)]
22struct OpenGithub;
23
24const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
25
26pub fn init(cx: &mut AppContext) {
27 if let Some(copilot) = Copilot::global(cx) {
28 let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None;
29 cx.observe(&copilot, move |copilot, cx| {
30 let status = copilot.read(cx).status();
31
32 match &status {
33 crate::Status::SigningIn { prompt } => {
34 if let Some(code_verification_handle) = code_verification.as_mut() {
35 let window_id = code_verification_handle.window_id();
36 let updated = cx.update_window(window_id, |cx| {
37 code_verification_handle.update(cx, |code_verification, cx| {
38 code_verification.set_status(status.clone(), cx)
39 });
40 cx.activate_window();
41 });
42 if updated.is_none() {
43 code_verification = Some(create_copilot_auth_window(cx, &status));
44 }
45 } else if let Some(_prompt) = prompt {
46 code_verification = Some(create_copilot_auth_window(cx, &status));
47 }
48 }
49 Status::Authorized | Status::Unauthorized => {
50 if let Some(code_verification) = code_verification.as_ref() {
51 let window_id = code_verification.window_id();
52 cx.update_window(window_id, |cx| {
53 code_verification.update(cx, |code_verification, cx| {
54 code_verification.set_status(status, cx)
55 });
56
57 cx.platform().activate(true);
58 cx.activate_window();
59 });
60 }
61 }
62 _ => {
63 if let Some(code_verification) = code_verification.take() {
64 cx.update_window(code_verification.window_id(), |cx| cx.remove_window());
65 }
66 }
67 }
68 })
69 .detach();
70
71 cx.add_action(
72 |code_verification: &mut CopilotCodeVerification, _: &ClickedConnect, _| {
73 code_verification.connect_clicked = true;
74 },
75 );
76 }
77}
78
79fn create_copilot_auth_window(
80 cx: &mut AppContext,
81 status: &Status,
82) -> ViewHandle<CopilotCodeVerification> {
83 let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
84 let window_options = WindowOptions {
85 bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
86 titlebar: None,
87 center: true,
88 focus: true,
89 kind: WindowKind::Normal,
90 is_movable: true,
91 screen: None,
92 };
93 let (_, view) = cx.add_window(window_options, |_cx| {
94 CopilotCodeVerification::new(status.clone())
95 });
96 view
97}
98
99pub struct CopilotCodeVerification {
100 status: Status,
101 connect_clicked: bool,
102}
103
104impl CopilotCodeVerification {
105 pub fn new(status: Status) -> Self {
106 Self {
107 status,
108 connect_clicked: false,
109 }
110 }
111
112 pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
113 self.status = status;
114 cx.notify();
115 }
116
117 fn render_device_code(
118 data: &PromptUserDeviceFlow,
119 style: &theme::Copilot,
120 cx: &mut ViewContext<Self>,
121 ) -> impl Element<Self> {
122 let copied = cx
123 .read_from_clipboard()
124 .map(|item| item.text() == &data.user_code)
125 .unwrap_or(false);
126
127 let device_code_style = &style.auth.prompting.device_code;
128
129 MouseEventHandler::<Self, _>::new(0, cx, |state, _cx| {
130 Flex::row()
131 .with_child(
132 Label::new(data.user_code.clone(), device_code_style.text.clone())
133 .aligned()
134 .contained()
135 .with_style(device_code_style.left_container)
136 .constrained()
137 .with_width(device_code_style.left),
138 )
139 .with_child(
140 Label::new(
141 if copied { "Copied!" } else { "Copy" },
142 device_code_style.cta.style_for(state, false).text.clone(),
143 )
144 .aligned()
145 .contained()
146 .with_style(*device_code_style.right_container.style_for(state, false))
147 .constrained()
148 .with_width(device_code_style.right),
149 )
150 .contained()
151 .with_style(device_code_style.cta.style_for(state, false).container)
152 })
153 .on_click(gpui::platform::MouseButton::Left, {
154 let user_code = data.user_code.clone();
155 move |_, _, cx| {
156 cx.platform()
157 .write_to_clipboard(ClipboardItem::new(user_code.clone()));
158 cx.notify();
159 }
160 })
161 .with_cursor_style(gpui::platform::CursorStyle::PointingHand)
162 }
163
164 fn render_prompting_modal(
165 connect_clicked: bool,
166 data: &PromptUserDeviceFlow,
167 style: &theme::Copilot,
168 cx: &mut ViewContext<Self>,
169 ) -> AnyElement<Self> {
170 enum ConnectButton {}
171
172 Flex::column()
173 .with_child(
174 Flex::column()
175 .with_children([
176 Label::new(
177 "Enable Copilot by connecting",
178 style.auth.prompting.subheading.text.clone(),
179 )
180 .aligned(),
181 Label::new(
182 "your existing license.",
183 style.auth.prompting.subheading.text.clone(),
184 )
185 .aligned(),
186 ])
187 .align_children_center()
188 .contained()
189 .with_style(style.auth.prompting.subheading.container),
190 )
191 .with_child(Self::render_device_code(data, &style, cx))
192 .with_child(
193 Flex::column()
194 .with_children([
195 Label::new(
196 "Paste this code into GitHub after",
197 style.auth.prompting.hint.text.clone(),
198 )
199 .aligned(),
200 Label::new(
201 "clicking the button below.",
202 style.auth.prompting.hint.text.clone(),
203 )
204 .aligned(),
205 ])
206 .align_children_center()
207 .contained()
208 .with_style(style.auth.prompting.hint.container.clone()),
209 )
210 .with_child(theme::ui::cta_button_with_click::<ConnectButton, _, _, _>(
211 if connect_clicked {
212 "Waiting for connection..."
213 } else {
214 "Connect to GitHub"
215 },
216 style.auth.content_width,
217 &style.auth.cta_button,
218 cx,
219 {
220 let verification_uri = data.verification_uri.clone();
221 move |_, _, cx| {
222 cx.platform().open_url(&verification_uri);
223 cx.dispatch_action(ClickedConnect)
224 }
225 },
226 ))
227 .align_children_center()
228 .into_any()
229 }
230
231 fn render_enabled_modal(
232 style: &theme::Copilot,
233 cx: &mut ViewContext<Self>,
234 ) -> AnyElement<Self> {
235 enum DoneButton {}
236
237 let enabled_style = &style.auth.authorized;
238 Flex::column()
239 .with_child(
240 Label::new("Copilot Enabled!", enabled_style.subheading.text.clone())
241 .contained()
242 .with_style(enabled_style.subheading.container)
243 .aligned(),
244 )
245 .with_child(
246 Flex::column()
247 .with_children([
248 Label::new(
249 "You can update your settings or",
250 enabled_style.hint.text.clone(),
251 )
252 .aligned(),
253 Label::new(
254 "sign out from the Copilot menu in",
255 enabled_style.hint.text.clone(),
256 )
257 .aligned(),
258 Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(),
259 ])
260 .align_children_center()
261 .contained()
262 .with_style(enabled_style.hint.container),
263 )
264 .with_child(theme::ui::cta_button_with_click::<DoneButton, _, _, _>(
265 "Done",
266 style.auth.content_width,
267 &style.auth.cta_button,
268 cx,
269 |_, _, cx| cx.remove_window(),
270 ))
271 .align_children_center()
272 .into_any()
273 }
274
275 fn render_unauthorized_modal(
276 style: &theme::Copilot,
277 cx: &mut ViewContext<Self>,
278 ) -> AnyElement<Self> {
279 let unauthorized_style = &style.auth.not_authorized;
280
281 Flex::column()
282 .with_child(
283 Flex::column()
284 .with_children([
285 Label::new(
286 "Enable Copilot by connecting",
287 unauthorized_style.subheading.text.clone(),
288 )
289 .aligned(),
290 Label::new(
291 "your existing license.",
292 unauthorized_style.subheading.text.clone(),
293 )
294 .aligned(),
295 ])
296 .align_children_center()
297 .contained()
298 .with_style(unauthorized_style.subheading.container),
299 )
300 .with_child(
301 Flex::column()
302 .with_children([
303 Label::new(
304 "You must have an active copilot",
305 unauthorized_style.warning.text.clone(),
306 )
307 .aligned(),
308 Label::new(
309 "license to use it in Zed.",
310 unauthorized_style.warning.text.clone(),
311 )
312 .aligned(),
313 ])
314 .align_children_center()
315 .contained()
316 .with_style(unauthorized_style.warning.container),
317 )
318 .with_child(theme::ui::cta_button_with_click::<Self, _, _, _>(
319 "Subscribe on GitHub",
320 style.auth.content_width,
321 &style.auth.cta_button,
322 cx,
323 |_, _, cx| {
324 cx.remove_window();
325 cx.platform().open_url(COPILOT_SIGN_UP_URL)
326 },
327 ))
328 .align_children_center()
329 .into_any()
330 }
331}
332
333impl Entity for CopilotCodeVerification {
334 type Event = ();
335}
336
337impl View for CopilotCodeVerification {
338 fn ui_name() -> &'static str {
339 "CopilotCodeVerification"
340 }
341
342 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
343 cx.notify()
344 }
345
346 fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
347 cx.notify()
348 }
349
350 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
351 enum ConnectModal {}
352
353 let style = cx.global::<Settings>().theme.clone();
354
355 modal::<ConnectModal, _, _, _, _>(
356 "Connect Copilot to Zed",
357 &style.copilot.modal,
358 cx,
359 |cx| {
360 Flex::column()
361 .with_children([
362 theme::ui::icon(&style.copilot.auth.header).into_any(),
363 match &self.status {
364 Status::SigningIn {
365 prompt: Some(prompt),
366 } => Self::render_prompting_modal(
367 self.connect_clicked,
368 &prompt,
369 &style.copilot,
370 cx,
371 ),
372 Status::Unauthorized => {
373 self.connect_clicked = false;
374 Self::render_unauthorized_modal(&style.copilot, cx)
375 }
376 Status::Authorized => {
377 self.connect_clicked = false;
378 Self::render_enabled_modal(&style.copilot, cx)
379 }
380 _ => Empty::new().into_any(),
381 },
382 ])
383 .align_children_center()
384 },
385 )
386 .into_any()
387 }
388}