repl: Use uv to install ipykernel for uv-managed venv (#51897)

Finn Eitreim created

## Context

Closes #51874

the repl is able to recognize that the venv is managed by uv, but still
runs `python -m pip install ipykernel`, despite this not working. this
PR fixes that behavior and uses uv to install ipkernel.

## How to Review

Added a path that uses uv to install ipykernel in repl_editor.rs
Added a function to repl_store.rs that allows updating the venv as
having ipykernel installed after installing it.


## Videos

Old Behavior:


https://github.com/user-attachments/assets/9de81cc9-cd78-4570-ad57-550f5ecabffa

New Behavior:


https://github.com/user-attachments/assets/391f54c7-ae67-4d85-8f4f-9d87ddc8db63


## Self-Review Checklist

<!-- Check before requesting review: -->
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- repl: Fixed installing ipykernel with uv managed environements

Change summary

crates/repl/src/kernels/mod.rs |  7 +++++++
crates/repl/src/repl_editor.rs | 30 +++++++++++++++++++++++++-----
crates/repl/src/repl_store.rs  | 21 +++++++++++++++++++--
3 files changed, 51 insertions(+), 7 deletions(-)

Detailed changes

crates/repl/src/kernels/mod.rs 🔗

@@ -177,6 +177,13 @@ impl PythonEnvKernelSpecification {
             kernelspec: self.kernelspec.clone(),
         }
     }
+
+    pub fn is_uv(&self) -> bool {
+        matches!(
+            self.environment_kind.as_deref(),
+            Some("uv" | "uv (Workspace)")
+        )
+    }
 }
 
 #[derive(Debug, Clone, PartialEq, Eq)]

crates/repl/src/repl_editor.rs 🔗

@@ -87,6 +87,7 @@ pub fn install_ipykernel_and_assign(
 
     let python_path = env_spec.path.clone();
     let env_name = env_spec.name.clone();
+    let is_uv = env_spec.is_uv();
     let env_spec = env_spec.clone();
 
     struct IpykernelInstall;
@@ -109,11 +110,25 @@ pub fn install_ipykernel_and_assign(
     let window_handle = window.window_handle();
 
     let install_task = cx.background_spawn(async move {
-        let output = util::command::new_command(python_path.to_string_lossy().as_ref())
-            .args(&["-m", "pip", "install", "ipykernel"])
-            .output()
-            .await
-            .context("failed to run pip install ipykernel")?;
+        let output = if is_uv {
+            util::command::new_command("uv")
+                .args(&[
+                    "pip",
+                    "install",
+                    "ipykernel",
+                    "--python",
+                    &python_path.to_string_lossy(),
+                ])
+                .output()
+                .await
+                .context("failed to run uv pip install ipykernel")?
+        } else {
+            util::command::new_command(python_path.to_string_lossy().as_ref())
+                .args(&["-m", "pip", "install", "ipykernel"])
+                .output()
+                .await
+                .context("failed to run pip install ipykernel")?
+        };
 
         if output.status.success() {
             anyhow::Ok(())
@@ -146,6 +161,11 @@ pub fn install_ipykernel_and_assign(
 
                 window_handle
                     .update(cx, |_, window, cx| {
+                        let store = ReplStore::global(cx);
+                        store.update(cx, |store, cx| {
+                            store.mark_ipykernel_installed(cx, &env_spec);
+                        });
+
                         let updated_spec =
                             KernelSpecification::PythonEnv(PythonEnvKernelSpecification {
                                 has_ipykernel: true,

crates/repl/src/repl_store.rs 🔗

@@ -13,8 +13,8 @@ use settings::{Settings, SettingsStore};
 use util::rel_path::RelPath;
 
 use crate::kernels::{
-    Kernel, list_remote_kernelspecs, local_kernel_specifications, python_env_kernel_specifications,
-    wsl_kernel_specifications,
+    Kernel, PythonEnvKernelSpecification, list_remote_kernelspecs, local_kernel_specifications,
+    python_env_kernel_specifications, wsl_kernel_specifications,
 };
 use crate::{JupyterSettings, KernelSpecification, Session};
 
@@ -136,6 +136,23 @@ impl ReplStore {
         cx.notify();
     }
 
+    pub fn mark_ipykernel_installed(
+        &mut self,
+        cx: &mut Context<Self>,
+        spec: &PythonEnvKernelSpecification,
+    ) {
+        for specs in self.kernel_specifications_for_worktree.values_mut() {
+            for kernel_spec in specs.iter_mut() {
+                if let KernelSpecification::PythonEnv(env_spec) = kernel_spec {
+                    if env_spec == spec {
+                        env_spec.has_ipykernel = true;
+                    }
+                }
+            }
+        }
+        cx.notify();
+    }
+
     pub fn refresh_python_kernelspecs(
         &mut self,
         worktree_id: WorktreeId,