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