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