repl: Replace REPL panel with sessions view (#14981)

Marshall Bowers created

This PR removes the REPL panel and replaces it with a new sessions view
that gets displayed in its own pane.

The sessions view can be opened with the `repl: sessions` command (we
can adjust the name, as needed).

There was a rather in-depth refactoring needed to extricate the various
REPL functionality on the editor from the `RuntimePanel`.

<img width="1136" alt="Screenshot 2024-07-22 at 4 12 12 PM"
src="https://github.com/user-attachments/assets/ac0da351-778e-4200-b08c-39f9e77d78bf">

<img width="1136" alt="Screenshot 2024-07-22 at 4 12 17 PM"
src="https://github.com/user-attachments/assets/6ca53476-6ac4-4f8b-afc8-f7863f7065c7">

Release Notes:

- N/A

Change summary

crates/quick_action_bar/src/repl_menu.rs |  64 --
crates/repl/src/jupyter_settings.rs      |  52 --
crates/repl/src/repl.rs                  |  11 
crates/repl/src/repl_editor.rs           | 185 +++++++++
crates/repl/src/repl_sessions_ui.rs      | 257 ++++++++++++
crates/repl/src/repl_store.rs            |   8 
crates/repl/src/runtime_panel.rs         | 523 --------------------------
crates/repl/src/session.rs               |   6 
crates/zed/src/zed.rs                    |   5 
9 files changed, 474 insertions(+), 637 deletions(-)

Detailed changes

crates/quick_action_bar/src/repl_menu.rs 🔗

@@ -2,8 +2,8 @@ use std::time::Duration;
 
 use gpui::{percentage, Animation, AnimationExt, AnyElement, Transformation, View};
 use repl::{
-    ExecutionState, JupyterSettings, Kernel, KernelSpecification, KernelStatus, RuntimePanel,
-    Session, SessionSupport,
+    ExecutionState, JupyterSettings, Kernel, KernelSpecification, KernelStatus, Session,
+    SessionSupport,
 };
 use ui::{
     prelude::*, ButtonLike, ContextMenu, IconWithIndicator, Indicator, IntoElement, PopoverMenu,
@@ -39,15 +39,7 @@ impl QuickActionBar {
             return None;
         }
 
-        let workspace = self.workspace.upgrade()?.read(cx);
-
-        let (editor, repl_panel) = if let (Some(editor), Some(repl_panel)) =
-            (self.active_editor(), workspace.panel::<RuntimePanel>(cx))
-        {
-            (editor, repl_panel)
-        } else {
-            return None;
-        };
+        let editor = self.active_editor()?;
 
         let has_nonempty_selection = {
             editor.update(cx, |this, cx| {
@@ -62,10 +54,7 @@ impl QuickActionBar {
             })
         };
 
-        let session = repl_panel.update(cx, |repl_panel, cx| {
-            repl_panel.session(editor.downgrade(), cx)
-        });
-
+        let session = repl::session(editor.downgrade(), cx);
         let session = match session {
             SessionSupport::ActiveSession(session) => session,
             SessionSupport::Inactive(spec) => {
@@ -84,18 +73,15 @@ impl QuickActionBar {
 
         let element_id = |suffix| ElementId::Name(format!("{}-{}", id, suffix).into());
 
-        let panel_clone = repl_panel.clone();
-        let editor_clone = editor.downgrade();
+        let editor = editor.downgrade();
         let dropdown_menu = PopoverMenu::new(element_id("menu"))
             .menu(move |cx| {
-                let panel_clone = panel_clone.clone();
-                let editor_clone = editor_clone.clone();
+                let editor = editor.clone();
                 let session = session.clone();
                 ContextMenu::build(cx, move |menu, cx| {
                     let menu_state = session_state(session, cx);
                     let status = menu_state.status;
-                    let editor_clone = editor_clone.clone();
-                    let panel_clone = panel_clone.clone();
+                    let editor = editor.clone();
 
                     menu.when_else(
                         status.is_connected(),
@@ -139,7 +125,6 @@ impl QuickActionBar {
                         },
                     )
                     .separator()
-                    // Run
                     .custom_entry(
                         move |_cx| {
                             Label::new(if has_nonempty_selection {
@@ -150,17 +135,12 @@ impl QuickActionBar {
                             .into_any_element()
                         },
                         {
-                            let panel_clone = panel_clone.clone();
-                            let editor_clone = editor_clone.clone();
+                            let editor = editor.clone();
                             move |cx| {
-                                let editor_clone = editor_clone.clone();
-                                panel_clone.update(cx, |this, cx| {
-                                    this.run(editor_clone.clone(), cx).log_err();
-                                });
+                                repl::run(editor.clone(), cx).log_err();
                             }
                         },
                     )
-                    // Interrupt
                     .custom_entry(
                         move |_cx| {
                             Label::new("Interrupt")
@@ -169,17 +149,12 @@ impl QuickActionBar {
                                 .into_any_element()
                         },
                         {
-                            let panel_clone = panel_clone.clone();
-                            let editor_clone = editor_clone.clone();
+                            let editor = editor.clone();
                             move |cx| {
-                                let editor_clone = editor_clone.clone();
-                                panel_clone.update(cx, |this, cx| {
-                                    this.interrupt(editor_clone, cx);
-                                });
+                                repl::interrupt(editor.clone(), cx);
                             }
                         },
                     )
-                    // Clear Outputs
                     .custom_entry(
                         move |_cx| {
                             Label::new("Clear Outputs")
@@ -188,13 +163,9 @@ impl QuickActionBar {
                                 .into_any_element()
                         },
                         {
-                            let panel_clone = panel_clone.clone();
-                            let editor_clone = editor_clone.clone();
+                            let editor = editor.clone();
                             move |cx| {
-                                let editor_clone = editor_clone.clone();
-                                panel_clone.update(cx, |this, cx| {
-                                    this.clear_outputs(editor_clone, cx);
-                                });
+                                repl::clear_outputs(editor.clone(), cx);
                             }
                         },
                     )
@@ -207,7 +178,6 @@ impl QuickActionBar {
                     )
                     // TODO: Add Restart action
                     // .action("Restart", Box::new(gpui::NoAction))
-                    // Shut down kernel
                     .custom_entry(
                         move |_cx| {
                             Label::new("Shut Down Kernel")
@@ -216,13 +186,9 @@ impl QuickActionBar {
                                 .into_any_element()
                         },
                         {
-                            let panel_clone = panel_clone.clone();
-                            let editor_clone = editor_clone.clone();
+                            let editor = editor.clone();
                             move |cx| {
-                                let editor_clone = editor_clone.clone();
-                                panel_clone.update(cx, |this, cx| {
-                                    this.shutdown(editor_clone, cx);
-                                });
+                                repl::shutdown(editor.clone(), cx);
                             }
                         },
                     )

crates/repl/src/jupyter_settings.rs 🔗

@@ -5,21 +5,9 @@ use gpui::AppContext;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsSources};
-use ui::Pixels;
-
-#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(rename_all = "snake_case")]
-pub enum JupyterDockPosition {
-    Left,
-    #[default]
-    Right,
-    Bottom,
-}
 
 #[derive(Debug, Default)]
 pub struct JupyterSettings {
-    pub dock: JupyterDockPosition,
-    pub default_width: Pixels,
     pub kernel_selections: HashMap<String, String>,
 }
 
@@ -34,31 +22,15 @@ impl JupyterSettings {
 
 #[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct JupyterSettingsContent {
-    /// Where to dock the Jupyter panel.
-    ///
-    /// Default: `right`
-    dock: Option<JupyterDockPosition>,
-    /// Default width in pixels when the jupyter panel is docked to the left or right.
-    ///
-    /// Default: 640
-    pub default_width: Option<f32>,
     /// Default kernels to select for each language.
     ///
     /// Default: `{}`
     pub kernel_selections: Option<HashMap<String, String>>,
 }
 
-impl JupyterSettingsContent {
-    pub fn set_dock(&mut self, dock: JupyterDockPosition) {
-        self.dock = Some(dock);
-    }
-}
-
 impl Default for JupyterSettingsContent {
     fn default() -> Self {
         JupyterSettingsContent {
-            dock: Some(JupyterDockPosition::Right),
-            default_width: Some(640.0),
             kernel_selections: Some(HashMap::new()),
         }
     }
@@ -79,14 +51,6 @@ impl Settings for JupyterSettings {
         let mut settings = JupyterSettings::default();
 
         for value in sources.defaults_and_customizations() {
-            if let Some(dock) = value.dock {
-                settings.dock = dock;
-            }
-
-            if let Some(default_width) = value.default_width {
-                settings.default_width = Pixels::from(default_width);
-            }
-
             if let Some(source) = &value.kernel_selections {
                 for (k, v) in source {
                     settings.kernel_selections.insert(k.clone(), v.clone());
@@ -114,14 +78,6 @@ mod tests {
         JupyterSettings::register(cx);
 
         assert_eq!(JupyterSettings::enabled(cx), false);
-        assert_eq!(
-            JupyterSettings::get_global(cx).dock,
-            JupyterDockPosition::Right
-        );
-        assert_eq!(
-            JupyterSettings::get_global(cx).default_width,
-            Pixels::from(640.0)
-        );
 
         // Setting a custom setting through user settings
         SettingsStore::update_global(cx, |store, cx| {
@@ -140,13 +96,5 @@ mod tests {
         });
 
         assert_eq!(JupyterSettings::enabled(cx), true);
-        assert_eq!(
-            JupyterSettings::get_global(cx).dock,
-            JupyterDockPosition::Left
-        );
-        assert_eq!(
-            JupyterSettings::get_global(cx).default_width,
-            Pixels::from(800.0)
-        );
     }
 }

crates/repl/src/repl.rs 🔗

@@ -7,15 +7,16 @@ use std::{sync::Arc, time::Duration};
 mod jupyter_settings;
 mod kernels;
 mod outputs;
+mod repl_editor;
+mod repl_sessions_ui;
 mod repl_store;
-mod runtime_panel;
 mod session;
 mod stdio;
 
 pub use jupyter_settings::JupyterSettings;
 pub use kernels::{Kernel, KernelSpecification, KernelStatus};
-pub use runtime_panel::{ClearOutputs, Interrupt, Run, Shutdown};
-pub use runtime_panel::{RuntimePanel, SessionSupport};
+pub use repl_editor::*;
+pub use repl_sessions_ui::{ClearOutputs, Interrupt, ReplSessionsPage, Run, Shutdown};
 pub use runtimelib::ExecutionState;
 pub use session::Session;
 
@@ -48,7 +49,7 @@ fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
 pub fn init(fs: Arc<dyn Fs>, cx: &mut AppContext) {
     set_dispatcher(zed_dispatcher(cx));
     JupyterSettings::register(cx);
-    editor::init_settings(cx);
-    runtime_panel::init(cx);
+    ::editor::init_settings(cx);
+    repl_sessions_ui::init(cx);
     ReplStore::init(fs, cx);
 }

crates/repl/src/repl_editor.rs 🔗

@@ -0,0 +1,185 @@
+//! REPL operations on an [`Editor`].
+
+use std::ops::Range;
+use std::sync::Arc;
+
+use anyhow::{Context, Result};
+use editor::{Anchor, Editor, RangeToAnchorExt};
+use gpui::{prelude::*, AppContext, View, WeakView, WindowContext};
+use language::{Language, Point};
+use multi_buffer::MultiBufferRow;
+
+use crate::repl_store::ReplStore;
+use crate::session::SessionEvent;
+use crate::{KernelSpecification, Session};
+
+pub fn run(editor: WeakView<Editor>, cx: &mut WindowContext) -> Result<()> {
+    let store = ReplStore::global(cx);
+    if !store.read(cx).is_enabled() {
+        return Ok(());
+    }
+
+    let (selected_text, language, anchor_range) = match snippet(editor.clone(), cx) {
+        Some(snippet) => snippet,
+        None => return Ok(()),
+    };
+
+    let entity_id = editor.entity_id();
+
+    let kernel_specification = store.update(cx, |store, cx| {
+        store
+            .kernelspec(&language, cx)
+            .with_context(|| format!("No kernel found for language: {}", language.name()))
+    })?;
+
+    let fs = store.read(cx).fs().clone();
+    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.clone(), fs, kernel_specification, cx));
+
+        editor.update(cx, |_editor, cx| {
+            cx.notify();
+
+            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());
+                        });
+                    }
+                }
+            })
+            .detach();
+        })?;
+
+        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);
+    });
+
+    anyhow::Ok(())
+}
+
+pub enum SessionSupport {
+    ActiveSession(View<Session>),
+    Inactive(Box<KernelSpecification>),
+    RequiresSetup(Arc<str>),
+    Unsupported,
+}
+
+pub fn session(editor: WeakView<Editor>, cx: &mut AppContext) -> 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 = get_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(editor: WeakView<Editor>, cx: &mut WindowContext) {
+    let store = ReplStore::global(cx);
+    let entity_id = editor.entity_id();
+    if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
+        session.update(cx, |session, cx| {
+            session.clear_outputs(cx);
+            cx.notify();
+        });
+    }
+}
+
+pub fn interrupt(editor: WeakView<Editor>, cx: &mut WindowContext) {
+    let store = ReplStore::global(cx);
+    let entity_id = editor.entity_id();
+    if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
+        session.update(cx, |session, cx| {
+            session.interrupt(cx);
+            cx.notify();
+        });
+    }
+}
+
+pub fn shutdown(editor: WeakView<Editor>, cx: &mut WindowContext) {
+    let store = ReplStore::global(cx);
+    let entity_id = editor.entity_id();
+    if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
+        session.update(cx, |session, cx| {
+            session.shutdown(cx);
+            cx.notify();
+        });
+    }
+}
+
+fn snippet(
+    editor: WeakView<Editor>,
+    cx: &mut WindowContext,
+) -> Option<(String, Arc<Language>, Range<Anchor>)> {
+    let editor = editor.upgrade()?;
+    let editor = editor.read(cx);
+
+    let buffer = editor.buffer().read(cx).snapshot(cx);
+
+    let selection = editor.selections.newest::<usize>(cx);
+    let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
+
+    let range = if selection.is_empty() {
+        let cursor = selection.head();
+
+        let cursor_row = multi_buffer_snapshot.offset_to_point(cursor).row;
+        let start_offset = multi_buffer_snapshot.point_to_offset(Point::new(cursor_row, 0));
+
+        let end_point = Point::new(
+            cursor_row,
+            multi_buffer_snapshot.line_len(MultiBufferRow(cursor_row)),
+        );
+        let end_offset = start_offset.saturating_add(end_point.column as usize);
+
+        // Create a range from the start to the end of the line
+        start_offset..end_offset
+    } else {
+        selection.range()
+    };
+
+    let anchor_range = range.to_anchors(&multi_buffer_snapshot);
+
+    let selected_text = buffer
+        .text_for_range(anchor_range.clone())
+        .collect::<String>();
+
+    let start_language = buffer.language_at(anchor_range.start)?;
+    let end_language = buffer.language_at(anchor_range.end)?;
+    if start_language != end_language {
+        return None;
+    }
+
+    Some((selected_text, start_language.clone(), anchor_range))
+}
+
+fn get_language(editor: WeakView<Editor>, cx: &mut AppContext) -> 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()
+}

crates/repl/src/repl_sessions_ui.rs 🔗

@@ -0,0 +1,257 @@
+use editor::Editor;
+use gpui::{
+    actions, prelude::*, AppContext, EventEmitter, FocusHandle, FocusableView, Subscription, View,
+};
+use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding};
+use util::ResultExt as _;
+use workspace::item::ItemEvent;
+use workspace::WorkspaceId;
+use workspace::{item::Item, Workspace};
+
+use crate::jupyter_settings::JupyterSettings;
+use crate::repl_store::ReplStore;
+
+actions!(
+    repl,
+    [
+        Run,
+        ClearOutputs,
+        Sessions,
+        Interrupt,
+        Shutdown,
+        RefreshKernelspecs
+    ]
+);
+actions!(repl_panel, [ToggleFocus]);
+
+pub fn init(cx: &mut AppContext) {
+    cx.observe_new_views(
+        |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
+            workspace.register_action(|workspace, _: &Sessions, cx| {
+                let existing = workspace
+                    .active_pane()
+                    .read(cx)
+                    .items()
+                    .find_map(|item| item.downcast::<ReplSessionsPage>());
+
+                if let Some(existing) = existing {
+                    workspace.activate_item(&existing, true, true, cx);
+                } else {
+                    let extensions_page = ReplSessionsPage::new(cx);
+                    workspace.add_item_to_active_pane(Box::new(extensions_page), None, true, cx)
+                }
+            });
+
+            workspace.register_action(|_workspace, _: &RefreshKernelspecs, cx| {
+                let store = ReplStore::global(cx);
+                store.update(cx, |store, cx| {
+                    store.refresh_kernelspecs(cx).detach();
+                });
+            });
+        },
+    )
+    .detach();
+
+    cx.observe_new_views(move |editor: &mut Editor, cx: &mut ViewContext<Editor>| {
+        if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() {
+            return;
+        }
+
+        let editor_handle = cx.view().downgrade();
+
+        editor
+            .register_action({
+                let editor_handle = editor_handle.clone();
+                move |_: &Run, cx| {
+                    if !JupyterSettings::enabled(cx) {
+                        return;
+                    }
+
+                    crate::run(editor_handle.clone(), cx).log_err();
+                }
+            })
+            .detach();
+
+        editor
+            .register_action({
+                let editor_handle = editor_handle.clone();
+                move |_: &ClearOutputs, cx| {
+                    if !JupyterSettings::enabled(cx) {
+                        return;
+                    }
+
+                    crate::clear_outputs(editor_handle.clone(), cx);
+                }
+            })
+            .detach();
+
+        editor
+            .register_action({
+                let editor_handle = editor_handle.clone();
+                move |_: &Interrupt, cx| {
+                    if !JupyterSettings::enabled(cx) {
+                        return;
+                    }
+
+                    crate::interrupt(editor_handle.clone(), cx);
+                }
+            })
+            .detach();
+
+        editor
+            .register_action({
+                let editor_handle = editor_handle.clone();
+                move |_: &Shutdown, cx| {
+                    if !JupyterSettings::enabled(cx) {
+                        return;
+                    }
+
+                    crate::shutdown(editor_handle.clone(), cx);
+                }
+            })
+            .detach();
+    })
+    .detach();
+}
+
+pub struct ReplSessionsPage {
+    focus_handle: FocusHandle,
+    _subscriptions: Vec<Subscription>,
+}
+
+impl ReplSessionsPage {
+    pub fn new(cx: &mut ViewContext<Workspace>) -> View<Self> {
+        cx.new_view(|cx: &mut ViewContext<Self>| {
+            let focus_handle = cx.focus_handle();
+
+            let subscriptions = vec![
+                cx.on_focus_in(&focus_handle, |_this, cx| cx.notify()),
+                cx.on_focus_out(&focus_handle, |_this, _event, cx| cx.notify()),
+            ];
+
+            Self {
+                focus_handle,
+                _subscriptions: subscriptions,
+            }
+        })
+    }
+}
+
+impl EventEmitter<ItemEvent> for ReplSessionsPage {}
+
+impl FocusableView for ReplSessionsPage {
+    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl Item for ReplSessionsPage {
+    type Event = ItemEvent;
+
+    fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
+        Some("REPL Sessions".into())
+    }
+
+    fn telemetry_event_text(&self) -> Option<&'static str> {
+        Some("repl sessions")
+    }
+
+    fn show_toolbar(&self) -> bool {
+        false
+    }
+
+    fn clone_on_split(
+        &self,
+        _workspace_id: Option<WorkspaceId>,
+        _: &mut ViewContext<Self>,
+    ) -> Option<View<Self>> {
+        None
+    }
+
+    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
+        f(*event)
+    }
+}
+
+impl Render for ReplSessionsPage {
+    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 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",
+                                )
+                            }),
+                    ),
+                )
+                .into_any_element();
+        }
+
+        // When there are no sessions, show the command to run code in an editor
+        if sessions.is_empty() {
+            return v_flex()
+                .p_4()
+                .size_full()
+                .gap_2()
+                .child(Label::new("No Jupyter Kernel Sessions").size(LabelSize::Large))
+                .child(
+                    v_flex().child(
+                        Label::new("To run code in a Jupyter kernel, select some code and use the 'repl::Run' command.")
+                            .size(LabelSize::Default)
+                    )
+                    .children(
+                            KeyBinding::for_action(&Run, cx)
+                            .map(|binding|
+                                binding.into_any_element()
+                            )
+                    )
+                )
+                .child(Label::new("Kernels available").size(LabelSize::Large))
+                .children(
+                    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))
+                    })
+                )
+
+                .into_any_element();
+        }
+
+        v_flex()
+            .p_4()
+            .child(Label::new("Jupyter Kernel Sessions").size(LabelSize::Large))
+            .children(
+                sessions
+                    .into_iter()
+                    .map(|session| session.clone().into_any_element()),
+            )
+            .into_any_element()
+    }
+}

