sign_in.rs

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