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