Detailed changes
@@ -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>;
}
@@ -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)
+ );
+ }
+}
@@ -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"]
+ );
+ }
+}
@@ -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()
@@ -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)
}
@@ -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()
+ }
+}
@@ -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)
- }
-}
@@ -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"]
- );
- }
-}
@@ -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));
+ }
+}
@@ -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 {
@@ -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();
@@ -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