Runtimes UI Starter (#13625)

Kyle Kelley and Kirill created

Initial runtimes UI panel. The main draw here is that all message
subscription occurs with two background tasks that run for the life of
the kernel. Follow on to #12062

* [x] Disable previous cmd-enter behavior only if runtimes are enabled
in settings
* [x] Only show the runtimes panel if it is enabled via settings
* [x] Create clean UI for the current sessions

### Running Kernels UI

<img width="205" alt="image"
src="https://github.com/zed-industries/zed/assets/836375/814ae79b-0807-4e23-bc95-77ce64f9d732">

* [x] List running kernels
* [x] Implement shutdown
* [x] Delete connection file on `drop` of `RunningKernel`
* [x] Implement interrupt

#### Project-specific Kernel Settings

- [x] Modify JupyterSettings to include a `kernel_selections` field
(`HashMap<String, String>`).
- [x] Implement saving and loading of kernel selections to/from
`.zed/settings.json` (by default, rather than global settings?)

#### Kernel Selection Persistence

- [x] Save the selected kernel for each language when the user makes a
choice.
- [x] Load these selections when the RuntimePanel is initialized.

#### Use Selected Kernels

- [x] Modify kernel launch to use the selected kernel for the detected
language.
- [x] Fallback to default behavior if no selection is made.

### Empty states

- [x] Create helpful UI for when the user has 0 kernels they can launch
and/or 0 kernels running

<img width="694" alt="image"
src="https://github.com/zed-industries/zed/assets/836375/d6a75939-e4e4-40fb-80fe-014da041cc3c">

## Future work

### Kernel Discovery

- Improve the kernel discovery process to handle various installation
methods (system, virtualenv, poetry, etc.).
- Create a way to refresh the available kernels on demand

### Documentation:

- Update documentation to explain how users can configure kernels for
their projects.
- Provide examples of .zed/settings.json configurations for kernel
selection.

### Kernel Selection UI

- Implement a new section in the RuntimePanel to display available
kernels.
- Group on the language name from the kernel specification 
- Create a dropdown for each language group to select the default
kernel.


Release Notes:

- N/A

---------

Co-authored-by: Kirill <kirill@zed.dev>

Change summary

crates/editor/src/editor.rs         |   2 
crates/repl/src/jupyter_settings.rs | 149 ++++++++
crates/repl/src/kernels.rs          | 395 ++++++++++++++++++++++
crates/repl/src/outputs.rs          |  43 +-
crates/repl/src/repl.rs             | 551 ------------------------------
crates/repl/src/runtime_panel.rs    | 443 ++++++++++++++++++++++++
crates/repl/src/runtime_settings.rs |  66 ---
crates/repl/src/runtimes.rs         | 329 ------------------
crates/repl/src/session.rs          | 400 ++++++++++++++++++++++
crates/repl/src/stdio.rs            |  15 
crates/zed/src/main.rs              |   2 
crates/zed/src/zed.rs               |   8 
12 files changed, 1,438 insertions(+), 965 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -12719,7 +12719,7 @@ pub(crate) fn split_words(text: &str) -> impl std::iter::Iterator<Item = &str> +
         })
 }
 
