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}