Show kernel options in a picker (#20274)

Kyle Kelley and Nate Butler created

Closes #18341

* [x] Remove "Change Kernel" Doc link from REPL menu
* [x] Remove chevron
* [x] Set a higher min width
* [x] Include the language along with the kernel name

Future PRs will address

* Add support for Python envs (#18291, #16757, #15563)
* Add support for Remote kernels
* Project settings support (#16898)

Release Notes:

- Added kernel picker for repl

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>

Change summary

Cargo.lock                                     |   2 
crates/quick_action_bar/Cargo.toml             |   1 
crates/quick_action_bar/src/repl_menu.rs       | 122 +++++++++--
crates/repl/Cargo.toml                         |   1 
crates/repl/src/components.rs                  |   2 
crates/repl/src/components/kernel_list_item.rs |   2 
crates/repl/src/components/kernel_options.rs   | 201 ++++++++++++++++++++
crates/repl/src/kernels.rs                     |  89 ++++++++
crates/repl/src/repl.rs                        |   4 
crates/repl/src/repl_editor.rs                 |  55 +++++
crates/repl/src/repl_sessions_ui.rs            |  37 ++-
crates/repl/src/repl_store.rs                  |  27 +
crates/repl/src/session.rs                     |  12 
13 files changed, 492 insertions(+), 63 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9354,6 +9354,7 @@ dependencies = [
  "editor",
  "gpui",
  "markdown_preview",
+ "picker",
  "repl",
  "search",
  "settings",
@@ -9857,6 +9858,7 @@ dependencies = [
  "menu",
  "multi_buffer",
  "nbformat",
+ "picker",
  "project",
  "runtimelib",
  "schemars",

crates/quick_action_bar/Cargo.toml 🔗

@@ -24,6 +24,7 @@ ui.workspace = true
 util.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
+picker.workspace = true
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }

crates/quick_action_bar/src/repl_menu.rs 🔗

@@ -1,13 +1,15 @@
 use std::time::Duration;
 
 use gpui::{percentage, Animation, AnimationExt, AnyElement, Transformation, View};
+use picker::Picker;
 use repl::{
+    components::{KernelPickerDelegate, KernelSelector},
     ExecutionState, JupyterSettings, Kernel, KernelSpecification, KernelStatus, Session,
     SessionSupport,
 };
 use ui::{
     prelude::*, ButtonLike, ContextMenu, IconWithIndicator, Indicator, IntoElement, PopoverMenu,
-    Tooltip,
+    PopoverMenuHandle, Tooltip,
 };
 
 use gpui::ElementId;
@@ -58,7 +60,6 @@ impl QuickActionBar {
         let session = match session {
             SessionSupport::ActiveSession(session) => session,
             SessionSupport::Inactive(spec) => {
-                let spec = *spec;
                 return self.render_repl_launch_menu(spec, cx);
             }
             SessionSupport::RequiresSetup(language) => {
@@ -246,44 +247,120 @@ impl QuickActionBar {
 
         Some(
             h_flex()
+                .child(self.render_kernel_selector(cx))
                 .child(button)
                 .child(dropdown_menu)
                 .into_any_element(),
         )
     }
-
     pub fn render_repl_launch_menu(
         &self,
         kernel_specification: KernelSpecification,
-        _cx: &mut ViewContext<Self>,
+        cx: &mut ViewContext<Self>,
     ) -> Option<AnyElement> {
         let tooltip: SharedString =
-            SharedString::from(format!("Start REPL for {}", kernel_specification.name));
+            SharedString::from(format!("Start REPL for {}", kernel_specification.name()));
 
         Some(
-            IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
-                .size(ButtonSize::Compact)
-                .icon_color(Color::Muted)
-                .style(ButtonStyle::Subtle)
-                .tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
-                .on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {})))
+            h_flex()
+                .child(self.render_kernel_selector(cx))
+                .child(
+                    IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
+                        .size(ButtonSize::Compact)
+                        .icon_color(Color::Muted)
+                        .style(ButtonStyle::Subtle)
+                        .tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
+                        .on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {}))),
+                )
                 .into_any_element(),
         )
     }
 
+    pub fn render_kernel_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let editor = if let Some(editor) = self.active_editor() {
+            editor
+        } else {
+            // todo!()
+            return div().into_any_element();
+        };
+
+        let session = repl::session(editor.downgrade(), cx);
+
+        let current_kernelspec = match session {
+            SessionSupport::ActiveSession(view) => Some(view.read(cx).kernel_specification.clone()),
+            SessionSupport::Inactive(kernel_specification) => Some(kernel_specification),
+            SessionSupport::RequiresSetup(_language_name) => None,
+            SessionSupport::Unsupported => None,
+        };
+
+        let current_kernel_name = current_kernelspec.as_ref().map(|spec| spec.name());
+
+        let menu_handle: PopoverMenuHandle<Picker<KernelPickerDelegate>> =
+            PopoverMenuHandle::default();
+        KernelSelector::new(
+            {
+                Box::new(move |kernelspec, cx| {
+                    repl::assign_kernelspec(kernelspec, editor.downgrade(), cx).ok();
+                })
+            },
+            current_kernelspec.clone(),
+            ButtonLike::new("kernel-selector")
+                .style(ButtonStyle::Subtle)
+                .child(
+                    h_flex()
+                        .w_full()
+                        .gap_0p5()
+                        .child(
+                            div()
+                                .overflow_x_hidden()
+                                .flex_grow()
+                                .whitespace_nowrap()
+                                .child(
+                                    Label::new(if let Some(name) = current_kernel_name {
+                                        name
+                                    } else {
+                                        SharedString::from("Select Kernel")
+                                    })
+                                    .size(LabelSize::Small)
+                                    .color(if current_kernelspec.is_some() {
+                                        Color::Default
+                                    } else {
+                                        Color::Placeholder
+                                    })
+                                    .into_any_element(),
+                                ),
+                        )
+                        .child(
+                            Icon::new(IconName::ChevronDown)
+                                .color(Color::Muted)
+                                .size(IconSize::XSmall),
+                        ),
+                )
+                .tooltip(move |cx| Tooltip::text("Select Kernel", cx)),
+        )
+        .with_handle(menu_handle.clone())
+        .into_any_element()
+    }
+
     pub fn render_repl_setup(
         &self,
         language: &str,
-        _cx: &mut ViewContext<Self>,
+        cx: &mut ViewContext<Self>,
     ) -> Option<AnyElement> {
         let tooltip: SharedString = SharedString::from(format!("Setup Zed REPL for {}", language));
         Some(
-            IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
-                .size(ButtonSize::Compact)
-                .icon_color(Color::Muted)
-                .style(ButtonStyle::Subtle)
-                .tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
-                .on_click(|_, cx| cx.open_url(&format!("{}#installation", ZED_REPL_DOCUMENTATION)))
+            h_flex()
+                .child(self.render_kernel_selector(cx))
+                .child(
+                    IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
+                        .size(ButtonSize::Compact)
+                        .icon_color(Color::Muted)
+                        .style(ButtonStyle::Subtle)
+                        .tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
+                        .on_click(|_, cx| {
+                            cx.open_url(&format!("{}#installation", ZED_REPL_DOCUMENTATION))
+                        }),
+                )
                 .into_any_element(),
         )
     }
