1use crate::{request::PromptUserDeviceFlow, Copilot, Status};
2use gpui::{
3 elements::*, geometry::rect::RectF, ClipboardItem, Element, Entity, MutableAppContext, View,
4 ViewContext, ViewHandle, WindowKind, WindowOptions,
5};
6use settings::Settings;
7
8#[derive(PartialEq, Eq, Debug, Clone)]
9struct CopyUserCode;
10
11#[derive(PartialEq, Eq, Debug, Clone)]
12struct OpenGithub;
13
14const _COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
15
16pub fn init(cx: &mut MutableAppContext) {
17 let copilot = Copilot::global(cx).unwrap();
18
19 let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None;
20 cx.observe(&copilot, move |copilot, cx| {
21 let status = copilot.read(cx).status();
22
23 match &status {
24 crate::Status::SigningIn { prompt } => {
25 if let Some(code_verification) = code_verification.as_ref() {
26 code_verification.update(cx, |code_verification, cx| {
27 code_verification.set_status(status, cx)
28 });
29 cx.activate_window(code_verification.window_id());
30 } else if let Some(_prompt) = prompt {
31 let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
32 let window_options = WindowOptions {
33 bounds: gpui::WindowBounds::Fixed(RectF::new(
34 Default::default(),
35 window_size,
36 )),
37 titlebar: None,
38 center: true,
39 focus: true,
40 kind: WindowKind::Normal,
41 is_movable: true,
42 screen: None,
43 };
44 let (_, view) =
45 cx.add_window(window_options, |_cx| CopilotCodeVerification::new(status));
46 code_verification = Some(view);
47 }
48 }
49 Status::Authorized | Status::Unauthorized => {
50 if let Some(code_verification) = code_verification.as_ref() {
51 code_verification.update(cx, |code_verification, cx| {
52 code_verification.set_status(status, cx)
53 });
54
55 cx.platform().activate(true);
56 cx.activate_window(code_verification.window_id());
57 }
58 }
59 _ => {
60 if let Some(code_verification) = code_verification.take() {
61 cx.remove_window(code_verification.window_id());
62 }
63 }
64 }
65 })
66 .detach();
67
68 // Modal theming test:
69 // let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
70 // let window_options = WindowOptions {
71 // bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
72 // titlebar: None,
73 // center: false,
74 // focus: false,
75 // kind: WindowKind::PopUp,
76 // is_movable: true,
77 // screen: None,
78 // };
79 // let (_, _view) = cx.add_window(window_options, |_cx| {
80 // CopilotCodeVerification::new(Status::SigningIn {
81 // prompt: Some(PromptUserDeviceFlow {
82 // user_code: "ABCD-1234".to_string(),
83 // verification_uri: "https://github.com/login/device".to_string(),
84 // }),
85 // })
86 // });
87
88 // let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
89 // let window_options = WindowOptions {
90 // bounds: gpui::WindowBounds::Fixed(RectF::new(vec2f(window_size.x(), 0.), window_size)),
91 // titlebar: None,
92 // center: false,
93 // focus: false,
94 // kind: WindowKind::PopUp,
95 // is_movable: true,
96 // screen: None,
97 // };
98 // let (_, _view) = cx.add_window(window_options, |_cx| {
99 // CopilotCodeVerification::new(Status::Authorized)
100 // });
101
102 // let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
103 // let window_options = WindowOptions {
104 // bounds: gpui::WindowBounds::Fixed(RectF::new(vec2f(0., window_size.y()), window_size)),
105 // titlebar: None,
106 // center: false,
107 // focus: false,
108 // kind: WindowKind::PopUp,
109 // is_movable: true,
110 // screen: None,
111 // };
112 // let (_, _view) = cx.add_window(window_options, |_cx| {
113 // CopilotCodeVerification::new(Status::Unauthorized)
114 // });
115}
116
117pub struct CopilotCodeVerification {
118 status: Status,
119}
120
121impl CopilotCodeVerification {
122 pub fn new(status: Status) -> Self {
123 Self { status }
124 }
125
126 pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
127 self.status = status;
128 cx.notify();
129 }
130
131 fn render_device_code(
132 data: &PromptUserDeviceFlow,
133 style: &theme::Copilot,
134 cx: &mut gpui::RenderContext<Self>,
135 ) -> ElementBox {
136 let copied = cx
137 .read_from_clipboard()
138 .map(|item| item.text() == &data.user_code)
139 .unwrap_or(false);
140
141 Flex::column()
142 .with_children([
143 MouseEventHandler::<Self>::new(0, cx, |state, _cx| {
144 Flex::row()
145 .with_children([
146 Label::new(data.user_code.clone(), style.auth.device_code.clone())
147 .aligned()
148 .contained()
149 .with_style(style.auth.device_code_left_container)
150 .constrained()
151 .with_width(style.auth.device_code_left)
152 .boxed(),
153 Empty::new()
154 .constrained()
155 .with_width(1.)
156 .with_height(style.auth.device_code_seperator_height)
157 .contained()
158 .with_background_color(
159 style
160 .auth
161 .cta_button
162 .style_for(state, false)
163 .container
164 .border
165 .color,
166 )
167 .boxed(),
168 Label::new(
169 if copied { "Copied!" } else { "Copy" },
170 style.auth.cta_button.style_for(state, false).text.clone(),
171 )
172 .aligned()
173 .contained()
174 .with_style(style.auth.device_code_right_container)
175 .constrained()
176 .with_width(style.auth.device_code_right)
177 .boxed(),
178 ])
179 .contained()
180 .with_style(style.auth.device_code_cta.style_for(state, false).container)
181 .constrained()
182 .with_width(style.auth.content_width)
183 .boxed()
184 })
185 .on_click(gpui::MouseButton::Left, {
186 let user_code = data.user_code.clone();
187 move |_, cx| {
188 cx.platform()
189 .write_to_clipboard(ClipboardItem::new(user_code.clone()));
190 cx.notify();
191 }
192 })
193 .with_cursor_style(gpui::CursorStyle::PointingHand)
194 .boxed(),
195 Flex::column()
196 .with_children([
197 Label::new(
198 "Paste this code into GitHub after",
199 style.auth.hint.text.clone(),
200 )
201 .boxed(),
202 Label::new("clicking the button below.", style.auth.hint.text.clone())
203 .boxed(),
204 ])
205 .align_children_center()
206 .contained()
207 .with_style(style.auth.hint.container.clone())
208 .boxed(),
209 ])
210 .align_children_center()
211 .contained()
212 .with_style(style.auth.device_code_group)
213 .aligned()
214 .boxed()
215 }
216
217 fn render_not_authorized_warning(style: &theme::Copilot) -> ElementBox {
218 Flex::column()
219 .with_children([
220 Flex::column()
221 .with_children([
222 Label::new(
223 "You must have an active copilot",
224 style.auth.warning.text.to_owned(),
225 )
226 .aligned()
227 .boxed(),
228 Label::new(
229 "license to use it in Zed.",
230 style.auth.warning.text.to_owned(),
231 )
232 .aligned()
233 .boxed(),
234 ])
235 .align_children_center()
236 .contained()
237 .with_style(style.auth.warning.container)
238 .boxed(),
239 Flex::column()
240 .with_children([
241 Label::new(
242 "Try connecting again once you",
243 style.auth.hint.text.to_owned(),
244 )
245 .aligned()
246 .boxed(),
247 Label::new(
248 "have activated a Copilot license.",
249 style.auth.hint.text.to_owned(),
250 )
251 .aligned()
252 .boxed(),
253 ])
254 .align_children_center()
255 .contained()
256 .with_style(style.auth.not_authorized_hint)
257 .boxed(),
258 ])
259 .align_children_center()
260 .boxed()
261 }
262
263 fn render_copilot_enabled(style: &theme::Copilot) -> ElementBox {
264 Flex::column()
265 .with_children([
266 Label::new(
267 "You can update your settings or",
268 style.auth.hint.text.clone(),
269 )
270 .aligned()
271 .boxed(),
272 Label::new(
273 "sign out from the Copilot menu in",
274 style.auth.hint.text.clone(),
275 )
276 .aligned()
277 .boxed(),
278 Label::new("the status bar.", style.auth.hint.text.clone())
279 .aligned()
280 .boxed(),
281 ])
282 .align_children_center()
283 .contained()
284 .with_style(style.auth.enabled_hint)
285 .boxed()
286 }
287
288 fn render_prompting_modal(
289 data: &PromptUserDeviceFlow,
290 style: &theme::Copilot,
291 cx: &mut gpui::RenderContext<Self>,
292 ) -> ElementBox {
293 theme::ui::modal("Connect Copilot to Zed", &style.modal, cx, |cx| {
294 Flex::column()
295 .with_children([
296 Flex::column()
297 .with_children([
298 Flex::row()
299 .with_children([
300 theme::ui::svg(&style.auth.copilot_icon).boxed(),
301 theme::ui::icon(&style.auth.plus_icon).boxed(),
302 theme::ui::svg(&style.auth.zed_icon).boxed(),
303 ])
304 .boxed(),
305 Flex::column()
306 .with_children([
307 Label::new(
308 "Enable Copilot by connecting",
309 style.auth.enable_text.clone(),
310 )
311 .boxed(),
312 Label::new(
313 "your existing license.",
314 style.auth.enable_text.clone(),
315 )
316 .boxed(),
317 ])
318 .align_children_center()
319 .contained()
320 .with_style(style.auth.enable_group.clone())
321 .boxed(),
322 ])
323 .align_children_center()
324 .contained()
325 .with_style(style.auth.header_group)
326 .aligned()
327 .boxed(),
328 Self::render_device_code(data, &style, cx),
329 // match &self.prompt {
330 // SignInContents::PromptingUser(data) => {
331
332 // }
333 // SignInContents::Unauthorized => Self::render_not_authorized_warning(&style),
334 // SignInContents::Enabled => Self::render_copilot_enabled(&style),
335 // },
336 Flex::column()
337 .with_child(
338 theme::ui::cta_button_with_click(
339 "Connect to GitHub",
340 style.auth.content_width,
341 &style.auth.cta_button,
342 cx,
343 {
344 let verification_uri = data.verification_uri.clone();
345 move |_, cx| cx.platform().open_url(&verification_uri)
346 },
347 ),
348 // {
349 // match &self.prompt {
350 // SignInContents::PromptingUser(data) => {
351
352 // }
353 // // SignInContents::Unauthorized => theme::ui::cta_button_with_click(
354 // // "Close",
355 // // style.auth.content_width,
356 // // &style.auth.cta_button,
357 // // cx,
358 // // |_, cx| {
359 // // let window_id = cx.window_id();
360 // // cx.remove_window(window_id)
361 // // },
362 // // ),
363 // // SignInContents::Enabled => theme::ui::cta_button_with_click(
364 // // "Done",
365 // // style.auth.content_width,
366 // // &style.auth.cta_button,
367 // // cx,
368 // // |_, cx| {
369 // // let window_id = cx.window_id();
370 // // cx.remove_window(window_id)
371 // // },
372 // // ),
373 // }
374 )
375 .align_children_center()
376 .contained()
377 .with_style(style.auth.github_group)
378 .aligned()
379 .boxed(),
380 ])
381 .align_children_center()
382 .constrained()
383 .with_width(style.auth.content_width)
384 .aligned()
385 .boxed()
386 })
387 }
388 fn render_enabled_modal(
389 style: &theme::Copilot,
390 cx: &mut gpui::RenderContext<Self>,
391 ) -> ElementBox {
392 theme::ui::modal("Connect Copilot to Zed", &style.modal, cx, |cx| {
393 Flex::column()
394 .with_children([
395 Flex::column()
396 .with_children([
397 Flex::row()
398 .with_children([
399 theme::ui::svg(&style.auth.copilot_icon).boxed(),
400 theme::ui::icon(&style.auth.plus_icon).boxed(),
401 theme::ui::svg(&style.auth.zed_icon).boxed(),
402 ])
403 .boxed(),
404 Label::new("Copilot Enabled!", style.auth.enable_text.clone()).boxed(),
405 ])
406 .align_children_center()
407 .contained()
408 .with_style(style.auth.header_group)
409 .aligned()
410 .boxed(),
411 Self::render_copilot_enabled(&style),
412 Flex::column()
413 .with_child(theme::ui::cta_button_with_click(
414 "Close",
415 style.auth.content_width,
416 &style.auth.cta_button,
417 cx,
418 |_, cx| {
419 let window_id = cx.window_id();
420 cx.remove_window(window_id)
421 },
422 ))
423 .align_children_center()
424 .contained()
425 .with_style(style.auth.github_group)
426 .aligned()
427 .boxed(),
428 ])
429 .align_children_center()
430 .constrained()
431 .with_width(style.auth.content_width)
432 .aligned()
433 .boxed()
434 })
435 }
436 fn render_unauthorized_modal(
437 style: &theme::Copilot,
438 cx: &mut gpui::RenderContext<Self>,
439 ) -> ElementBox {
440 theme::ui::modal("Connect Copilot to Zed", &style.modal, cx, |cx| {
441 Flex::column()
442 .with_children([
443 Flex::column()
444 .with_children([
445 Flex::row()
446 .with_children([
447 theme::ui::svg(&style.auth.copilot_icon).boxed(),
448 theme::ui::icon(&style.auth.plus_icon).boxed(),
449 theme::ui::svg(&style.auth.zed_icon).boxed(),
450 ])
451 .boxed(),
452 Flex::column()
453 .with_children([
454 Label::new(
455 "Enable Copilot by connecting",
456 style.auth.enable_text.clone(),
457 )
458 .boxed(),
459 Label::new(
460 "your existing license.",
461 style.auth.enable_text.clone(),
462 )
463 .boxed(),
464 ])
465 .align_children_center()
466 .contained()
467 .with_style(style.auth.enable_group.clone())
468 .boxed(),
469 ])
470 .align_children_center()
471 .contained()
472 .with_style(style.auth.header_group)
473 .aligned()
474 .boxed(),
475 Self::render_not_authorized_warning(&style),
476 Flex::column()
477 .with_child(theme::ui::cta_button_with_click(
478 "Close",
479 style.auth.content_width,
480 &style.auth.cta_button,
481 cx,
482 |_, cx| {
483 let window_id = cx.window_id();
484 cx.remove_window(window_id)
485 },
486 ))
487 .align_children_center()
488 .contained()
489 .with_style(style.auth.github_group)
490 .aligned()
491 .boxed(),
492 ])
493 .align_children_center()
494 .constrained()
495 .with_width(style.auth.content_width)
496 .aligned()
497 .boxed()
498 })
499 }
500}
501
502impl Entity for CopilotCodeVerification {
503 type Event = ();
504}
505
506impl View for CopilotCodeVerification {
507 fn ui_name() -> &'static str {
508 "CopilotCodeVerification"
509 }
510
511 fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext<Self>) {
512 cx.notify()
513 }
514
515 fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext<Self>) {
516 cx.notify()
517 }
518
519 fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
520 let style = cx.global::<Settings>().theme.copilot.clone();
521 match &self.status {
522 Status::SigningIn {
523 prompt: Some(prompt),
524 } => Self::render_prompting_modal(&prompt, &style, cx),
525 Status::Unauthorized => Self::render_unauthorized_modal(&style, cx),
526 Status::Authorized => Self::render_enabled_modal(&style, cx),
527 _ => Empty::new().boxed(),
528 }
529 }
530}