sign_in.rs

  1use crate::{request::PromptUserDeviceFlow, Copilot, Status};
  2use gpui::{
  3    div, AppContext, ClipboardItem, DismissEvent, Element, EventEmitter, FocusHandle,
  4    FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent, ParentElement, Render,
  5    Styled, Subscription, ViewContext,
  6};
  7use ui::{prelude::*, Button, Label, Vector, VectorName};
  8use workspace::ModalView;
  9
 10const COPILOT_SIGN_UP_URL: &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 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().as_ref() == Some(&data.user_code))
 59            .unwrap_or(false);
 60        h_flex()
 61            .w_full()
 62            .p_1()
 63            .border_1()
 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_string(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            .child(
120                Button::new("copilot-enable-cancel-button", "Cancel")
121                    .full_width()
122                    .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
123            )
124    }
125    fn render_enabled_modal(cx: &mut ViewContext<Self>) -> impl Element {
126        v_flex()
127            .gap_2()
128            .child(Headline::new("Copilot Enabled!").size(HeadlineSize::Large))
129            .child(Label::new(
130                "You can update your settings or sign out from the Copilot menu in the status bar.",
131            ))
132            .child(
133                Button::new("copilot-enabled-done-button", "Done")
134                    .full_width()
135                    .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
136            )
137    }
138
139    fn render_unauthorized_modal(cx: &mut ViewContext<Self>) -> impl Element {
140        v_flex()
141            .child(Headline::new("You must have an active GitHub Copilot subscription.").size(HeadlineSize::Large))
142
143            .child(Label::new(
144                "You can enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.",
145            ).color(Color::Warning))
146            .child(
147                Button::new("copilot-subscribe-button", "Subscribe on GitHub")
148                    .full_width()
149                    .on_click(|_, cx| cx.open_url(COPILOT_SIGN_UP_URL)),
150            )
151            .child(
152                Button::new("copilot-subscribe-cancel-button", "Cancel")
153                    .full_width()
154                    .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
155            )
156    }
157
158    fn render_disabled_modal() -> impl Element {
159        v_flex()
160            .child(Headline::new("Copilot is disabled").size(HeadlineSize::Large))
161            .child(Label::new("You can enable Copilot in your settings."))
162    }
163}
164
165impl Render for CopilotCodeVerification {
166    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
167        let prompt = match &self.status {
168            Status::SigningIn {
169                prompt: Some(prompt),
170            } => Self::render_prompting_modal(self.connect_clicked, prompt, cx).into_any_element(),
171            Status::Unauthorized => {
172                self.connect_clicked = false;
173                Self::render_unauthorized_modal(cx).into_any_element()
174            }
175            Status::Authorized => {
176                self.connect_clicked = false;
177                Self::render_enabled_modal(cx).into_any_element()
178            }
179            Status::Disabled => {
180                self.connect_clicked = false;
181                Self::render_disabled_modal().into_any_element()
182            }
183            _ => div().into_any_element(),
184        };
185
186        v_flex()
187            .id("copilot code verification")
188            .track_focus(&self.focus_handle(cx))
189            .elevation_3(cx)
190            .w_96()
191            .items_center()
192            .p_4()
193            .gap_2()
194            .on_action(cx.listener(|_, _: &menu::Cancel, cx| {
195                cx.emit(DismissEvent);
196            }))
197            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, cx| {
198                cx.focus(&this.focus_handle);
199            }))
200            .child(
201                Vector::new(VectorName::ZedXCopilot, rems(8.), rems(4.))
202                    .color(Color::Custom(cx.theme().colors().icon)),
203            )
204            .child(prompt)
205    }
206}