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                        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}