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.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                    // TODO: Add Restart action
177                    // .action("Restart", Box::new(gpui::NoAction))
178                    .custom_entry(
179                        move |_cx| {
180                            Label::new("Shut Down Kernel")
181                                .size(LabelSize::Small)
182                                .color(Color::Error)
183                                .into_any_element()
184                        },
185                        {
186                            let editor = editor.clone();
187                            move |cx| {
188                                repl::shutdown(editor.clone(), cx);
189                            }
190                        },
191                    )
192                    .separator()
193                    .action("View Sessions", Box::new(repl::Sessions))
194                    // TODO: Add shut down all kernels action
195                    // .action("Shut Down all Kernels", Box::new(gpui::NoAction))
196                })
197                .into()
198            })
199            .trigger(
200                ButtonLike::new_rounded_right(element_id("dropdown"))
201                    .child(
202                        Icon::new(IconName::ChevronDownSmall)
203                            .size(IconSize::XSmall)
204                            .color(Color::Muted),
205                    )
206                    .tooltip(move |cx| Tooltip::text("REPL Menu", cx))
207                    .width(rems(1.).into())
208                    .disabled(menu_state.popover_disabled),
209            );
210
211        let button = ButtonLike::new_rounded_left("toggle_repl_icon")
212            .child(if menu_state.icon_is_animating {
213                Icon::new(menu_state.icon)
214                    .color(menu_state.icon_color)
215                    .with_animation(
216                        "arrow-circle",
217                        Animation::new(Duration::from_secs(5)).repeat(),
218                        |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
219                    )
220                    .into_any_element()
221            } else {
222                IconWithIndicator::new(
223                    Icon::new(IconName::ReplNeutral).color(menu_state.icon_color),
224                    menu_state.indicator,
225                )
226                .indicator_border_color(Some(cx.theme().colors().toolbar_background))
227                .into_any_element()
228            })
229            .size(ButtonSize::Compact)
230            .style(ButtonStyle::Subtle)
231            .tooltip(move |cx| Tooltip::text(menu_state.tooltip.clone(), cx))
232            .on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {})))
233            .into_any_element();
234
235        Some(
236            h_flex()
237                .child(button)
238                .child(dropdown_menu)
239                .into_any_element(),
240        )
241    }
242
243    pub fn render_repl_launch_menu(
244        &self,
245        kernel_specification: KernelSpecification,
246        _cx: &mut ViewContext<Self>,
247    ) -> Option<AnyElement> {
248        let tooltip: SharedString =
249            SharedString::from(format!("Start REPL for {}", kernel_specification.name));
250
251        Some(
252            IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
253                .size(ButtonSize::Compact)
254                .icon_color(Color::Muted)
255                .style(ButtonStyle::Subtle)
256                .tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
257                .on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {})))
258                .into_any_element(),
259        )
260    }
261
262    pub fn render_repl_setup(
263        &self,
264        language: &str,
265        _cx: &mut ViewContext<Self>,
266    ) -> Option<AnyElement> {
267        let tooltip: SharedString = SharedString::from(format!("Setup Zed REPL for {}", language));
268        Some(
269            IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
270                .size(ButtonSize::Compact)
271                .icon_color(Color::Muted)
272                .style(ButtonStyle::Subtle)
273                .tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
274                .on_click(|_, cx| cx.open_url(&format!("{}#installation", ZED_REPL_DOCUMENTATION)))
275                .into_any_element(),
276        )
277    }
278}
279
280fn session_state(session: View<Session>, cx: &WindowContext) -> ReplMenuState {
281    let session = session.read(cx);
282
283    let kernel_name: SharedString = session.kernel_specification.name.clone().into();
284    let kernel_language: SharedString = session
285        .kernel_specification
286        .kernelspec
287        .language
288        .clone()
289        .into();
290
291    let fill_fields = || {
292        ReplMenuState {
293            tooltip: "Nothing running".into(),
294            icon: IconName::ReplNeutral,
295            icon_color: Color::Default,
296            icon_is_animating: false,
297            popover_disabled: false,
298            indicator: None,
299            kernel_name: kernel_name.clone(),
300            kernel_language: kernel_language.clone(),
301            // todo!(): Technically not shutdown, but indeterminate
302            status: KernelStatus::Shutdown,
303            // current_delta: Duration::default(),
304        }
305    };
306
307    let menu_state = match &session.kernel {
308        Kernel::RunningKernel(kernel) => match &kernel.execution_state {
309            ExecutionState::Idle => ReplMenuState {
310                tooltip: format!("Run code on {} ({})", kernel_name, kernel_language).into(),
311                indicator: Some(Indicator::dot().color(Color::Success)),
312                status: session.kernel.status(),
313                ..fill_fields()
314            },
315            ExecutionState::Busy => ReplMenuState {
316                tooltip: format!("Interrupt {} ({})", kernel_name, kernel_language).into(),
317                icon_is_animating: true,
318                popover_disabled: false,
319                indicator: None,
320                status: session.kernel.status(),
321                ..fill_fields()
322            },
323        },
324        Kernel::StartingKernel(_) => ReplMenuState {
325            tooltip: format!("{} is starting", kernel_name).into(),
326            icon_is_animating: true,
327            popover_disabled: true,
328            icon_color: Color::Muted,
329            indicator: Some(Indicator::dot().color(Color::Muted)),
330            status: session.kernel.status(),
331            ..fill_fields()
332        },
333        Kernel::ErroredLaunch(e) => ReplMenuState {
334            tooltip: format!("Error with kernel {}: {}", kernel_name, e).into(),
335            popover_disabled: false,
336            indicator: Some(Indicator::dot().color(Color::Error)),
337            status: session.kernel.status(),
338            ..fill_fields()
339        },
340        Kernel::ShuttingDown => ReplMenuState {
341            tooltip: format!("{} is shutting down", kernel_name).into(),
342            popover_disabled: true,
343            icon_color: Color::Muted,
344            indicator: Some(Indicator::dot().color(Color::Muted)),
345            status: session.kernel.status(),
346            ..fill_fields()
347        },
348        Kernel::Shutdown => ReplMenuState {
349            tooltip: "Nothing running".into(),
350            icon: IconName::ReplNeutral,
351            icon_color: Color::Default,
352            icon_is_animating: false,
353            popover_disabled: false,
354            indicator: None,
355            status: KernelStatus::Shutdown,
356            ..fill_fields()
357        },
358    };
359
360    menu_state
361}