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