1use crate::{request::PromptUserDeviceFlow, Copilot, Status};
2use gpui::{
3 div, size, AppContext, Bounds, ClipboardItem, Element, GlobalPixels, InteractiveElement,
4 IntoElement, ParentElement, Point, Render, Styled, ViewContext, VisualContext, WindowBounds,
5 WindowHandle, WindowKind, WindowOptions,
6};
7use theme::ActiveTheme;
8use ui::{prelude::*, Button, Icon, IconElement, Label};
9
10const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
11
12pub fn init(cx: &mut AppContext) {
13 if let Some(copilot) = Copilot::global(cx) {
14 let mut verification_window: Option<WindowHandle<CopilotCodeVerification>> = None;
15 cx.observe(&copilot, move |copilot, cx| {
16 let status = copilot.read(cx).status();
17
18 match &status {
19 crate::Status::SigningIn { prompt } => {
20 if let Some(window) = verification_window.as_mut() {
21 let updated = window
22 .update(cx, |verification, cx| {
23 verification.set_status(status.clone(), cx);
24 cx.activate_window();
25 })
26 .is_ok();
27 if !updated {
28 verification_window = Some(create_copilot_auth_window(cx, &status));
29 }
30 } else if let Some(_prompt) = prompt {
31 verification_window = Some(create_copilot_auth_window(cx, &status));
32 }
33 }
34 Status::Authorized | Status::Unauthorized => {
35 if let Some(window) = verification_window.as_ref() {
36 window
37 .update(cx, |verification, cx| {
38 verification.set_status(status, cx);
39 cx.activate(true);
40 cx.activate_window();
41 })
42 .ok();
43 }
44 }
45 _ => {
46 if let Some(code_verification) = verification_window.take() {
47 code_verification
48 .update(cx, |_, cx| cx.remove_window())
49 .ok();
50 }
51 }
52 }
53 })
54 .detach();
55 }
56}
57
58fn create_copilot_auth_window(
59 cx: &mut AppContext,
60 status: &Status,
61) -> WindowHandle<CopilotCodeVerification> {
62 let window_size = size(GlobalPixels::from(280.), GlobalPixels::from(280.));
63 let window_options = WindowOptions {
64 bounds: WindowBounds::Fixed(Bounds::new(Point::default(), window_size)),
65 titlebar: None,
66 center: true,
67 focus: true,
68 show: true,
69 kind: WindowKind::PopUp,
70 is_movable: true,
71 display_id: None,
72 };
73 let window = cx.open_window(window_options, |cx| {
74 cx.new_view(|_| CopilotCodeVerification::new(status.clone()))
75 });
76 window
77}
78
79pub struct CopilotCodeVerification {
80 status: Status,
81 connect_clicked: bool,
82}
83
84impl CopilotCodeVerification {
85 pub fn new(status: Status) -> Self {
86 Self {
87 status,
88 connect_clicked: false,
89 }
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 cx: &mut ViewContext<Self>,
100 ) -> impl IntoElement {
101 let copied = cx
102 .read_from_clipboard()
103 .map(|item| item.text() == &data.user_code)
104 .unwrap_or(false);
105 h_stack()
106 .cursor_pointer()
107 .justify_between()
108 .on_mouse_down(gpui::MouseButton::Left, {
109 let user_code = data.user_code.clone();
110 move |_, cx| {
111 cx.write_to_clipboard(ClipboardItem::new(user_code.clone()));
112 cx.notify();
113 }
114 })
115 .child(Label::new(data.user_code.clone()))
116 .child(div())
117 .child(Label::new(if copied { "Copied!" } else { "Copy" }))
118 }
119
120 fn render_prompting_modal(
121 connect_clicked: bool,
122 data: &PromptUserDeviceFlow,
123 cx: &mut ViewContext<Self>,
124 ) -> impl Element {
125 let connect_button_label = if connect_clicked {
126 "Waiting for connection..."
127 } else {
128 "Connect to Github"
129 };
130 v_stack()
131 .flex_1()
132 .items_center()
133 .justify_between()
134 .w_full()
135 .child(Label::new(
136 "Enable Copilot by connecting your existing license",
137 ))
138 .child(Self::render_device_code(data, cx))
139 .child(
140 Label::new("Paste this code into GitHub after clicking the button below.")
141 .size(ui::LabelSize::Small),
142 )
143 .child(
144 Button::new("connect-button", connect_button_label).on_click({
145 let verification_uri = data.verification_uri.clone();
146 cx.listener(move |this, _, cx| {
147 cx.open_url(&verification_uri);
148 this.connect_clicked = true;
149 })
150 }),
151 )
152 }
153 fn render_enabled_modal() -> impl Element {
154 v_stack()
155 .child(Label::new("Copilot Enabled!"))
156 .child(Label::new(
157 "You can update your settings or sign out from the Copilot menu in the status bar.",
158 ))
159 .child(
160 Button::new("copilot-enabled-done-button", "Done")
161 .on_click(|_, cx| cx.remove_window()),
162 )
163 }
164
165 fn render_unauthorized_modal() -> impl Element {
166 v_stack()
167 .child(Label::new(
168 "Enable Copilot by connecting your existing license.",
169 ))
170 .child(
171 Label::new("You must have an active Copilot license to use it in Zed.")
172 .color(Color::Warning),
173 )
174 .child(
175 Button::new("copilot-subscribe-button", "Subscibe on Github").on_click(|_, cx| {
176 cx.remove_window();
177 cx.open_url(COPILOT_SIGN_UP_URL)
178 }),
179 )
180 }
181}
182
183impl Render for CopilotCodeVerification {
184 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
185 let prompt = match &self.status {
186 Status::SigningIn {
187 prompt: Some(prompt),
188 } => Self::render_prompting_modal(self.connect_clicked, &prompt, cx).into_any_element(),
189 Status::Unauthorized => {
190 self.connect_clicked = false;
191 Self::render_unauthorized_modal().into_any_element()
192 }
193 Status::Authorized => {
194 self.connect_clicked = false;
195 Self::render_enabled_modal().into_any_element()
196 }
197 _ => div().into_any_element(),
198 };
199 div()
200 .id("copilot code verification")
201 .flex()
202 .flex_col()
203 .size_full()
204 .items_center()
205 .p_10()
206 .bg(cx.theme().colors().element_background)
207 .child(ui::Label::new("Connect Copilot to Zed"))
208 .child(IconElement::new(Icon::ZedXCopilot))
209 .child(prompt)
210 }
211}