diff --git a/crates/quick_action_bar/src/repl_menu.rs b/crates/quick_action_bar/src/repl_menu.rs index ee31583388..b3d2041a57 100644 --- a/crates/quick_action_bar/src/repl_menu.rs +++ b/crates/quick_action_bar/src/repl_menu.rs @@ -2,8 +2,8 @@ use std::time::Duration; use gpui::{percentage, Animation, AnimationExt, AnyElement, Transformation, View}; use repl::{ - ExecutionState, JupyterSettings, Kernel, KernelSpecification, KernelStatus, RuntimePanel, - Session, SessionSupport, + ExecutionState, JupyterSettings, Kernel, KernelSpecification, KernelStatus, Session, + SessionSupport, }; use ui::{ prelude::*, ButtonLike, ContextMenu, IconWithIndicator, Indicator, IntoElement, PopoverMenu, @@ -39,15 +39,7 @@ impl QuickActionBar { return None; } - let workspace = self.workspace.upgrade()?.read(cx); - - let (editor, repl_panel) = if let (Some(editor), Some(repl_panel)) = - (self.active_editor(), workspace.panel::(cx)) - { - (editor, repl_panel) - } else { - return None; - }; + let editor = self.active_editor()?; let has_nonempty_selection = { editor.update(cx, |this, cx| { @@ -62,10 +54,7 @@ impl QuickActionBar { }) }; - let session = repl_panel.update(cx, |repl_panel, cx| { - repl_panel.session(editor.downgrade(), cx) - }); - + let session = repl::session(editor.downgrade(), cx); let session = match session { SessionSupport::ActiveSession(session) => session, SessionSupport::Inactive(spec) => { @@ -84,18 +73,15 @@ impl QuickActionBar { let element_id = |suffix| ElementId::Name(format!("{}-{}", id, suffix).into()); - let panel_clone = repl_panel.clone(); - let editor_clone = editor.downgrade(); + let editor = editor.downgrade(); let dropdown_menu = PopoverMenu::new(element_id("menu")) .menu(move |cx| { - let panel_clone = panel_clone.clone(); - let editor_clone = editor_clone.clone(); + let editor = editor.clone(); let session = session.clone(); ContextMenu::build(cx, move |menu, cx| { let menu_state = session_state(session, cx); let status = menu_state.status; - let editor_clone = editor_clone.clone(); - let panel_clone = panel_clone.clone(); + let editor = editor.clone(); menu.when_else( status.is_connected(), @@ -139,7 +125,6 @@ impl QuickActionBar { }, ) .separator() - // Run .custom_entry( move |_cx| { Label::new(if has_nonempty_selection { @@ -150,17 +135,12 @@ impl QuickActionBar { .into_any_element() }, { - let panel_clone = panel_clone.clone(); - let editor_clone = editor_clone.clone(); + let editor = editor.clone(); move |cx| { - let editor_clone = editor_clone.clone(); - panel_clone.update(cx, |this, cx| { - this.run(editor_clone.clone(), cx).log_err(); - }); + repl::run(editor.clone(), cx).log_err(); } }, ) - // Interrupt .custom_entry( move |_cx| { Label::new("Interrupt") @@ -169,17 +149,12 @@ impl QuickActionBar { .into_any_element() }, { - let panel_clone = panel_clone.clone(); - let editor_clone = editor_clone.clone(); + let editor = editor.clone(); move |cx| { - let editor_clone = editor_clone.clone(); - panel_clone.update(cx, |this, cx| { - this.interrupt(editor_clone, cx); - }); + repl::interrupt(editor.clone(), cx); } }, ) - // Clear Outputs .custom_entry( move |_cx| { Label::new("Clear Outputs") @@ -188,13 +163,9 @@ impl QuickActionBar { .into_any_element() }, { - let panel_clone = panel_clone.clone(); - let editor_clone = editor_clone.clone(); + let editor = editor.clone(); move |cx| { - let editor_clone = editor_clone.clone(); - panel_clone.update(cx, |this, cx| { - this.clear_outputs(editor_clone, cx); - }); + repl::clear_outputs(editor.clone(), cx); } }, ) @@ -207,7 +178,6 @@ impl QuickActionBar { ) // TODO: Add Restart action // .action("Restart", Box::new(gpui::NoAction)) - // Shut down kernel .custom_entry( move |_cx| { Label::new("Shut Down Kernel") @@ -216,13 +186,9 @@ impl QuickActionBar { .into_any_element() }, { - let panel_clone = panel_clone.clone(); - let editor_clone = editor_clone.clone(); + let editor = editor.clone(); move |cx| { - let editor_clone = editor_clone.clone(); - panel_clone.update(cx, |this, cx| { - this.shutdown(editor_clone, cx); - }); + repl::shutdown(editor.clone(), cx); } }, ) diff --git a/crates/repl/src/jupyter_settings.rs b/crates/repl/src/jupyter_settings.rs index b99de743c4..3875c764a7 100644 --- a/crates/repl/src/jupyter_settings.rs +++ b/crates/repl/src/jupyter_settings.rs @@ -5,21 +5,9 @@ use gpui::AppContext; 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 dock: JupyterDockPosition, - pub default_width: Pixels, pub kernel_selections: HashMap, } @@ -34,31 +22,15 @@ impl JupyterSettings { #[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)] pub struct JupyterSettingsContent { - /// Where to dock the Jupyter panel. - /// - /// Default: `right` - dock: Option, - /// Default width in pixels when the jupyter panel is docked to the left or right. - /// - /// Default: 640 - pub default_width: Option, /// Default kernels to select for each language. /// /// Default: `{}` pub kernel_selections: Option>, } -impl JupyterSettingsContent { - pub fn set_dock(&mut self, dock: JupyterDockPosition) { - self.dock = Some(dock); - } -} - impl Default for JupyterSettingsContent { fn default() -> Self { JupyterSettingsContent { - dock: Some(JupyterDockPosition::Right), - default_width: Some(640.0), kernel_selections: Some(HashMap::new()), } } @@ -79,14 +51,6 @@ impl Settings for JupyterSettings { let mut settings = JupyterSettings::default(); for value in sources.defaults_and_customizations() { - 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()); @@ -114,14 +78,6 @@ mod tests { JupyterSettings::register(cx); assert_eq!(JupyterSettings::enabled(cx), 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| { @@ -140,13 +96,5 @@ mod tests { }); assert_eq!(JupyterSettings::enabled(cx), true); - assert_eq!( - JupyterSettings::get_global(cx).dock, - JupyterDockPosition::Left - ); - assert_eq!( - JupyterSettings::get_global(cx).default_width, - Pixels::from(800.0) - ); } } diff --git a/crates/repl/src/repl.rs b/crates/repl/src/repl.rs index aba3bc3dee..0210bd47f8 100644 --- a/crates/repl/src/repl.rs +++ b/crates/repl/src/repl.rs @@ -7,15 +7,16 @@ use std::{sync::Arc, time::Duration}; mod jupyter_settings; mod kernels; mod outputs; +mod repl_editor; +mod repl_sessions_ui; mod repl_store; -mod runtime_panel; mod session; mod stdio; pub use jupyter_settings::JupyterSettings; pub use kernels::{Kernel, KernelSpecification, KernelStatus}; -pub use runtime_panel::{ClearOutputs, Interrupt, Run, Shutdown}; -pub use runtime_panel::{RuntimePanel, SessionSupport}; +pub use repl_editor::*; +pub use repl_sessions_ui::{ClearOutputs, Interrupt, ReplSessionsPage, Run, Shutdown}; pub use runtimelib::ExecutionState; pub use session::Session; @@ -48,7 +49,7 @@ fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher { pub fn init(fs: Arc, cx: &mut AppContext) { set_dispatcher(zed_dispatcher(cx)); JupyterSettings::register(cx); - editor::init_settings(cx); - runtime_panel::init(cx); + ::editor::init_settings(cx); + repl_sessions_ui::init(cx); ReplStore::init(fs, cx); } diff --git a/crates/repl/src/repl_editor.rs b/crates/repl/src/repl_editor.rs new file mode 100644 index 0000000000..3fa2284fac --- /dev/null +++ b/crates/repl/src/repl_editor.rs @@ -0,0 +1,185 @@ +//! REPL operations on an [`Editor`]. + +use std::ops::Range; +use std::sync::Arc; + +use anyhow::{Context, Result}; +use editor::{Anchor, Editor, RangeToAnchorExt}; +use gpui::{prelude::*, AppContext, View, WeakView, WindowContext}; +use language::{Language, Point}; +use multi_buffer::MultiBufferRow; + +use crate::repl_store::ReplStore; +use crate::session::SessionEvent; +use crate::{KernelSpecification, Session}; + +pub fn run(editor: WeakView, cx: &mut WindowContext) -> Result<()> { + let store = ReplStore::global(cx); + if !store.read(cx).is_enabled() { + return Ok(()); + } + + let (selected_text, language, anchor_range) = match snippet(editor.clone(), cx) { + Some(snippet) => snippet, + None => return Ok(()), + }; + + let entity_id = editor.entity_id(); + + let kernel_specification = store.update(cx, |store, cx| { + store + .kernelspec(&language, cx) + .with_context(|| format!("No kernel found for language: {}", language.name())) + })?; + + let fs = store.read(cx).fs().clone(); + 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.clone(), fs, kernel_specification, cx)); + + editor.update(cx, |_editor, cx| { + cx.notify(); + + cx.subscribe(&session, { + let store = store.clone(); + move |_this, _session, event, cx| match event { + SessionEvent::Shutdown(shutdown_event) => { + store.update(cx, |store, _cx| { + store.remove_session(shutdown_event.entity_id()); + }); + } + } + }) + .detach(); + })?; + + 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); + }); + + anyhow::Ok(()) +} + +pub enum SessionSupport { + ActiveSession(View), + Inactive(Box), + RequiresSetup(Arc), + Unsupported, +} + +pub fn session(editor: WeakView, cx: &mut AppContext) -> SessionSupport { + let store = ReplStore::global(cx); + let entity_id = editor.entity_id(); + + if let Some(session) = store.read(cx).get_session(entity_id).cloned() { + return SessionSupport::ActiveSession(session); + }; + + let language = get_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(editor: WeakView, cx: &mut WindowContext) { + 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); + cx.notify(); + }); + } +} + +pub fn interrupt(editor: WeakView, cx: &mut WindowContext) { + 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.interrupt(cx); + cx.notify(); + }); + } +} + +pub fn shutdown(editor: WeakView, cx: &mut WindowContext) { + 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.shutdown(cx); + cx.notify(); + }); + } +} + +fn snippet( + editor: WeakView, + cx: &mut WindowContext, +) -> Option<(String, Arc, Range)> { + let editor = editor.upgrade()?; + let editor = editor.read(cx); + + let buffer = editor.buffer().read(cx).snapshot(cx); + + let selection = editor.selections.newest::(cx); + let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx); + + let range = if selection.is_empty() { + let cursor = selection.head(); + + let cursor_row = multi_buffer_snapshot.offset_to_point(cursor).row; + let start_offset = multi_buffer_snapshot.point_to_offset(Point::new(cursor_row, 0)); + + let end_point = Point::new( + cursor_row, + multi_buffer_snapshot.line_len(MultiBufferRow(cursor_row)), + ); + let end_offset = start_offset.saturating_add(end_point.column as usize); + + // Create a range from the start to the end of the line + start_offset..end_offset + } else { + selection.range() + }; + + let anchor_range = range.to_anchors(&multi_buffer_snapshot); + + let selected_text = buffer + .text_for_range(anchor_range.clone()) + .collect::(); + + let start_language = buffer.language_at(anchor_range.start)?; + let end_language = buffer.language_at(anchor_range.end)?; + if start_language != end_language { + return None; + } + + Some((selected_text, start_language.clone(), anchor_range)) +} + +fn get_language(editor: WeakView, cx: &mut AppContext) -> 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() +} diff --git a/crates/repl/src/repl_sessions_ui.rs b/crates/repl/src/repl_sessions_ui.rs new file mode 100644 index 0000000000..76dec091aa --- /dev/null +++ b/crates/repl/src/repl_sessions_ui.rs @@ -0,0 +1,257 @@ +use editor::Editor; +use gpui::{ + actions, prelude::*, AppContext, EventEmitter, FocusHandle, FocusableView, Subscription, View, +}; +use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding}; +use util::ResultExt as _; +use workspace::item::ItemEvent; +use workspace::WorkspaceId; +use workspace::{item::Item, Workspace}; + +use crate::jupyter_settings::JupyterSettings; +use crate::repl_store::ReplStore; + +actions!( + repl, + [ + Run, + ClearOutputs, + Sessions, + Interrupt, + Shutdown, + RefreshKernelspecs + ] +); +actions!(repl_panel, [ToggleFocus]); + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views( + |workspace: &mut Workspace, _cx: &mut ViewContext| { + workspace.register_action(|workspace, _: &Sessions, cx| { + let existing = workspace + .active_pane() + .read(cx) + .items() + .find_map(|item| item.downcast::()); + + if let Some(existing) = existing { + workspace.activate_item(&existing, true, true, cx); + } else { + let extensions_page = ReplSessionsPage::new(cx); + workspace.add_item_to_active_pane(Box::new(extensions_page), None, true, cx) + } + }); + + workspace.register_action(|_workspace, _: &RefreshKernelspecs, cx| { + let store = ReplStore::global(cx); + store.update(cx, |store, cx| { + store.refresh_kernelspecs(cx).detach(); + }); + }); + }, + ) + .detach(); + + cx.observe_new_views(move |editor: &mut Editor, cx: &mut ViewContext| { + if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() { + return; + } + + let editor_handle = cx.view().downgrade(); + + editor + .register_action({ + let editor_handle = editor_handle.clone(); + move |_: &Run, cx| { + if !JupyterSettings::enabled(cx) { + return; + } + + crate::run(editor_handle.clone(), cx).log_err(); + } + }) + .detach(); + + editor + .register_action({ + let editor_handle = editor_handle.clone(); + move |_: &ClearOutputs, cx| { + if !JupyterSettings::enabled(cx) { + return; + } + + crate::clear_outputs(editor_handle.clone(), cx); + } + }) + .detach(); + + editor + .register_action({ + let editor_handle = editor_handle.clone(); + move |_: &Interrupt, cx| { + if !JupyterSettings::enabled(cx) { + return; + } + + crate::interrupt(editor_handle.clone(), cx); + } + }) + .detach(); + + editor + .register_action({ + let editor_handle = editor_handle.clone(); + move |_: &Shutdown, cx| { + if !JupyterSettings::enabled(cx) { + return; + } + + crate::shutdown(editor_handle.clone(), cx); + } + }) + .detach(); + }) + .detach(); +} + +pub struct ReplSessionsPage { + focus_handle: FocusHandle, + _subscriptions: Vec, +} + +impl ReplSessionsPage { + pub fn new(cx: &mut ViewContext) -> View { + cx.new_view(|cx: &mut ViewContext| { + let focus_handle = cx.focus_handle(); + + let subscriptions = vec![ + cx.on_focus_in(&focus_handle, |_this, cx| cx.notify()), + cx.on_focus_out(&focus_handle, |_this, _event, cx| cx.notify()), + ]; + + Self { + focus_handle, + _subscriptions: subscriptions, + } + }) + } +} + +impl EventEmitter for ReplSessionsPage {} + +impl FocusableView for ReplSessionsPage { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Item for ReplSessionsPage { + type Event = ItemEvent; + + fn tab_content_text(&self, _cx: &WindowContext) -> Option { + Some("REPL Sessions".into()) + } + + fn telemetry_event_text(&self) -> Option<&'static str> { + Some("repl sessions") + } + + fn show_toolbar(&self) -> bool { + false + } + + fn clone_on_split( + &self, + _workspace_id: Option, + _: &mut ViewContext, + ) -> Option> { + None + } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { + f(*event) + } +} + +impl Render for ReplSessionsPage { + 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 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 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() + ) + ) + ) + .child(Label::new("Kernels available").size(LabelSize::Large)) + .children( + 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)) + }) + ) + + .into_any_element(); + } + + v_flex() + .p_4() + .child(Label::new("Jupyter Kernel Sessions").size(LabelSize::Large)) + .children( + sessions + .into_iter() + .map(|session| session.clone().into_any_element()), + ) + .into_any_element() + } +} diff --git a/crates/repl/src/repl_store.rs b/crates/repl/src/repl_store.rs index bda8122b8c..4d02ef5bf2 100644 --- a/crates/repl/src/repl_store.rs +++ b/crates/repl/src/repl_store.rs @@ -28,6 +28,10 @@ impl ReplStore { pub(crate) fn init(fs: Arc, cx: &mut AppContext) { let store = cx.new_model(move |cx| Self::new(fs, cx)); + store + .update(cx, |store, cx| store.refresh_kernelspecs(cx)) + .detach_and_log_err(cx); + cx.set_global(GlobalReplStore(store)) } @@ -49,6 +53,10 @@ impl ReplStore { } } + pub fn fs(&self) -> &Arc { + &self.fs + } + pub fn is_enabled(&self) -> bool { self.enabled } diff --git a/crates/repl/src/runtime_panel.rs b/crates/repl/src/runtime_panel.rs deleted file mode 100644 index 0721059b4c..0000000000 --- a/crates/repl/src/runtime_panel.rs +++ /dev/null @@ -1,523 +0,0 @@ -use crate::repl_store::ReplStore; -use crate::{ - jupyter_settings::{JupyterDockPosition, JupyterSettings}, - kernels::KernelSpecification, - session::{Session, SessionEvent}, -}; -use anyhow::{Context as _, Result}; -use editor::{Anchor, Editor, RangeToAnchorExt}; -use gpui::{ - 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 _; -use std::{ops::Range, sync::Arc}; -use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding}; -use util::ResultExt as _; -use workspace::{ - dock::{Panel, PanelEvent}, - Workspace, -}; - -actions!( - repl, - [Run, ClearOutputs, Interrupt, Shutdown, RefreshKernelspecs] -); -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| { - workspace.register_action(|workspace, _: &ToggleFocus, cx| { - workspace.toggle_panel_focus::(cx); - }); - - workspace.register_action(|_workspace, _: &RefreshKernelspecs, cx| { - let store = ReplStore::global(cx); - store.update(cx, |store, cx| { - store.refresh_kernelspecs(cx).detach(); - }); - }); - }, - ) - .detach(); - - cx.observe_new_views(move |editor: &mut Editor, cx: &mut ViewContext| { - // Only allow editors that support vim mode and are singleton buffers - if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() { - return; - } - - editor - .register_action(cx.listener( - move |editor: &mut Editor, _: &Run, cx: &mut ViewContext| { - if !JupyterSettings::enabled(cx) { - return; - } - let Some(workspace) = editor.workspace() else { - return; - }; - let Some(panel) = workspace.read(cx).panel::(cx) else { - return; - }; - let weak_editor = cx.view().downgrade(); - panel.update(cx, |_, cx| { - cx.defer(|panel, cx| { - panel.run(weak_editor, cx).log_err(); - }); - }) - }, - )) - .detach(); - - editor - .register_action(cx.listener( - move |editor: &mut Editor, _: &ClearOutputs, cx: &mut ViewContext| { - if !JupyterSettings::enabled(cx) { - return; - } - let Some(workspace) = editor.workspace() else { - return; - }; - let Some(panel) = workspace.read(cx).panel::(cx) else { - return; - }; - let weak_editor = cx.view().downgrade(); - panel.update(cx, |_, cx| { - cx.defer(|panel, cx| { - panel.clear_outputs(weak_editor, cx); - }); - }) - }, - )) - .detach(); - - editor - .register_action(cx.listener( - move |editor: &mut Editor, _: &Interrupt, cx: &mut ViewContext| { - if !JupyterSettings::enabled(cx) { - return; - } - let Some(workspace) = editor.workspace() else { - return; - }; - let Some(panel) = workspace.read(cx).panel::(cx) else { - return; - }; - let weak_editor = cx.view().downgrade(); - panel.update(cx, |_, cx| { - cx.defer(|panel, cx| { - panel.interrupt(weak_editor, cx); - }); - }) - }, - )) - .detach(); - - editor - .register_action(cx.listener( - move |editor: &mut Editor, _: &Shutdown, cx: &mut ViewContext| { - if !JupyterSettings::enabled(cx) { - return; - } - let Some(workspace) = editor.workspace() else { - return; - }; - let Some(panel) = workspace.read(cx).panel::(cx) else { - return; - }; - let weak_editor = cx.view().downgrade(); - panel.update(cx, |_, cx| { - cx.defer(|panel, cx| { - panel.shutdown(weak_editor, cx); - }); - }) - }, - )) - .detach(); - }) - .detach(); -} - -pub struct RuntimePanel { - fs: Arc, - focus_handle: FocusHandle, - width: Option, - _subscriptions: Vec, -} - -impl RuntimePanel { - pub fn load( - workspace: WeakView, - cx: AsyncWindowContext, - ) -> Task>> { - cx.spawn(|mut cx| async move { - let view = workspace.update(&mut cx, |workspace, cx| { - cx.new_view::(|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), - ]; - - let runtime_panel = Self { - fs, - width: None, - focus_handle, - _subscriptions: subscriptions, - }; - - runtime_panel - }) - })?; - - view.update(&mut cx, |_panel, cx| { - let store = ReplStore::global(cx); - store.update(cx, |store, cx| store.refresh_kernelspecs(cx)) - })? - .await?; - - Ok(view) - }) - } - - fn focus_in(&mut self, cx: &mut ViewContext) { - cx.notify(); - } - - fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext) { - cx.notify(); - } - - fn snippet( - editor: WeakView, - cx: &mut ViewContext, - ) -> Option<(String, Arc, Range)> { - let editor = editor.upgrade()?; - let editor = editor.read(cx); - - let buffer = editor.buffer().read(cx).snapshot(cx); - - let selection = editor.selections.newest::(cx); - let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx); - - let range = if selection.is_empty() { - let cursor = selection.head(); - - let cursor_row = multi_buffer_snapshot.offset_to_point(cursor).row; - let start_offset = multi_buffer_snapshot.point_to_offset(Point::new(cursor_row, 0)); - - let end_point = Point::new( - cursor_row, - multi_buffer_snapshot.line_len(MultiBufferRow(cursor_row)), - ); - let end_offset = start_offset.saturating_add(end_point.column as usize); - - // Create a range from the start to the end of the line - start_offset..end_offset - } else { - selection.range() - }; - - let anchor_range = range.to_anchors(&multi_buffer_snapshot); - - let selected_text = buffer - .text_for_range(anchor_range.clone()) - .collect::(); - - let start_language = buffer.language_at(anchor_range.start)?; - let end_language = buffer.language_at(anchor_range.end)?; - if start_language != end_language { - return None; - } - - Some((selected_text, start_language.clone(), anchor_range)) - } - - 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 run(&mut self, editor: WeakView, cx: &mut ViewContext) -> Result<()> { - let store = ReplStore::global(cx); - - if !store.read(cx).is_enabled() { - return Ok(()); - } - - 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 = store.update(cx, |store, cx| { - store - .kernelspec(&language, cx) - .with_context(|| format!("No kernel found for language: {}", language.name())) - })?; - - 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(&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(); - - 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); - }); - - anyhow::Ok(()) - } - - 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) = 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); - }); - cx.notify(); - } - } - - 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) = store.read(cx).get_session(entity_id).cloned() { - session.update(cx, |session, cx| { - session.interrupt(cx); - }); - cx.notify(); - } - } - - pub fn shutdown(&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.shutdown(cx); - }); - cx.notify(); - } - } -} - -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, - ) { - settings::update_settings_file::(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, _cx: &mut ViewContext) { - self.width = size; - } - - fn icon(&self, cx: &ui::WindowContext) -> Option { - let store = ReplStore::global(cx); - - if !store.read(cx).is_enabled() { - return None; - } - - Some(IconName::Code) - } - - fn icon_tooltip(&self, _cx: &ui::WindowContext) -> Option<&'static str> { - Some("Runtime Panel") - } - - fn toggle_action(&self) -> Box { - Box::new(ToggleFocus) - } -} - -impl EventEmitter 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) -> 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 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 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() - ) - ) - ) - .child(Label::new("Kernels available").size(LabelSize::Large)) - .children( - 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)) - }) - ) - - .into_any_element(); - } - - v_flex() - .p_4() - .child(Label::new("Jupyter Kernel Sessions").size(LabelSize::Large)) - .children( - sessions - .into_iter() - .map(|session| session.clone().into_any_element()), - ) - .into_any_element() - } -} diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index c6b010c7d5..b5283fe96c 100644 --- a/crates/repl/src/session.rs +++ b/crates/repl/src/session.rs @@ -80,7 +80,7 @@ impl EditorBlock { 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(), on_close.clone()), + render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()), disposition: BlockDisposition::Below, }; @@ -111,7 +111,7 @@ impl EditorBlock { self.block_id, ( Some(self.execution_view.num_lines(cx).saturating_add(1)), - Self::create_output_area_render( + Self::create_output_area_renderer( self.execution_view.clone(), self.on_close.clone(), ), @@ -122,7 +122,7 @@ impl EditorBlock { .ok(); } - fn create_output_area_render( + fn create_output_area_renderer( execution_view: View, on_close: CloseBlockFn, ) -> RenderBlock { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 7c7329dd55..23573d4588 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -199,8 +199,6 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { let assistant_panel = assistant::AssistantPanel::load(workspace_handle.clone(), cx.clone()); - 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()); @@ -218,7 +216,6 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { outline_panel, terminal_panel, assistant_panel, - runtime_panel, channels_panel, chat_panel, notification_panel, @@ -227,7 +224,6 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { outline_panel, terminal_panel, assistant_panel, - runtime_panel, channels_panel, chat_panel, notification_panel, @@ -235,7 +231,6 @@ pub fn initialize_workspace(app_state: Arc, 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);