repl_menu.rs

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