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