runtime_panel.rs

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