repl: Factor out `ReplStore` (#14970)

Marshall Bowers created

This PR factors a `ReplStore` out of the `RuntimePanel`.

Since we're planning to remove the `RuntimePanel` and replace it with an
ephemeral tab that can be opened, we need the kernel specifications and
sessions to have somewhere long-lived that they can reside in.

Release Notes:

- N/A

Change summary

crates/repl/src/kernels.rs       |  20 +-
crates/repl/src/repl.rs          |  10 
crates/repl/src/repl_store.rs    | 118 +++++++++++++
crates/repl/src/runtime_panel.rs | 295 ++++++++++++++-------------------
crates/zed/src/main.rs           |   2 
crates/zed/src/zed.rs            |   2 
6 files changed, 263 insertions(+), 184 deletions(-)

Detailed changes

crates/repl/src/kernels.rs 🔗

@@ -29,7 +29,7 @@ pub struct KernelSpecification {
 
 impl KernelSpecification {
     #[must_use]
-    fn command(&self, connection_path: &PathBuf) -> anyhow::Result<Command> {
+    fn command(&self, connection_path: &PathBuf) -> Result<Command> {
         let argv = &self.kernelspec.argv;
 
         anyhow::ensure!(!argv.is_empty(), "Empty argv in kernelspec {}", self.name);
@@ -60,7 +60,7 @@ impl KernelSpecification {
 
 // Find a set of open ports. This creates a listener with port set to 0. The listener will be closed at the end when it goes out of scope.
 // There's a race condition between closing the ports and usage by a kernel, but it's inherent to the Jupyter protocol.
-async fn peek_ports(ip: IpAddr) -> anyhow::Result<[u16; 5]> {
+async fn peek_ports(ip: IpAddr) -> Result<[u16; 5]> {
     let mut addr_zeroport: SocketAddr = SocketAddr::new(ip, 0);
     addr_zeroport.set_port(0);
     let mut ports: [u16; 5] = [0; 5];
@@ -166,10 +166,10 @@ impl Kernel {
 
 pub struct RunningKernel {
     pub process: smol::process::Child,
-    _shell_task: Task<anyhow::Result<()>>,
-    _iopub_task: Task<anyhow::Result<()>>,
-    _control_task: Task<anyhow::Result<()>>,
-    _routing_task: Task<anyhow::Result<()>>,
+    _shell_task: Task<Result<()>>,
+    _iopub_task: Task<Result<()>>,
+    _control_task: Task<Result<()>>,
+    _routing_task: Task<Result<()>>,
     connection_path: PathBuf,
     pub working_directory: PathBuf,
     pub request_tx: mpsc::Sender<JupyterMessage>,
@@ -194,7 +194,7 @@ impl RunningKernel {
         working_directory: PathBuf,
         fs: Arc<dyn Fs>,
         cx: &mut AppContext,
-    ) -> Task<anyhow::Result<(Self, JupyterMessageChannel)>> {
+    ) -> Task<Result<(Self, JupyterMessageChannel)>> {
         cx.spawn(|cx| async move {
             let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
             let ports = peek_ports(ip).await?;
@@ -332,7 +332,7 @@ async fn read_kernelspec_at(
     // /usr/local/share/jupyter/kernels/python3
     kernel_dir: PathBuf,
     fs: &dyn Fs,
-) -> anyhow::Result<KernelSpecification> {
+) -> Result<KernelSpecification> {
     let path = kernel_dir;
     let kernel_name = if let Some(kernel_name) = path.file_name() {
         kernel_name.to_string_lossy().to_string()
@@ -356,7 +356,7 @@ async fn read_kernelspec_at(
 }
 
 /// Read a directory of kernelspec directories
-async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> anyhow::Result<Vec<KernelSpecification>> {
+async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> Result<Vec<KernelSpecification>> {
     let mut kernelspec_dirs = fs.read_dir(&path).await?;
 
     let mut valid_kernelspecs = Vec::new();
@@ -376,7 +376,7 @@ async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> anyhow::Result<Vec<Kern
     Ok(valid_kernelspecs)
 }
 
-pub async fn kernel_specifications(fs: Arc<dyn Fs>) -> anyhow::Result<Vec<KernelSpecification>> {
+pub async fn kernel_specifications(fs: Arc<dyn Fs>) -> Result<Vec<KernelSpecification>> {
     let data_dirs = dirs::data_dirs();
     let kernel_dirs = data_dirs
         .iter()

crates/repl/src/repl.rs 🔗

@@ -1,11 +1,13 @@
 use async_dispatcher::{set_dispatcher, Dispatcher, Runnable};
 use gpui::{AppContext, PlatformDispatcher};
+use project::Fs;
 use settings::Settings as _;
 use std::{sync::Arc, time::Duration};
 
 mod jupyter_settings;
 mod kernels;
 mod outputs;
+mod repl_store;
 mod runtime_panel;
 mod session;
 mod stdio;
@@ -17,6 +19,8 @@ pub use runtime_panel::{RuntimePanel, SessionSupport};
 pub use runtimelib::ExecutionState;
 pub use session::Session;
 
+use crate::repl_store::ReplStore;
+
 fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
     struct ZedDispatcher {
         dispatcher: Arc<dyn PlatformDispatcher>,
@@ -41,8 +45,10 @@ fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
     }
 }
 
-pub fn init(cx: &mut AppContext) {
+pub fn init(fs: Arc<dyn Fs>, cx: &mut AppContext) {
     set_dispatcher(zed_dispatcher(cx));
     JupyterSettings::register(cx);
-    runtime_panel::init(cx)
+    editor::init_settings(cx);
+    runtime_panel::init(cx);
+    ReplStore::init(fs, cx);
 }

crates/repl/src/repl_store.rs 🔗

@@ -0,0 +1,118 @@
+use std::sync::Arc;
+
+use anyhow::Result;
+use collections::HashMap;
+use gpui::{
+    prelude::*, AppContext, EntityId, Global, Model, ModelContext, Subscription, Task, View,
+};
+use language::Language;
+use project::Fs;
+use settings::{Settings, SettingsStore};
+
+use crate::kernels::kernel_specifications;
+use crate::{JupyterSettings, KernelSpecification, Session};
+
+struct GlobalReplStore(Model<ReplStore>);
+
+impl Global for GlobalReplStore {}
+
+pub struct ReplStore {
+    fs: Arc<dyn Fs>,
+    enabled: bool,
+    sessions: HashMap<EntityId, View<Session>>,
+    kernel_specifications: Vec<KernelSpecification>,
+    _subscriptions: Vec<Subscription>,
+}
+
+impl ReplStore {
+    pub(crate) fn init(fs: Arc<dyn Fs>, cx: &mut AppContext) {
+        let store = cx.new_model(move |cx| Self::new(fs, cx));
+
+        cx.set_global(GlobalReplStore(store))
+    }
+
+    pub fn global(cx: &AppContext) -> Model<Self> {
+        cx.global::<GlobalReplStore>().0.clone()
+    }
+
+    pub fn new(fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
+        let subscriptions = vec![cx.observe_global::<SettingsStore>(move |this, cx| {
+            this.set_enabled(JupyterSettings::enabled(cx), cx);
+        })];
+
+        Self {
+            fs,
+            enabled: JupyterSettings::enabled(cx),
+            sessions: HashMap::default(),
+            kernel_specifications: Vec::new(),
+            _subscriptions: subscriptions,
+        }
+    }
+
+    pub fn is_enabled(&self) -> bool {
+        self.enabled
+    }
+
+    pub fn kernel_specifications(&self) -> impl Iterator<Item = &KernelSpecification> {
+        self.kernel_specifications.iter()
+    }
+
+    pub fn sessions(&self) -> impl Iterator<Item = &View<Session>> {
+        self.sessions.values()
+    }
+
+    fn set_enabled(&mut self, enabled: bool, cx: &mut ModelContext<Self>) {
+        if self.enabled != enabled {
+            self.enabled = enabled;
+            cx.notify();
+        }
+    }
+
+    pub fn refresh_kernelspecs(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        let kernel_specifications = kernel_specifications(self.fs.clone());
+        cx.spawn(|this, mut cx| async move {
+            let kernel_specifications = kernel_specifications.await?;
+
+            this.update(&mut cx, |this, cx| {
+                this.kernel_specifications = kernel_specifications;
+                cx.notify();
+            })
+        })
+    }
+
+    pub fn kernelspec(
+        &self,
+        language: &Language,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<KernelSpecification> {
+        let settings = JupyterSettings::get_global(cx);
+        let language_name = language.code_fence_block_name();
+        let selected_kernel = settings.kernel_selections.get(language_name.as_ref());
+
+        self.kernel_specifications
+            .iter()
+            .find(|runtime_specification| {
+                if let Some(selected) = selected_kernel {
+                    // Top priority is the selected kernel
+                    runtime_specification.name.to_lowercase() == selected.to_lowercase()
+                } else {
+                    // Otherwise, we'll try to find a kernel that matches the language
+                    runtime_specification.kernelspec.language.to_lowercase()
+                        == language_name.to_lowercase()
+                }
+            })
+            .cloned()
+    }
+
+    pub fn get_session(&self, entity_id: EntityId) -> Option<&View<Session>> {
+        self.sessions.get(&entity_id)
+    }
+
+    pub fn insert_session(&mut self, entity_id: EntityId, session: View<Session>) {
+        self.sessions.insert(entity_id, session);
+    }
+
+    pub fn remove_session(&mut self, entity_id: EntityId) {
+        self.sessions.remove(&entity_id);
+    }
+}

crates/repl/src/runtime_panel.rs 🔗

@@ -1,19 +1,19 @@
+use crate::repl_store::ReplStore;
 use crate::{
     jupyter_settings::{JupyterDockPosition, JupyterSettings},
-    kernels::{kernel_specifications, KernelSpecification},
+    kernels::KernelSpecification,
     session::{Session, SessionEvent},
 };
 use anyhow::{Context as _, Result};
-use collections::HashMap;
 use editor::{Anchor, Editor, RangeToAnchorExt};
 use gpui::{
-    actions, prelude::*, AppContext, AsyncWindowContext, EntityId, EventEmitter, FocusHandle,
-    FocusOutEvent, FocusableView, Subscription, Task, View, WeakView,
+    actions, prelude::*, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusOutEvent,
+    FocusableView, Subscription, Task, View, WeakView,
 };
 use language::{Language, Point};
 use multi_buffer::MultiBufferRow;
 use project::Fs;
-use settings::{Settings as _, SettingsStore};
+use settings::Settings as _;
 use std::{ops::Range, sync::Arc};
 use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding};
 use util::ResultExt as _;
@@ -28,6 +28,13 @@ actions!(
 );
 actions!(repl_panel, [ToggleFocus]);
 
+pub enum SessionSupport {
+    ActiveSession(View<Session>),
+    Inactive(Box<KernelSpecification>),
+    RequiresSetup(Arc<str>),
+    Unsupported,
+}
+
 pub fn init(cx: &mut AppContext) {
     cx.observe_new_views(
         |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
@@ -35,12 +42,11 @@ pub fn init(cx: &mut AppContext) {
                 workspace.toggle_panel_focus::<RuntimePanel>(cx);
             });
 
-            workspace.register_action(|workspace, _: &RefreshKernelspecs, cx| {
-                if let Some(panel) = workspace.panel::<RuntimePanel>(cx) {
-                    panel.update(cx, |panel, cx| {
-                        panel.refresh_kernelspecs(cx).detach();
-                    });
-                }
+            workspace.register_action(|_workspace, _: &RefreshKernelspecs, cx| {
+                let store = ReplStore::global(cx);
+                store.update(cx, |store, cx| {
+                    store.refresh_kernelspecs(cx).detach();
+                });
             });
         },
     )
@@ -145,11 +151,8 @@ pub fn init(cx: &mut AppContext) {
 
 pub struct RuntimePanel {
     fs: Arc<dyn Fs>,
-    enabled: bool,
     focus_handle: FocusHandle,
     width: Option<Pixels>,
-    sessions: HashMap<EntityId, View<Session>>,
-    kernel_specifications: Vec<KernelSpecification>,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -168,39 +171,29 @@ impl RuntimePanel {
                     let subscriptions = vec![
                         cx.on_focus_in(&focus_handle, Self::focus_in),
                         cx.on_focus_out(&focus_handle, Self::focus_out),
-                        cx.observe_global::<SettingsStore>(move |this, cx| {
-                            this.set_enabled(JupyterSettings::enabled(cx), cx);
-                        }),
                     ];
 
                     let runtime_panel = Self {
-                        fs: fs.clone(),
+                        fs,
                         width: None,
                         focus_handle,
-                        kernel_specifications: Vec::new(),
-                        sessions: Default::default(),
                         _subscriptions: subscriptions,
-                        enabled: JupyterSettings::enabled(cx),
                     };
 
                     runtime_panel
                 })
             })?;
 
-            view.update(&mut cx, |this, cx| this.refresh_kernelspecs(cx))?
-                .await?;
+            view.update(&mut cx, |_panel, cx| {
+                let store = ReplStore::global(cx);
+                store.update(cx, |store, cx| store.refresh_kernelspecs(cx))
+            })?
+            .await?;
 
             Ok(view)
         })
     }
 
-    fn set_enabled(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
-        if self.enabled != enabled {
-            self.enabled = enabled;
-            cx.notify();
-        }
-    }
-
     fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
         cx.notify();
     }
@@ -209,8 +202,7 @@ impl RuntimePanel {
         cx.notify();
     }
 
-    pub fn snippet(
-        &self,
+    fn snippet(
         editor: WeakView<Editor>,
         cx: &mut ViewContext<Self>,
     ) -> Option<(String, Arc<Language>, Range<Anchor>)> {
@@ -255,93 +247,59 @@ impl RuntimePanel {
         Some((selected_text, start_language.clone(), anchor_range))
     }
 
-    pub fn language(
-        &self,
-        editor: WeakView<Editor>,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<Arc<Language>> {
+    fn language(editor: WeakView<Editor>, cx: &mut ViewContext<Self>) -> Option<Arc<Language>> {
         let editor = editor.upgrade()?;
         let selection = editor.read(cx).selections.newest::<usize>(cx);
         let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
         buffer.language_at(selection.head()).cloned()
     }
 
-    pub fn refresh_kernelspecs(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
-        let kernel_specifications = kernel_specifications(self.fs.clone());
-        cx.spawn(|this, mut cx| async move {
-            let kernel_specifications = kernel_specifications.await?;
-
-            this.update(&mut cx, |this, cx| {
-                this.kernel_specifications = kernel_specifications;
-                cx.notify();
-            })
-        })
-    }
+    pub fn run(&mut self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) -> Result<()> {
+        let store = ReplStore::global(cx);
 
-    pub fn kernelspec(
-        &self,
-        language: &Language,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<KernelSpecification> {
-        let settings = JupyterSettings::get_global(cx);
-        let language_name = language.code_fence_block_name();
-        let selected_kernel = settings.kernel_selections.get(language_name.as_ref());
-
-        self.kernel_specifications
-            .iter()
-            .find(|runtime_specification| {
-                if let Some(selected) = selected_kernel {
-                    // Top priority is the selected kernel
-                    runtime_specification.name.to_lowercase() == selected.to_lowercase()
-                } else {
-                    // Otherwise, we'll try to find a kernel that matches the language
-                    runtime_specification.kernelspec.language.to_lowercase()
-                        == language_name.to_lowercase()
-                }
-            })
-            .cloned()
-    }
-
-    pub fn run(
-        &mut self,
-        editor: WeakView<Editor>,
-        cx: &mut ViewContext<Self>,
-    ) -> anyhow::Result<()> {
-        if !self.enabled {
+        if !store.read(cx).is_enabled() {
             return Ok(());
         }
 
-        let (selected_text, language, anchor_range) = match self.snippet(editor.clone(), cx) {
+        let (selected_text, language, anchor_range) = match Self::snippet(editor.clone(), cx) {
             Some(snippet) => snippet,
             None => return Ok(()),
         };
 
         let entity_id = editor.entity_id();
 
-        let kernel_specification = self
-            .kernelspec(&language, cx)
-            .with_context(|| format!("No kernel found for language: {}", language.name()))?;
+        let kernel_specification = store.update(cx, |store, cx| {
+            store
+                .kernelspec(&language, cx)
+                .with_context(|| format!("No kernel found for language: {}", language.name()))
+        })?;
 
-        let session = self.sessions.entry(entity_id).or_insert_with(|| {
-            let view =
+        let session = if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
+            session
+        } else {
+            let session =
                 cx.new_view(|cx| Session::new(editor, self.fs.clone(), kernel_specification, cx));
             cx.notify();
 
-            let subscription = cx.subscribe(
-                &view,
-                |panel: &mut RuntimePanel, _session: View<Session>, event: &SessionEvent, _cx| {
-                    match event {
-                        SessionEvent::Shutdown(shutdown_event) => {
-                            panel.sessions.remove(&shutdown_event.entity_id());
-                        }
+            let subscription = cx.subscribe(&session, {
+                let store = store.clone();
+                move |_this, _session, event, cx| match event {
+                    SessionEvent::Shutdown(shutdown_event) => {
+                        store.update(cx, |store, _cx| {
+                            store.remove_session(shutdown_event.entity_id());
+                        });
                     }
-                },
-            );
+                }
+            });
 
             subscription.detach();
 
-            view
-        });
+            store.update(cx, |store, _cx| {
+                store.insert_session(entity_id, session.clone());
+            });
+
+            session
+        };
 
         session.update(cx, |session, cx| {
             session.execute(&selected_text, anchor_range, cx);
@@ -350,9 +308,38 @@ impl RuntimePanel {
         anyhow::Ok(())
     }
 
+    pub fn session(
+        &mut self,
+        editor: WeakView<Editor>,
+        cx: &mut ViewContext<Self>,
+    ) -> SessionSupport {
+        let store = ReplStore::global(cx);
+        let entity_id = editor.entity_id();
+
+        if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
+            return SessionSupport::ActiveSession(session);
+        };
+
+        let language = Self::language(editor, cx);
+        let language = match language {
+            Some(language) => language,
+            None => return SessionSupport::Unsupported,
+        };
+        let kernelspec = store.update(cx, |store, cx| store.kernelspec(&language, cx));
+
+        match kernelspec {
+            Some(kernelspec) => SessionSupport::Inactive(Box::new(kernelspec)),
+            None => match language.name().as_ref() {
+                "TypeScript" | "Python" => SessionSupport::RequiresSetup(language.name()),
+                _ => SessionSupport::Unsupported,
+            },
+        }
+    }
+
     pub fn clear_outputs(&mut self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) {
+        let store = ReplStore::global(cx);
         let entity_id = editor.entity_id();
-        if let Some(session) = self.sessions.get_mut(&entity_id) {
+        if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
             session.update(cx, |session, cx| {
                 session.clear_outputs(cx);
             });
@@ -361,8 +348,9 @@ impl RuntimePanel {
     }
 
     pub fn interrupt(&mut self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) {
+        let store = ReplStore::global(cx);
         let entity_id = editor.entity_id();
-        if let Some(session) = self.sessions.get_mut(&entity_id) {
+        if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
             session.update(cx, |session, cx| {
                 session.interrupt(cx);
             });
@@ -370,9 +358,10 @@ impl RuntimePanel {
         }
     }
 
-    pub fn shutdown(&self, editor: WeakView<Editor>, cx: &mut ViewContext<RuntimePanel>) {
+    pub fn shutdown(&self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) {
+        let store = ReplStore::global(cx);
         let entity_id = editor.entity_id();
-        if let Some(session) = self.sessions.get(&entity_id) {
+        if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
             session.update(cx, |session, cx| {
                 session.shutdown(cx);
             });
@@ -381,51 +370,6 @@ impl RuntimePanel {
     }
 }
 
-pub enum SessionSupport {
-    ActiveSession(View<Session>),
-    Inactive(Box<KernelSpecification>),
-    RequiresSetup(Arc<str>),
-    Unsupported,
-}
-
-impl RuntimePanel {
-    pub fn session(
-        &mut self,
-        editor: WeakView<Editor>,
-        cx: &mut ViewContext<Self>,
-    ) -> SessionSupport {
-        let entity_id = editor.entity_id();
-        let session = self.sessions.get(&entity_id).cloned();
-
-        match session {
-            Some(session) => SessionSupport::ActiveSession(session),
-            None => {
-                let language = self.language(editor, cx);
-                let language = match language {
-                    Some(language) => language,
-                    None => return SessionSupport::Unsupported,
-                };
-                // Check for kernelspec
-                let kernelspec = self.kernelspec(&language, cx);
-
-                match kernelspec {
-                    Some(kernelspec) => SessionSupport::Inactive(Box::new(kernelspec)),
-                    None => {
-                        // If no kernelspec but language is one of typescript or python
-                        // then we return RequiresSetup
-                        match language.name().as_ref() {
-                            "TypeScript" | "Python" => {
-                                SessionSupport::RequiresSetup(language.name())
-                            }
-                            _ => SessionSupport::Unsupported,
-                        }
-                    }
-                }
-            }
-        }
-    }
-}
-
 impl Panel for RuntimePanel {
     fn persistent_name() -> &'static str {
         "RuntimePanel"
@@ -468,8 +412,10 @@ impl Panel for RuntimePanel {
         self.width = size;
     }
 
-    fn icon(&self, _cx: &ui::WindowContext) -> Option<ui::IconName> {
-        if !self.enabled {
+    fn icon(&self, cx: &ui::WindowContext) -> Option<ui::IconName> {
+        let store = ReplStore::global(cx);
+
+        if !store.read(cx).is_enabled() {
             return None;
         }
 
@@ -495,38 +441,47 @@ impl FocusableView for RuntimePanel {
 
 impl Render for RuntimePanel {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let store = ReplStore::global(cx);
+
+        let (kernel_specifications, sessions) = store.update(cx, |store, _cx| {
+            (
+                store.kernel_specifications().cloned().collect::<Vec<_>>(),
+                store.sessions().cloned().collect::<Vec<_>>(),
+            )
+        });
+
         // When there are no kernel specifications, show a link to the Zed docs explaining how to
         // install kernels. It can be assumed they don't have a running kernel if we have no
         // specifications.
-        if self.kernel_specifications.is_empty() {
+        if kernel_specifications.is_empty() {
             return v_flex()
                 .p_4()
                 .size_full()
                 .gap_2()
-                        .child(Label::new("No Jupyter Kernels Available").size(LabelSize::Large))
-                        .child(
-                            Label::new("To start interactively running code in your editor, you need to install and configure Jupyter kernels.")
-                                .size(LabelSize::Default),
-                        )
-                        .child(
-                            h_flex().w_full().p_4().justify_center().gap_2().child(
-                                ButtonLike::new("install-kernels")
-                                    .style(ButtonStyle::Filled)
-                                    .size(ButtonSize::Large)
-                                    .layer(ElevationIndex::ModalSurface)
-                                    .child(Label::new("Install Kernels"))
-                                    .on_click(move |_, cx| {
-                                        cx.open_url(
-                                        "https://docs.jupyter.org/en/latest/install/kernels.html",
-                                    )
-                                    }),
-                            ),
-                        )
+                .child(Label::new("No Jupyter Kernels Available").size(LabelSize::Large))
+                .child(
+                    Label::new("To start interactively running code in your editor, you need to install and configure Jupyter kernels.")
+                        .size(LabelSize::Default),
+                )
+                .child(
+                    h_flex().w_full().p_4().justify_center().gap_2().child(
+                        ButtonLike::new("install-kernels")
+                            .style(ButtonStyle::Filled)
+                            .size(ButtonSize::Large)
+                            .layer(ElevationIndex::ModalSurface)
+                            .child(Label::new("Install Kernels"))
+                            .on_click(move |_, cx| {
+                                cx.open_url(
+                                    "https://docs.jupyter.org/en/latest/install/kernels.html",
+                                )
+                            }),
+                    ),
+                )
                 .into_any_element();
         }
 
         // When there are no sessions, show the command to run code in an editor
-        if self.sessions.is_empty() {
+        if sessions.is_empty() {
             return v_flex()
                 .p_4()
                 .size_full()
@@ -546,7 +501,7 @@ impl Render for RuntimePanel {
                 )
                 .child(Label::new("Kernels available").size(LabelSize::Large))
                 .children(
-                    self.kernel_specifications.iter().map(|spec| {
+                    kernel_specifications.into_iter().map(|spec| {
                         h_flex().gap_2().child(Label::new(spec.name.clone()))
                             .child(Label::new(spec.kernelspec.language.clone()).color(Color::Muted))
                     })
@@ -559,8 +514,8 @@ impl Render for RuntimePanel {
             .p_4()
             .child(Label::new("Jupyter Kernel Sessions").size(LabelSize::Large))
             .children(
-                self.sessions
-                    .values()
+                sessions
+                    .into_iter()
                     .map(|session| session.clone().into_any_element()),
             )
             .into_any_element()

crates/zed/src/main.rs 🔗

@@ -167,7 +167,7 @@ fn init_common(app_state: Arc<AppState>, cx: &mut AppContext) {
     supermaven::init(app_state.client.clone(), cx);
     inline_completion_registry::init(app_state.client.telemetry().clone(), cx);
     assistant::init(app_state.fs.clone(), app_state.client.clone(), cx);
-    repl::init(cx);
+    repl::init(app_state.fs.clone(), cx);
     extension::init(
         app_state.fs.clone(),
         app_state.client.clone(),

crates/zed/src/zed.rs 🔗

@@ -3417,7 +3417,7 @@ mod tests {
             outline_panel::init((), cx);
             terminal_view::init(cx);
             assistant::init(app_state.fs.clone(), app_state.client.clone(), cx);
-            repl::init(cx);
+            repl::init(app_state.fs.clone(), cx);
             tasks_ui::init(cx);
             initialize_workspace(app_state.clone(), cx);
             app_state