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