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