sign_in.rs

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