1use copilot::{request::PromptUserDeviceFlow, Copilot, Status};
2use gpui::{
3 div, size, svg, AppContext, Bounds, ClipboardItem, Element, GlobalPixels, InteractiveElement,
4 IntoElement, ParentElement, Point, Render, Styled, ViewContext, VisualContext, WindowBounds,
5 WindowHandle, WindowKind, WindowOptions,
6};
7use ui::{prelude::*, Button, Icon, Label};
8
9const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
10
11pub fn init(cx: &mut AppContext) {
12 if let Some(copilot) = Copilot::global(cx) {
13 let mut verification_window: Option<WindowHandle<CopilotCodeVerification>> = None;
14 cx.observe(&copilot, move |copilot, cx| {
15 let status = copilot.read(cx).status();
16
17 match &status {
18 crate::Status::SigningIn { prompt } => {
19 if let Some(window) = verification_window.as_mut() {
20 let updated = window
21 .update(cx, |verification, cx| {
22 verification.set_status(status.clone(), cx);
23 cx.activate_window();
24 })
25 .is_ok();
26 if !updated {
27 verification_window = Some(create_copilot_auth_window(cx, &status));
28 }
29 } else if let Some(_prompt) = prompt {
30 verification_window = Some(create_copilot_auth_window(cx, &status));
31 }
32 }
33 Status::Authorized | Status::Unauthorized => {
34 if let Some(window) = verification_window.as_ref() {
35 window
36 .update(cx, |verification, cx| {
37 verification.set_status(status, cx);
38 cx.activate(true);
39 cx.activate_window();
40 })
41 .ok();
42 }
43 }
44 _ => {
45 if let Some(code_verification) = verification_window.take() {
46 code_verification
47 .update(cx, |_, cx| cx.remove_window())
48 .ok();
49 }
50 }
51 }
52 })
53 .detach();
54 }
55}
56
57fn create_copilot_auth_window(
58 cx: &mut AppContext,
59 status: &Status,
60) -> WindowHandle<CopilotCodeVerification> {
61 let window_size = size(GlobalPixels::from(400.), GlobalPixels::from(480.));
62 let window_options = WindowOptions {
63 bounds: WindowBounds::Fixed(Bounds::new(Point::default(), window_size)),
64 titlebar: None,
65 center: true,
66 focus: true,
67 show: true,
68 kind: WindowKind::PopUp,
69 is_movable: true,
70 display_id: None,
71 };
72 let window = cx.open_window(window_options, |cx| {
73 cx.new_view(|_| CopilotCodeVerification::new(status.clone()))
74 });
75 window
76}
77
78pub struct CopilotCodeVerification {
79 status: Status,
80 connect_clicked: bool,
81}
82
83//impl ModalView for CopilotCodeVerification {}
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 .gap_2()
133 .items_center()
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)
145 .on_click({
146 let verification_uri = data.verification_uri.clone();
147 cx.listener(move |this, _, cx| {
148 cx.open_url(&verification_uri);
149 this.connect_clicked = true;
150 })
151 })
152 .full_width()
153 .style(ButtonStyle::Filled),
154 )
155 }
156 fn render_enabled_modal() -> impl Element {
157 v_stack()
158 .child(Label::new("Copilot Enabled!"))
159 .child(Label::new(
160 "You can update your settings or sign out from the Copilot menu in the status bar.",
161 ))
162 .child(
163 Button::new("copilot-enabled-done-button", "Done")
164 .on_click(|_, cx| cx.remove_window()),
165 )
166 }
167
168 fn render_unauthorized_modal() -> impl Element {
169 v_stack()
170 .child(Label::new(
171 "Enable Copilot by connecting your existing license.",
172 ))
173 .child(
174 Label::new("You must have an active Copilot license to use it in Zed.")
175 .color(Color::Warning),
176 )
177 .child(
178 Button::new("copilot-subscribe-button", "Subscibe on Github").on_click(|_, cx| {
179 cx.remove_window();
180 cx.open_url(COPILOT_SIGN_UP_URL)
181 }),
182 )
183 }
184}
185
186impl Render for CopilotCodeVerification {
187 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
188 let prompt = match &self.status {
189 Status::SigningIn {
190 prompt: Some(prompt),
191 } => Self::render_prompting_modal(self.connect_clicked, &prompt, cx).into_any_element(),
192 Status::Unauthorized => {
193 self.connect_clicked = false;
194 Self::render_unauthorized_modal().into_any_element()
195 }
196 Status::Authorized => {
197 self.connect_clicked = false;
198 Self::render_enabled_modal().into_any_element()
199 }
200 _ => div().into_any_element(),
201 };
202
203 v_stack()
204 .id("copilot code verification")
205 .elevation_3(cx)
206 .size_full()
207 .items_center()
208 .p_4()
209 .gap_4()
210 .child(Headline::new("Connect Copilot to Zed").size(HeadlineSize::Large))
211 .child(
212 svg()
213 .w_32()
214 .h_16()
215 .flex_none()
216 .path(Icon::ZedXCopilot.path())
217 .text_color(cx.theme().colors().icon),
218 )
219 .child(prompt)
220 }
221}