diff --git a/crates/quick_action_bar/src/repl_menu.rs b/crates/quick_action_bar/src/repl_menu.rs index e9bf998506..d2649d4180 100644 --- a/crates/quick_action_bar/src/repl_menu.rs +++ b/crates/quick_action_bar/src/repl_menu.rs @@ -4,8 +4,8 @@ use gpui::{percentage, Animation, AnimationExt, AnyElement, Transformation, View use picker::Picker; use repl::{ components::{KernelPickerDelegate, KernelSelector}, - ExecutionState, JupyterSettings, Kernel, KernelSpecification, KernelStatus, Session, - SessionSupport, + worktree_id_for_editor, ExecutionState, JupyterSettings, Kernel, KernelSpecification, + KernelStatus, Session, SessionSupport, }; use ui::{ prelude::*, ButtonLike, ContextMenu, IconWithIndicator, Indicator, IntoElement, PopoverMenu, @@ -30,9 +30,6 @@ struct ReplMenuState { status: KernelStatus, kernel_name: SharedString, kernel_language: SharedString, - // TODO: Persist rotation state so the - // icon doesn't reset on every state change - // current_delta: Duration, } impl QuickActionBar { @@ -178,12 +175,6 @@ impl QuickActionBar { }, ) .separator() - .link( - "Change Kernel", - Box::new(zed_actions::OpenBrowser { - url: format!("{}#change-kernel", ZED_REPL_DOCUMENTATION), - }), - ) .custom_entry( move |_cx| { Label::new("Shut Down Kernel") @@ -290,7 +281,10 @@ impl QuickActionBar { let editor = if let Some(editor) = self.active_editor() { editor } else { - // todo!() + return div().into_any_element(); + }; + + let Some(worktree_id) = worktree_id_for_editor(editor.downgrade(), cx) else { return div().into_any_element(); }; @@ -313,7 +307,7 @@ impl QuickActionBar { repl::assign_kernelspec(kernelspec, editor.downgrade(), cx).ok(); }) }, - current_kernelspec.clone(), + worktree_id, ButtonLike::new("kernel-selector") .style(ButtonStyle::Subtle) .child( diff --git a/crates/repl/src/components/kernel_options.rs b/crates/repl/src/components/kernel_options.rs index 361275c907..fc0213e54e 100644 --- a/crates/repl/src/components/kernel_options.rs +++ b/crates/repl/src/components/kernel_options.rs @@ -4,8 +4,10 @@ use crate::KERNEL_DOCS_URL; use gpui::DismissEvent; +use gpui::FontWeight; use picker::Picker; use picker::PickerDelegate; +use project::WorktreeId; use std::sync::Arc; use ui::ListItemSpacing; @@ -22,7 +24,7 @@ pub struct KernelSelector { on_select: OnSelect, trigger: T, info_text: Option, - current_kernelspec: Option, + worktree_id: WorktreeId, } pub struct KernelPickerDelegate { @@ -33,17 +35,13 @@ pub struct KernelPickerDelegate { } impl KernelSelector { - pub fn new( - on_select: OnSelect, - current_kernelspec: Option, - trigger: T, - ) -> Self { + pub fn new(on_select: OnSelect, worktree_id: WorktreeId, trigger: T) -> Self { KernelSelector { on_select, handle: None, trigger, info_text: None, - current_kernelspec, + worktree_id, } } @@ -130,24 +128,34 @@ impl PickerDelegate for KernelPickerDelegate { .spacing(ListItemSpacing::Sparse) .selected(selected) .child( - h_flex().w_full().justify_between().min_w(px(200.)).child( - h_flex() - .gap_1p5() - .child(Label::new(kernelspec.name())) - .child( - Label::new(kernelspec.type_name()) - .size(LabelSize::XSmall) - .color(Color::Muted), - ), - ), + v_flex() + .min_w(px(600.)) + .w_full() + .gap_0p5() + .child( + h_flex() + .w_full() + .gap_1() + .child(Label::new(kernelspec.name()).weight(FontWeight::MEDIUM)) + .child( + Label::new(kernelspec.language()) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + .child( + Label::new(kernelspec.path()) + .size(LabelSize::XSmall) + .color(Color::Muted), + ), ) - .end_slot(div().when(is_selected, |this| { - this.child( + .when(is_selected, |item| { + item.end_slot( Icon::new(IconName::Check) .color(Color::Accent) .size(IconSize::Small), ) - })), + }), ) } @@ -175,10 +183,13 @@ impl PickerDelegate for KernelPickerDelegate { impl RenderOnce for KernelSelector { fn render(self, cx: &mut WindowContext) -> impl IntoElement { let store = ReplStore::global(cx).read(cx); - let all_kernels: Vec = - store.kernel_specifications().cloned().collect(); - let selected_kernelspec = self.current_kernelspec; + let all_kernels: Vec = store + .kernel_specifications_for_worktree(self.worktree_id) + .cloned() + .collect(); + + let selected_kernelspec = store.active_kernelspec(self.worktree_id, None, cx); let delegate = KernelPickerDelegate { on_select: self.on_select, diff --git a/crates/repl/src/kernels.rs b/crates/repl/src/kernels.rs index eb0b886507..8ad8a05648 100644 --- a/crates/repl/src/kernels.rs +++ b/crates/repl/src/kernels.rs @@ -5,8 +5,9 @@ use futures::{ stream::{self, SelectAll, StreamExt}, SinkExt as _, }; -use gpui::{AppContext, EntityId, Task}; -use project::Fs; +use gpui::{AppContext, EntityId, Model, Task}; +use language::LanguageName; +use project::{Fs, Project, WorktreeId}; use runtimelib::{ dirs, ConnectionInfo, ExecutionState, JupyterKernelspec, JupyterMessage, JupyterMessageContent, KernelInfoReply, @@ -15,6 +16,7 @@ use smol::{net::TcpListener, process::Command}; use std::{ env, fmt::Debug, + future::Future, net::{IpAddr, Ipv4Addr, SocketAddr}, path::PathBuf, sync::Arc, @@ -465,6 +467,72 @@ async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> Result, + worktree_id: WorktreeId, + cx: &mut AppContext, +) -> impl Future>> { + let python_language = LanguageName::new("Python"); + let toolchains = project + .read(cx) + .available_toolchains(worktree_id, python_language, cx); + let background_executor = cx.background_executor().clone(); + + async move { + let toolchains = if let Some(toolchains) = toolchains.await { + toolchains + } else { + return Ok(Vec::new()); + }; + + let kernelspecs = toolchains.toolchains.into_iter().map(|toolchain| { + background_executor.spawn(async move { + let python_path = toolchain.path.to_string(); + + // Check if ipykernel is installed + let ipykernel_check = Command::new(&python_path) + .args(&["-c", "import ipykernel"]) + .output() + .await; + + if ipykernel_check.is_ok() && ipykernel_check.unwrap().status.success() { + // Create a default kernelspec for this environment + let default_kernelspec = JupyterKernelspec { + argv: vec![ + python_path.clone(), + "-m".to_string(), + "ipykernel_launcher".to_string(), + "-f".to_string(), + "{connection_file}".to_string(), + ], + display_name: toolchain.name.to_string(), + language: "python".to_string(), + interrupt_mode: None, + metadata: None, + env: None, + }; + + Some(KernelSpecification::PythonEnv(LocalKernelSpecification { + name: toolchain.name.to_string(), + path: PathBuf::from(&python_path), + kernelspec: default_kernelspec, + })) + } else { + None + } + }) + }); + + let kernel_specs = futures::future::join_all(kernelspecs) + .await + .into_iter() + .flatten() + .collect(); + + anyhow::Ok(kernel_specs) + } +} + pub async fn local_kernel_specifications(fs: Arc) -> Result> { let mut data_dirs = dirs::data_dirs(); diff --git a/crates/repl/src/repl_editor.rs b/crates/repl/src/repl_editor.rs index c6ab173204..b032b1804a 100644 --- a/crates/repl/src/repl_editor.rs +++ b/crates/repl/src/repl_editor.rs @@ -7,6 +7,7 @@ use anyhow::{Context, Result}; use editor::Editor; use gpui::{prelude::*, Entity, View, WeakView, WindowContext}; use language::{BufferSnapshot, Language, LanguageName, Point}; +use project::{Item as _, WorktreeId}; use crate::repl_store::ReplStore; use crate::session::SessionEvent; @@ -24,6 +25,13 @@ pub fn assign_kernelspec( return Ok(()); } + let worktree_id = crate::repl_editor::worktree_id_for_editor(weak_editor.clone(), cx) + .context("editor is not in a worktree")?; + + store.update(cx, |store, cx| { + store.set_active_kernelspec(worktree_id, kernel_specification.clone(), cx); + }); + let fs = store.read(cx).fs().clone(); let telemetry = store.read(cx).telemetry().clone(); @@ -79,6 +87,10 @@ pub fn run(editor: WeakView, move_down: bool, cx: &mut WindowContext) -> return Ok(()); }; + let Some(project_path) = buffer.read(cx).project_path(cx) else { + return Ok(()); + }; + let (runnable_ranges, next_cell_point) = runnable_ranges(&buffer.read(cx).snapshot(), selected_range); @@ -87,11 +99,10 @@ pub fn run(editor: WeakView, move_down: bool, cx: &mut WindowContext) -> continue; }; - let kernel_specification = store.update(cx, |store, cx| { - store - .kernelspec(language.code_fence_block_name().as_ref(), cx) - .with_context(|| format!("No kernel found for language: {}", language.name())) - })?; + let kernel_specification = store + .read(cx) + .active_kernelspec(project_path.worktree_id, Some(language.clone()), cx) + .ok_or_else(|| anyhow::anyhow!("No kernel found for language: {}", language.name()))?; let fs = store.read(cx).fs().clone(); let telemetry = store.read(cx).telemetry().clone(); @@ -156,6 +167,22 @@ pub enum SessionSupport { Unsupported, } +pub fn worktree_id_for_editor( + editor: WeakView, + cx: &mut WindowContext, +) -> Option { + editor.upgrade().and_then(|editor| { + editor + .read(cx) + .buffer() + .read(cx) + .as_singleton()? + .read(cx) + .project_path(cx) + .map(|path| path.worktree_id) + }) +} + pub fn session(editor: WeakView, cx: &mut WindowContext) -> SessionSupport { let store = ReplStore::global(cx); let entity_id = editor.entity_id(); @@ -164,17 +191,24 @@ pub fn session(editor: WeakView, cx: &mut WindowContext) -> SessionSuppo return SessionSupport::ActiveSession(session); }; - let Some(language) = get_language(editor, cx) else { + let Some(language) = get_language(editor.clone(), cx) else { return SessionSupport::Unsupported; }; - let kernelspec = store.update(cx, |store, cx| { - store.kernelspec(language.code_fence_block_name().as_ref(), cx) - }); + + let worktree_id = worktree_id_for_editor(editor.clone(), cx); + + let Some(worktree_id) = worktree_id else { + return SessionSupport::Unsupported; + }; + + let kernelspec = store + .read(cx) + .active_kernelspec(worktree_id, Some(language.clone()), cx); match kernelspec { Some(kernelspec) => SessionSupport::Inactive(kernelspec), None => { - if language_supported(&language) { + if language_supported(&language.clone()) { SessionSupport::RequiresSetup(language.name()) } else { SessionSupport::Unsupported diff --git a/crates/repl/src/repl_sessions_ui.rs b/crates/repl/src/repl_sessions_ui.rs index 5b01b971ba..32b91ce28c 100644 --- a/crates/repl/src/repl_sessions_ui.rs +++ b/crates/repl/src/repl_sessions_ui.rs @@ -1,10 +1,10 @@ use editor::Editor; use gpui::{ actions, prelude::*, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, - FontWeight, Subscription, View, + Subscription, View, }; -use std::collections::HashMap; -use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding, ListItem, Tooltip}; +use project::Item as _; +use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding}; use util::ResultExt as _; use workspace::item::ItemEvent; use workspace::WorkspaceId; @@ -12,7 +12,6 @@ use workspace::{item::Item, Workspace}; use crate::jupyter_settings::JupyterSettings; use crate::repl_store::ReplStore; -use crate::{KernelSpecification, KERNEL_DOCS_URL}; actions!( repl, @@ -63,17 +62,34 @@ pub fn init(cx: &mut AppContext) { cx.defer(|editor, cx| { let workspace = Workspace::for_window(cx); + let project = workspace.map(|workspace| workspace.read(cx).project().clone()); - let is_local_project = workspace - .map(|workspace| workspace.read(cx).project().read(cx).is_local()) + let is_local_project = project + .as_ref() + .map(|project| project.read(cx).is_local()) .unwrap_or(false); if !is_local_project { return; } + let project_path = editor + .buffer() + .read(cx) + .as_singleton() + .and_then(|buffer| buffer.read(cx).project_path(cx)); + let editor_handle = cx.view().downgrade(); + if let (Some(project_path), Some(project)) = (project_path, project) { + let store = ReplStore::global(cx); + store.update(cx, |store, cx| { + store + .refresh_python_kernelspecs(project_path.worktree_id, &project, cx) + .detach_and_log_err(cx); + }); + } + editor .register_action({ let editor_handle = editor_handle.clone(); @@ -169,7 +185,10 @@ impl Render for ReplSessionsPage { let (kernel_specifications, sessions) = store.update(cx, |store, _cx| { ( - store.kernel_specifications().cloned().collect::>(), + store + .pure_jupyter_kernel_specifications() + .cloned() + .collect::>(), store.sessions().cloned().collect::>(), ) }); @@ -198,97 +217,18 @@ impl Render for ReplSessionsPage { ); } - let mut kernels_by_language: HashMap> = - kernel_specifications - .iter() - .map(|spec| (spec.language(), spec)) - .fold(HashMap::new(), |mut acc, (language, spec)| { - acc.entry(language).or_default().push(spec); - acc - }); - - for kernels in kernels_by_language.values_mut() { - kernels.sort_by_key(|a| a.name()) - } - - // Convert to a sorted Vec of tuples - let mut sorted_kernels: Vec<(SharedString, Vec<&KernelSpecification>)> = - kernels_by_language.into_iter().collect(); - sorted_kernels.sort_by(|a, b| a.0.cmp(&b.0)); - - let kernels_available = v_flex() - .child(Label::new("Kernels available").size(LabelSize::Large)) - .gap_2() - .child( - h_flex() - .child(Label::new( - "Defaults indicated with a checkmark. Learn how to change your default kernel in the ", - )) - .child( - ButtonLike::new("configure-kernels") - .style(ButtonStyle::Filled) - // .size(ButtonSize::Compact) - .layer(ElevationIndex::Surface) - .child(Label::new("REPL documentation")) - .child(Icon::new(IconName::Link)) - .on_click(move |_, cx| { - cx.open_url(KERNEL_DOCS_URL) - }), - ), - ) - .children(sorted_kernels.into_iter().map(|(language, specs)| { - let chosen_kernel = store.read(cx).kernelspec(&language, cx); - - v_flex() - .gap_1() - .child(Label::new(language.clone()).weight(FontWeight::BOLD)) - .children(specs.into_iter().map(|spec| { - let is_choice = if let Some(chosen_kernel) = &chosen_kernel { - chosen_kernel == spec - } else { - false - }; - - let path = spec.path(); - - ListItem::new(path.clone()) - .selectable(false) - .tooltip({ - let path = path.clone(); - move |cx| Tooltip::text(path.clone(), cx)}) - .child( - h_flex() - .gap_1() - .child(div().id(path.clone()).child(Label::new(spec.name()))) - .when(is_choice, |el| { - - let language = language.clone(); - - el.child( - - div().id("check").tooltip(move |cx| Tooltip::text(format!("Default Kernel for {language}"), cx)) - .child(Icon::new(IconName::Check)))}), - ) - - })) - })); - // When there are no sessions, show the command to run code in an editor if sessions.is_empty() { let instructions = "To run code in a Jupyter kernel, select some code and use the 'repl::Run' command."; - return ReplSessionsContainer::new("No Jupyter Kernel Sessions") - .child( - v_flex() - .child(Label::new(instructions)) - .children(KeyBinding::for_action(&Run, cx)), - ) - .child(div().pt_3().child(kernels_available)); + return ReplSessionsContainer::new("No Jupyter Kernel Sessions").child( + v_flex() + .child(Label::new(instructions)) + .children(KeyBinding::for_action(&Run, cx)), + ); } - ReplSessionsContainer::new("Jupyter Kernel Sessions") - .children(sessions) - .child(kernels_available) + ReplSessionsContainer::new("Jupyter Kernel Sessions").children(sessions) } } diff --git a/crates/repl/src/repl_store.rs b/crates/repl/src/repl_store.rs index b4ff41cee8..a4863b809b 100644 --- a/crates/repl/src/repl_store.rs +++ b/crates/repl/src/repl_store.rs @@ -7,10 +7,11 @@ use command_palette_hooks::CommandPaletteFilter; use gpui::{ prelude::*, AppContext, EntityId, Global, Model, ModelContext, Subscription, Task, View, }; -use project::Fs; +use language::Language; +use project::{Fs, Project, WorktreeId}; use settings::{Settings, SettingsStore}; -use crate::kernels::local_kernel_specifications; +use crate::kernels::{local_kernel_specifications, python_env_kernel_specifications}; use crate::{JupyterSettings, KernelSpecification, Session}; struct GlobalReplStore(Model); @@ -22,6 +23,8 @@ pub struct ReplStore { enabled: bool, sessions: HashMap>, kernel_specifications: Vec, + selected_kernel_for_worktree: HashMap, + kernel_specifications_for_worktree: HashMap>, telemetry: Arc, _subscriptions: Vec, } @@ -55,6 +58,8 @@ impl ReplStore { sessions: HashMap::default(), kernel_specifications: Vec::new(), _subscriptions: subscriptions, + kernel_specifications_for_worktree: HashMap::default(), + selected_kernel_for_worktree: HashMap::default(), }; this.on_enabled_changed(cx); this @@ -72,7 +77,18 @@ impl ReplStore { self.enabled } - pub fn kernel_specifications(&self) -> impl Iterator { + pub fn kernel_specifications_for_worktree( + &self, + worktree_id: WorktreeId, + ) -> impl Iterator { + self.kernel_specifications_for_worktree + .get(&worktree_id) + .into_iter() + .flat_map(|specs| specs.iter()) + .chain(self.kernel_specifications.iter()) + } + + pub fn pure_jupyter_kernel_specifications(&self) -> impl Iterator { self.kernel_specifications.iter() } @@ -105,8 +121,29 @@ impl ReplStore { cx.notify(); } + pub fn refresh_python_kernelspecs( + &mut self, + worktree_id: WorktreeId, + project: &Model, + cx: &mut ModelContext, + ) -> Task> { + let kernel_specifications = python_env_kernel_specifications(project, worktree_id, cx); + cx.spawn(move |this, mut cx| async move { + let kernel_specifications = kernel_specifications + .await + .map_err(|e| anyhow::anyhow!("Failed to get python kernelspecs: {:?}", e))?; + + this.update(&mut cx, |this, cx| { + this.kernel_specifications_for_worktree + .insert(worktree_id, kernel_specifications); + cx.notify(); + }) + }) + } + pub fn refresh_kernelspecs(&mut self, cx: &mut ModelContext) -> Task> { let local_kernel_specifications = local_kernel_specifications(self.fs.clone()); + cx.spawn(|this, mut cx| async move { let local_kernel_specifications = local_kernel_specifications.await?; @@ -122,9 +159,41 @@ impl ReplStore { }) } - pub fn kernelspec(&self, language_name: &str, cx: &AppContext) -> Option { + pub fn set_active_kernelspec( + &mut self, + worktree_id: WorktreeId, + kernelspec: KernelSpecification, + _cx: &mut ModelContext, + ) { + self.selected_kernel_for_worktree + .insert(worktree_id, kernelspec); + } + + pub fn active_kernelspec( + &self, + worktree_id: WorktreeId, + language_at_cursor: Option>, + cx: &AppContext, + ) -> Option { + let selected_kernelspec = self.selected_kernel_for_worktree.get(&worktree_id).cloned(); + + if let Some(language_at_cursor) = language_at_cursor { + selected_kernelspec + .or_else(|| self.kernelspec_legacy_by_lang_only(language_at_cursor, cx)) + } else { + selected_kernelspec + } + } + + fn kernelspec_legacy_by_lang_only( + &self, + language_at_cursor: Arc, + cx: &AppContext, + ) -> Option { let settings = JupyterSettings::get_global(cx); - let selected_kernel = settings.kernel_selections.get(language_name); + let selected_kernel = settings + .kernel_selections + .get(language_at_cursor.code_fence_block_name().as_ref()); let found_by_name = self .kernel_specifications @@ -149,10 +218,15 @@ impl ReplStore { .find(|kernel_option| match kernel_option { KernelSpecification::Jupyter(runtime_specification) => { runtime_specification.kernelspec.language.to_lowercase() - == language_name.to_lowercase() + == language_at_cursor.code_fence_block_name().to_lowercase() + } + KernelSpecification::PythonEnv(runtime_specification) => { + runtime_specification.kernelspec.language.to_lowercase() + == language_at_cursor.code_fence_block_name().to_lowercase() + } + KernelSpecification::Remote(_) => { + unimplemented!() } - // todo!() - _ => false, }) .cloned() }