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