@@ -292,13 +369,8 @@ impl QuickActionBar {
 fn session_state(session: View<Session>, cx: &WindowContext) -> ReplMenuState {
     let session = session.read(cx);
 
-    let kernel_name: SharedString = session.kernel_specification.name.clone().into();
-    let kernel_language: SharedString = session
-        .kernel_specification
-        .kernelspec
-        .language
-        .clone()
-        .into();
+    let kernel_name = session.kernel_specification.name();
+    let kernel_language: SharedString = session.kernel_specification.language();
 
     let fill_fields = || {
         ReplMenuState {

crates/repl/Cargo.toml 🔗

@@ -45,6 +45,7 @@ ui.workspace = true
 util.workspace = true
 uuid.workspace = true
 workspace.workspace = true
+picker.workspace = true
 
 [target.'cfg(target_os = "windows")'.dependencies]
 windows.workspace = true

crates/repl/src/components/kernel_list_item.rs 🔗

@@ -46,7 +46,7 @@ impl ParentElement for KernelListItem {
 
 impl RenderOnce for KernelListItem {
     fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
-        ListItem::new(SharedString::from(self.kernel_specification.name.clone()))
+        ListItem::new(self.kernel_specification.name())
             .selectable(false)
             .start_slot(
                 h_flex()

crates/repl/src/components/kernel_options.rs 🔗

@@ -0,0 +1,201 @@
+use crate::kernels::KernelSpecification;
+use crate::repl_store::ReplStore;
+use crate::KERNEL_DOCS_URL;
+
+use gpui::DismissEvent;
+
+use picker::Picker;
+use picker::PickerDelegate;
+
+use std::sync::Arc;
+use ui::ListItemSpacing;
+
+use gpui::SharedString;
+use gpui::Task;
+use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
+
+type OnSelect = Box<dyn Fn(KernelSpecification, &mut WindowContext)>;
+
+#[derive(IntoElement)]
+pub struct KernelSelector<T: PopoverTrigger> {
+    handle: Option<PopoverMenuHandle<Picker<KernelPickerDelegate>>>,
+    on_select: OnSelect,
+    trigger: T,
+    info_text: Option<SharedString>,
+    current_kernelspec: Option<KernelSpecification>,
+}
+
+pub struct KernelPickerDelegate {
+    all_kernels: Vec<KernelSpecification>,
+    filtered_kernels: Vec<KernelSpecification>,
+    selected_kernelspec: Option<KernelSpecification>,
+    on_select: OnSelect,
+}
+
+impl<T: PopoverTrigger> KernelSelector<T> {
+    pub fn new(
+        on_select: OnSelect,
+        current_kernelspec: Option<KernelSpecification>,
+        trigger: T,
+    ) -> Self {
+        KernelSelector {
+            on_select,
+            handle: None,
+            trigger,
+            info_text: None,
+            current_kernelspec,
+        }
+    }
+
+    pub fn with_handle(mut self, handle: PopoverMenuHandle<Picker<KernelPickerDelegate>>) -> Self {
+        self.handle = Some(handle);
+        self
+    }
+
+    pub fn with_info_text(mut self, text: impl Into<SharedString>) -> Self {
+        self.info_text = Some(text.into());
+        self
+    }
+}
+
+impl PickerDelegate for KernelPickerDelegate {
+    type ListItem = ListItem;
+
+    fn match_count(&self) -> usize {
+        self.filtered_kernels.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        if let Some(kernelspec) = self.selected_kernelspec.as_ref() {
+            self.filtered_kernels
+                .iter()
+                .position(|k| k == kernelspec)
+                .unwrap_or(0)
+        } else {
+            0
+        }
+    }
+
+    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
+        self.selected_kernelspec = self.filtered_kernels.get(ix).cloned();
+        cx.notify();
+    }
+
+    fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
+        "Select a kernel...".into()
+    }
+
+    fn update_matches(&mut self, query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
+        let all_kernels = self.all_kernels.clone();
+
+        if query.is_empty() {
+            self.filtered_kernels = all_kernels;
+            return Task::Ready(Some(()));
+        }
+
+        self.filtered_kernels = if query.is_empty() {
+            all_kernels
+        } else {
+            all_kernels
+                .into_iter()
+                .filter(|kernel| kernel.name().to_lowercase().contains(&query.to_lowercase()))
+                .collect()
+        };
+
+        return Task::Ready(Some(()));
+    }
+
+    fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
+        if let Some(kernelspec) = &self.selected_kernelspec {
+            (self.on_select)(kernelspec.clone(), cx.window_context());
+            cx.emit(DismissEvent);
+        }
+    }
+
+    fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        _cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        let kernelspec = self.filtered_kernels.get(ix)?;
+
+        let is_selected = self.selected_kernelspec.as_ref() == Some(kernelspec);
+
+        Some(
+            ListItem::new(ix)
+                .inset(true)
+                .spacing(ListItemSpacing::Sparse)
+                .selected(selected)
+                .child(
+                    h_flex().w_full().justify_between().min_w(px(200.)).child(
+                        h_flex()
+                            .gap_1p5()
+                            .child(Label::new(kernelspec.name()))
+                            .child(
+                                Label::new(kernelspec.type_name())
+                                    .size(LabelSize::XSmall)
+                                    .color(Color::Muted),
+                            ),
+                    ),
+                )
+                .end_slot(div().when(is_selected, |this| {
+                    this.child(
+                        Icon::new(IconName::Check)
+                            .color(Color::Accent)
+                            .size(IconSize::Small),
+                    )
+                })),
+        )
+    }
+
+    fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<gpui::AnyElement> {
+        Some(
+            h_flex()
+                .w_full()
+                .border_t_1()
+                .border_color(cx.theme().colors().border_variant)
+                .p_1()
+                .gap_4()
+                .child(
+                    Button::new("kernel-docs", "Kernel Docs")
+                        .icon(IconName::ExternalLink)
+                        .icon_size(IconSize::XSmall)
+                        .icon_color(Color::Muted)
+                        .icon_position(IconPosition::End)
+                        .on_click(move |_, cx| cx.open_url(KERNEL_DOCS_URL)),
+                )
+                .into_any(),
+        )
+    }
+}
+
+impl<T: PopoverTrigger> RenderOnce for KernelSelector<T> {
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        let store = ReplStore::global(cx).read(cx);
+        let all_kernels: Vec<KernelSpecification> =
+            store.kernel_specifications().cloned().collect();
+
+        let selected_kernelspec = self.current_kernelspec;
+
+        let delegate = KernelPickerDelegate {
+            on_select: self.on_select,
+            all_kernels: all_kernels.clone(),
+            filtered_kernels: all_kernels,
+            selected_kernelspec,
+        };
+
+        let picker_view = cx.new_view(|cx| {
+            let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()));
+            picker
+        });
+
+        PopoverMenu::new("kernel-switcher")
+            .menu(move |_cx| Some(picker_view.clone()))
+            .trigger(self.trigger)
+            .attach(gpui::AnchorCorner::BottomLeft)
+            .when_some(self.handle, |menu, handle| menu.with_handle(handle))
+    }
+}

