1use crate::{
2 jupyter_settings::{JupyterDockPosition, JupyterSettings},
3 kernels::{kernel_specifications, KernelSpecification},
4 session::{Session, SessionEvent},
5};
6use anyhow::{Context as _, Result};
7use collections::HashMap;
8use editor::{Anchor, Editor, RangeToAnchorExt};
9use futures::StreamExt as _;
10use gpui::{
11 actions, prelude::*, AppContext, AsyncWindowContext, EntityId, EventEmitter, FocusHandle,
12 FocusOutEvent, FocusableView, Subscription, Task, View, WeakView,
13};
14use language::{Language, Point};
15use multi_buffer::MultiBufferRow;
16use project::Fs;
17use settings::{Settings as _, SettingsStore};
18use std::{ops::Range, sync::Arc};
19use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding};
20use util::ResultExt as _;
21use workspace::{
22 dock::{Panel, PanelEvent},
23 Workspace,
24};
25
26actions!(repl, [Run, ClearOutputs, Interrupt, Shutdown]);
27actions!(repl_panel, [ToggleFocus]);
28
29pub fn init(cx: &mut AppContext) {
30 cx.observe_new_views(
31 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
32 workspace.register_action(|workspace, _: &ToggleFocus, cx| {
33 workspace.toggle_panel_focus::<RuntimePanel>(cx);
34 });
35 },
36 )
37 .detach();
38}
39
40pub struct RuntimePanel {
41 fs: Arc<dyn Fs>,
42 enabled: bool,
43 focus_handle: FocusHandle,
44 width: Option<Pixels>,
45 sessions: HashMap<EntityId, View<Session>>,
46 kernel_specifications: Vec<KernelSpecification>,
47 _subscriptions: Vec<Subscription>,
48 _editor_events_task: Task<()>,
49}
50
51pub enum ReplEvent {
52 Run(WeakView<Editor>),
53 ClearOutputs(WeakView<Editor>),
54 Interrupt(WeakView<Editor>),
55 Shutdown(WeakView<Editor>),
56}
57
58impl RuntimePanel {
59 pub fn load(
60 workspace: WeakView<Workspace>,
61 cx: AsyncWindowContext,
62 ) -> Task<Result<View<Self>>> {
63 cx.spawn(|mut cx| async move {
64 let view = workspace.update(&mut cx, |workspace, cx| {
65 cx.new_view::<Self>(|cx| {
66 let focus_handle = cx.focus_handle();
67
68 let fs = workspace.app_state().fs.clone();
69
70 // Make a channel that we receive editor events on (for repl::Run, repl::ClearOutputs)
71 // This allows us to inject actions on the editor from the repl panel without requiring the editor to
72 // depend on the `repl` crate.
73 let (repl_editor_event_tx, mut repl_editor_event_rx) =
74 futures::channel::mpsc::unbounded::<ReplEvent>();
75
76 let subscriptions = vec![
77 cx.on_focus_in(&focus_handle, Self::focus_in),
78 cx.on_focus_out(&focus_handle, Self::focus_out),
79 cx.observe_global::<SettingsStore>(move |this, cx| {
80 this.set_enabled(JupyterSettings::enabled(cx), cx);
81 }),
82 cx.observe_new_views(
83 move |editor: &mut Editor, cx: &mut ViewContext<Editor>| {
84 let editor_view = cx.view().downgrade();
85 let run_event_tx = repl_editor_event_tx.clone();
86 let clear_event_tx = repl_editor_event_tx.clone();
87 editor
88 .register_action(move |_: &Run, cx: &mut WindowContext| {
89 if !JupyterSettings::enabled(cx) {
90 return;
91 }
92 run_event_tx
93 .unbounded_send(ReplEvent::Run(editor_view.clone()))
94 .ok();
95 })
96 .detach();
97
98 let editor_view = cx.view().downgrade();
99 editor
100 .register_action(
101 move |_: &ClearOutputs, cx: &mut WindowContext| {
102 if !JupyterSettings::enabled(cx) {
103 return;
104 }
105 clear_event_tx
106 .unbounded_send(ReplEvent::ClearOutputs(
107 editor_view.clone(),
108 ))
109 .ok();
110 },
111 )
112 .detach();
113
114 editor
115 .register_action({
116 let editor = cx.view().downgrade();
117 let repl_editor_event_tx = repl_editor_event_tx.clone();
118
119 move |_: &Interrupt, cx: &mut WindowContext| {
120 if !JupyterSettings::enabled(cx) {
121 return;
122 }
123 repl_editor_event_tx
124 .unbounded_send(ReplEvent::Interrupt(
125 editor.clone(),
126 ))
127 .ok();
128 }
129 })
130 .detach();
131
132 editor
133 .register_action({
134 let editor = cx.view().downgrade();
135 let repl_editor_event_tx = repl_editor_event_tx.clone();
136
137 move |_: &Shutdown, cx: &mut WindowContext| {
138 if !JupyterSettings::enabled(cx) {
139 return;
140 }
141 repl_editor_event_tx
142 .unbounded_send(ReplEvent::Shutdown(editor.clone()))
143 .ok();
144 }
145 })
146 .detach();
147 },
148 ),
149 ];
150
151 // Listen for events from the editor on the `repl_editor_event_rx` channel
152 let _editor_events_task = cx.spawn(
153 move |this: WeakView<RuntimePanel>, mut cx: AsyncWindowContext| async move {
154 while let Some(event) = repl_editor_event_rx.next().await {
155 this.update(&mut cx, |runtime_panel, cx| match event {
156 ReplEvent::Run(editor) => {
157 runtime_panel.run(editor, cx).log_err();
158 }
159 ReplEvent::ClearOutputs(editor) => {
160 runtime_panel.clear_outputs(editor, cx);
161 }
162 ReplEvent::Interrupt(editor) => {
163 runtime_panel.interrupt(editor, cx);
164 }
165 ReplEvent::Shutdown(editor) => {
166 runtime_panel.shutdown(editor, cx);
167 }
168 })
169 .ok();
170 }
171 },
172 );
173
174 let runtime_panel = Self {
175 fs: fs.clone(),
176 width: None,
177 focus_handle,
178 kernel_specifications: Vec::new(),
179 sessions: Default::default(),
180 _subscriptions: subscriptions,
181 enabled: JupyterSettings::enabled(cx),
182 _editor_events_task,
183 };
184
185 runtime_panel
186 })
187 })?;
188
189 view.update(&mut cx, |this, cx| this.refresh_kernelspecs(cx))?
190 .await?;
191
192 Ok(view)
193 })
194 }
195
196 fn set_enabled(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
197 if self.enabled != enabled {
198 self.enabled = enabled;
199 cx.notify();
200 }
201 }
202
203 fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
204 cx.notify();
205 }
206
207 fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext<Self>) {
208 cx.notify();
209 }
210
211 pub fn snippet(
212 &self,
213 editor: WeakView<Editor>,
214 cx: &mut ViewContext<Self>,
215 ) -> Option<(String, Arc<Language>, Range<Anchor>)> {
216 let editor = editor.upgrade()?;
217 let editor = editor.read(cx);
218
219 let buffer = editor.buffer().read(cx).snapshot(cx);
220
221 let selection = editor.selections.newest::<usize>(cx);
222 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
223
224 let range = if selection.is_empty() {
225 let cursor = selection.head();
226
227 let cursor_row = multi_buffer_snapshot.offset_to_point(cursor).row;
228 let start_offset = multi_buffer_snapshot.point_to_offset(Point::new(cursor_row, 0));
229
230 let end_point = Point::new(
231 cursor_row,
232 multi_buffer_snapshot.line_len(MultiBufferRow(cursor_row)),
233 );
234 let end_offset = start_offset.saturating_add(end_point.column as usize);
235
236 // Create a range from the start to the end of the line
237 start_offset..end_offset
238 } else {
239 selection.range()
240 };
241
242 let anchor_range = range.to_anchors(&multi_buffer_snapshot);
243
244 let selected_text = buffer
245 .text_for_range(anchor_range.clone())
246 .collect::<String>();
247
248 let start_language = buffer.language_at(anchor_range.start)?;
249 let end_language = buffer.language_at(anchor_range.end)?;
250 if start_language != end_language {
251 return None;
252 }
253
254 Some((selected_text, start_language.clone(), anchor_range))
255 }
256
257 pub fn language(
258 &self,
259 editor: WeakView<Editor>,
260 cx: &mut ViewContext<Self>,
261 ) -> Option<Arc<Language>> {
262 let editor = editor.upgrade()?;
263 let selection = editor.read(cx).selections.newest::<usize>(cx);
264 let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
265 buffer.language_at(selection.head()).cloned()
266 }
267
268 pub fn refresh_kernelspecs(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
269 let kernel_specifications = kernel_specifications(self.fs.clone());
270 cx.spawn(|this, mut cx| async move {
271 let kernel_specifications = kernel_specifications.await?;
272
273 this.update(&mut cx, |this, cx| {
274 this.kernel_specifications = kernel_specifications;
275 cx.notify();
276 })
277 })
278 }
279
280 pub fn kernelspec(
281 &self,
282 language: &Language,
283 cx: &mut ViewContext<Self>,
284 ) -> Option<KernelSpecification> {
285 let settings = JupyterSettings::get_global(cx);
286 let language_name = language.code_fence_block_name();
287 let selected_kernel = settings.kernel_selections.get(language_name.as_ref());
288
289 self.kernel_specifications
290 .iter()
291 .find(|runtime_specification| {
292 if let Some(selected) = selected_kernel {
293 // Top priority is the selected kernel
294 runtime_specification.name.to_lowercase() == selected.to_lowercase()
295 } else {
296 // Otherwise, we'll try to find a kernel that matches the language
297 runtime_specification.kernelspec.language.to_lowercase()
298 == language_name.to_lowercase()
299 }
300 })
301 .cloned()
302 }
303
304 pub fn run(
305 &mut self,
306 editor: WeakView<Editor>,
307 cx: &mut ViewContext<Self>,
308 ) -> anyhow::Result<()> {
309 if !self.enabled {
310 return Ok(());
311 }
312
313 let (selected_text, language, anchor_range) = match self.snippet(editor.clone(), cx) {
314 Some(snippet) => snippet,
315 None => return Ok(()),
316 };
317
318 let entity_id = editor.entity_id();
319
320 let kernel_specification = self
321 .kernelspec(&language, cx)
322 .with_context(|| format!("No kernel found for language: {}", language.name()))?;
323
324 let session = self.sessions.entry(entity_id).or_insert_with(|| {
325 let view =
326 cx.new_view(|cx| Session::new(editor, self.fs.clone(), kernel_specification, cx));
327 cx.notify();
328
329 let subscription = cx.subscribe(
330 &view,
331 |panel: &mut RuntimePanel, _session: View<Session>, event: &SessionEvent, _cx| {
332 match event {
333 SessionEvent::Shutdown(shutdown_event) => {
334 panel.sessions.remove(&shutdown_event.entity_id());
335 }
336 }
337 },
338 );
339
340 subscription.detach();
341
342 view
343 });
344
345 session.update(cx, |session, cx| {
346 session.execute(&selected_text, anchor_range, cx);
347 });
348
349 anyhow::Ok(())
350 }
351
352 pub fn clear_outputs(&mut self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) {
353 let entity_id = editor.entity_id();
354 if let Some(session) = self.sessions.get_mut(&entity_id) {
355 session.update(cx, |session, cx| {
356 session.clear_outputs(cx);
357 });
358 cx.notify();
359 }
360 }
361
362 pub fn interrupt(&mut self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) {
363 let entity_id = editor.entity_id();
364 if let Some(session) = self.sessions.get_mut(&entity_id) {
365 session.update(cx, |session, cx| {
366 session.interrupt(cx);
367 });
368 cx.notify();
369 }
370 }
371
372 pub fn shutdown(&self, editor: WeakView<Editor>, cx: &mut ViewContext<RuntimePanel>) {
373 let entity_id = editor.entity_id();
374 if let Some(session) = self.sessions.get(&entity_id) {
375 session.update(cx, |session, cx| {
376 session.shutdown(cx);
377 });
378 cx.notify();
379 }
380 }
381}
382
383pub enum SessionSupport {
384 ActiveSession(View<Session>),
385 Inactive(Box<KernelSpecification>),
386 RequiresSetup(Arc<str>),
387 Unsupported,
388}
389
390impl RuntimePanel {
391 pub fn session(
392 &mut self,
393 editor: WeakView<Editor>,
394 cx: &mut ViewContext<Self>,
395 ) -> SessionSupport {
396 let entity_id = editor.entity_id();
397 let session = self.sessions.get(&entity_id).cloned();
398
399 match session {
400 Some(session) => SessionSupport::ActiveSession(session),
401 None => {
402 let language = self.language(editor, cx);
403 let language = match language {
404 Some(language) => language,
405 None => return SessionSupport::Unsupported,
406 };
407 // Check for kernelspec
408 let kernelspec = self.kernelspec(&language, cx);
409
410 match kernelspec {
411 Some(kernelspec) => SessionSupport::Inactive(Box::new(kernelspec)),
412 None => {
413 // If no kernelspec but language is one of typescript or python
414 // then we return RequiresSetup
415 match language.name().as_ref() {
416 "TypeScript" | "Python" => {
417 SessionSupport::RequiresSetup(language.name())
418 }
419 _ => SessionSupport::Unsupported,
420 }
421 }
422 }
423 }
424 }
425 }
426}
427
428impl Panel for RuntimePanel {
429 fn persistent_name() -> &'static str {
430 "RuntimePanel"
431 }
432
433 fn position(&self, cx: &ui::WindowContext) -> workspace::dock::DockPosition {
434 match JupyterSettings::get_global(cx).dock {
435 JupyterDockPosition::Left => workspace::dock::DockPosition::Left,
436 JupyterDockPosition::Right => workspace::dock::DockPosition::Right,
437 JupyterDockPosition::Bottom => workspace::dock::DockPosition::Bottom,
438 }
439 }
440
441 fn position_is_valid(&self, _position: workspace::dock::DockPosition) -> bool {
442 true
443 }
444
445 fn set_position(
446 &mut self,
447 position: workspace::dock::DockPosition,
448 cx: &mut ViewContext<Self>,
449 ) {
450 settings::update_settings_file::<JupyterSettings>(self.fs.clone(), cx, move |settings| {
451 let dock = match position {
452 workspace::dock::DockPosition::Left => JupyterDockPosition::Left,
453 workspace::dock::DockPosition::Right => JupyterDockPosition::Right,
454 workspace::dock::DockPosition::Bottom => JupyterDockPosition::Bottom,
455 };
456 settings.set_dock(dock);
457 })
458 }
459
460 fn size(&self, cx: &ui::WindowContext) -> Pixels {
461 let settings = JupyterSettings::get_global(cx);
462
463 self.width.unwrap_or(settings.default_width)
464 }
465
466 fn set_size(&mut self, size: Option<ui::Pixels>, _cx: &mut ViewContext<Self>) {
467 self.width = size;
468 }
469
470 fn icon(&self, _cx: &ui::WindowContext) -> Option<ui::IconName> {
471 if !self.enabled {
472 return None;
473 }
474
475 Some(IconName::Code)
476 }
477
478 fn icon_tooltip(&self, _cx: &ui::WindowContext) -> Option<&'static str> {
479 Some("Runtime Panel")
480 }
481
482 fn toggle_action(&self) -> Box<dyn gpui::Action> {
483 Box::new(ToggleFocus)
484 }
485}
486
487impl EventEmitter<PanelEvent> for RuntimePanel {}
488
489impl FocusableView for RuntimePanel {
490 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
491 self.focus_handle.clone()
492 }
493}
494
495impl Render for RuntimePanel {
496 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
497 // When there are no kernel specifications, show a link to the Zed docs explaining how to
498 // install kernels. It can be assumed they don't have a running kernel if we have no
499 // specifications.
500 if self.kernel_specifications.is_empty() {
501 return v_flex()
502 .p_4()
503 .size_full()
504 .gap_2()
505 .child(Label::new("No Jupyter Kernels Available").size(LabelSize::Large))
506 .child(
507 Label::new("To start interactively running code in your editor, you need to install and configure Jupyter kernels.")
508 .size(LabelSize::Default),
509 )
510 .child(
511 h_flex().w_full().p_4().justify_center().gap_2().child(
512 ButtonLike::new("install-kernels")
513 .style(ButtonStyle::Filled)
514 .size(ButtonSize::Large)
515 .layer(ElevationIndex::ModalSurface)
516 .child(Label::new("Install Kernels"))
517 .on_click(move |_, cx| {
518 cx.open_url(
519 "https://docs.jupyter.org/en/latest/install/kernels.html",
520 )
521 }),
522 ),
523 )
524 .into_any_element();
525 }
526
527 // When there are no sessions, show the command to run code in an editor
528 if self.sessions.is_empty() {
529 return v_flex()
530 .p_4()
531 .size_full()
532 .gap_2()
533 .child(Label::new("No Jupyter Kernel Sessions").size(LabelSize::Large))
534 .child(
535 v_flex().child(
536 Label::new("To run code in a Jupyter kernel, select some code and use the 'repl::Run' command.")
537 .size(LabelSize::Default)
538 )
539 .children(
540 KeyBinding::for_action(&Run, cx)
541 .map(|binding|
542 binding.into_any_element()
543 )
544 )
545 )
546
547 .into_any_element();
548 }
549
550 v_flex()
551 .p_4()
552 .child(Label::new("Jupyter Kernel Sessions").size(LabelSize::Large))
553 .children(
554 self.sessions
555 .values()
556 .map(|session| session.clone().into_any_element()),
557 )
558 .into_any_element()
559 }
560}