-trait RangeToAnchorExt {
+pub trait RangeToAnchorExt {
     fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor>;
 }
 

crates/repl/src/jupyter_settings.rs 🔗

@@ -0,0 +1,149 @@
+use std::collections::HashMap;
+
+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 enabled: bool,
+    pub dock: JupyterDockPosition,
+    pub default_width: Pixels,
+    pub kernel_selections: HashMap<String, String>,
+}
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct JupyterSettingsContent {
+    /// Whether the Jupyter feature is enabled.
+    ///
+    /// Default: `false`
+    enabled: Option<bool>,
+    /// 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 {
+            enabled: Some(false),
+            dock: Some(JupyterDockPosition::Right),
+            default_width: Some(640.0),
+            kernel_selections: Some(HashMap::new()),
+        }
+    }
+}
+
+impl Settings for JupyterSettings {
+    const KEY: Option<&'static str> = Some("jupyter");
+
+    type FileContent = JupyterSettingsContent;
+
+    fn load(
+        sources: SettingsSources<Self::FileContent>,
+        _cx: &mut gpui::AppContext,
+    ) -> anyhow::Result<Self>
+    where
+        Self: Sized,
+    {
+        let mut settings = JupyterSettings::default();
+
+        for value in sources.defaults_and_customizations() {
+            if let Some(enabled) = value.enabled {
+                settings.enabled = enabled;
+            }
+            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());
+                }
+            }
+        }
+
+        Ok(settings)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use gpui::{AppContext, UpdateGlobal};
+    use settings::SettingsStore;
+
+    use super::*;
+
+    #[gpui::test]
+    fn test_deserialize_jupyter_settings(cx: &mut AppContext) {
+        let store = settings::SettingsStore::test(cx);
+        cx.set_global(store);
+
+        JupyterSettings::register(cx);
+
+        assert_eq!(JupyterSettings::get_global(cx).enabled, 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| {
+            store
+                .set_user_settings(
+                    r#"{
+                        "jupyter": {
+                            "enabled": true,
+                            "dock": "left",
+                            "default_width": 800.0
+                        }
+                    }"#,
+                    cx,
+                )
+                .unwrap();
+        });
+
+        assert_eq!(JupyterSettings::get_global(cx).enabled, 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/kernels.rs 🔗

@@ -0,0 +1,395 @@
+use anyhow::{Context as _, Result};
+use futures::{
+    channel::mpsc::{self, Receiver},
+    future::Shared,
+    stream::{self, SelectAll, StreamExt},
+    SinkExt as _,
+};
+use gpui::{AppContext, EntityId, Task};
+use project::Fs;
+use runtimelib::{
+    dirs, ConnectionInfo, ExecutionState, JupyterKernelspec, JupyterMessage, JupyterMessageContent,
+    KernelInfoReply,
+};
+use smol::{net::TcpListener, process::Command};
+use std::{
+    fmt::Debug,
+    net::{IpAddr, Ipv4Addr, SocketAddr},
+    path::PathBuf,
+    sync::Arc,
+};
+use ui::{Color, Indicator};
+
+#[derive(Debug, Clone)]
+pub struct KernelSpecification {
+    pub name: String,
+    pub path: PathBuf,
+    pub kernelspec: JupyterKernelspec,
+}
+
+impl KernelSpecification {
+    #[must_use]
+    fn command(&self, connection_path: &PathBuf) -> anyhow::Result<Command> {
+        let argv = &self.kernelspec.argv;
+
+        anyhow::ensure!(!argv.is_empty(), "Empty argv in kernelspec {}", self.name);
+        anyhow::ensure!(argv.len() >= 2, "Invalid argv in kernelspec {}", self.name);
+        anyhow::ensure!(
+            argv.iter().any(|arg| arg == "{connection_file}"),
+            "Missing 'connection_file' in argv in kernelspec {}",
+            self.name
+        );
+
+        let mut cmd = Command::new(&argv[0]);
+
+        for arg in &argv[1..] {
+            if arg == "{connection_file}" {
+                cmd.arg(connection_path);
+            } else {
+                cmd.arg(arg);
+            }
+        }
+
+        if let Some(env) = &self.kernelspec.env {
+            cmd.envs(env);
+        }
+
+        Ok(cmd)
+    }
+}
+
+// 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]> {
+    let mut addr_zeroport: SocketAddr = SocketAddr::new(ip, 0);
+    addr_zeroport.set_port(0);
+    let mut ports: [u16; 5] = [0; 5];
+    for i in 0..5 {
+        let listener = TcpListener::bind(addr_zeroport).await?;
+        let addr = listener.local_addr()?;
+        ports[i] = addr.port();
+    }
+    Ok(ports)
+}
+
+#[derive(Debug)]
+pub enum Kernel {
+    RunningKernel(RunningKernel),
+    StartingKernel(Shared<Task<()>>),
+    ErroredLaunch(String),
+    ShuttingDown,
+    Shutdown,
+}
+
+impl Kernel {
+    pub fn dot(&mut self) -> Indicator {
+        match self {
+            Kernel::RunningKernel(kernel) => match kernel.execution_state {
+                ExecutionState::Idle => Indicator::dot().color(Color::Success),
+                ExecutionState::Busy => Indicator::dot().color(Color::Modified),
+            },
+            Kernel::StartingKernel(_) => Indicator::dot().color(Color::Modified),
+            Kernel::ErroredLaunch(_) => Indicator::dot().color(Color::Error),
+            Kernel::ShuttingDown => Indicator::dot().color(Color::Modified),
+            Kernel::Shutdown => Indicator::dot().color(Color::Disabled),
+        }
+    }
+
+    pub fn set_execution_state(&mut self, status: &ExecutionState) {
+        match self {
+            Kernel::RunningKernel(running_kernel) => {
+                running_kernel.execution_state = status.clone();
+            }
+            _ => {}
+        }
+    }
+
+    pub fn set_kernel_info(&mut self, kernel_info: &KernelInfoReply) {
+        match self {
+            Kernel::RunningKernel(running_kernel) => {
+                running_kernel.kernel_info = Some(kernel_info.clone());
+            }
+            _ => {}
+        }
+    }
+}
+
+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<()>>,
+    connection_path: PathBuf,
+    pub request_tx: mpsc::Sender<JupyterMessage>,
+    pub execution_state: ExecutionState,
+    pub kernel_info: Option<KernelInfoReply>,
+}
+
+type JupyterMessageChannel = stream::SelectAll<Receiver<JupyterMessage>>;
+
+impl Debug for RunningKernel {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("RunningKernel")
+            .field("process", &self.process)
+            .finish()
+    }
+}
+
+impl RunningKernel {
+    pub fn new(
+        kernel_specification: KernelSpecification,
+        entity_id: EntityId,
+        fs: Arc<dyn Fs>,
+        cx: &mut AppContext,
+    ) -> Task<anyhow::Result<(Self, JupyterMessageChannel)>> {
+        cx.spawn(|cx| async move {
+            let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
+            let ports = peek_ports(ip).await?;
+
+            let connection_info = ConnectionInfo {
+                transport: "tcp".to_string(),
+                ip: ip.to_string(),
+                stdin_port: ports[0],
+                control_port: ports[1],
+                hb_port: ports[2],
+                shell_port: ports[3],
+                iopub_port: ports[4],
+                signature_scheme: "hmac-sha256".to_string(),
+                key: uuid::Uuid::new_v4().to_string(),
+                kernel_name: Some(format!("zed-{}", kernel_specification.name)),
+            };
+
+            let connection_path = dirs::runtime_dir().join(format!("kernel-zed-{entity_id}.json"));
+            let content = serde_json::to_string(&connection_info)?;
+            // write out file to disk for kernel
+            fs.atomic_write(connection_path.clone(), content).await?;
+
+            let mut cmd = kernel_specification.command(&connection_path)?;
+            let process = cmd
+                // .stdout(Stdio::null())
+                // .stderr(Stdio::null())
+                .kill_on_drop(true)
+                .spawn()
+                .context("failed to start the kernel process")?;
+
+            let mut iopub_socket = connection_info.create_client_iopub_connection("").await?;
+            let mut shell_socket = connection_info.create_client_shell_connection().await?;
+            let mut control_socket = connection_info.create_client_control_connection().await?;
+
+            let (mut iopub, iosub) = futures::channel::mpsc::channel(100);
+
+            let (request_tx, mut request_rx) =
+                futures::channel::mpsc::channel::<JupyterMessage>(100);
+
+            let (mut control_reply_tx, control_reply_rx) = futures::channel::mpsc::channel(100);
+            let (mut shell_reply_tx, shell_reply_rx) = futures::channel::mpsc::channel(100);
+
+            let mut messages_rx = SelectAll::new();
+            messages_rx.push(iosub);
+            messages_rx.push(control_reply_rx);
+            messages_rx.push(shell_reply_rx);
+
+            let _iopub_task = cx.background_executor().spawn({
+                async move {
+                    while let Ok(message) = iopub_socket.read().await {
+                        iopub.send(message).await?;
+                    }
+                    anyhow::Ok(())
+                }
+            });
+
+            let (mut control_request_tx, mut control_request_rx) =
+                futures::channel::mpsc::channel(100);
+            let (mut shell_request_tx, mut shell_request_rx) = futures::channel::mpsc::channel(100);
+
+            let _routing_task = cx.background_executor().spawn({
+                async move {
+                    while let Some(message) = request_rx.next().await {
+                        match message.content {
+                            JupyterMessageContent::DebugRequest(_)
+                            | JupyterMessageContent::InterruptRequest(_)
+                            | JupyterMessageContent::ShutdownRequest(_) => {
+                                control_request_tx.send(message).await?;
+                            }
+                            _ => {
+                                shell_request_tx.send(message).await?;
+                            }
+                        }
+                    }
+                    anyhow::Ok(())
+                }
+            });
+
+            let _shell_task = cx.background_executor().spawn({
+                async move {
+                    while let Some(message) = shell_request_rx.next().await {
+                        shell_socket.send(message).await.ok();
+                        let reply = shell_socket.read().await?;
+                        shell_reply_tx.send(reply).await?;
+                    }
+                    anyhow::Ok(())
+                }
+            });
+
+            let _control_task = cx.background_executor().spawn({
+                async move {
+                    while let Some(message) = control_request_rx.next().await {
+                        control_socket.send(message).await.ok();
+                        let reply = control_socket.read().await?;
+                        control_reply_tx.send(reply).await?;
+                    }
+                    anyhow::Ok(())
+                }
+            });
+
+            anyhow::Ok((
+                Self {
+                    process,
+                    request_tx,
+                    _shell_task,
+                    _iopub_task,
+                    _control_task,
+                    _routing_task,
+                    connection_path,
+                    execution_state: ExecutionState::Busy,
+                    kernel_info: None,
+                },
+                messages_rx,
+            ))
+        })
+    }
+}
+
+impl Drop for RunningKernel {
+    fn drop(&mut self) {
+        std::fs::remove_file(&self.connection_path).ok();
+
+        self.request_tx.close_channel();
+    }
+}
+
+async fn read_kernelspec_at(
+    // Path should be a directory to a jupyter kernelspec, as in
+    // /usr/local/share/jupyter/kernels/python3
+    kernel_dir: PathBuf,
+    fs: &dyn Fs,
+) -> anyhow::Result<KernelSpecification> {
+    let path = kernel_dir;
+    let kernel_name = if let Some(kernel_name) = path.file_name() {
+        kernel_name.to_string_lossy().to_string()
+    } else {
+        anyhow::bail!("Invalid kernelspec directory: {path:?}");
+    };
+
+    if !fs.is_dir(path.as_path()).await {
+        anyhow::bail!("Not a directory: {path:?}");
+    }
+
+    let expected_kernel_json = path.join("kernel.json");
+    let spec = fs.load(expected_kernel_json.as_path()).await?;
+    let spec = serde_json::from_str::<JupyterKernelspec>(&spec)?;
+
+    Ok(KernelSpecification {
+        name: kernel_name,
+        path,
+        kernelspec: spec,
+    })
+}
+
+/// Read a directory of kernelspec directories
+async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> anyhow::Result<Vec<KernelSpecification>> {
+    let mut kernelspec_dirs = fs.read_dir(&path).await?;
+
+    let mut valid_kernelspecs = Vec::new();
+    while let Some(path) = kernelspec_dirs.next().await {
+        match path {
+            Ok(path) => {
+                if fs.is_dir(path.as_path()).await {
+                    if let Ok(kernelspec) = read_kernelspec_at(path, fs).await {
+                        valid_kernelspecs.push(kernelspec);
+                    }
+                }
+            }
+            Err(err) => log::warn!("Error reading kernelspec directory: {err:?}"),
+        }
+    }
+
+    Ok(valid_kernelspecs)
+}
+
+pub async fn kernel_specifications(fs: Arc<dyn Fs>) -> anyhow::Result<Vec<KernelSpecification>> {
+    let data_dirs = dirs::data_dirs();
+    let kernel_dirs = data_dirs
+        .iter()
+        .map(|dir| dir.join("kernels"))
+        .map(|path| read_kernels_dir(path, fs.as_ref()))
+        .collect::<Vec<_>>();
+
+    let kernel_dirs = futures::future::join_all(kernel_dirs).await;
+    let kernel_dirs = kernel_dirs
+        .into_iter()
+        .filter_map(Result::ok)
+        .flatten()
+        .collect::<Vec<_>>();
+
+    Ok(kernel_dirs)
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use std::path::PathBuf;
+
+    use gpui::TestAppContext;
+    use project::FakeFs;
+    use serde_json::json;
+
+    #[gpui::test]
+    async fn test_get_kernelspecs(cx: &mut TestAppContext) {
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            "/jupyter",
+            json!({
+                ".zed": {
+                    "settings.json": r#"{ "tab_size": 8 }"#,
+                    "tasks.json": r#"[{
+                        "label": "cargo check",
+                        "command": "cargo",
+                        "args": ["check", "--all"]
+                    },]"#,
+                },
+                "kernels": {
+                    "python": {
+                        "kernel.json": r#"{
+                            "display_name": "Python 3",
+                            "language": "python",
+                            "argv": ["python3", "-m", "ipykernel_launcher", "-f", "{connection_file}"],
+                            "env": {}
+                        }"#
+                    },
+                    "deno": {
+                        "kernel.json": r#"{
+                            "display_name": "Deno",
+                            "language": "typescript",
+                            "argv": ["deno", "run", "--unstable", "--allow-net", "--allow-read", "https://deno.land/std/http/file_server.ts", "{connection_file}"],
+                            "env": {}
+                        }"#
+                    }
+                },
+            }),
+        )
+        .await;
+
+        let mut kernels = read_kernels_dir(PathBuf::from("/jupyter/kernels"), fs.as_ref())
+            .await
+            .unwrap();
+
+        kernels.sort_by(|a, b| a.name.cmp(&b.name));
+
+        assert_eq!(
+            kernels.iter().map(|c| c.name.clone()).collect::<Vec<_>>(),
+            vec!["deno", "python"]
+        );
+    }
+}

crates/repl/src/outputs.rs 🔗

@@ -301,14 +301,17 @@ impl From<&MimeBundle> for OutputType {
     }
 }
 
