sign_in.rs

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