crates/repl/src/repl_store.rs 🔗

@@ -28,6 +28,10 @@ impl ReplStore {
     pub(crate) fn init(fs: Arc<dyn Fs>, cx: &mut AppContext) {
         let store = cx.new_model(move |cx| Self::new(fs, cx));
 
+        store
+            .update(cx, |store, cx| store.refresh_kernelspecs(cx))
+            .detach_and_log_err(cx);
+
         cx.set_global(GlobalReplStore(store))
     }
 
@@ -49,6 +53,10 @@ impl ReplStore {
         }
     }
 
+    pub fn fs(&self) -> &Arc<dyn Fs> {
+        &self.fs
+    }
+
     pub fn is_enabled(&self) -> bool {
         self.enabled
     }

crates/repl/src/runtime_panel.rs 🔗

@@ -1,523 +0,0 @@
-use crate::repl_store::ReplStore;
-use crate::{
-    jupyter_settings::{JupyterDockPosition, JupyterSettings},
-    kernels::KernelSpecification,
-    session::{Session, SessionEvent},
-};
-use anyhow::{Context as _, Result};
-use editor::{Anchor, Editor, RangeToAnchorExt};
-use gpui::{
-    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 _;
-use std::{ops::Range, sync::Arc};
-use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding};
-use util::ResultExt as _;
-use workspace::{
-    dock::{Panel, PanelEvent},
-    Workspace,
-};
-
-actions!(
-    repl,
-    [Run, ClearOutputs, Interrupt, Shutdown, RefreshKernelspecs]
-);
-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>| {
-            workspace.register_action(|workspace, _: &ToggleFocus, cx| {
-                workspace.toggle_panel_focus::<RuntimePanel>(cx);
-            });
-
-            workspace.register_action(|_workspace, _: &RefreshKernelspecs, cx| {
-                let store = ReplStore::global(cx);
-                store.update(cx, |store, cx| {
-                    store.refresh_kernelspecs(cx).detach();
-                });
-            });
-        },
-    )
-    .detach();
-
-    cx.observe_new_views(move |editor: &mut Editor, cx: &mut ViewContext<Editor>| {
-        // Only allow editors that support vim mode and are singleton buffers
-        if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() {
-            return;
-        }
-
-        editor
-            .register_action(cx.listener(
-                move |editor: &mut Editor, _: &Run, cx: &mut ViewContext<Editor>| {
-                    if !JupyterSettings::enabled(cx) {
-                        return;
-                    }
-                    let Some(workspace) = editor.workspace() else {
-                        return;
-                    };
-                    let Some(panel) = workspace.read(cx).panel::<RuntimePanel>(cx) else {
-                        return;
-                    };
-                    let weak_editor = cx.view().downgrade();
-                    panel.update(cx, |_, cx| {
-                        cx.defer(|panel, cx| {
-                            panel.run(weak_editor, cx).log_err();
-                        });
-                    })
-                },
-            ))
-            .detach();
-
-        editor
-            .register_action(cx.listener(
-                move |editor: &mut Editor, _: &ClearOutputs, cx: &mut ViewContext<Editor>| {
-                    if !JupyterSettings::enabled(cx) {
-                        return;
-                    }
-                    let Some(workspace) = editor.workspace() else {
-                        return;
-                    };
-                    let Some(panel) = workspace.read(cx).panel::<RuntimePanel>(cx) else {
-                        return;
-                    };
-                    let weak_editor = cx.view().downgrade();
-                    panel.update(cx, |_, cx| {
-                        cx.defer(|panel, cx| {
-                            panel.clear_outputs(weak_editor, cx);
-                        });
-                    })
-                },
-            ))
-            .detach();
-
-        editor
-            .register_action(cx.listener(
-                move |editor: &mut Editor, _: &Interrupt, cx: &mut ViewContext<Editor>| {
-                    if !JupyterSettings::enabled(cx) {
-                        return;
-                    }
-                    let Some(workspace) = editor.workspace() else {
-                        return;
-                    };
-                    let Some(panel) = workspace.read(cx).panel::<RuntimePanel>(cx) else {
-                        return;
-                    };
-                    let weak_editor = cx.view().downgrade();
-                    panel.update(cx, |_, cx| {
-                        cx.defer(|panel, cx| {
-                            panel.interrupt(weak_editor, cx);
-                        });
-                    })
-                },
-            ))
-            .detach();
-
-        editor
-            .register_action(cx.listener(
-                move |editor: &mut Editor, _: &Shutdown, cx: &mut ViewContext<Editor>| {
-                    if !JupyterSettings::enabled(cx) {
-                        return;
-                    }
-                    let Some(workspace) = editor.workspace() else {
-                        return;
-                    };
-                    let Some(panel) = workspace.read(cx).panel::<RuntimePanel>(cx) else {
-                        return;
-                    };
-                    let weak_editor = cx.view().downgrade();
-                    panel.update(cx, |_, cx| {
-                        cx.defer(|panel, cx| {
-                            panel.shutdown(weak_editor, cx);
-                        });
-                    })
-                },
-            ))
-            .detach();
-    })
-    .detach();
-}
-
-pub struct RuntimePanel {
-    fs: Arc<dyn Fs>,
-    focus_handle: FocusHandle,
-    width: Option<Pixels>,
-    _subscriptions: Vec<Subscription>,
-}
-
-impl RuntimePanel {
-    pub fn load(
-        workspace: WeakView<Workspace>,
-        cx: AsyncWindowContext,
-    ) -> Task<Result<View<Self>>> {
-        cx.spawn(|mut cx| async move {
-            let view = workspace.update(&mut cx, |workspace, cx| {
-                cx.new_view::<Self>(|cx| {
-                    let focus_handle = cx.focus_handle();
-
-                    let fs = workspace.app_state().fs.clone();
-
-                    let subscriptions = vec![
-                        cx.on_focus_in(&focus_handle, Self::focus_in),
-                        cx.on_focus_out(&focus_handle, Self::focus_out),
-                    ];
-
-                    let runtime_panel = Self {
-                        fs,
-                        width: None,
-                        focus_handle,
-                        _subscriptions: subscriptions,
-                    };
-
-                    runtime_panel
-                })
-            })?;
-
-            view.update(&mut cx, |_panel, cx| {
-                let store = ReplStore::global(cx);
-                store.update(cx, |store, cx| store.refresh_kernelspecs(cx))
-            })?
-            .await?;
-
-            Ok(view)
-        })
-    }
-
-    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
-        cx.notify();
-    }
-
-    fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext<Self>) {
-        cx.notify();
-    }
-
-    fn snippet(
-        editor: WeakView<Editor>,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<(String, Arc<Language>, Range<Anchor>)> {
-        let editor = editor.upgrade()?;
-        let editor = editor.read(cx);
-
-        let buffer = editor.buffer().read(cx).snapshot(cx);
-
-        let selection = editor.selections.newest::<usize>(cx);
-        let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
-
-        let range = if selection.is_empty() {
-            let cursor = selection.head();
-
-            let cursor_row = multi_buffer_snapshot.offset_to_point(cursor).row;
-            let start_offset = multi_buffer_snapshot.point_to_offset(Point::new(cursor_row, 0));
-
-            let end_point = Point::new(
-                cursor_row,
-                multi_buffer_snapshot.line_len(MultiBufferRow(cursor_row)),
-            );
-            let end_offset = start_offset.saturating_add(end_point.column as usize);
-
-            // Create a range from the start to the end of the line
-            start_offset..end_offset
-        } else {
-            selection.range()
-        };
-
-        let anchor_range = range.to_anchors(&multi_buffer_snapshot);
-
-        let selected_text = buffer
-            .text_for_range(anchor_range.clone())
-            .collect::<String>();
-
-        let start_language = buffer.language_at(anchor_range.start)?;
-        let end_language = buffer.language_at(anchor_range.end)?;
-        if start_language != end_language {
-            return None;
-        }
-
-        Some((selected_text, start_language.clone(), anchor_range))
-    }
-
-    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 run(&mut self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) -> Result<()> {
-        let store = ReplStore::global(cx);
-
-        if !store.read(cx).is_enabled() {
-            return Ok(());
-        }
-
-        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 = store.update(cx, |store, cx| {
-            store
-                .kernelspec(&language, cx)
-                .with_context(|| format!("No kernel found for language: {}", language.name()))
-        })?;
-
-        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(&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();
-
-            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);
-        });
-
-        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) = store.read(cx).get_session(entity_id).cloned() {
-            session.update(cx, |session, cx| {
-                session.clear_outputs(cx);
-            });
-            cx.notify();
-        }
-    }
-
-    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) = store.read(cx).get_session(entity_id).cloned() {
-            session.update(cx, |session, cx| {
-                session.interrupt(cx);
-            });
-            cx.notify();
-        }
-    }
-
-    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) = store.read(cx).get_session(entity_id).cloned() {
-            session.update(cx, |session, cx| {
-                session.shutdown(cx);
-            });
-            cx.notify();
-        }
-    }
-}
-
-impl Panel for RuntimePanel {
-    fn persistent_name() -> &'static str {
-        "RuntimePanel"
-    }
-
-    fn position(&self, cx: &ui::WindowContext) -> workspace::dock::DockPosition {
-        match JupyterSettings::get_global(cx).dock {
-            JupyterDockPosition::Left => workspace::dock::DockPosition::Left,
-            JupyterDockPosition::Right => workspace::dock::DockPosition::Right,
-            JupyterDockPosition::Bottom => workspace::dock::DockPosition::Bottom,
-        }
-    }
-
-    fn position_is_valid(&self, _position: workspace::dock::DockPosition) -> bool {
-        true
-    }
-
-    fn set_position(
-        &mut self,
-        position: workspace::dock::DockPosition,
-        cx: &mut ViewContext<Self>,
-    ) {
-        settings::update_settings_file::<JupyterSettings>(self.fs.clone(), cx, move |settings| {
-            let dock = match position {
-                workspace::dock::DockPosition::Left => JupyterDockPosition::Left,
-                workspace::dock::DockPosition::Right => JupyterDockPosition::Right,
-                workspace::dock::DockPosition::Bottom => JupyterDockPosition::Bottom,
-            };
-            settings.set_dock(dock);
-        })
-    }
-
-    fn size(&self, cx: &ui::WindowContext) -> Pixels {
-        let settings = JupyterSettings::get_global(cx);
-
-        self.width.unwrap_or(settings.default_width)
-    }
-
-    fn set_size(&mut self, size: Option<ui::Pixels>, _cx: &mut ViewContext<Self>) {
-        self.width = size;
-    }
-
-    fn icon(&self, cx: &ui::WindowContext) -> Option<ui::IconName> {
-        let store = ReplStore::global(cx);
-
-        if !store.read(cx).is_enabled() {
-            return None;
-        }
-
-        Some(IconName::Code)
-    }
-
-    fn icon_tooltip(&self, _cx: &ui::WindowContext) -> Option<&'static str> {
-        Some("Runtime Panel")
-    }
-
-    fn toggle_action(&self) -> Box<dyn gpui::Action> {
-        Box::new(ToggleFocus)
-    }
-}
-
-impl EventEmitter<PanelEvent> for RuntimePanel {}
-
-impl FocusableView for RuntimePanel {
-    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-
-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 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",
-                                )
-                            }),
-                    ),
-                )
-                .into_any_element();
-        }
-
-        // When there are no sessions, show the command to run code in an editor
-        if sessions.is_empty() {
-            return v_flex()
-                .p_4()
-                .size_full()
-                .gap_2()
-                .child(Label::new("No Jupyter Kernel Sessions").size(LabelSize::Large))
-                .child(
-                    v_flex().child(
-                        Label::new("To run code in a Jupyter kernel, select some code and use the 'repl::Run' command.")
-                            .size(LabelSize::Default)
-                    )
-                    .children(
-                            KeyBinding::for_action(&Run, cx)
-                            .map(|binding|
-                                binding.into_any_element()
-                            )
-                    )
-                )
-                .child(Label::new("Kernels available").size(LabelSize::Large))
-                .children(
-                    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))
-                    })
-                )
-
-                .into_any_element();
-        }
-
-        v_flex()
-            .p_4()
-            .child(Label::new("Jupyter Kernel Sessions").size(LabelSize::Large))
-            .children(
-                sessions
-                    .into_iter()
-                    .map(|session| session.clone().into_any_element()),
-            )
-            .into_any_element()
-    }
-}

