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