repl_menu.rs

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