-#[derive(Default)]
+#[derive(Default, Clone)]
 pub enum ExecutionStatus {
     #[default]
     Unknown,
-    #[allow(unused)]
     ConnectingToKernel,
+    Queued,
     Executing,
     Finished,
+    ShuttingDown,
+    Shutdown,
+    KernelErrored(String),
 }
 
 pub struct ExecutionView {
@@ -317,10 +320,10 @@ pub struct ExecutionView {
 }
 
 impl ExecutionView {
-    pub fn new(_cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(status: ExecutionStatus, _cx: &mut ViewContext<Self>) -> Self {
         Self {
             outputs: Default::default(),
-            status: ExecutionStatus::Unknown,
+            status,
         }
     }
 
@@ -358,14 +361,16 @@ impl ExecutionView {
                             self.outputs.push(output);
                         }
 
+                        // Comments from @rgbkrk, reach out with questions
+
                         // Set next input adds text to the next cell. Not required to support.
-                        // However, this could be implemented by
+                        // However, this could be implemented by adding text to the buffer.
                         // runtimelib::Payload::SetNextInput { text, replace } => todo!(),
 
                         // Not likely to be used in the context of Zed, where someone could just open the buffer themselves
                         // runtimelib::Payload::EditMagic { filename, line_number } => todo!(),
 
-                        //
+                        // Ask the user if they want to exit the kernel. Not required to support.
                         // runtimelib::Payload::AskExit { keepkernel } => todo!(),
                         _ => {}
                     }
@@ -431,28 +436,24 @@ impl ExecutionView {
         new_terminal.append_text(text);
         Some(OutputType::Stream(new_terminal))
     }
-
-    pub fn set_status(&mut self, status: ExecutionStatus, cx: &mut ViewContext<Self>) {
-        self.status = status;
-        cx.notify();
-    }
 }
 
 impl Render for ExecutionView {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         if self.outputs.len() == 0 {
-            match self.status {
-                ExecutionStatus::ConnectingToKernel => {
-                    return div().child("Connecting to kernel...").into_any_element()
-                }
-                ExecutionStatus::Executing => {
-                    return div().child("Executing...").into_any_element()
-                }
-                ExecutionStatus::Finished => {
-                    return div().child(Icon::new(IconName::Check)).into_any_element()
+            return match &self.status {
+                ExecutionStatus::ConnectingToKernel => div().child("Connecting to kernel..."),
+                ExecutionStatus::Executing => div().child("Executing..."),
+                ExecutionStatus::Finished => div().child(Icon::new(IconName::Check)),
+                ExecutionStatus::Unknown => div().child("..."),
+                ExecutionStatus::ShuttingDown => div().child("Kernel shutting down..."),
+                ExecutionStatus::Shutdown => div().child("Kernel shutdown"),
+                ExecutionStatus::Queued => div().child("Queued"),
+                ExecutionStatus::KernelErrored(error) => {
+                    div().child(format!("Kernel error: {}", error))
                 }
-                ExecutionStatus::Unknown => return div().child("...").into_any_element(),
             }
+            .into_any_element();
         }
 
         div()

crates/repl/src/repl.rs 🔗

@@ -1,51 +1,19 @@
-use anyhow::{anyhow, Context as _, Result};
-use async_dispatcher::{set_dispatcher, timeout, Dispatcher, Runnable};
-use collections::{HashMap, HashSet};
-use editor::{
-    display_map::{
-        BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock,
-    },
-    Anchor, AnchorRangeExt, Editor,
-};
-use futures::{
-    channel::mpsc::{self, UnboundedSender},
-    future::Shared,
-    Future, FutureExt, SinkExt as _, StreamExt,
-};
-use gpui::prelude::*;
-use gpui::{
-    actions, AppContext, Context, EntityId, Global, Model, ModelContext, PlatformDispatcher, Task,
-    WeakView,
-};
-use gpui::{Entity, View};
-use language::Point;
-use outputs::{ExecutionStatus, ExecutionView, LineHeight as _};
-use project::Fs;
-use runtime_settings::JupyterSettings;
-use runtimelib::JupyterMessageContent;
-use settings::{Settings as _, SettingsStore};
-use std::{ops::Range, time::Instant};
+use async_dispatcher::{set_dispatcher, Dispatcher, Runnable};
+use gpui::{AppContext, PlatformDispatcher};
+use settings::Settings as _;
 use std::{sync::Arc, time::Duration};
-use theme::{ActiveTheme, ThemeSettings};
-use ui::prelude::*;
-use workspace::Workspace;
 
+mod jupyter_settings;
+mod kernels;
 mod outputs;
-// mod runtime_panel;
-mod runtime_settings;
-mod runtimes;
+mod runtime_panel;
+mod session;
 mod stdio;
 
-use runtimes::{get_runtime_specifications, Request, RunningKernel, RuntimeSpecification};
+pub use jupyter_settings::JupyterSettings;
+pub use runtime_panel::RuntimePanel;
 
-actions!(repl, [Run]);
-
-#[derive(Clone)]
-pub struct RuntimeManagerGlobal(Model<RuntimeManager>);
-
-impl Global for RuntimeManagerGlobal {}
-
-pub fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
+fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
     struct ZedDispatcher {
         dispatcher: Arc<dyn PlatformDispatcher>,
     }
@@ -69,503 +37,8 @@ pub fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
     }
 }
 
-pub fn init(fs: Arc<dyn Fs>, cx: &mut AppContext) {
+pub fn init(cx: &mut AppContext) {
     set_dispatcher(zed_dispatcher(cx));
     JupyterSettings::register(cx);
-
-    observe_jupyter_settings_changes(fs.clone(), cx);
-
-    cx.observe_new_views(
-        |workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
-            workspace.register_action(run);
-        },
-    )
-    .detach();
-
-    let settings = JupyterSettings::get_global(cx);
-
-    if !settings.enabled {
-        return;
-    }
-
-    initialize_runtime_manager(fs, cx);
-}
-
-fn initialize_runtime_manager(fs: Arc<dyn Fs>, cx: &mut AppContext) {
-    let runtime_manager = cx.new_model(|cx| RuntimeManager::new(fs.clone(), cx));
-    RuntimeManager::set_global(runtime_manager.clone(), cx);
-
-    cx.spawn(|mut cx| async move {
-        let fs = fs.clone();
-
-        let runtime_specifications = get_runtime_specifications(fs).await?;
-
-        runtime_manager.update(&mut cx, |this, _cx| {
-            this.runtime_specifications = runtime_specifications;
-        })?;
-
-        anyhow::Ok(())
-    })
-    .detach_and_log_err(cx);
-}
-
-fn observe_jupyter_settings_changes(fs: Arc<dyn Fs>, cx: &mut AppContext) {
-    cx.observe_global::<SettingsStore>(move |cx| {
-        let settings = JupyterSettings::get_global(cx);
-        if settings.enabled && RuntimeManager::global(cx).is_none() {
-            initialize_runtime_manager(fs.clone(), cx);
-        } else {
-            RuntimeManager::remove_global(cx);
-            // todo!(): Remove action from workspace(s)
-        }
-    })
-    .detach();
-}
-
-#[derive(Debug)]
-pub enum Kernel {
-    RunningKernel(RunningKernel),
-    StartingKernel(Shared<Task<()>>),
-    FailedLaunch,
-}
-
-// Per workspace
-pub struct RuntimeManager {
-    fs: Arc<dyn Fs>,
-    runtime_specifications: Vec<RuntimeSpecification>,
-
-    instances: HashMap<EntityId, Kernel>,
-    editors: HashMap<WeakView<Editor>, EditorRuntimeState>,
-    // todo!(): Next
-    // To reduce the number of open tasks and channels we have, let's feed the response
-    // messages by ID over to the paired ExecutionView
-    _execution_views_by_id: HashMap<String, View<ExecutionView>>,
-}
-
-#[derive(Debug, Clone)]
-struct EditorRuntimeState {
-    blocks: Vec<EditorRuntimeBlock>,
-    // todo!(): Store a subscription to the editor so we can drop them when the editor is dropped
-    // subscription: gpui::Subscription,
-}
-
-#[derive(Debug, Clone)]
-struct EditorRuntimeBlock {
-    code_range: Range<Anchor>,
-    _execution_id: String,
-    block_id: BlockId,
-    _execution_view: View<ExecutionView>,
-}
-
-impl RuntimeManager {
-    pub fn new(fs: Arc<dyn Fs>, _cx: &mut AppContext) -> Self {
-        Self {
-            fs,
-            runtime_specifications: Default::default(),
-            instances: Default::default(),
-            editors: Default::default(),
-            _execution_views_by_id: Default::default(),
-        }
-    }
-
-    fn get_or_launch_kernel(
-        &mut self,
-        entity_id: EntityId,
-        language_name: Arc<str>,
-        cx: &mut ModelContext<Self>,
-    ) -> Task<Result<UnboundedSender<Request>>> {
-        let kernel = self.instances.get(&entity_id);
-        let pending_kernel_start = match kernel {
-            Some(Kernel::RunningKernel(running_kernel)) => {
-                return Task::ready(anyhow::Ok(running_kernel.request_tx.clone()));
-            }
-            Some(Kernel::StartingKernel(task)) => task.clone(),
-            Some(Kernel::FailedLaunch) | None => {
-                self.instances.remove(&entity_id);
-
-                let kernel = self.launch_kernel(entity_id, language_name, cx);
-                let pending_kernel = cx
-                    .spawn(|this, mut cx| async move {
-                        let running_kernel = kernel.await;
-
-                        match running_kernel {
-                            Ok(running_kernel) => {
-                                let _ = this.update(&mut cx, |this, _cx| {
-                                    this.instances
-                                        .insert(entity_id, Kernel::RunningKernel(running_kernel));
-                                });
-                            }
-                            Err(_err) => {
-                                let _ = this.update(&mut cx, |this, _cx| {
-                                    this.instances.insert(entity_id, Kernel::FailedLaunch);
-                                });
-                            }
-                        }
-                    })
-                    .shared();
-
-                self.instances
-                    .insert(entity_id, Kernel::StartingKernel(pending_kernel.clone()));
-
-                pending_kernel
-            }
-        };
-
-        cx.spawn(|this, mut cx| async move {
-            pending_kernel_start.await;
-
-            this.update(&mut cx, |this, _cx| {
-                let kernel = this
-                    .instances
-                    .get(&entity_id)
-                    .ok_or(anyhow!("unable to get a running kernel"))?;
-
-                match kernel {
-                    Kernel::RunningKernel(running_kernel) => Ok(running_kernel.request_tx.clone()),
-                    _ => Err(anyhow!("unable to get a running kernel")),
-                }
-            })?
-        })
-    }
-
-    fn launch_kernel(
-        &mut self,
-        entity_id: EntityId,
-        language_name: Arc<str>,
-        cx: &mut ModelContext<Self>,
-    ) -> Task<Result<RunningKernel>> {
-        // Get first runtime that matches the language name (for now)
-        let runtime_specification =
-            self.runtime_specifications
-                .iter()
-                .find(|runtime_specification| {
-                    runtime_specification.kernelspec.language == language_name.to_string()
-                });
-
-        let runtime_specification = match runtime_specification {
-            Some(runtime_specification) => runtime_specification,
-            None => {
-                return Task::ready(Err(anyhow::anyhow!(
-                    "No runtime found for language {}",
-                    language_name
-                )));
-            }
-        };
-
-        let runtime_specification = runtime_specification.clone();
-
-        let fs = self.fs.clone();
-
-        cx.spawn(|_, cx| async move {
-            let running_kernel =
-                RunningKernel::new(runtime_specification, entity_id, fs.clone(), cx);
-
-            let running_kernel = running_kernel.await?;
-
-            let mut request_tx = running_kernel.request_tx.clone();
-
-            let overall_timeout_duration = Duration::from_secs(10);
-
-            let start_time = Instant::now();
-
-            loop {
-                if start_time.elapsed() > overall_timeout_duration {
-                    // todo!(): Kill the kernel
-                    return Err(anyhow::anyhow!("Kernel did not respond in time"));
-                }
-
-                let (tx, rx) = mpsc::unbounded();
-                match request_tx
-                    .send(Request {
-                        request: runtimelib::KernelInfoRequest {}.into(),
-                        responses_rx: tx,
-                    })
-                    .await
-                {
-                    Ok(_) => {}
-                    Err(_err) => {
-                        break;
-                    }
-                };
-
-                let mut rx = rx.fuse();
-
-                let kernel_info_timeout = Duration::from_secs(1);
-
-                let mut got_kernel_info = false;
-                while let Ok(Some(message)) = timeout(kernel_info_timeout, rx.next()).await {
-                    match message {
-                        JupyterMessageContent::KernelInfoReply(_) => {
-                            got_kernel_info = true;
-                        }
-                        _ => {}
-                    }
-                }
-
-                if got_kernel_info {
-                    break;
-                }
-            }
-
-            anyhow::Ok(running_kernel)
-        })
-    }
-
-    fn execute_code(
-        &mut self,
-        entity_id: EntityId,
-        language_name: Arc<str>,
-        code: String,
-        cx: &mut ModelContext<Self>,
-    ) -> impl Future<Output = Result<mpsc::UnboundedReceiver<JupyterMessageContent>>> {
-        let (tx, rx) = mpsc::unbounded();
-
-        let request_tx = self.get_or_launch_kernel(entity_id, language_name, cx);
-
-        async move {
-            let request_tx = request_tx.await?;
-
-            request_tx
-                .unbounded_send(Request {
-                    request: runtimelib::ExecuteRequest {
-                        code,
-                        allow_stdin: false,
-                        silent: false,
-                        store_history: true,
-                        stop_on_error: true,
-                        ..Default::default()
-                    }
-                    .into(),
-                    responses_rx: tx,
-                })
-                .context("Failed to send execution request")?;
-
-            Ok(rx)
-        }
-    }
-
-    pub fn global(cx: &AppContext) -> Option<Model<Self>> {
-        cx.try_global::<RuntimeManagerGlobal>()
-            .map(|runtime_manager| runtime_manager.0.clone())
-    }
-
-    pub fn set_global(runtime_manager: Model<Self>, cx: &mut AppContext) {
-        cx.set_global(RuntimeManagerGlobal(runtime_manager));
-    }
-
-    pub fn remove_global(cx: &mut AppContext) {
-        if RuntimeManager::global(cx).is_some() {
-            cx.remove_global::<RuntimeManagerGlobal>();
-        }
-    }
-}
-
-pub fn get_active_editor(
-    workspace: &mut Workspace,
-    cx: &mut ViewContext<Workspace>,
-) -> Option<View<Editor>> {
-    workspace
-        .active_item(cx)
-        .and_then(|item| item.act_as::<Editor>(cx))
-}
-
-// Gets the active selection in the editor or the current line
-pub fn selection(editor: View<Editor>, cx: &mut ViewContext<Workspace>) -> Range<Anchor> {
-    let editor = editor.read(cx);
-    let selection = editor.selections.newest::<usize>(cx);
-    let buffer = editor.buffer().read(cx).snapshot(cx);
-
-    let range = if selection.is_empty() {
-        let cursor = selection.head();
-
-        let line_start = buffer.offset_to_point(cursor).row;
-        let mut start_offset = buffer.point_to_offset(Point::new(line_start, 0));
-
-        // Iterate backwards to find the start of the line
-        while start_offset > 0 {
-            let ch = buffer.chars_at(start_offset - 1).next().unwrap_or('\0');
-            if ch == '\n' {
-                break;
-            }
-            start_offset -= 1;
-        }
-
-        let mut end_offset = cursor;
-
-        // Iterate forwards to find the end of the line
-        while end_offset < buffer.len() {
-            let ch = buffer.chars_at(end_offset).next().unwrap_or('\0');
-            if ch == '\n' {
-                break;
-            }
-            end_offset += 1;
-        }
-
-        // Create a range from the start to the end of the line
-        start_offset..end_offset
-    } else {
-        selection.range()
-    };
-
-    let anchor_range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
-    anchor_range
-}
-
-pub fn run(workspace: &mut Workspace, _: &Run, cx: &mut ViewContext<Workspace>) {
-    let (editor, runtime_manager) = if let (Some(editor), Some(runtime_manager)) =
-        (get_active_editor(workspace, cx), RuntimeManager::global(cx))
-    {
-        (editor, runtime_manager)
-    } else {
-        log::warn!("No active editor or runtime manager found");
-        return;
-    };
-
-    let anchor_range = selection(editor.clone(), cx);
-
-    let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
-
-    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);
-
-    let language_name = if start_language == end_language {
-        start_language
-            .map(|language| language.code_fence_block_name())
-            .filter(|lang| **lang != *"markdown")
-    } else {
-        // If the selection spans multiple languages, don't run it
-        return;
-    };
-
-    let language_name = if let Some(language_name) = language_name {
-        language_name
-    } else {
-        return;
-    };
-
-    let entity_id = editor.entity_id();
-
-    let execution_view = cx.new_view(|cx| ExecutionView::new(cx));
-
-    // If any block overlaps with the new block, remove it
-    // TODO: When inserting a new block, put it in order so that search is efficient
-    let blocks_to_remove = runtime_manager.update(cx, |runtime_manager, _cx| {
-        // Get the current `EditorRuntimeState` for this runtime_manager, inserting it if it doesn't exist
-        let editor_runtime_state = runtime_manager
-            .editors
-            .entry(editor.downgrade())
-            .or_insert_with(|| EditorRuntimeState { blocks: Vec::new() });
-
-        let mut blocks_to_remove: HashSet<BlockId> = HashSet::default();
-
-        editor_runtime_state.blocks.retain(|block| {
-            if anchor_range.overlaps(&block.code_range, &buffer) {
-                blocks_to_remove.insert(block.block_id);
-                // Drop this block
-                false
-            } else {
-                true
-            }
-        });
-
-        blocks_to_remove
-    });
-
-    let blocks_to_remove = blocks_to_remove.clone();
-
-    let block_id = editor.update(cx, |editor, cx| {
-        editor.remove_blocks(blocks_to_remove, None, cx);
-        let block = BlockProperties {
-            position: anchor_range.end,
-            height: execution_view.num_lines(cx).saturating_add(1),
-            style: BlockStyle::Sticky,
-            render: create_output_area_render(execution_view.clone()),
-            disposition: BlockDisposition::Below,
-        };
-
-        editor.insert_blocks([block], None, cx)[0]
-    });
-
-    let receiver = runtime_manager.update(cx, |runtime_manager, cx| {
-        let editor_runtime_state = runtime_manager
-            .editors
-            .entry(editor.downgrade())
-            .or_insert_with(|| EditorRuntimeState { blocks: Vec::new() });
-
-        let editor_runtime_block = EditorRuntimeBlock {
-            code_range: anchor_range.clone(),
-            block_id,
-            _execution_view: execution_view.clone(),
-            _execution_id: Default::default(),
-        };
-
-        editor_runtime_state
-            .blocks
-            .push(editor_runtime_block.clone());
-
-        runtime_manager.execute_code(entity_id, language_name, selected_text.clone(), cx)
-    });
-
-    cx.spawn(|_this, mut cx| async move {
-        execution_view.update(&mut cx, |execution_view, cx| {
-            execution_view.set_status(ExecutionStatus::ConnectingToKernel, cx);
-        })?;
-        let mut receiver = receiver.await?;
-
-        let execution_view = execution_view.clone();
-        while let Some(content) = receiver.next().await {
-            execution_view.update(&mut cx, |execution_view, cx| {
-                execution_view.push_message(&content, cx)
-            })?;
-
-            editor.update(&mut cx, |editor, cx| {
-                let mut replacements = HashMap::default();
-                replacements.insert(
-                    block_id,
-                    (
-                        Some(execution_view.num_lines(cx).saturating_add(1)),
-                        create_output_area_render(execution_view.clone()),
-                    ),
-                );
-                editor.replace_blocks(replacements, None, cx);
-            })?;
-        }
-        anyhow::Ok(())
-    })
-    .detach_and_log_err(cx);
-}
-
-fn create_output_area_render(execution_view: View<ExecutionView>) -> RenderBlock {
-    let render = move |cx: &mut BlockContext| {
-        let execution_view = execution_view.clone();
-        let text_font = ThemeSettings::get_global(cx).buffer_font.family.clone();
-        // Note: we'll want to use `cx.anchor_x` when someone runs something with no output -- just show a checkmark and not make the full block below the line
-
-        let gutter_width = cx.gutter_dimensions.width;
-
-        h_flex()
-            .w_full()
-            .bg(cx.theme().colors().background)
-            .border_y_1()
-            .border_color(cx.theme().colors().border)
-            .pl(gutter_width)
-            .child(
-                div()
-                    .font_family(text_font)
-                    // .ml(gutter_width)
-                    .mx_1()
-                    .my_2()
-                    .h_full()
-                    .w_full()
-                    .mr(gutter_width)
-                    .child(execution_view),
-            )
-            .into_any_element()
-    };
-
-    Box::new(render)
+    runtime_panel::init(cx)
 }

crates/repl/src/runtime_panel.rs 🔗

@@ -0,0 +1,443 @@
+use crate::{
+    jupyter_settings::{JupyterDockPosition, JupyterSettings},
+    kernels::{kernel_specifications, KernelSpecification},
+    session::{Session, SessionEvent},
+};
+use anyhow::{Context as _, Result};
+use collections::HashMap;
+use editor::{Anchor, Editor, RangeToAnchorExt};
+use gpui::{
+    actions, prelude::*, AppContext, AsyncWindowContext, Entity, EntityId, EventEmitter,
+    FocusHandle, FocusOutEvent, FocusableView, Subscription, Task, View, WeakView,
+};
+use language::Point;
+use project::Fs;
+use settings::{Settings as _, SettingsStore};
+use std::{ops::Range, sync::Arc};
+use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding};
+use workspace::{
+    dock::{Panel, PanelEvent},
+    Workspace,
+};
+
+actions!(repl, [Run, ToggleFocus, ClearOutputs]);
+
+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);
+                })
+                .register_action(run)
+                .register_action(clear_outputs);
+        },
+    )
+    .detach();
+}
+
+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>,
+}
+
+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),
+                        cx.observe_global::<SettingsStore>(move |this, cx| {
+                            let settings = JupyterSettings::get_global(cx);
+                            this.set_enabled(settings.enabled, cx);
+                        }),
+                    ];
+
+                    let enabled = JupyterSettings::get_global(cx).enabled;
+
+                    Self {
+                        fs,
+                        width: None,
+                        focus_handle,
+                        kernel_specifications: Vec::new(),
+                        sessions: Default::default(),
+                        _subscriptions: subscriptions,
+                        enabled,
+                    }
+                })
+            })?;
+
+            view.update(&mut cx, |this, cx| this.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();
+    }
+
+    fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext<Self>) {
+        cx.notify();
+    }
+
+    // Gets the active selection in the editor or the current line
+    fn selection(&self, editor: View<Editor>, cx: &mut ViewContext<Self>) -> Range<Anchor> {
+        let editor = editor.read(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 line_start = multi_buffer_snapshot.offset_to_point(cursor).row;
+            let mut start_offset = multi_buffer_snapshot.point_to_offset(Point::new(line_start, 0));
+
+            // Iterate backwards to find the start of the line
+            while start_offset > 0 {
+                let ch = multi_buffer_snapshot
+                    .chars_at(start_offset - 1)
+                    .next()
+                    .unwrap_or('\0');
+                if ch == '\n' {
+                    break;
+                }
+                start_offset -= 1;
+            }
+
+            let mut end_offset = cursor;
+
+            // Iterate forwards to find the end of the line
+            while end_offset < multi_buffer_snapshot.len() {
+                let ch = multi_buffer_snapshot
+                    .chars_at(end_offset)
+                    .next()
+                    .unwrap_or('\0');
+                if ch == '\n' {
+                    break;
+                }
+                end_offset += 1;
+            }
+
+            // Create a range from the start to the end of the line
+            start_offset..end_offset
+        } else {
+            selection.range()
+        };
+
+        range.to_anchors(&multi_buffer_snapshot)
+    }
+
+    pub fn snippet(
+        &self,
+        editor: View<Editor>,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<(String, Arc<str>, Range<Anchor>)> {
+        let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
+        let anchor_range = self.selection(editor, cx);
+
+        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);
+
+        let language_name = if start_language == end_language {
+            start_language
+                .map(|language| language.code_fence_block_name())
+                .filter(|lang| **lang != *"markdown")?
+        } else {
+            // If the selection spans multiple languages, don't run it
+            return None;
+        };
+
+        Some((selected_text, language_name, anchor_range))
+    }
+
+    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 kernelspec(
+        &self,
+        language_name: &str,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<KernelSpecification> {
+        let settings = JupyterSettings::get_global(cx);
+        let selected_kernel = settings.kernel_selections.get(language_name);
+
+        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: View<Editor>,
+        fs: Arc<dyn Fs>,
+        cx: &mut ViewContext<Self>,
+    ) -> anyhow::Result<()> {
+        if !self.enabled {
+            return Ok(());
+        }
+
+        let (selected_text, language_name, 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_name, cx)
+            .with_context(|| format!("No kernel found for language: {language_name}"))?;
+
+        let session = self.sessions.entry(entity_id).or_insert_with(|| {
+            let view = cx.new_view(|cx| Session::new(editor, 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());
+                        }
+                    }
+                    //
+                },
+            );
+
+            subscription.detach();
+
+            view
+        });
+
+        session.update(cx, |session, cx| {
+            session.execute(&selected_text, anchor_range, cx);
+        });
+
+        anyhow::Ok(())
+    }
+
+    pub fn clear_outputs(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
+        let entity_id = editor.entity_id();
+        if let Some(session) = self.sessions.get_mut(&entity_id) {
+            session.update(cx, |session, cx| {
+                session.clear_outputs(cx);
+            });
+            cx.notify();
+        }
+    }
+}
+
+pub fn run(workspace: &mut Workspace, _: &Run, cx: &mut ViewContext<Workspace>) {
+    let settings = JupyterSettings::get_global(cx);
+    if !settings.enabled {
+        return;
+    }
+
+    let editor = workspace
+        .active_item(cx)
+        .and_then(|item| item.act_as::<Editor>(cx));
+
+    if let (Some(editor), Some(runtime_panel)) = (editor, workspace.panel::<RuntimePanel>(cx)) {
+        runtime_panel.update(cx, |runtime_panel, cx| {
+            runtime_panel
+                .run(editor, workspace.app_state().fs.clone(), cx)
+                .ok();
+        });
+    }
+}
+
+pub fn clear_outputs(workspace: &mut Workspace, _: &ClearOutputs, cx: &mut ViewContext<Workspace>) {
+    let settings = JupyterSettings::get_global(cx);
+    if !settings.enabled {
+        return;
+    }
+
+    let editor = workspace
+        .active_item(cx)
+        .and_then(|item| item.act_as::<Editor>(cx));
+
+    if let (Some(editor), Some(runtime_panel)) = (editor, workspace.panel::<RuntimePanel>(cx)) {
+        runtime_panel.update(cx, |runtime_panel, cx| {
+            runtime_panel.clear_outputs(editor, cx);
+        });
+    }
+}
+
+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> {
+        if !self.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 {
+        // 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() {
+            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 self.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()
+                            )
+                    )
+                )
+
+                .into_any_element();
+        }
+
+        v_flex()
+            .p_4()
+            .child(Label::new("Jupyter Kernel Sessions").size(LabelSize::Large))
+            .children(
+                self.sessions
+                    .values()
+                    .map(|session| session.clone().into_any_element()),
+            )
+            .into_any_element()
+    }
+}