crates/repl/src/session.rs 🔗

@@ -80,7 +80,7 @@ impl EditorBlock {
                 position: code_range.end,
                 height: execution_view.num_lines(cx).saturating_add(1),
                 style: BlockStyle::Sticky,
-                render: Self::create_output_area_render(execution_view.clone(), on_close.clone()),
+                render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()),
                 disposition: BlockDisposition::Below,
             };
 
@@ -111,7 +111,7 @@ impl EditorBlock {
                     self.block_id,
                     (
                         Some(self.execution_view.num_lines(cx).saturating_add(1)),
-                        Self::create_output_area_render(
+                        Self::create_output_area_renderer(
                             self.execution_view.clone(),
                             self.on_close.clone(),
                         ),
@@ -122,7 +122,7 @@ impl EditorBlock {
             .ok();
     }
 
-    fn create_output_area_render(
+    fn create_output_area_renderer(
         execution_view: View<ExecutionView>,
         on_close: CloseBlockFn,
     ) -> RenderBlock {

crates/zed/src/zed.rs 🔗

@@ -199,8 +199,6 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
             let assistant_panel =
                 assistant::AssistantPanel::load(workspace_handle.clone(), cx.clone());
 
-            let runtime_panel = repl::RuntimePanel::load(workspace_handle.clone(), cx.clone());
-
             let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
             let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
             let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
@@ -218,7 +216,6 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
                 outline_panel,
                 terminal_panel,
                 assistant_panel,
-                runtime_panel,
                 channels_panel,
                 chat_panel,
                 notification_panel,
@@ -227,7 +224,6 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
                 outline_panel,
                 terminal_panel,
                 assistant_panel,
-                runtime_panel,
                 channels_panel,
                 chat_panel,
                 notification_panel,
@@ -235,7 +231,6 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
 
             workspace_handle.update(&mut cx, |workspace, cx| {
                 workspace.add_panel(assistant_panel, cx);
-                workspace.add_panel(runtime_panel, cx);
                 workspace.add_panel(project_panel, cx);
                 workspace.add_panel(outline_panel, cx);
                 workspace.add_panel(terminal_panel, cx);