runtime_panel.rs

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