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 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}