crates/repl/src/runtime_settings.rs 🔗

@@ -1,66 +0,0 @@
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources};
-
-#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum RuntimesDockPosition {
-    Left,
-    #[default]
-    Right,
-    Bottom,
-}
-
-#[derive(Debug, Default)]
-pub struct JupyterSettings {
-    pub enabled: bool,
-    pub dock: RuntimesDockPosition,
-}
-
-#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
-pub struct JupyterSettingsContent {
-    /// Whether the Runtimes feature is enabled.
-    ///
-    /// Default: `false`
-    enabled: Option<bool>,
-    /// Where to dock the runtimes panel.
-    ///
-    /// Default: `right`
-    dock: Option<RuntimesDockPosition>,
-}
-
-impl Default for JupyterSettingsContent {
-    fn default() -> Self {
-        JupyterSettingsContent {
-            enabled: Some(false),
-            dock: Some(RuntimesDockPosition::Right),
-        }
-    }
-}
-
-impl Settings for JupyterSettings {
-    const KEY: Option<&'static str> = Some("jupyter");
-
-    type FileContent = JupyterSettingsContent;
-
-    fn load(
-        sources: SettingsSources<Self::FileContent>,
-        _cx: &mut gpui::AppContext,
-    ) -> anyhow::Result<Self>
-    where
-        Self: Sized,
-    {
-        let mut settings = JupyterSettings::default();
-
-        for value in sources.defaults_and_customizations() {
-            if let Some(enabled) = value.enabled {
-                settings.enabled = enabled;
-            }
-            if let Some(dock) = value.dock {
-                settings.dock = dock;
-            }
-        }
-
-        Ok(settings)
-    }
-}

