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