diff --git a/crates/repl/src/kernels.rs b/crates/repl/src/kernels.rs index ad2f70339f..7e9d6cad75 100644 --- a/crates/repl/src/kernels.rs +++ b/crates/repl/src/kernels.rs @@ -29,7 +29,7 @@ pub struct KernelSpecification { impl KernelSpecification { #[must_use] - fn command(&self, connection_path: &PathBuf) -> anyhow::Result { + fn command(&self, connection_path: &PathBuf) -> Result { let argv = &self.kernelspec.argv; anyhow::ensure!(!argv.is_empty(), "Empty argv in kernelspec {}", self.name); @@ -60,7 +60,7 @@ impl KernelSpecification { // 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]> { +async fn peek_ports(ip: IpAddr) -> Result<[u16; 5]> { let mut addr_zeroport: SocketAddr = SocketAddr::new(ip, 0); addr_zeroport.set_port(0); let mut ports: [u16; 5] = [0; 5]; @@ -166,10 +166,10 @@ impl Kernel { pub struct RunningKernel { pub process: smol::process::Child, - _shell_task: Task>, - _iopub_task: Task>, - _control_task: Task>, - _routing_task: Task>, + _shell_task: Task>, + _iopub_task: Task>, + _control_task: Task>, + _routing_task: Task>, connection_path: PathBuf, pub working_directory: PathBuf, pub request_tx: mpsc::Sender, @@ -194,7 +194,7 @@ impl RunningKernel { working_directory: PathBuf, fs: Arc, cx: &mut AppContext, - ) -> Task> { + ) -> Task> { cx.spawn(|cx| async move { let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); let ports = peek_ports(ip).await?; @@ -332,7 +332,7 @@ async fn read_kernelspec_at( // /usr/local/share/jupyter/kernels/python3 kernel_dir: PathBuf, fs: &dyn Fs, -) -> anyhow::Result { +) -> Result { let path = kernel_dir; let kernel_name = if let Some(kernel_name) = path.file_name() { kernel_name.to_string_lossy().to_string() @@ -356,7 +356,7 @@ async fn read_kernelspec_at( } /// Read a directory of kernelspec directories -async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> anyhow::Result> { +async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> Result> { let mut kernelspec_dirs = fs.read_dir(&path).await?; let mut valid_kernelspecs = Vec::new(); @@ -376,7 +376,7 @@ async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> anyhow::Result) -> anyhow::Result> { +pub async fn kernel_specifications(fs: Arc) -> Result> { let data_dirs = dirs::data_dirs(); let kernel_dirs = data_dirs .iter() diff --git a/crates/repl/src/repl.rs b/crates/repl/src/repl.rs index ac27f2581d..aba3bc3dee 100644 --- a/crates/repl/src/repl.rs +++ b/crates/repl/src/repl.rs @@ -1,11 +1,13 @@ use async_dispatcher::{set_dispatcher, Dispatcher, Runnable}; use gpui::{AppContext, PlatformDispatcher}; +use project::Fs; use settings::Settings as _; use std::{sync::Arc, time::Duration}; mod jupyter_settings; mod kernels; mod outputs; +mod repl_store; mod runtime_panel; mod session; mod stdio; @@ -17,6 +19,8 @@ pub use runtime_panel::{RuntimePanel, SessionSupport}; pub use runtimelib::ExecutionState; pub use session::Session; +use crate::repl_store::ReplStore; + fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher { struct ZedDispatcher { dispatcher: Arc, @@ -41,8 +45,10 @@ fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher { } } -pub fn init(cx: &mut AppContext) { +pub fn init(fs: Arc, cx: &mut AppContext) { set_dispatcher(zed_dispatcher(cx)); JupyterSettings::register(cx); - runtime_panel::init(cx) + editor::init_settings(cx); + runtime_panel::init(cx); + ReplStore::init(fs, cx); } diff --git a/crates/repl/src/repl_store.rs b/crates/repl/src/repl_store.rs new file mode 100644 index 0000000000..bda8122b8c --- /dev/null +++ b/crates/repl/src/repl_store.rs @@ -0,0 +1,118 @@ +use std::sync::Arc; + +use anyhow::Result; +use collections::HashMap; +use gpui::{ + prelude::*, AppContext, EntityId, Global, Model, ModelContext, Subscription, Task, View, +}; +use language::Language; +use project::Fs; +use settings::{Settings, SettingsStore}; + +use crate::kernels::kernel_specifications; +use crate::{JupyterSettings, KernelSpecification, Session}; + +struct GlobalReplStore(Model); + +impl Global for GlobalReplStore {} + +pub struct ReplStore { + fs: Arc, + enabled: bool, + sessions: HashMap>, + kernel_specifications: Vec, + _subscriptions: Vec, +} + +impl ReplStore { + pub(crate) fn init(fs: Arc, cx: &mut AppContext) { + let store = cx.new_model(move |cx| Self::new(fs, cx)); + + cx.set_global(GlobalReplStore(store)) + } + + pub fn global(cx: &AppContext) -> Model { + cx.global::().0.clone() + } + + pub fn new(fs: Arc, cx: &mut ModelContext) -> Self { + let subscriptions = vec![cx.observe_global::(move |this, cx| { + this.set_enabled(JupyterSettings::enabled(cx), cx); + })]; + + Self { + fs, + enabled: JupyterSettings::enabled(cx), + sessions: HashMap::default(), + kernel_specifications: Vec::new(), + _subscriptions: subscriptions, + } + } + + pub fn is_enabled(&self) -> bool { + self.enabled + } + + pub fn kernel_specifications(&self) -> impl Iterator { + self.kernel_specifications.iter() + } + + pub fn sessions(&self) -> impl Iterator> { + self.sessions.values() + } + + fn set_enabled(&mut self, enabled: bool, cx: &mut ModelContext) { + if self.enabled != enabled { + self.enabled = enabled; + cx.notify(); + } + } + + pub fn refresh_kernelspecs(&mut self, cx: &mut ModelContext) -> Task> { + 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: &Language, + cx: &mut ModelContext, + ) -> Option { + let settings = JupyterSettings::get_global(cx); + let language_name = language.code_fence_block_name(); + let selected_kernel = settings.kernel_selections.get(language_name.as_ref()); + + 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 get_session(&self, entity_id: EntityId) -> Option<&View> { + self.sessions.get(&entity_id) + } + + pub fn insert_session(&mut self, entity_id: EntityId, session: View) { + self.sessions.insert(entity_id, session); + } + + pub fn remove_session(&mut self, entity_id: EntityId) { + self.sessions.remove(&entity_id); + } +} diff --git a/crates/repl/src/runtime_panel.rs b/crates/repl/src/runtime_panel.rs index 31c140ede7..0721059b4c 100644 --- a/crates/repl/src/runtime_panel.rs +++ b/crates/repl/src/runtime_panel.rs @@ -1,19 +1,19 @@ +use crate::repl_store::ReplStore; use crate::{ jupyter_settings::{JupyterDockPosition, JupyterSettings}, - kernels::{kernel_specifications, KernelSpecification}, + kernels::KernelSpecification, session::{Session, SessionEvent}, }; use anyhow::{Context as _, Result}; -use collections::HashMap; use editor::{Anchor, Editor, RangeToAnchorExt}; use gpui::{ - actions, prelude::*, AppContext, AsyncWindowContext, EntityId, EventEmitter, FocusHandle, - FocusOutEvent, FocusableView, Subscription, Task, View, WeakView, + actions, prelude::*, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusOutEvent, + FocusableView, Subscription, Task, View, WeakView, }; use language::{Language, Point}; use multi_buffer::MultiBufferRow; use project::Fs; -use settings::{Settings as _, SettingsStore}; +use settings::Settings as _; use std::{ops::Range, sync::Arc}; use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding}; use util::ResultExt as _; @@ -28,6 +28,13 @@ actions!( ); actions!(repl_panel, [ToggleFocus]); +pub enum SessionSupport { + ActiveSession(View), + Inactive(Box), + RequiresSetup(Arc), + Unsupported, +} + pub fn init(cx: &mut AppContext) { cx.observe_new_views( |workspace: &mut Workspace, _cx: &mut ViewContext| { @@ -35,12 +42,11 @@ pub fn init(cx: &mut AppContext) { workspace.toggle_panel_focus::(cx); }); - workspace.register_action(|workspace, _: &RefreshKernelspecs, cx| { - if let Some(panel) = workspace.panel::(cx) { - panel.update(cx, |panel, cx| { - panel.refresh_kernelspecs(cx).detach(); - }); - } + workspace.register_action(|_workspace, _: &RefreshKernelspecs, cx| { + let store = ReplStore::global(cx); + store.update(cx, |store, cx| { + store.refresh_kernelspecs(cx).detach(); + }); }); }, ) @@ -145,11 +151,8 @@ pub fn init(cx: &mut AppContext) { pub struct RuntimePanel { fs: Arc, - enabled: bool, focus_handle: FocusHandle, width: Option, - sessions: HashMap>, - kernel_specifications: Vec, _subscriptions: Vec, } @@ -168,39 +171,29 @@ impl RuntimePanel { let subscriptions = vec![ cx.on_focus_in(&focus_handle, Self::focus_in), cx.on_focus_out(&focus_handle, Self::focus_out), - cx.observe_global::(move |this, cx| { - this.set_enabled(JupyterSettings::enabled(cx), cx); - }), ]; let runtime_panel = Self { - fs: fs.clone(), + fs, width: None, focus_handle, - kernel_specifications: Vec::new(), - sessions: Default::default(), _subscriptions: subscriptions, - enabled: JupyterSettings::enabled(cx), }; runtime_panel }) })?; - view.update(&mut cx, |this, cx| this.refresh_kernelspecs(cx))? - .await?; + view.update(&mut cx, |_panel, cx| { + let store = ReplStore::global(cx); + store.update(cx, |store, cx| store.refresh_kernelspecs(cx)) + })? + .await?; Ok(view) }) } - fn set_enabled(&mut self, enabled: bool, cx: &mut ViewContext) { - if self.enabled != enabled { - self.enabled = enabled; - cx.notify(); - } - } - fn focus_in(&mut self, cx: &mut ViewContext) { cx.notify(); } @@ -209,8 +202,7 @@ impl RuntimePanel { cx.notify(); } - pub fn snippet( - &self, + fn snippet( editor: WeakView, cx: &mut ViewContext, ) -> Option<(String, Arc, Range)> { @@ -255,93 +247,59 @@ impl RuntimePanel { Some((selected_text, start_language.clone(), anchor_range)) } - pub fn language( - &self, - editor: WeakView, - cx: &mut ViewContext, - ) -> Option> { + fn language(editor: WeakView, cx: &mut ViewContext) -> Option> { let editor = editor.upgrade()?; let selection = editor.read(cx).selections.newest::(cx); let buffer = editor.read(cx).buffer().read(cx).snapshot(cx); buffer.language_at(selection.head()).cloned() } - pub fn refresh_kernelspecs(&mut self, cx: &mut ViewContext) -> Task> { - let kernel_specifications = kernel_specifications(self.fs.clone()); - cx.spawn(|this, mut cx| async move { - let kernel_specifications = kernel_specifications.await?; + pub fn run(&mut self, editor: WeakView, cx: &mut ViewContext) -> Result<()> { + let store = ReplStore::global(cx); - this.update(&mut cx, |this, cx| { - this.kernel_specifications = kernel_specifications; - cx.notify(); - }) - }) - } - - pub fn kernelspec( - &self, - language: &Language, - cx: &mut ViewContext, - ) -> Option { - let settings = JupyterSettings::get_global(cx); - let language_name = language.code_fence_block_name(); - let selected_kernel = settings.kernel_selections.get(language_name.as_ref()); - - 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: WeakView, - cx: &mut ViewContext, - ) -> anyhow::Result<()> { - if !self.enabled { + if !store.read(cx).is_enabled() { return Ok(()); } - let (selected_text, language, anchor_range) = match self.snippet(editor.clone(), cx) { + let (selected_text, language, anchor_range) = match Self::snippet(editor.clone(), cx) { Some(snippet) => snippet, None => return Ok(()), }; let entity_id = editor.entity_id(); - let kernel_specification = self - .kernelspec(&language, cx) - .with_context(|| format!("No kernel found for language: {}", language.name()))?; + let kernel_specification = store.update(cx, |store, cx| { + store + .kernelspec(&language, cx) + .with_context(|| format!("No kernel found for language: {}", language.name())) + })?; - let session = self.sessions.entry(entity_id).or_insert_with(|| { - let view = + let session = if let Some(session) = store.read(cx).get_session(entity_id).cloned() { + session + } else { + let session = cx.new_view(|cx| Session::new(editor, self.fs.clone(), kernel_specification, cx)); cx.notify(); - let subscription = cx.subscribe( - &view, - |panel: &mut RuntimePanel, _session: View, event: &SessionEvent, _cx| { - match event { - SessionEvent::Shutdown(shutdown_event) => { - panel.sessions.remove(&shutdown_event.entity_id()); - } + let subscription = cx.subscribe(&session, { + let store = store.clone(); + move |_this, _session, event, cx| match event { + SessionEvent::Shutdown(shutdown_event) => { + store.update(cx, |store, _cx| { + store.remove_session(shutdown_event.entity_id()); + }); } - }, - ); + } + }); subscription.detach(); - view - }); + store.update(cx, |store, _cx| { + store.insert_session(entity_id, session.clone()); + }); + + session + }; session.update(cx, |session, cx| { session.execute(&selected_text, anchor_range, cx); @@ -350,9 +308,38 @@ impl RuntimePanel { anyhow::Ok(()) } - pub fn clear_outputs(&mut self, editor: WeakView, cx: &mut ViewContext) { + pub fn session( + &mut self, + editor: WeakView, + cx: &mut ViewContext, + ) -> SessionSupport { + let store = ReplStore::global(cx); let entity_id = editor.entity_id(); - if let Some(session) = self.sessions.get_mut(&entity_id) { + + if let Some(session) = store.read(cx).get_session(entity_id).cloned() { + return SessionSupport::ActiveSession(session); + }; + + let language = Self::language(editor, cx); + let language = match language { + Some(language) => language, + None => return SessionSupport::Unsupported, + }; + let kernelspec = store.update(cx, |store, cx| store.kernelspec(&language, cx)); + + match kernelspec { + Some(kernelspec) => SessionSupport::Inactive(Box::new(kernelspec)), + None => match language.name().as_ref() { + "TypeScript" | "Python" => SessionSupport::RequiresSetup(language.name()), + _ => SessionSupport::Unsupported, + }, + } + } + + pub fn clear_outputs(&mut self, editor: WeakView, cx: &mut ViewContext) { + let store = ReplStore::global(cx); + let entity_id = editor.entity_id(); + if let Some(session) = store.read(cx).get_session(entity_id).cloned() { session.update(cx, |session, cx| { session.clear_outputs(cx); }); @@ -361,8 +348,9 @@ impl RuntimePanel { } pub fn interrupt(&mut self, editor: WeakView, cx: &mut ViewContext) { + let store = ReplStore::global(cx); let entity_id = editor.entity_id(); - if let Some(session) = self.sessions.get_mut(&entity_id) { + if let Some(session) = store.read(cx).get_session(entity_id).cloned() { session.update(cx, |session, cx| { session.interrupt(cx); }); @@ -370,9 +358,10 @@ impl RuntimePanel { } } - pub fn shutdown(&self, editor: WeakView, cx: &mut ViewContext) { + pub fn shutdown(&self, editor: WeakView, cx: &mut ViewContext) { + let store = ReplStore::global(cx); let entity_id = editor.entity_id(); - if let Some(session) = self.sessions.get(&entity_id) { + if let Some(session) = store.read(cx).get_session(entity_id).cloned() { session.update(cx, |session, cx| { session.shutdown(cx); }); @@ -381,51 +370,6 @@ impl RuntimePanel { } } -pub enum SessionSupport { - ActiveSession(View), - Inactive(Box), - RequiresSetup(Arc), - Unsupported, -} - -impl RuntimePanel { - pub fn session( - &mut self, - editor: WeakView, - cx: &mut ViewContext, - ) -> SessionSupport { - let entity_id = editor.entity_id(); - let session = self.sessions.get(&entity_id).cloned(); - - match session { - Some(session) => SessionSupport::ActiveSession(session), - None => { - let language = self.language(editor, cx); - let language = match language { - Some(language) => language, - None => return SessionSupport::Unsupported, - }; - // Check for kernelspec - let kernelspec = self.kernelspec(&language, cx); - - match kernelspec { - Some(kernelspec) => SessionSupport::Inactive(Box::new(kernelspec)), - None => { - // If no kernelspec but language is one of typescript or python - // then we return RequiresSetup - match language.name().as_ref() { - "TypeScript" | "Python" => { - SessionSupport::RequiresSetup(language.name()) - } - _ => SessionSupport::Unsupported, - } - } - } - } - } - } -} - impl Panel for RuntimePanel { fn persistent_name() -> &'static str { "RuntimePanel" @@ -468,8 +412,10 @@ impl Panel for RuntimePanel { self.width = size; } - fn icon(&self, _cx: &ui::WindowContext) -> Option { - if !self.enabled { + fn icon(&self, cx: &ui::WindowContext) -> Option { + let store = ReplStore::global(cx); + + if !store.read(cx).is_enabled() { return None; } @@ -495,38 +441,47 @@ impl FocusableView for RuntimePanel { impl Render for RuntimePanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let store = ReplStore::global(cx); + + let (kernel_specifications, sessions) = store.update(cx, |store, _cx| { + ( + store.kernel_specifications().cloned().collect::>(), + store.sessions().cloned().collect::>(), + ) + }); + // 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() { + if kernel_specifications.is_empty() { return v_flex() .p_4() .size_full() .gap_2() - .child(Label::new("No Jupyter Kernels Available").size(LabelSize::Large)) - .child( - Label::new("To start interactively running code in your editor, you need to install and configure Jupyter kernels.") - .size(LabelSize::Default), - ) - .child( - h_flex().w_full().p_4().justify_center().gap_2().child( - ButtonLike::new("install-kernels") - .style(ButtonStyle::Filled) - .size(ButtonSize::Large) - .layer(ElevationIndex::ModalSurface) - .child(Label::new("Install Kernels")) - .on_click(move |_, cx| { - cx.open_url( - "https://docs.jupyter.org/en/latest/install/kernels.html", - ) - }), - ), - ) + .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() { + if sessions.is_empty() { return v_flex() .p_4() .size_full() @@ -546,7 +501,7 @@ impl Render for RuntimePanel { ) .child(Label::new("Kernels available").size(LabelSize::Large)) .children( - self.kernel_specifications.iter().map(|spec| { + kernel_specifications.into_iter().map(|spec| { h_flex().gap_2().child(Label::new(spec.name.clone())) .child(Label::new(spec.kernelspec.language.clone()).color(Color::Muted)) }) @@ -559,8 +514,8 @@ impl Render for RuntimePanel { .p_4() .child(Label::new("Jupyter Kernel Sessions").size(LabelSize::Large)) .children( - self.sessions - .values() + sessions + .into_iter() .map(|session| session.clone().into_any_element()), ) .into_any_element() diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 659a5daabe..c0c09578a4 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -167,7 +167,7 @@ fn init_common(app_state: Arc, cx: &mut AppContext) { supermaven::init(app_state.client.clone(), cx); inline_completion_registry::init(app_state.client.telemetry().clone(), cx); assistant::init(app_state.fs.clone(), app_state.client.clone(), cx); - repl::init(cx); + repl::init(app_state.fs.clone(), cx); extension::init( app_state.fs.clone(), app_state.client.clone(), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 75cf126b60..7c7329dd55 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -3417,7 +3417,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); + repl::init(app_state.fs.clone(), cx); tasks_ui::init(cx); initialize_workspace(app_state.clone(), cx); app_state