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