crates/repl/src/kernels.rs 🔗

@@ -19,16 +19,82 @@ use std::{
     path::PathBuf,
     sync::Arc,
 };
+use ui::SharedString;
 use uuid::Uuid;
 
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum KernelSpecification {
+    Remote(RemoteKernelSpecification),
+    Jupyter(LocalKernelSpecification),
+    PythonEnv(LocalKernelSpecification),
+}
+
+impl KernelSpecification {
+    pub fn name(&self) -> SharedString {
+        match self {
+            Self::Jupyter(spec) => spec.name.clone().into(),
+            Self::PythonEnv(spec) => spec.name.clone().into(),
+            Self::Remote(spec) => spec.name.clone().into(),
+        }
+    }
+
+    pub fn type_name(&self) -> SharedString {
+        match self {
+            Self::Jupyter(_) => "Jupyter".into(),
+            Self::PythonEnv(_) => "Python Environment".into(),
+            Self::Remote(_) => "Remote".into(),
+        }
+    }
+
+    pub fn path(&self) -> SharedString {
+        SharedString::from(match self {
+            Self::Jupyter(spec) => spec.path.to_string_lossy().to_string(),
+            Self::PythonEnv(spec) => spec.path.to_string_lossy().to_string(),
+            Self::Remote(spec) => spec.url.to_string(),
+        })
+    }
+
+    pub fn language(&self) -> SharedString {
+        SharedString::from(match self {
+            Self::Jupyter(spec) => spec.kernelspec.language.clone(),
+            Self::PythonEnv(spec) => spec.kernelspec.language.clone(),
+            Self::Remote(spec) => spec.kernelspec.language.clone(),
+        })
+    }
+}
+
 #[derive(Debug, Clone)]