crates/repl/src/runtimes.rs 🔗

@@ -1,329 +0,0 @@
-use anyhow::{Context as _, Result};
-use collections::HashMap;
-use futures::lock::Mutex;
-use futures::{channel::mpsc, SinkExt as _, StreamExt as _};
-use gpui::{AsyncAppContext, EntityId};
-use project::Fs;
-use runtimelib::{dirs, ConnectionInfo, JupyterKernelspec, JupyterMessage, JupyterMessageContent};
-use smol::{net::TcpListener, process::Command};
-use std::fmt::Debug;
-use std::net::{IpAddr, Ipv4Addr, SocketAddr};
-use std::{path::PathBuf, sync::Arc};
-
-#[derive(Debug)]
-pub struct Request {
-    pub request: runtimelib::JupyterMessageContent,
-    pub responses_rx: mpsc::UnboundedSender<JupyterMessageContent>,
-}
-
-#[derive(Debug, Clone)]
-pub struct RuntimeSpecification {
-    pub name: String,
-    pub path: PathBuf,
-    pub kernelspec: JupyterKernelspec,
-}
-
-impl RuntimeSpecification {
-    #[must_use]
-    fn command(&self, connection_path: &PathBuf) -> Result<Command> {
-        let argv = &self.kernelspec.argv;
-
-        if argv.is_empty() {
-            return Err(anyhow::anyhow!("Empty argv in kernelspec {}", self.name));
-        }
-
-        if argv.len() < 2 {
-            return Err(anyhow::anyhow!("Invalid argv in kernelspec {}", self.name));
-        }
-
-        if !argv.contains(&"{connection_file}".to_string()) {
-            return Err(anyhow::anyhow!(
-                "Missing 'connection_file' in argv in kernelspec {}",
-                self.name
-            ));
-        }
-
-        let mut cmd = Command::new(&argv[0]);
-
-        for arg in &argv[1..] {
-            if arg == "{connection_file}" {
-                cmd.arg(connection_path);
-            } else {
-                cmd.arg(arg);
-            }
-        }
-
-        if let Some(env) = &self.kernelspec.env {
-            cmd.envs(env);
-        }
-
-        Ok(cmd)
-    }
-}
-
-// 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]> {
-    let mut addr_zeroport: SocketAddr = SocketAddr::new(ip, 0);
-    addr_zeroport.set_port(0);
-    let mut ports: [u16; 5] = [0; 5];
-    for i in 0..5 {
-        let listener = TcpListener::bind(addr_zeroport).await?;
-        let addr = listener.local_addr()?;
-        ports[i] = addr.port();
-    }
-    Ok(ports)
-}
-
-#[derive(Debug)]
-pub struct RunningKernel {
-    #[allow(unused)]
-    runtime: RuntimeSpecification,
-    #[allow(unused)]
-    process: smol::process::Child,
-    pub request_tx: mpsc::UnboundedSender<Request>,
-}
-
-impl RunningKernel {
-    pub async fn new(
-        runtime: RuntimeSpecification,
-        entity_id: EntityId,
-        fs: Arc<dyn Fs>,
-        cx: AsyncAppContext,
-    ) -> anyhow::Result<Self> {
-        let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
-        let ports = peek_ports(ip).await?;
-
-        let connection_info = ConnectionInfo {
-            transport: "tcp".to_string(),
-            ip: ip.to_string(),
-            stdin_port: ports[0],
-            control_port: ports[1],
-            hb_port: ports[2],
-            shell_port: ports[3],
-            iopub_port: ports[4],
-            signature_scheme: "hmac-sha256".to_string(),
-            key: uuid::Uuid::new_v4().to_string(),
-            kernel_name: Some(format!("zed-{}", runtime.name)),
-        };
-
-        let connection_path = dirs::runtime_dir().join(format!("kernel-zed-{}.json", entity_id));
-        let content = serde_json::to_string(&connection_info)?;
-        // write out file to disk for kernel
-        fs.atomic_write(connection_path.clone(), content).await?;
-
-        let mut cmd = runtime.command(&connection_path)?;
-        let process = cmd
-            // .stdout(Stdio::null())
-            // .stderr(Stdio::null())
-            .kill_on_drop(true)
-            .spawn()
-            .context("failed to start the kernel process")?;
-
-        let mut iopub = connection_info.create_client_iopub_connection("").await?;
-        let mut shell = connection_info.create_client_shell_connection().await?;
-
-        // Spawn a background task to handle incoming messages from the kernel as well
-        // as outgoing messages to the kernel
-
-        let child_messages: Arc<
-            Mutex<HashMap<String, mpsc::UnboundedSender<JupyterMessageContent>>>,
-        > = Default::default();
-
-        let (request_tx, mut request_rx) = mpsc::unbounded::<Request>();
-
-        cx.background_executor()
-            .spawn({
-                let child_messages = child_messages.clone();
-
-                async move {
-                    let child_messages = child_messages.clone();
-                    while let Ok(message) = iopub.read().await {
-                        if let Some(parent_header) = message.parent_header {
-                            let child_messages = child_messages.lock().await;
-
-                            let sender = child_messages.get(&parent_header.msg_id);
-
-                            match sender {
-                                Some(mut sender) => {
-                                    sender.send(message.content).await?;
-                                }
-                                None => {}
-                            }
-                        }
-                    }
-
-                    anyhow::Ok(())
-                }
-            })
-            .detach();
-
-        cx.background_executor()
-            .spawn({
-                let child_messages = child_messages.clone();
-                async move {
-                    while let Some(request) = request_rx.next().await {
-                        let rx = request.responses_rx.clone();
-
-                        let request: JupyterMessage = request.request.into();
-                        let msg_id = request.header.msg_id.clone();
-
-                        let mut sender = rx.clone();
-
-                        child_messages
-                            .lock()
-                            .await
-                            .insert(msg_id.clone(), sender.clone());
-
-                        shell.send(request).await?;
-
-                        let response = shell.read().await?;
-
-                        sender.send(response.content).await?;
-                    }
-
-                    anyhow::Ok(())
-                }
-            })
-            .detach();
-
-        Ok(Self {
-            runtime,
-            process,
-            request_tx,
-        })
-    }
-}
-
-async fn read_kernelspec_at(
-    // Path should be a directory to a jupyter kernelspec, as in
-    // /usr/local/share/jupyter/kernels/python3
-    kernel_dir: PathBuf,
-    fs: Arc<dyn Fs>,
-) -> anyhow::Result<RuntimeSpecification> {
-    let path = kernel_dir;
-    let kernel_name = if let Some(kernel_name) = path.file_name() {
-        kernel_name.to_string_lossy().to_string()
-    } else {
-        return Err(anyhow::anyhow!("Invalid kernelspec directory: {:?}", path));
-    };
-
-    if !fs.is_dir(path.as_path()).await {
-        return Err(anyhow::anyhow!("Not a directory: {:?}", path));
-    }
-
-    let expected_kernel_json = path.join("kernel.json");
-    let spec = fs.load(expected_kernel_json.as_path()).await?;
-    let spec = serde_json::from_str::<JupyterKernelspec>(&spec)?;
-
-    Ok(RuntimeSpecification {
-        name: kernel_name,
-        path,
-        kernelspec: spec,
-    })
-}
-
-/// Read a directory of kernelspec directories
-async fn read_kernels_dir(
-    path: PathBuf,
-    fs: Arc<dyn Fs>,
-) -> anyhow::Result<Vec<RuntimeSpecification>> {
-    let mut kernelspec_dirs = fs.read_dir(&path).await?;
-
-    let mut valid_kernelspecs = Vec::new();
-    while let Some(path) = kernelspec_dirs.next().await {
-        match path {
-            Ok(path) => {
-                if fs.is_dir(path.as_path()).await {
-                    let fs = fs.clone();
-                    if let Ok(kernelspec) = read_kernelspec_at(path, fs).await {
-                        valid_kernelspecs.push(kernelspec);
-                    }
-                }
-            }
-            Err(err) => {
-                log::warn!("Error reading kernelspec directory: {:?}", err);
-            }
-        }
-    }
-
-    Ok(valid_kernelspecs)
-}
-
-pub async fn get_runtime_specifications(
-    fs: Arc<dyn Fs>,
-) -> anyhow::Result<Vec<RuntimeSpecification>> {
-    let data_dirs = dirs::data_dirs();
-    let kernel_dirs = data_dirs
-        .iter()
-        .map(|dir| dir.join("kernels"))
-        .map(|path| read_kernels_dir(path, fs.clone()))
-        .collect::<Vec<_>>();
-
-    let kernel_dirs = futures::future::join_all(kernel_dirs).await;
-    let kernel_dirs = kernel_dirs
-        .into_iter()
-        .filter_map(Result::ok)
-        .flatten()
-        .collect::<Vec<_>>();
-
-    Ok(kernel_dirs)
-}
-
-#[cfg(test)]
-mod test {
-    use super::*;
-    use std::path::PathBuf;
-
-    use gpui::TestAppContext;
-    use project::FakeFs;
-    use serde_json::json;
-
-    #[gpui::test]
-    async fn test_get_kernelspecs(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            "/jupyter",
-            json!({
-                ".zed": {
-                    "settings.json": r#"{ "tab_size": 8 }"#,
-                    "tasks.json": r#"[{
-                        "label": "cargo check",
-                        "command": "cargo",
-                        "args": ["check", "--all"]
-                    },]"#,
-                },
-                "kernels": {
-                    "python": {
-                        "kernel.json": r#"{
-                            "display_name": "Python 3",
-                            "language": "python",
-                            "argv": ["python3", "-m", "ipykernel_launcher", "-f", "{connection_file}"],
-                            "env": {}
-                        }"#
-                    },
-                    "deno": {
-                        "kernel.json": r#"{
-                            "display_name": "Deno",
-                            "language": "typescript",
-                            "argv": ["deno", "run", "--unstable", "--allow-net", "--allow-read", "https://deno.land/std/http/file_server.ts", "{connection_file}"],
-                            "env": {}
-                        }"#
-                    }
-                },
-            }),
-        )
-        .await;
-
-        let mut kernels = read_kernels_dir(PathBuf::from("/jupyter/kernels"), fs)
-            .await
-            .unwrap();
-
-        kernels.sort_by(|a, b| a.name.cmp(&b.name));
-
-        assert_eq!(
-            kernels.iter().map(|c| c.name.clone()).collect::<Vec<_>>(),
-            vec!["deno", "python"]
-        );
-    }
-}

