sign_in.rs

  1use crate::{request::PromptUserDeviceFlow, Copilot, Status};
  2use gpui::{
  3    div, App, ClipboardItem, Context, DismissEvent, Element, Entity, EventEmitter, FocusHandle,
  4    Focusable, InteractiveElement, IntoElement, MouseDownEvent, ParentElement, Render, Styled,
  5    Subscription, Window,
  6};
  7use ui::{prelude::*, Button, Label, Vector, VectorName};
  8use util::ResultExt as _;
  9use workspace::notifications::NotificationId;
 10use workspace::{ModalView, Toast, Workspace};
 11
 12const COPILOT_SIGN_UP_URL: &str = "https://github.com/features/copilot";
 13
 14struct CopilotStartingToast;
 15
 16pub fn initiate_sign_in(window: &mut Window, cx: &mut App) {
 17    let Some(copilot) = Copilot::global(cx) else {
 18        return;
 19    };
 20    let status = copilot.read(cx).status();
 21    let Some(workspace) = window.root::<Workspace>().flatten() else {
 22        return;
 23    };
 24    match status {
 25        Status::Starting { task } => {
 26            workspace.update(cx, |workspace, cx| {
 27                workspace.show_toast(
 28                    Toast::new(
 29                        NotificationId::unique::<CopilotStartingToast>(),
 30                        "Copilot is starting...",
 31                    ),
 32                    cx,
 33                );
 34            });
 35
 36            let workspace = workspace.downgrade();
 37            cx.spawn(|mut cx| async move {
 38                task.await;
 39                if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() {
 40                    workspace
 41                        .update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
 42                            Status::Authorized => workspace.show_toast(
 43                                Toast::new(
 44                                    NotificationId::unique::<CopilotStartingToast>(),
 45                                    "Copilot has started!",
 46                                ),
 47                                cx,
 48                            ),
 49                            _ => {
 50                                workspace.dismiss_toast(
 51                                    &NotificationId::unique::<CopilotStartingToast>(),
 52                                    cx,
 53                                );
 54                                copilot
 55                                    .update(cx, |copilot, cx| copilot.sign_in(cx))
 56                                    .detach_and_log_err(cx);
 57                            }
 58                        })
 59                        .log_err();
 60                }
 61            })
 62            .detach();
 63        }
 64        _ => {
 65            copilot.update(cx, |this, cx| this.sign_in(cx)).detach();
 66            workspace.update(cx, |this, cx| {
 67                this.toggle_modal(window, cx, |_, cx| {
 68                    CopilotCodeVerification::new(&copilot, cx)
 69                });
 70            });
 71        }
 72    }
 73}
 74
 75pub struct CopilotCodeVerification {
 76    status: Status,
 77    connect_clicked: bool,
 78    focus_handle: FocusHandle,
 79    _subscription: Subscription,
 80}
 81
 82impl Focusable for CopilotCodeVerification {
 83    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
 84        self.focus_handle.clone()
 85    }
 86}
 87
 88impl EventEmitter<DismissEvent> for CopilotCodeVerification {}
 89impl ModalView for CopilotCodeVerification {}
 90
 91impl CopilotCodeVerification {
 92    pub fn new(copilot: &Entity<Copilot>, cx: &mut Context<Self>) -> Self {
 93        let status = copilot.read(cx).status();
 94        Self {
 95            status,
 96            connect_clicked: false,
 97            focus_handle: cx.focus_handle(),
 98            _subscription: cx.observe(copilot, |this, copilot, cx| {
 99                let status = copilot.read(cx).status();
100                match status {
101                    Status::Authorized | Status::Unauthorized | Status::SigningIn { .. } => {
102                        this.set_status(status, cx)
103                    }
104                    _ => cx.emit(DismissEvent),
105                }
106            }),
107        }
108    }
109
110    pub fn set_status(&mut self, status: Status, cx: &mut Context<Self>) {
111        self.status = status;
112        cx.notify();
113    }
114
115    fn render_device_code(data: &PromptUserDeviceFlow, cx: &mut Context<Self>) -> impl IntoElement {
116        let copied = cx
117            .read_from_clipboard()
118            .map(|item| item.text().as_ref() == Some(&data.user_code))
119            .unwrap_or(false);
120        h_flex()
121            .w_full()
122            .p_1()
123            .border_1()
124            .border_muted(cx)
125            .rounded_md()
126            .cursor_pointer()
127            .justify_between()
128            .on_mouse_down(gpui::MouseButton::Left, {
129                let user_code = data.user_code.clone();
130                move |_, window, cx| {
131                    cx.write_to_clipboard(ClipboardItem::new_string(user_code.clone()));
132                    window.refresh();
133                }
134            })
135            .child(div().flex_1().child(Label::new(data.user_code.clone())))
136            .child(div().flex_none().px_1().child(Label::new(if copied {
137                "Copied!"
138            } else {
139                "Copy"
140            })))
141    }
142
143    fn render_prompting_modal(
144        connect_clicked: bool,
145        data: &PromptUserDeviceFlow,
146
147        cx: &mut Context<Self>,
148    ) -> impl Element {
149        let connect_button_label = if connect_clicked {
150            "Waiting for connection..."
151        } else {
152            "Connect to GitHub"
153        };
154        v_flex()
155            .flex_1()
156            .gap_2()
157            .items_center()
158            .child(Headline::new("Use GitHub Copilot in Zed.").size(HeadlineSize::Large))
159            .child(
160                Label::new("Using Copilot requires an active subscription on GitHub.")
161                    .color(Color::Muted),
162            )
163            .child(Self::render_device_code(data, cx))
164            .child(
165                Label::new("Paste this code into GitHub after clicking the button below.")
166                    .size(ui::LabelSize::Small),
167            )
168            .child(
169                Button::new("connect-button", connect_button_label)
170                    .on_click({
171                        let verification_uri = data.verification_uri.clone();
172                        cx.listener(move |this, _, _window, cx| {
173                            cx.open_url(&verification_uri);
174                            this.connect_clicked = true;
175                        })
176                    })
177                    .full_width()
178                    .style(ButtonStyle::Filled),
179            )
180            .child(
181                Button::new("copilot-enable-cancel-button", "Cancel")
182                    .full_width()
183                    .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
184            )
185    }
186    fn render_enabled_modal(cx: &mut Context<Self>) -> impl Element {
187        v_flex()
188            .gap_2()
189            .child(Headline::new("Copilot Enabled!").size(HeadlineSize::Large))
190            .child(Label::new(
191                "You can update your settings or sign out from the Copilot menu in the status bar.",
192            ))
193            .child(
194                Button::new("copilot-enabled-done-button", "Done")
195                    .full_width()
196                    .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
197            )
198    }
199
200    fn render_unauthorized_modal(cx: &mut Context<Self>) -> impl Element {
201        v_flex()
202            .child(Headline::new("You must have an active GitHub Copilot subscription.").size(HeadlineSize::Large))
203
204            .child(Label::new(
205                "You can enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.",
206            ).color(Color::Warning))
207            .child(
208                Button::new("copilot-subscribe-button", "Subscribe on GitHub")
209                    .full_width()
210                    .on_click(|_, _, cx| cx.open_url(COPILOT_SIGN_UP_URL)),
211            )
212            .child(
213                Button::new("copilot-subscribe-cancel-button", "Cancel")
214                    .full_width()
215                    .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
216            )
217    }
218
219    fn render_disabled_modal() -> impl Element {
220        v_flex()
221            .child(Headline::new("Copilot is disabled").size(HeadlineSize::Large))
222            .child(Label::new("You can enable Copilot in your settings."))
223    }
224}
225
226impl Render for CopilotCodeVerification {
227    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
228        let prompt = match &self.status {
229            Status::SigningIn {
230                prompt: Some(prompt),
231            } => Self::render_prompting_modal(self.connect_clicked, prompt, cx).into_any_element(),
232            Status::Unauthorized => {
233                self.connect_clicked = false;
234                Self::render_unauthorized_modal(cx).into_any_element()
235            }
236            Status::Authorized => {
237                self.connect_clicked = false;
238                Self::render_enabled_modal(cx).into_any_element()
239            }
240            Status::Disabled => {
241                self.connect_clicked = false;
242                Self::render_disabled_modal().into_any_element()
243            }
244            _ => div().into_any_element(),
245        };
246
247        v_flex()
248            .id("copilot code verification")
249            .track_focus(&self.focus_handle(cx))
250            .elevation_3(cx)
251            .w_96()
252            .items_center()
253            .p_4()
254            .gap_2()
255            .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
256                cx.emit(DismissEvent);
257            }))
258            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _| {
259                window.focus(&this.focus_handle);
260            }))
261            .child(
262                Vector::new(VectorName::ZedXCopilot, rems(8.), rems(4.))
263                    .color(Color::Custom(cx.theme().colors().icon)),
264            )
265            .child(prompt)
266    }
267}