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