sign_in.rs

  1use copilot::{request::PromptUserDeviceFlow, Copilot, Status};
  2use gpui::{
  3    div, svg, AppContext, ClipboardItem, DismissEvent, Element, EventEmitter, FocusHandle,
  4    FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render, Styled,
  5    Subscription, ViewContext,
  6};
  7use ui::{prelude::*, Button, IconName, Label};
  8use workspace::ModalView;
  9
 10const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
 11
 12pub struct CopilotCodeVerification {
 13    status: Status,
 14    connect_clicked: bool,
 15    focus_handle: FocusHandle,
 16    _subscription: Subscription,
 17}
 18
 19impl FocusableView for CopilotCodeVerification {
 20    fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
 21        self.focus_handle.clone()
 22    }
 23}
 24
 25impl EventEmitter<DismissEvent> for CopilotCodeVerification {}
 26impl ModalView for CopilotCodeVerification {}
 27
 28impl CopilotCodeVerification {
 29    pub(crate) fn new(copilot: &Model<Copilot>, cx: &mut ViewContext<Self>) -> Self {
 30        let status = copilot.read(cx).status();
 31        Self {
 32            status,
 33            connect_clicked: false,
 34            focus_handle: cx.focus_handle(),
 35            _subscription: cx.observe(copilot, |this, copilot, cx| {
 36                let status = copilot.read(cx).status();
 37                match status {
 38                    Status::Authorized | Status::Unauthorized | Status::SigningIn { .. } => {
 39                        this.set_status(status, cx)
 40                    }
 41                    _ => cx.emit(DismissEvent),
 42                }
 43            }),
 44        }
 45    }
 46
 47    pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
 48        self.status = status;
 49        cx.notify();
 50    }
 51
 52    fn render_device_code(
 53        data: &PromptUserDeviceFlow,
 54        cx: &mut ViewContext<Self>,
 55    ) -> impl IntoElement {
 56        let copied = cx
 57            .read_from_clipboard()
 58            .map(|item| item.text() == &data.user_code)
 59            .unwrap_or(false);
 60        h_flex()
 61            .w_full()
 62            .p_1()
 63            .border()
 64            .border_muted(cx)
 65            .rounded_md()
 66            .cursor_pointer()
 67            .justify_between()
 68            .on_mouse_down(gpui::MouseButton::Left, {
 69                let user_code = data.user_code.clone();
 70                move |_, cx| {
 71                    cx.write_to_clipboard(ClipboardItem::new(user_code.clone()));
 72                    cx.refresh();
 73                }
 74            })
 75            .child(div().flex_1().child(Label::new(data.user_code.clone())))
 76            .child(div().flex_none().px_1().child(Label::new(if copied {
 77                "Copied!"
 78            } else {
 79                "Copy"
 80            })))
 81    }
 82
 83    fn render_prompting_modal(
 84        connect_clicked: bool,
 85        data: &PromptUserDeviceFlow,
 86        cx: &mut ViewContext<Self>,
 87    ) -> impl Element {
 88        let connect_button_label = if connect_clicked {
 89            "Waiting for connection..."
 90        } else {
 91            "Connect to GitHub"
 92        };
 93        v_flex()
 94            .flex_1()
 95            .gap_2()
 96            .items_center()
 97            .child(Headline::new("Use GitHub Copilot in Zed.").size(HeadlineSize::Large))
 98            .child(
 99                Label::new("Using Copilot requires an active subscription on GitHub.")
100                    .color(Color::Muted),
101            )
102            .child(Self::render_device_code(data, cx))
103            .child(
104                Label::new("Paste this code into GitHub after clicking the button below.")
105                    .size(ui::LabelSize::Small),
106            )
107            .child(
108                Button::new("connect-button", connect_button_label)
109                    .on_click({
110                        let verification_uri = data.verification_uri.clone();
111                        cx.listener(move |this, _, cx| {
112                            cx.open_url(&verification_uri);
113                            this.connect_clicked = true;
114                        })
115                    })
116                    .full_width()
117                    .style(ButtonStyle::Filled),
118            )
119    }
120    fn render_enabled_modal(cx: &mut ViewContext<Self>) -> impl Element {
121        v_flex()
122            .gap_2()
123            .child(Headline::new("Copilot Enabled!").size(HeadlineSize::Large))
124            .child(Label::new(
125                "You can update your settings or sign out from the Copilot menu in the status bar.",
126            ))
127            .child(
128                Button::new("copilot-enabled-done-button", "Done")
129                    .full_width()
130                    .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
131            )
132    }
133
134    fn render_unauthorized_modal() -> impl Element {
135        v_flex()
136            .child(Headline::new("You must have an active GitHub Copilot subscription.").size(HeadlineSize::Large))
137
138            .child(Label::new(
139                "You can enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.",
140            ).color(Color::Warning))
141            .child(
142                Button::new("copilot-subscribe-button", "Subscribe on GitHub")
143                    .full_width()
144                    .on_click(|_, cx| cx.open_url(COPILOT_SIGN_UP_URL)),
145            )
146    }
147}
148
149impl Render for CopilotCodeVerification {
150    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
151        let prompt = match &self.status {
152            Status::SigningIn {
153                prompt: Some(prompt),
154            } => Self::render_prompting_modal(self.connect_clicked, &prompt, cx).into_any_element(),
155            Status::Unauthorized => {
156                self.connect_clicked = false;
157                Self::render_unauthorized_modal().into_any_element()
158            }
159            Status::Authorized => {
160                self.connect_clicked = false;
161                Self::render_enabled_modal(cx).into_any_element()
162            }
163            _ => div().into_any_element(),
164        };
165
166        v_flex()
167            .id("copilot code verification")
168            .elevation_3(cx)
169            .w_96()
170            .items_center()
171            .p_4()
172            .gap_2()
173            .child(
174                svg()
175                    .w_32()
176                    .h_16()
177                    .flex_none()
178                    .path(IconName::ZedXCopilot.path())
179                    .text_color(cx.theme().colors().icon),
180            )
181            .child(prompt)
182    }
183}