1use std::time::Duration;
2
3use gpui::{percentage, Animation, AnimationExt, AnyElement, Transformation, View};
4use repl::{
5 ExecutionState, JupyterSettings, Kernel, KernelSpecification, KernelStatus, Session,
6 SessionSupport,
7};
8use ui::{
9 prelude::*, ButtonLike, ContextMenu, IconWithIndicator, Indicator, IntoElement, PopoverMenu,
10 Tooltip,
11};
12
13use gpui::ElementId;
14use util::ResultExt;
15
16use crate::QuickActionBar;
17
18const ZED_REPL_DOCUMENTATION: &str = "https://zed.dev/docs/repl";
19
20struct ReplMenuState {
21 tooltip: SharedString,
22 icon: IconName,
23 icon_color: Color,
24 icon_is_animating: bool,
25 popover_disabled: bool,
26 indicator: Option<Indicator>,
27
28 status: KernelStatus,
29 kernel_name: SharedString,
30 kernel_language: SharedString,
31 // TODO: Persist rotation state so the
32 // icon doesn't reset on every state change
33 // current_delta: Duration,
34}
35
36impl QuickActionBar {
37 pub fn render_repl_menu(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
38 if !JupyterSettings::enabled(cx) {
39 return None;
40 }
41
42 let editor = self.active_editor()?;
43
44 let has_nonempty_selection = {
45 editor.update(cx, |this, cx| {
46 this.selections
47 .count()
48 .ne(&0)
49 .then(|| {
50 let latest = this.selections.newest_display(cx);
51 !latest.is_empty()
52 })
53 .unwrap_or_default()
54 })
55 };
56
57 let session = repl::session(editor.downgrade(), cx);
58 let session = match session {
59 SessionSupport::ActiveSession(session) => session,
60 SessionSupport::Inactive(spec) => {
61 let spec = *spec;
62 return self.render_repl_launch_menu(spec, cx);
63 }
64 SessionSupport::RequiresSetup(language) => {
65 return self.render_repl_setup(&language.0, cx);
66 }
67 SessionSupport::Unsupported => return None,
68 };
69
70 let menu_state = session_state(session.clone(), cx);
71
72 let id = "repl-menu".to_string();
73
74 let element_id = |suffix| ElementId::Name(format!("{}-{}", id, suffix).into());
75
76 let editor = editor.downgrade();
77 let dropdown_menu = PopoverMenu::new(element_id("menu"))
78 .menu(move |cx| {
79 let editor = editor.clone();
80 let session = session.clone();
81 ContextMenu::build(cx, move |menu, cx| {
82 let menu_state = session_state(session, cx);
83 let status = menu_state.status;
84 let editor = editor.clone();
85
86 menu.map(|menu| {
87 if status.is_connected() {
88 let status = status.clone();
89 menu.custom_row(move |_cx| {
90 h_flex()
91 .child(
92 Label::new(format!(
93 "kernel: {} ({})",
94 menu_state.kernel_name.clone(),
95 menu_state.kernel_language.clone()
96 ))
97 .size(LabelSize::Small)
98 .color(Color::Muted),
99 )
100 .into_any_element()
101 })
102 .custom_row(move |_cx| {
103 h_flex()
104 .child(
105 Label::new(status.clone().to_string())
106 .size(LabelSize::Small)
107 .color(Color::Muted),
108 )
109 .into_any_element()
110 })
111 } else {
112 let status = status.clone();
113 menu.custom_row(move |_cx| {
114 h_flex()
115 .child(
116 Label::new(format!("{}...", status.clone().to_string()))
117 .size(LabelSize::Small)
118 .color(Color::Muted),
119 )
120 .into_any_element()
121 })
122 }
123 })
124 .separator()
125 .custom_entry(
126 move |_cx| {
127 Label::new(if has_nonempty_selection {
128 "Run Selection"
129 } else {
130 "Run Line"
131 })
132 .into_any_element()
133 },
134 {
135 let editor = editor.clone();
136 move |cx| {
137 repl::run(editor.clone(), true, cx).log_err();
138 }
139 },
140 )
141 .custom_entry(
142 move |_cx| {
143 Label::new("Interrupt")
144 .size(LabelSize::Small)
145 .color(Color::Error)
146 .into_any_element()
147 },
148 {
149 let editor = editor.clone();
150 move |cx| {
151 repl::interrupt(editor.clone(), cx);
152 }
153 },
154 )
155 .custom_entry(
156 move |_cx| {
157 Label::new("Clear Outputs")
158 .size(LabelSize::Small)
159 .color(Color::Muted)
160 .into_any_element()
161 },
162 {
163 let editor = editor.clone();
164 move |cx| {
165 repl::clear_outputs(editor.clone(), cx);
166 }
167 },
168 )
169 .separator()
170 .link(
171 "Change Kernel",
172 Box::new(zed_actions::OpenBrowser {
173 url: format!("{}#change-kernel", ZED_REPL_DOCUMENTATION),
174 }),
175 )
176 .custom_entry(
177 move |_cx| {
178 Label::new("Shut Down Kernel")
179 .size(LabelSize::Small)
180 .color(Color::Error)
181 .into_any_element()
182 },
183 {
184 let editor = editor.clone();
185 move |cx| {
186 repl::shutdown(editor.clone(), cx);
187 }
188 },
189 )
190 .custom_entry(
191 move |_cx| {
192 Label::new("Restart Kernel")
193 .size(LabelSize::Small)
194 .color(Color::Error)
195 .into_any_element()
196 },
197 {
198 let editor = editor.clone();
199 move |cx| {
200 repl::restart(editor.clone(), cx);
201 }
202 },
203 )
204 .separator()
205 .action("View Sessions", Box::new(repl::Sessions))
206 // TODO: Add shut down all kernels action
207 // .action("Shut Down all Kernels", Box::new(gpui::NoAction))
208 })
209 .into()
210 })
211 .trigger(
212 ButtonLike::new_rounded_right(element_id("dropdown"))
213 .child(
214 Icon::new(IconName::ChevronDownSmall)
215 .size(IconSize::XSmall)
216 .color(Color::Muted),
217 )
218 .tooltip(move |cx| Tooltip::text("REPL Menu", cx))
219 .width(rems(1.).into())
220 .disabled(menu_state.popover_disabled),
221 );
222
223 let button = ButtonLike::new_rounded_left("toggle_repl_icon")
224 .child(if menu_state.icon_is_animating {
225 Icon::new(menu_state.icon)
226 .color(menu_state.icon_color)
227 .with_animation(
228 "arrow-circle",
229 Animation::new(Duration::from_secs(5)).repeat(),
230 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
231 )
232 .into_any_element()
233 } else {
234 IconWithIndicator::new(
235 Icon::new(IconName::ReplNeutral).color(menu_state.icon_color),
236 menu_state.indicator,
237 )
238 .indicator_border_color(Some(cx.theme().colors().toolbar_background))
239 .into_any_element()
240 })
241 .size(ButtonSize::Compact)
242 .style(ButtonStyle::Subtle)
243 .tooltip(move |cx| Tooltip::text(menu_state.tooltip.clone(), cx))
244 .on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {})))
245 .into_any_element();
246
247 Some(
248 h_flex()
249 .child(button)
250 .child(dropdown_menu)
251 .into_any_element(),
252 )
253 }
254
255 pub fn render_repl_launch_menu(
256 &self,
257 kernel_specification: KernelSpecification,
258 _cx: &mut ViewContext<Self>,
259 ) -> Option<AnyElement> {
260 let tooltip: SharedString =
261 SharedString::from(format!("Start REPL for {}", kernel_specification.name));
262
263 Some(
264 IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
265 .size(ButtonSize::Compact)
266 .icon_color(Color::Muted)
267 .style(ButtonStyle::Subtle)
268 .tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
269 .on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {})))
270 .into_any_element(),
271 )
272 }
273
274 pub fn render_repl_setup(
275 &self,
276 language: &str,
277 _cx: &mut ViewContext<Self>,
278 ) -> Option<AnyElement> {
279 let tooltip: SharedString = SharedString::from(format!("Setup Zed REPL for {}", language));
280 Some(
281 IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
282 .size(ButtonSize::Compact)
283 .icon_color(Color::Muted)
284 .style(ButtonStyle::Subtle)
285 .tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
286 .on_click(|_, cx| cx.open_url(&format!("{}#installation", ZED_REPL_DOCUMENTATION)))
287 .into_any_element(),
288 )
289 }
290}
291
292fn session_state(session: View<Session>, cx: &WindowContext) -> ReplMenuState {
293 let session = session.read(cx);
294
295 let kernel_name: SharedString = session.kernel_specification.name.clone().into();
296 let kernel_language: SharedString = session
297 .kernel_specification
298 .kernelspec
299 .language
300 .clone()
301 .into();
302
303 let fill_fields = || {
304 ReplMenuState {
305 tooltip: "Nothing running".into(),
306 icon: IconName::ReplNeutral,
307 icon_color: Color::Default,
308 icon_is_animating: false,
309 popover_disabled: false,
310 indicator: None,
311 kernel_name: kernel_name.clone(),
312 kernel_language: kernel_language.clone(),
313 // todo!(): Technically not shutdown, but indeterminate
314 status: KernelStatus::Shutdown,
315 // current_delta: Duration::default(),
316 }
317 };
318
319 match &session.kernel {
320 Kernel::Restarting => ReplMenuState {
321 tooltip: format!("Restarting {}", kernel_name).into(),
322 icon_is_animating: true,
323 popover_disabled: true,
324 icon_color: Color::Muted,
325 indicator: Some(Indicator::dot().color(Color::Muted)),
326 status: session.kernel.status(),
327 ..fill_fields()
328 },
329 Kernel::RunningKernel(kernel) => match &kernel.execution_state {
330 ExecutionState::Idle => ReplMenuState {
331 tooltip: format!("Run code on {} ({})", kernel_name, kernel_language).into(),
332 indicator: Some(Indicator::dot().color(Color::Success)),
333 status: session.kernel.status(),
334 ..fill_fields()
335 },
336 ExecutionState::Busy => ReplMenuState {
337 tooltip: format!("Interrupt {} ({})", kernel_name, kernel_language).into(),
338 icon_is_animating: true,
339 popover_disabled: false,
340 indicator: None,
341 status: session.kernel.status(),
342 ..fill_fields()
343 },
344 },
345 Kernel::StartingKernel(_) => ReplMenuState {
346 tooltip: format!("{} is starting", kernel_name).into(),
347 icon_is_animating: true,
348 popover_disabled: true,
349 icon_color: Color::Muted,
350 indicator: Some(Indicator::dot().color(Color::Muted)),
351 status: session.kernel.status(),
352 ..fill_fields()
353 },
354 Kernel::ErroredLaunch(e) => ReplMenuState {
355 tooltip: format!("Error with kernel {}: {}", kernel_name, e).into(),
356 popover_disabled: false,
357 indicator: Some(Indicator::dot().color(Color::Error)),
358 status: session.kernel.status(),
359 ..fill_fields()
360 },
361 Kernel::ShuttingDown => ReplMenuState {
362 tooltip: format!("{} is shutting down", kernel_name).into(),
363 popover_disabled: true,
364 icon_color: Color::Muted,
365 indicator: Some(Indicator::dot().color(Color::Muted)),
366 status: session.kernel.status(),
367 ..fill_fields()
368 },
369 Kernel::Shutdown => ReplMenuState {
370 tooltip: "Nothing running".into(),
371 icon: IconName::ReplNeutral,
372 icon_color: Color::Default,
373 icon_is_animating: false,
374 popover_disabled: false,
375 indicator: None,
376 status: KernelStatus::Shutdown,
377 ..fill_fields()
378 },
379 }
380}