-pub struct KernelSpecification {
+pub struct LocalKernelSpecification {
     pub name: String,
     pub path: PathBuf,
     pub kernelspec: JupyterKernelspec,
 }
 
-impl KernelSpecification {
+impl PartialEq for LocalKernelSpecification {
+    fn eq(&self, other: &Self) -> bool {
+        self.name == other.name && self.path == other.path
+    }
+}
+
+impl Eq for LocalKernelSpecification {}
+
+#[derive(Debug, Clone)]
+pub struct RemoteKernelSpecification {
+    pub name: String,
+    pub url: String,
+    pub token: String,
+    pub kernelspec: JupyterKernelspec,
+}
+
+impl PartialEq for RemoteKernelSpecification {
+    fn eq(&self, other: &Self) -> bool {
+        self.name == other.name && self.url == other.url
+    }
+}
+
+impl Eq for RemoteKernelSpecification {}
+
+impl LocalKernelSpecification {
     #[must_use]
     fn command(&self, connection_path: &PathBuf) -> Result<Command> {
         let argv = &self.kernelspec.argv;
@@ -198,6 +264,17 @@ impl RunningKernel {
         fs: Arc<dyn Fs>,
         cx: &mut AppContext,
     ) -> Task<Result<(Self, JupyterMessageChannel)>> {
+        let kernel_specification = match kernel_specification {
+            KernelSpecification::Jupyter(spec) => spec,
+            KernelSpecification::PythonEnv(spec) => spec,
+            KernelSpecification::Remote(_spec) => {
+                // todo!(): Implement remote kernel specification
+                return Task::ready(Err(anyhow::anyhow!(
+                    "Running remote kernels is not supported"
+                )));
+            }
+        };
+
         cx.spawn(|cx| async move {
             let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
             let ports = peek_ports(ip).await?;
@@ -344,7 +421,7 @@ async fn read_kernelspec_at(
     // /usr/local/share/jupyter/kernels/python3
     kernel_dir: PathBuf,
     fs: &dyn Fs,
-) -> Result<KernelSpecification> {
+) -> Result<LocalKernelSpecification> {
     let path = kernel_dir;
     let kernel_name = if let Some(kernel_name) = path.file_name() {
         kernel_name.to_string_lossy().to_string()
@@ -360,7 +437,7 @@ async fn read_kernelspec_at(
     let spec = fs.load(expected_kernel_json.as_path()).await?;
     let spec = serde_json::from_str::<JupyterKernelspec>(&spec)?;
 
-    Ok(KernelSpecification {
+    Ok(LocalKernelSpecification {
         name: kernel_name,
         path,
         kernelspec: spec,
@@ -368,7 +445,7 @@ async fn read_kernelspec_at(
 }
 
 /// Read a directory of kernelspec directories
-async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> Result<Vec<KernelSpecification>> {
+async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> Result<Vec<LocalKernelSpecification>> {
     let mut kernelspec_dirs = fs.read_dir(&path).await?;
 
     let mut valid_kernelspecs = Vec::new();
@@ -388,7 +465,7 @@ async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> Result<Vec<KernelSpecif
     Ok(valid_kernelspecs)
 }
 
-pub async fn kernel_specifications(fs: Arc<dyn Fs>) -> Result<Vec<KernelSpecification>> {
+pub async fn local_kernel_specifications(fs: Arc<dyn Fs>) -> Result<Vec<LocalKernelSpecification>> {
     let mut data_dirs = dirs::data_dirs();
 
     // Pick up any kernels from conda or conda environment

crates/repl/src/repl.rs 🔗

@@ -1,4 +1,4 @@
-mod components;
+pub mod components;
 mod jupyter_settings;
 mod kernels;
 pub mod notebook;
@@ -26,6 +26,8 @@ use crate::repl_store::ReplStore;
 pub use crate::session::Session;
 use client::telemetry::Telemetry;
 
+pub const KERNEL_DOCS_URL: &str = "https://zed.dev/docs/repl#changing-kernels";
+
 pub fn init(fs: Arc<dyn Fs>, telemetry: Arc<Telemetry>, cx: &mut AppContext) {
     set_dispatcher(zed_dispatcher(cx));
     JupyterSettings::register(cx);

crates/repl/src/repl_editor.rs 🔗

@@ -12,6 +12,56 @@ use crate::repl_store::ReplStore;
 use crate::session::SessionEvent;
 use crate::{KernelSpecification, Session};
 
+pub fn assign_kernelspec(
+    kernel_specification: KernelSpecification,
+    weak_editor: WeakView<Editor>,
+    cx: &mut WindowContext,
+) -> Result<()> {
+    let store = ReplStore::global(cx);
+    if !store.read(cx).is_enabled() {
+        return Ok(());
+    }
+
+    let fs = store.read(cx).fs().clone();
+    let telemetry = store.read(cx).telemetry().clone();
+
+    if let Some(session) = store.read(cx).get_session(weak_editor.entity_id()).cloned() {
+        // Drop previous session, start new one
+        session.update(cx, |session, cx| {
+            session.clear_outputs(cx);
+            session.shutdown(cx);
+            cx.notify();
+        });
+    }
+
+    let session = cx
+        .new_view(|cx| Session::new(weak_editor.clone(), fs, telemetry, kernel_specification, cx));
+
+    weak_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();
+        })
+        .ok();
+
+    store.update(cx, |store, _cx| {
+        store.insert_session(weak_editor.entity_id(), session.clone());
+    });
+
+    Ok(())
+}
+
 pub fn run(editor: WeakView<Editor>, move_down: bool, cx: &mut WindowContext) -> Result<()> {
     let store = ReplStore::global(cx);
     if !store.read(cx).is_enabled() {
@@ -96,9 +146,10 @@ pub fn run(editor: WeakView<Editor>, move_down: bool, cx: &mut WindowContext) ->
     anyhow::Ok(())
 }
 
+#[allow(clippy::large_enum_variant)]
 pub enum SessionSupport {
     ActiveSession(View<Session>),
-    Inactive(Box<KernelSpecification>),
+    Inactive(KernelSpecification),
     RequiresSetup(LanguageName),
     Unsupported,
 }
@@ -119,7 +170,7 @@ pub fn session(editor: WeakView<Editor>, cx: &mut WindowContext) -> SessionSuppo
     });
 
     match kernelspec {
-        Some(kernelspec) => SessionSupport::Inactive(Box::new(kernelspec)),
+        Some(kernelspec) => SessionSupport::Inactive(kernelspec),
         None => {
             if language_supported(&language) {
                 SessionSupport::RequiresSetup(language.name())

crates/repl/src/repl_sessions_ui.rs 🔗

@@ -1,9 +1,9 @@
-use collections::HashMap;
 use editor::Editor;
 use gpui::{
     actions, prelude::*, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView,
     FontWeight, Subscription, View,
 };
+use std::collections::HashMap;
 use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding, ListItem, Tooltip};
 use util::ResultExt as _;
 use workspace::item::ItemEvent;
@@ -12,7 +12,7 @@ use workspace::{item::Item, Workspace};
 
 use crate::jupyter_settings::JupyterSettings;
 use crate::repl_store::ReplStore;
-use crate::KernelSpecification;
+use crate::{KernelSpecification, KERNEL_DOCS_URL};
 
 actions!(
     repl,
@@ -238,14 +238,24 @@ impl Render for ReplSessionsPage {
                 );
         }
 
-        let mut kernels_by_language: HashMap<String, Vec<KernelSpecification>> = HashMap::default();
-        for spec in kernel_specifications {
-            kernels_by_language
-                .entry(spec.kernelspec.language.clone())
-                .or_default()
-                .push(spec);
+        let mut kernels_by_language: HashMap<SharedString, Vec<&KernelSpecification>> =
+            kernel_specifications
+                .iter()
+                .map(|spec| (spec.language(), spec))
+                .fold(HashMap::new(), |mut acc, (language, spec)| {
+                    acc.entry(language).or_default().push(spec);
+                    acc
+                });
+
+        for kernels in kernels_by_language.values_mut() {
+            kernels.sort_by_key(|a| a.name())
         }
 
+        // Convert to a sorted Vec of tuples
+        let mut sorted_kernels: Vec<(SharedString, Vec<&KernelSpecification>)> =
+            kernels_by_language.into_iter().collect();
+        sorted_kernels.sort_by(|a, b| a.0.cmp(&b.0));
+
         let kernels_available = v_flex()
             .child(Label::new("Kernels available").size(LabelSize::Large))
             .gap_2()
@@ -262,11 +272,11 @@ impl Render for ReplSessionsPage {
                             .child(Label::new("REPL documentation"))
                             .child(Icon::new(IconName::Link))
                             .on_click(move |_, cx| {
-                                cx.open_url("https://zed.dev/docs/repl#changing-kernels")
+                                cx.open_url(KERNEL_DOCS_URL)
                             }),
                     ),
             )
-            .children(kernels_by_language.into_iter().map(|(language, specs)| {
+            .children(sorted_kernels.into_iter().map(|(language, specs)| {
                 let chosen_kernel = store.read(cx).kernelspec(&language, cx);
 
                 v_flex()
@@ -274,13 +284,12 @@ impl Render for ReplSessionsPage {
                     .child(Label::new(language.clone()).weight(FontWeight::BOLD))
                     .children(specs.into_iter().map(|spec| {
                         let is_choice = if let Some(chosen_kernel) = &chosen_kernel {
-                            chosen_kernel.name.to_lowercase() == spec.name.to_lowercase()
-                                && chosen_kernel.path == spec.path
+                            chosen_kernel == spec
                         } else {
                             false
                         };
 
-                        let path = SharedString::from(spec.path.to_string_lossy().to_string());
+                        let path = spec.path();
 
                         ListItem::new(path.clone())
                             .selectable(false)
@@ -290,7 +299,7 @@ impl Render for ReplSessionsPage {
                             .child(
                                 h_flex()
                                     .gap_1()
-                                    .child(div().id(path.clone()).child(Label::new(spec.name.clone())))
+                                    .child(div().id(path.clone()).child(Label::new(spec.name())))
                                     .when(is_choice, |el| {
 
                                         let language = language.clone();

crates/repl/src/repl_store.rs 🔗

@@ -10,7 +10,7 @@ use gpui::{
 use project::Fs;
 use settings::{Settings, SettingsStore};
 
-use crate::kernels::kernel_specifications;
+use crate::kernels::local_kernel_specifications;
 use crate::{JupyterSettings, KernelSpecification, Session};
 
 struct GlobalReplStore(Model<ReplStore>);
@@ -106,12 +106,17 @@ impl ReplStore {
     }
 
     pub fn refresh_kernelspecs(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
-        let kernel_specifications = kernel_specifications(self.fs.clone());
+        let local_kernel_specifications = local_kernel_specifications(self.fs.clone());
         cx.spawn(|this, mut cx| async move {
-            let kernel_specifications = kernel_specifications.await?;
+            let local_kernel_specifications = local_kernel_specifications.await?;
+
+            let mut kernel_options = Vec::new();
+            for kernel_specification in local_kernel_specifications {
+                kernel_options.push(KernelSpecification::Jupyter(kernel_specification));
+            }
 
             this.update(&mut cx, |this, cx| {
-                this.kernel_specifications = kernel_specifications;
+                this.kernel_specifications = kernel_options;
                 cx.notify();
             })
         })
@@ -125,7 +130,9 @@ impl ReplStore {
             .kernel_specifications
             .iter()
             .find(|runtime_specification| {
-                if let Some(selected) = selected_kernel {
+                if let (Some(selected), KernelSpecification::Jupyter(runtime_specification)) =
+                    (selected_kernel, runtime_specification)
+                {
                     // Top priority is the selected kernel
                     return runtime_specification.name.to_lowercase() == selected.to_lowercase();
                 }
@@ -139,9 +146,13 @@ impl ReplStore {
 
         self.kernel_specifications
             .iter()
-            .find(|runtime_specification| {
-                runtime_specification.kernelspec.language.to_lowercase()
-                    == language_name.to_lowercase()
+            .find(|kernel_option| match kernel_option {
+                KernelSpecification::Jupyter(runtime_specification) => {
+                    runtime_specification.kernelspec.language.to_lowercase()
+                        == language_name.to_lowercase()
+                }
+                // todo!()
+                _ => false,
             })
             .cloned()
     }

crates/repl/src/session.rs 🔗

@@ -1,8 +1,8 @@
 use crate::components::KernelListItem;
-use crate::KernelStatus;
 use crate::{
     kernels::{Kernel, KernelSpecification, RunningKernel},
     outputs::{ExecutionStatus, ExecutionView},
+    KernelStatus,
 };
 use client::telemetry::Telemetry;
 use collections::{HashMap, HashSet};
@@ -224,7 +224,7 @@ impl Session {
     }
 
     fn start_kernel(&mut self, cx: &mut ViewContext<Self>) {
-        let kernel_language = self.kernel_specification.kernelspec.language.clone();
+        let kernel_language = self.kernel_specification.language();
         let entity_id = self.editor.entity_id();
         let working_directory = self
             .editor
@@ -233,7 +233,7 @@ impl Session {
             .unwrap_or_else(temp_dir);
 
         self.telemetry.report_repl_event(
-            kernel_language.clone(),
+            kernel_language.into(),
             KernelStatus::Starting.to_string(),
             cx.entity_id().to_string(),
         );
@@ -556,7 +556,7 @@ impl Session {
                 self.kernel.set_execution_state(&status.execution_state);
 
                 self.telemetry.report_repl_event(
-                    self.kernel_specification.kernelspec.language.clone(),
+                    self.kernel_specification.language().into(),
                     KernelStatus::from(&self.kernel).to_string(),
                     cx.entity_id().to_string(),
                 );
@@ -607,7 +607,7 @@ impl Session {
         }
 
         let kernel_status = KernelStatus::from(&kernel).to_string();
-        let kernel_language = self.kernel_specification.kernelspec.language.clone();
+        let kernel_language = self.kernel_specification.language().into();
 
         self.telemetry.report_repl_event(
             kernel_language,
@@ -749,7 +749,7 @@ impl Render for Session {
                 Kernel::Shutdown => Color::Disabled,
                 Kernel::Restarting => Color::Modified,
             })
-            .child(Label::new(self.kernel_specification.name.clone()))
+            .child(Label::new(self.kernel_specification.name()))
             .children(status_text.map(|status_text| Label::new(format!("({status_text})"))))
             .button(
                 Button::new("shutdown", "Shutdown")