1use crate::{request::PromptUserDeviceFlow, Copilot, Status};
2use gpui::{
3 elements::*,
4 geometry::rect::RectF,
5 platform::{WindowBounds, WindowKind, WindowOptions},
6 AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
7 WindowHandle,
8};
9use theme::ui::modal;
10
11#[derive(PartialEq, Eq, Debug, Clone)]
12struct CopyUserCode;
13
14#[derive(PartialEq, Eq, Debug, Clone)]
15struct OpenGithub;
16
17const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
18
19pub fn init(cx: &mut AppContext) {
20 if let Some(copilot) = Copilot::global(cx) {
21 let mut verification_window: Option<WindowHandle<CopilotCodeVerification>> = None;
22 cx.observe(&copilot, move |copilot, cx| {
23 let status = copilot.read(cx).status();
24
25 match &status {
26 crate::Status::SigningIn { prompt } => {
27 if let Some(window) = verification_window.as_mut() {
28 let updated = window
29 .root(cx)
30 .map(|root| {
31 root.update(cx, |verification, cx| {
32 verification.set_status(status.clone(), cx);
33 cx.activate_window();
34 })
35 })
36 .is_some();
37 if !updated {
38 verification_window = Some(create_copilot_auth_window(cx, &status));
39 }
40 } else if let Some(_prompt) = prompt {
41 verification_window = Some(create_copilot_auth_window(cx, &status));
42 }
43 }
44 Status::Authorized | Status::Unauthorized => {
45 if let Some(window) = verification_window.as_ref() {
46 if let Some(verification) = window.root(cx) {
47 verification.update(cx, |verification, cx| {
48 verification.set_status(status, cx);
49 cx.platform().activate(true);
50 cx.activate_window();
51 });
52 }
53 }
54 }
55 _ => {
56 if let Some(code_verification) = verification_window.take() {
57 code_verification.update(cx, |cx| cx.remove_window());
58 }
59 }
60 }
61 })
62 .detach();
63 }
64}
65
66fn create_copilot_auth_window(
67 cx: &mut AppContext,
68 status: &Status,
69) -> WindowHandle<CopilotCodeVerification> {
70 let window_size = theme::current(cx).copilot.modal.dimensions();
71 let window_options = WindowOptions {
72 bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
73 titlebar: None,
74 center: true,
75 focus: true,
76 show: true,
77 kind: WindowKind::Normal,
78 is_movable: true,
79 screen: None,
80 };
81 cx.add_window(window_options, |_cx| {
82 CopilotCodeVerification::new(status.clone())
83 })
84}
85
86pub struct CopilotCodeVerification {
87 status: Status,
88 connect_clicked: bool,
89}
90
91impl CopilotCodeVerification {
92 pub fn new(status: Status) -> Self {
93 Self {
94 status,
95 connect_clicked: false,
96 }
97 }
98
99 pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
100 self.status = status;
101 cx.notify();
102 }
103
104 fn render_device_code(
105 data: &PromptUserDeviceFlow,
106 style: &theme::Copilot,
107 cx: &mut ViewContext<Self>,
108 ) -> impl Element<Self> {
109 let copied = cx
110 .read_from_clipboard()
111 .map(|item| item.text() == &data.user_code)
112 .unwrap_or(false);
113
114 let device_code_style = &style.auth.prompting.device_code;
115
116 MouseEventHandler::new::<Self, _>(0, cx, |state, _cx| {
117 Flex::row()
118 .with_child(
119 Label::new(data.user_code.clone(), device_code_style.text.clone())
120 .aligned()
121 .contained()
122 .with_style(device_code_style.left_container)
123 .constrained()
124 .with_width(device_code_style.left),
125 )
126 .with_child(
127 Label::new(
128 if copied { "Copied!" } else { "Copy" },
129 device_code_style.cta.style_for(state).text.clone(),
130 )
131 .aligned()
132 .contained()
133 .with_style(*device_code_style.right_container.style_for(state))
134 .constrained()
135 .with_width(device_code_style.right),
136 )
137 .contained()
138 .with_style(device_code_style.cta.style_for(state).container)
139 })
140 .on_click(gpui::platform::MouseButton::Left, {
141 let user_code = data.user_code.clone();
142 move |_, _, cx| {
143 cx.platform()
144 .write_to_clipboard(ClipboardItem::new(user_code.clone()));
145 cx.notify();
146 }
147 })
148 .with_cursor_style(gpui::platform::CursorStyle::PointingHand)
149 }
150
151 fn render_prompting_modal(
152 connect_clicked: bool,
153 data: &PromptUserDeviceFlow,
154 style: &theme::Copilot,
155 cx: &mut ViewContext<Self>,
156 ) -> AnyElement<Self> {
157 enum ConnectButton {}
158
159 Flex::column()
160 .with_child(
161 Flex::column()
162 .with_children([
163 Label::new(
164 "Enable Copilot by connecting",
165 style.auth.prompting.subheading.text.clone(),
166 )
167 .aligned(),
168 Label::new(
169 "your existing license.",
170 style.auth.prompting.subheading.text.clone(),
171 )
172 .aligned(),
173 ])
174 .align_children_center()
175 .contained()
176 .with_style(style.auth.prompting.subheading.container),
177 )
178 .with_child(Self::render_device_code(data, &style, cx))
179 .with_child(
180 Flex::column()
181 .with_children([
182 Label::new(
183 "Paste this code into GitHub after",
184 style.auth.prompting.hint.text.clone(),
185 )
186 .aligned(),
187 Label::new(
188 "clicking the button below.",
189 style.auth.prompting.hint.text.clone(),
190 )
191 .aligned(),
192 ])
193 .align_children_center()
194 .contained()
195 .with_style(style.auth.prompting.hint.container.clone()),
196 )
197 .with_child(theme::ui::cta_button::<ConnectButton, _, _, _>(
198 if connect_clicked {
199 "Waiting for connection..."
200 } else {
201 "Connect to GitHub"
202 },
203 style.auth.content_width,
204 &style.auth.cta_button,
205 cx,
206 {
207 let verification_uri = data.verification_uri.clone();
208 move |_, verification, cx| {
209 cx.platform().open_url(&verification_uri);
210 verification.connect_clicked = true;
211 }
212 },
213 ))
214 .align_children_center()
215 .into_any()
216 }
217
218 fn render_enabled_modal(
219 style: &theme::Copilot,
220 cx: &mut ViewContext<Self>,
221 ) -> AnyElement<Self> {
222 enum DoneButton {}
223
224 let enabled_style = &style.auth.authorized;
225 Flex::column()
226 .with_child(
227 Label::new("Copilot Enabled!", enabled_style.subheading.text.clone())
228 .contained()
229 .with_style(enabled_style.subheading.container)
230 .aligned(),
231 )
232 .with_child(
233 Flex::column()
234 .with_children([
235 Label::new(
236 "You can update your settings or",
237 enabled_style.hint.text.clone(),
238 )
239 .aligned(),
240 Label::new(
241 "sign out from the Copilot menu in",
242 enabled_style.hint.text.clone(),
243 )
244 .aligned(),
245 Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(),
246 ])
247 .align_children_center()
248 .contained()
249 .with_style(enabled_style.hint.container),
250 )
251 .with_child(theme::ui::cta_button::<DoneButton, _, _, _>(
252 "Done",
253 style.auth.content_width,
254 &style.auth.cta_button,
255 cx,
256 |_, _, cx| cx.remove_window(),
257 ))
258 .align_children_center()
259 .into_any()
260 }
261
262 fn render_unauthorized_modal(
263 style: &theme::Copilot,
264 cx: &mut ViewContext<Self>,
265 ) -> AnyElement<Self> {
266 let unauthorized_style = &style.auth.not_authorized;
267
268 Flex::column()
269 .with_child(
270 Flex::column()
271 .with_children([
272 Label::new(
273 "Enable Copilot by connecting",
274 unauthorized_style.subheading.text.clone(),
275 )
276 .aligned(),
277 Label::new(
278 "your existing license.",
279 unauthorized_style.subheading.text.clone(),
280 )
281 .aligned(),
282 ])
283 .align_children_center()
284 .contained()
285 .with_style(unauthorized_style.subheading.container),
286 )
287 .with_child(
288 Flex::column()
289 .with_children([
290 Label::new(
291 "You must have an active copilot",
292 unauthorized_style.warning.text.clone(),
293 )
294 .aligned(),
295 Label::new(
296 "license to use it in Zed.",
297 unauthorized_style.warning.text.clone(),
298 )
299 .aligned(),
300 ])
301 .align_children_center()
302 .contained()
303 .with_style(unauthorized_style.warning.container),
304 )
305 .with_child(theme::ui::cta_button::<Self, _, _, _>(
306 "Subscribe on GitHub",
307 style.auth.content_width,
308 &style.auth.cta_button,
309 cx,
310 |_, _, cx| {
311 cx.remove_window();
312 cx.platform().open_url(COPILOT_SIGN_UP_URL)
313 },
314 ))
315 .align_children_center()
316 .into_any()
317 }
318}
319
320impl Entity for CopilotCodeVerification {
321 type Event = ();
322}
323
324impl View for CopilotCodeVerification {
325 fn ui_name() -> &'static str {
326 "CopilotCodeVerification"
327 }
328
329 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
330 cx.notify()
331 }
332
333 fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
334 cx.notify()
335 }
336
337 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
338 enum ConnectModal {}
339
340 let style = theme::current(cx).clone();
341
342 modal::<ConnectModal, _, _, _, _>(
343 "Connect Copilot to Zed",
344 &style.copilot.modal,
345 cx,
346 |cx| {
347 Flex::column()
348 .with_children([
349 theme::ui::icon(&style.copilot.auth.header).into_any(),
350 match &self.status {
351 Status::SigningIn {
352 prompt: Some(prompt),
353 } => Self::render_prompting_modal(
354 self.connect_clicked,
355 &prompt,
356 &style.copilot,
357 cx,
358 ),
359 Status::Unauthorized => {
360 self.connect_clicked = false;
361 Self::render_unauthorized_modal(&style.copilot, cx)
362 }
363 Status::Authorized => {
364 self.connect_clicked = false;
365 Self::render_enabled_modal(&style.copilot, cx)
366 }
367 _ => Empty::new().into_any(),
368 },
369 ])
370 .align_children_center()
371 },
372 )
373 .into_any()
374 }
375}