crates/repl/src/session.rs 🔗

@@ -0,0 +1,400 @@
+use crate::{
+    kernels::{Kernel, KernelSpecification, RunningKernel},
+    outputs::{ExecutionStatus, ExecutionView, LineHeight as _},
+};
+use collections::{HashMap, HashSet};
+use editor::{
+    display_map::{
+        BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock,
+    },
+    Anchor, AnchorRangeExt as _, Editor,
+};
+use futures::{FutureExt as _, StreamExt as _};
+use gpui::{div, prelude::*, Entity, EventEmitter, Render, Task, View, ViewContext};
+use project::Fs;
+use runtimelib::{
+    ExecuteRequest, InterruptRequest, JupyterMessage, JupyterMessageContent, KernelInfoRequest,
+    ShutdownRequest,
+};
+use settings::Settings as _;
+use std::{ops::Range, sync::Arc, time::Duration};
+use theme::{ActiveTheme, ThemeSettings};
+use ui::{h_flex, prelude::*, v_flex, ButtonLike, ButtonStyle, Label};
+
+pub struct Session {
+    editor: View<Editor>,
+    kernel: Kernel,
+    blocks: HashMap<String, EditorBlock>,
+    messaging_task: Task<()>,
+    kernel_specification: KernelSpecification,
+}
+
+#[derive(Debug)]
+struct EditorBlock {
+    editor: View<Editor>,
+    code_range: Range<Anchor>,
+    block_id: BlockId,
+    execution_view: View<ExecutionView>,
+}
+
+impl EditorBlock {
+    fn new(
+        editor: View<Editor>,
+        code_range: Range<Anchor>,
+        status: ExecutionStatus,
+        cx: &mut ViewContext<Session>,
+    ) -> Self {
+        let execution_view = cx.new_view(|cx| ExecutionView::new(status, cx));
+
+        let block_id = editor.update(cx, |editor, cx| {
+            let block = BlockProperties {
+                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()),
+                disposition: BlockDisposition::Below,
+            };
+
+            editor.insert_blocks([block], None, cx)[0]
+        });
+
+        Self {
+            editor,
+            code_range,
+            block_id,
+            execution_view,
+        }
+    }
+
+    fn handle_message(&mut self, message: &JupyterMessage, cx: &mut ViewContext<Session>) {
+        self.execution_view.update(cx, |execution_view, cx| {
+            execution_view.push_message(&message.content, cx);
+        });
+
+        self.editor.update(cx, |editor, cx| {
+            let mut replacements = HashMap::default();
+            replacements.insert(
+                self.block_id,
+                (
+                    Some(self.execution_view.num_lines(cx).saturating_add(1)),
+                    Self::create_output_area_render(self.execution_view.clone()),
+                ),
+            );
+            editor.replace_blocks(replacements, None, cx);
+        })
+    }
+
+    fn create_output_area_render(execution_view: View<ExecutionView>) -> RenderBlock {
+        let render = move |cx: &mut BlockContext| {
+            let execution_view = execution_view.clone();
+            let text_font = ThemeSettings::get_global(cx).buffer_font.family.clone();
+            // Note: we'll want to use `cx.anchor_x` when someone runs something with no output -- just show a checkmark and not make the full block below the line
+
+            let gutter_width = cx.gutter_dimensions.width;
+
+            h_flex()
+                .w_full()
+                .bg(cx.theme().colors().background)
+                .border_y_1()
+                .border_color(cx.theme().colors().border)
+                .pl(gutter_width)
+                .child(
+                    div()
+                        .font_family(text_font)
+                        // .ml(gutter_width)
+                        .mx_1()
+                        .my_2()
+                        .h_full()
+                        .w_full()
+                        .mr(gutter_width)
+                        .child(execution_view),
+                )
+                .into_any_element()
+        };
+
+        Box::new(render)
+    }
+}
+
+impl Session {
+    pub fn new(
+        editor: View<Editor>,
+        fs: Arc<dyn Fs>,
+        kernel_specification: KernelSpecification,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let entity_id = editor.entity_id();
+        let kernel = RunningKernel::new(kernel_specification.clone(), entity_id, fs.clone(), cx);
+
+        let pending_kernel = cx
+            .spawn(|this, mut cx| async move {
+                let kernel = kernel.await;
+
+                match kernel {
+                    Ok((kernel, mut messages_rx)) => {
+                        this.update(&mut cx, |this, cx| {
+                            // At this point we can create a new kind of kernel that has the process and our long running background tasks
+                            this.kernel = Kernel::RunningKernel(kernel);
+
+                            this.messaging_task = cx.spawn(|session, mut cx| async move {
+                                while let Some(message) = messages_rx.next().await {
+                                    session
+                                        .update(&mut cx, |session, cx| {
+                                            session.route(&message, cx);
+                                        })
+                                        .ok();
+                                }
+                            });
+
+                            // For some reason sending a kernel info request will brick the ark (R) kernel.
+                            // Note that Deno and Python do not have this issue.
+                            if this.kernel_specification.name == "ark" {
+                                return;
+                            }
+
+                            // Get kernel info after (possibly) letting the kernel start
+                            cx.spawn(|this, mut cx| async move {
+                                cx.background_executor()
+                                    .timer(Duration::from_millis(120))
+                                    .await;
+                                this.update(&mut cx, |this, _cx| {
+                                    this.send(KernelInfoRequest {}.into(), _cx).ok();
+                                })
+                                .ok();
+                            })
+                            .detach();
+                        })
+                        .ok();
+                    }
+                    Err(err) => {
+                        this.update(&mut cx, |this, _cx| {
+                            this.kernel = Kernel::ErroredLaunch(err.to_string());
+                        })
+                        .ok();
+                    }
+                }
+            })
+            .shared();
+
+        return Self {
+            editor,
+            kernel: Kernel::StartingKernel(pending_kernel),
+            messaging_task: Task::ready(()),
+            blocks: HashMap::default(),
+            kernel_specification,
+        };
+    }
+
+    fn send(&mut self, message: JupyterMessage, _cx: &mut ViewContext<Self>) -> anyhow::Result<()> {
+        match &mut self.kernel {
+            Kernel::RunningKernel(kernel) => {
+                kernel.request_tx.try_send(message).ok();
+            }
+            _ => {}
+        }
+
+        anyhow::Ok(())
+    }
+
+    pub fn clear_outputs(&mut self, cx: &mut ViewContext<Self>) {
+        let blocks_to_remove: HashSet<BlockId> =
+            self.blocks.values().map(|block| block.block_id).collect();
+
+        self.editor.update(cx, |editor, cx| {
+            editor.remove_blocks(blocks_to_remove, None, cx);
+        });
+
+        self.blocks.clear();
+    }
+
+    pub fn execute(&mut self, code: &str, anchor_range: Range<Anchor>, cx: &mut ViewContext<Self>) {
+        let execute_request = ExecuteRequest {
+            code: code.to_string(),
+            ..ExecuteRequest::default()
+        };
+
+        let message: JupyterMessage = execute_request.into();
+
+        let mut blocks_to_remove: HashSet<BlockId> = HashSet::default();
+
+        let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
+
+        self.blocks.retain(|_key, block| {
+            if anchor_range.overlaps(&block.code_range, &buffer) {
+                blocks_to_remove.insert(block.block_id);
+                false
+            } else {
+                true
+            }
+        });
+
+        self.editor.update(cx, |editor, cx| {
+            editor.remove_blocks(blocks_to_remove, None, cx);
+        });
+
+        let status = match &self.kernel {
+            Kernel::RunningKernel(_) => ExecutionStatus::Queued,
+            Kernel::StartingKernel(_) => ExecutionStatus::ConnectingToKernel,
+            Kernel::ErroredLaunch(error) => ExecutionStatus::KernelErrored(error.clone()),
+            Kernel::ShuttingDown => ExecutionStatus::ShuttingDown,
+            Kernel::Shutdown => ExecutionStatus::Shutdown,
+        };
+
+        let editor_block = EditorBlock::new(self.editor.clone(), anchor_range, status, cx);
+
+        self.blocks
+            .insert(message.header.msg_id.clone(), editor_block);
+
+        match &self.kernel {
+            Kernel::RunningKernel(_) => {
+                self.send(message, cx).ok();
+            }
+            Kernel::StartingKernel(task) => {
+                // Queue up the execution as a task to run after the kernel starts
+                let task = task.clone();
+                let message = message.clone();
+
+                cx.spawn(|this, mut cx| async move {
+                    task.await;
+                    this.update(&mut cx, |this, cx| {
+                        this.send(message, cx).ok();
+                    })
+                    .ok();
+                })
+                .detach();
+            }
+            _ => {}
+        }
+    }
+
+    fn route(&mut self, message: &JupyterMessage, cx: &mut ViewContext<Self>) {
+        let parent_message_id = match message.parent_header.as_ref() {
+            Some(header) => &header.msg_id,
+            None => return,
+        };
+
+        match &message.content {
+            JupyterMessageContent::Status(status) => {
+                self.kernel.set_execution_state(&status.execution_state);
+                cx.notify();
+            }
+            JupyterMessageContent::KernelInfoReply(reply) => {
+                self.kernel.set_kernel_info(&reply);
+                cx.notify();
+            }
+            _ => {}
+        }
+
+        if let Some(block) = self.blocks.get_mut(parent_message_id) {
+            block.handle_message(&message, cx);
+            return;
+        }
+    }
+
+    fn interrupt(&mut self, cx: &mut ViewContext<Self>) {
+        match &mut self.kernel {
+            Kernel::RunningKernel(_kernel) => {
+                self.send(InterruptRequest {}.into(), cx).ok();
+            }
+            Kernel::StartingKernel(_task) => {
+                // NOTE: If we switch to a literal queue instead of chaining on to the task, clear all queued executions
+            }
+            _ => {}
+        }
+    }
+
+    fn shutdown(&mut self, cx: &mut ViewContext<Self>) {
+        let kernel = std::mem::replace(&mut self.kernel, Kernel::ShuttingDown);
+        // todo!(): emit event for the runtime panel to remove this session once in shutdown state
+
+        match kernel {
+            Kernel::RunningKernel(mut kernel) => {
+                let mut request_tx = kernel.request_tx.clone();
+
+                cx.spawn(|this, mut cx| async move {
+                    let message: JupyterMessage = ShutdownRequest { restart: false }.into();
+                    request_tx.try_send(message).ok();
+
+                    // Give the kernel a bit of time to clean up
+                    cx.background_executor().timer(Duration::from_secs(3)).await;
+
+                    kernel.process.kill().ok();
+
+                    this.update(&mut cx, |this, cx| {
+                        cx.emit(SessionEvent::Shutdown(this.editor.clone()));
+                        this.clear_outputs(cx);
+                        this.kernel = Kernel::Shutdown;
+                        cx.notify();
+                    })
+                    .ok();
+                })
+                .detach();
+            }
+            Kernel::StartingKernel(_kernel) => {
+                self.kernel = Kernel::Shutdown;
+            }
+            _ => {
+                self.kernel = Kernel::Shutdown;
+            }
+        }
+        cx.notify();
+    }
+}
+
+pub enum SessionEvent {
+    Shutdown(View<Editor>),
+}
+
+impl EventEmitter<SessionEvent> for Session {}
+
+impl Render for Session {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let mut buttons = vec![];
+
+        buttons.push(
+            ButtonLike::new("shutdown")
+                .child(Label::new("Shutdown"))
+                .style(ButtonStyle::Subtle)
+                .on_click(cx.listener(move |session, _, cx| {
+                    session.shutdown(cx);
+                })),
+        );
+
+        let status_text = match &self.kernel {
+            Kernel::RunningKernel(kernel) => {
+                buttons.push(
+                    ButtonLike::new("interrupt")
+                        .child(Label::new("Interrupt"))
+                        .style(ButtonStyle::Subtle)
+                        .on_click(cx.listener(move |session, _, cx| {
+                            session.interrupt(cx);
+                        })),
+                );
+                let mut name = self.kernel_specification.name.clone();
+
+                if let Some(info) = &kernel.kernel_info {
+                    name.push_str(" (");
+                    name.push_str(&info.language_info.name);
+                    name.push_str(")");
+                }
+                name
+            }
+            Kernel::StartingKernel(_) => format!("{} (Starting)", self.kernel_specification.name),
+            Kernel::ErroredLaunch(err) => {
+                format!("{} (Error: {})", self.kernel_specification.name, err)
+            }
+            Kernel::ShuttingDown => format!("{} (Shutting Down)", self.kernel_specification.name),
+            Kernel::Shutdown => format!("{} (Shutdown)", self.kernel_specification.name),
+        };
+
+        return v_flex()
+            .gap_1()
+            .child(
+                h_flex()
+                    .gap_2()
+                    .child(self.kernel.dot())
+                    .child(Label::new(status_text)),
+            )
+            .child(h_flex().gap_2().children(buttons));
+    }
+}

