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