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