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