crates/repl/src/stdio.rs 🔗

@@ -55,11 +55,11 @@ impl TerminalOutput {
     pub fn render(&self, cx: &ViewContext<ExecutionView>) -> AnyElement {
         let theme = cx.theme();
         let buffer_font = ThemeSettings::get_global(cx).buffer_font.family.clone();
-        let mut text_runs = self.handler.text_runs.clone();
-        text_runs.push(self.handler.current_text_run.clone());
-
-        let runs = text_runs
+        let runs = self
+            .handler
+            .text_runs
             .iter()
+            .chain(Some(&self.handler.current_text_run))
             .map(|ansi_run| {
                 let color = terminal_view::terminal_element::convert_color(&ansi_run.fg, theme);
                 let background_color = Some(terminal_view::terminal_element::convert_color(
@@ -88,16 +88,15 @@ impl TerminalOutput {
 
 impl LineHeight for TerminalOutput {
     fn num_lines(&self, _cx: &mut WindowContext) -> u8 {
-        // todo!(): Track this over time with our parser and just return it when needed
         self.handler.buffer.lines().count() as u8
     }
 }
 
 #[derive(Clone)]
 struct AnsiTextRun {
-    pub len: usize,
-    pub fg: alacritty_terminal::vte::ansi::Color,
-    pub bg: alacritty_terminal::vte::ansi::Color,
+    len: usize,
+    fg: alacritty_terminal::vte::ansi::Color,
+    bg: alacritty_terminal::vte::ansi::Color,
 }
 
 impl AnsiTextRun {

crates/zed/src/main.rs 🔗

@@ -221,7 +221,7 @@ fn init_ui(app_state: Arc<AppState>, cx: &mut AppContext) -> Result<()> {
 
     assistant::init(app_state.fs.clone(), app_state.client.clone(), cx);
 
-    repl::init(app_state.fs.clone(), cx);
+    repl::init(cx);
 
     cx.observe_global::<SettingsStore>({
         let languages = app_state.languages.clone();

crates/zed/src/zed.rs 🔗

@@ -197,6 +197,10 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
         cx.spawn(|workspace_handle, mut cx| async move {
             let assistant_panel =
                 assistant::AssistantPanel::load(workspace_handle.clone(), cx.clone());
+
+            // todo!(): enable/disable this based on settings
+            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());
@@ -214,6 +218,7 @@ 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,
@@ -222,6 +227,7 @@ 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,
@@ -229,6 +235,7 @@ 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);
@@ -3188,6 +3195,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);
             tasks_ui::init(cx);
             initialize_workspace(app_state.clone(), cx);
             app_state