sign_in.rs

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