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 CopilotStatusToast;
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 workspace.update(cx, |workspace, cx| {
25 let is_reinstall = false;
26 initiate_sign_in_within_workspace(workspace, copilot, is_reinstall, window, cx)
27 });
28}
29
30pub fn reinstall_and_sign_in(window: &mut Window, cx: &mut App) {
31 let Some(copilot) = Copilot::global(cx) else {
32 return;
33 };
34 let Some(workspace) = window.root::<Workspace>().flatten() else {
35 return;
36 };
37 workspace.update(cx, |workspace, cx| {
38 reinstall_and_sign_in_within_workspace(workspace, copilot, window, cx);
39 });
40}
41
42pub fn reinstall_and_sign_in_within_workspace(
43 workspace: &mut Workspace,
44 copilot: Entity<Copilot>,
45 window: &mut Window,
46 cx: &mut Context<Workspace>,
47) {
48 let _ = copilot.update(cx, |copilot, cx| copilot.reinstall(cx));
49 let is_reinstall = true;
50 initiate_sign_in_within_workspace(workspace, copilot, is_reinstall, window, cx);
51}
52
53pub fn initiate_sign_in_within_workspace(
54 workspace: &mut Workspace,
55 copilot: Entity<Copilot>,
56 is_reinstall: bool,
57 window: &mut Window,
58 cx: &mut Context<Workspace>,
59) {
60 if matches!(copilot.read(cx).status(), Status::Disabled) {
61 copilot.update(cx, |copilot, cx| copilot.start_copilot(false, true, cx));
62 }
63 match copilot.read(cx).status() {
64 Status::Starting { task } => {
65 workspace.show_toast(
66 Toast::new(
67 NotificationId::unique::<CopilotStatusToast>(),
68 if is_reinstall {
69 "Copilot is reinstalling..."
70 } else {
71 "Copilot is starting..."
72 },
73 ),
74 cx,
75 );
76
77 cx.spawn_in(window, async move |workspace, cx| {
78 task.await;
79 if let Some(copilot) = cx.update(|_window, cx| Copilot::global(cx)).ok().flatten() {
80 workspace
81 .update_in(cx, |workspace, window, cx| {
82 match copilot.read(cx).status() {
83 Status::Authorized => workspace.show_toast(
84 Toast::new(
85 NotificationId::unique::<CopilotStatusToast>(),
86 "Copilot has started.",
87 ),
88 cx,
89 ),
90 _ => {
91 workspace.dismiss_toast(
92 &NotificationId::unique::<CopilotStatusToast>(),
93 cx,
94 );
95 copilot
96 .update(cx, |copilot, cx| copilot.sign_in(cx))
97 .detach_and_log_err(cx);
98 workspace.toggle_modal(window, cx, |_, cx| {
99 CopilotCodeVerification::new(&copilot, cx)
100 });
101 }
102 }
103 })
104 .log_err();
105 }
106 })
107 .detach();
108 }
109 _ => {
110 copilot
111 .update(cx, |copilot, cx| copilot.sign_in(cx))
112 .detach();
113 workspace.toggle_modal(window, cx, |_, cx| {
114 CopilotCodeVerification::new(&copilot, cx)
115 });
116 }
117 }
118}
119
120pub fn sign_out_within_workspace(
121 workspace: &mut Workspace,
122 copilot: Entity<Copilot>,
123 cx: &mut Context<Workspace>,
124) {
125 workspace.show_toast(
126 Toast::new(
127 NotificationId::unique::<CopilotStatusToast>(),
128 "Signing out of Copilot...",
129 ),
130 cx,
131 );
132 let sign_out_task = copilot.update(cx, |copilot, cx| copilot.sign_out(cx));
133 cx.spawn(async move |workspace, cx| match sign_out_task.await {
134 Ok(()) => {
135 workspace
136 .update(cx, |workspace, cx| {
137 workspace.show_toast(
138 Toast::new(
139 NotificationId::unique::<CopilotStatusToast>(),
140 "Signed out of Copilot.",
141 ),
142 cx,
143 )
144 })
145 .ok();
146 }
147 Err(err) => {
148 workspace
149 .update(cx, |workspace, cx| {
150 workspace.show_error(&err, cx);
151 })
152 .ok();
153 }
154 })
155 .detach();
156}
157
158pub struct CopilotCodeVerification {
159 status: Status,
160 connect_clicked: bool,
161 focus_handle: FocusHandle,
162 copilot: Entity<Copilot>,
163 _subscription: Subscription,
164}
165
166impl Focusable for CopilotCodeVerification {
167 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
168 self.focus_handle.clone()
169 }
170}
171
172impl EventEmitter<DismissEvent> for CopilotCodeVerification {}
173impl ModalView for CopilotCodeVerification {
174 fn on_before_dismiss(
175 &mut self,
176 _: &mut Window,
177 cx: &mut Context<Self>,
178 ) -> workspace::DismissDecision {
179 self.copilot.update(cx, |copilot, cx| {
180 if matches!(copilot.status(), Status::SigningIn { .. }) {
181 copilot.sign_out(cx).detach_and_log_err(cx);
182 }
183 });
184 workspace::DismissDecision::Dismiss(true)
185 }
186}
187
188impl CopilotCodeVerification {
189 pub fn new(copilot: &Entity<Copilot>, cx: &mut Context<Self>) -> Self {
190 let status = copilot.read(cx).status();
191 Self {
192 status,
193 connect_clicked: false,
194 focus_handle: cx.focus_handle(),
195 copilot: copilot.clone(),
196 _subscription: cx.observe(copilot, |this, copilot, cx| {
197 let status = copilot.read(cx).status();
198 match status {
199 Status::Authorized | Status::Unauthorized | Status::SigningIn { .. } => {
200 this.set_status(status, cx)
201 }
202 _ => cx.emit(DismissEvent),
203 }
204 }),
205 }
206 }
207
208 pub fn set_status(&mut self, status: Status, cx: &mut Context<Self>) {
209 self.status = status;
210 cx.notify();
211 }
212
213 fn render_device_code(data: &PromptUserDeviceFlow, cx: &mut Context<Self>) -> impl IntoElement {
214 let copied = cx
215 .read_from_clipboard()
216 .map(|item| item.text().as_ref() == Some(&data.user_code))
217 .unwrap_or(false);
218 h_flex()
219 .w_full()
220 .p_1()
221 .border_1()
222 .border_muted(cx)
223 .rounded_sm()
224 .cursor_pointer()
225 .justify_between()
226 .on_mouse_down(gpui::MouseButton::Left, {
227 let user_code = data.user_code.clone();
228 move |_, window, cx| {
229 cx.write_to_clipboard(ClipboardItem::new_string(user_code.clone()));
230 window.refresh();
231 }
232 })
233 .child(div().flex_1().child(Label::new(data.user_code.clone())))
234 .child(div().flex_none().px_1().child(Label::new(if copied {
235 "Copied!"
236 } else {
237 "Copy"
238 })))
239 }
240
241 fn render_prompting_modal(
242 connect_clicked: bool,
243 data: &PromptUserDeviceFlow,
244
245 cx: &mut Context<Self>,
246 ) -> impl Element {
247 let connect_button_label = if connect_clicked {
248 "Waiting for connection..."
249 } else {
250 "Connect to GitHub"
251 };
252 v_flex()
253 .flex_1()
254 .gap_2()
255 .items_center()
256 .child(Headline::new("Use GitHub Copilot in Zed.").size(HeadlineSize::Large))
257 .child(
258 Label::new("Using Copilot requires an active subscription on GitHub.")
259 .color(Color::Muted),
260 )
261 .child(Self::render_device_code(data, cx))
262 .child(
263 Label::new("Paste this code into GitHub after clicking the button below.")
264 .size(ui::LabelSize::Small),
265 )
266 .child(
267 Button::new("connect-button", connect_button_label)
268 .on_click({
269 let verification_uri = data.verification_uri.clone();
270 cx.listener(move |this, _, _window, cx| {
271 cx.open_url(&verification_uri);
272 this.connect_clicked = true;
273 })
274 })
275 .full_width()
276 .style(ButtonStyle::Filled),
277 )
278 .child(
279 Button::new("copilot-enable-cancel-button", "Cancel")
280 .full_width()
281 .on_click(cx.listener(|_, _, _, cx| {
282 cx.emit(DismissEvent);
283 })),
284 )
285 }
286
287 fn render_enabled_modal(cx: &mut Context<Self>) -> impl Element {
288 v_flex()
289 .gap_2()
290 .child(Headline::new("Copilot Enabled!").size(HeadlineSize::Large))
291 .child(Label::new(
292 "You can update your settings or sign out from the Copilot menu in the status bar.",
293 ))
294 .child(
295 Button::new("copilot-enabled-done-button", "Done")
296 .full_width()
297 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
298 )
299 }
300
301 fn render_unauthorized_modal(cx: &mut Context<Self>) -> impl Element {
302 v_flex()
303 .child(Headline::new("You must have an active GitHub Copilot subscription.").size(HeadlineSize::Large))
304
305 .child(Label::new(
306 "You can enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.",
307 ).color(Color::Warning))
308 .child(
309 Button::new("copilot-subscribe-button", "Subscribe on GitHub")
310 .full_width()
311 .on_click(|_, _, cx| cx.open_url(COPILOT_SIGN_UP_URL)),
312 )
313 .child(
314 Button::new("copilot-subscribe-cancel-button", "Cancel")
315 .full_width()
316 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
317 )
318 }
319
320 fn render_loading(window: &mut Window, _: &mut Context<Self>) -> impl Element {
321 let loading_icon = svg()
322 .size_8()
323 .path(IconName::ArrowCircle.path())
324 .text_color(window.text_style().color)
325 .with_animation(
326 "icon_circle_arrow",
327 Animation::new(Duration::from_secs(2)).repeat(),
328 |svg, delta| svg.with_transformation(Transformation::rotate(percentage(delta))),
329 );
330
331 h_flex().justify_center().child(loading_icon)
332 }
333}
334
335impl Render for CopilotCodeVerification {
336 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
337 let prompt = match &self.status {
338 Status::SigningIn { prompt: None } => {
339 Self::render_loading(window, cx).into_any_element()
340 }
341 Status::SigningIn {
342 prompt: Some(prompt),
343 } => Self::render_prompting_modal(self.connect_clicked, prompt, cx).into_any_element(),
344 Status::Unauthorized => {
345 self.connect_clicked = false;
346 Self::render_unauthorized_modal(cx).into_any_element()
347 }
348 Status::Authorized => {
349 self.connect_clicked = false;
350 Self::render_enabled_modal(cx).into_any_element()
351 }
352 _ => div().into_any_element(),
353 };
354
355 v_flex()
356 .id("copilot code verification")
357 .track_focus(&self.focus_handle(cx))
358 .elevation_3(cx)
359 .w_96()
360 .items_center()
361 .p_4()
362 .gap_2()
363 .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
364 cx.emit(DismissEvent);
365 }))
366 .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _| {
367 window.focus(&this.focus_handle);
368 }))
369 .child(
370 Vector::new(VectorName::ZedXCopilot, rems(8.), rems(4.))
371 .color(Color::Custom(cx.theme().colors().icon)),
372 )
373 .child(prompt)
374 }
375}