repl_menu.rs

  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}