repl_menu.rs

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