Detailed changes
@@ -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(
@@ -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<T: PopoverTrigger> {
on_select: OnSelect,
trigger: T,
info_text: Option<SharedString>,
- current_kernelspec: Option<KernelSpecification>,
+ worktree_id: WorktreeId,
}
pub struct KernelPickerDelegate {
@@ -33,17 +35,13 @@ pub struct KernelPickerDelegate {
}
impl<T: PopoverTrigger> KernelSelector<T> {
- pub fn new(
- on_select: OnSelect,
- current_kernelspec: Option<KernelSpecification>,
- 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<T: PopoverTrigger> RenderOnce for KernelSelector<T> {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let store = ReplStore::global(cx).read(cx);
- let all_kernels: Vec<KernelSpecification> =
- store.kernel_specifications().cloned().collect();
- let selected_kernelspec = self.current_kernelspec;
+ let all_kernels: Vec<KernelSpecification> = 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,
@@ -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<Vec<LocalKernelS
Ok(valid_kernelspecs)
}
+pub fn python_env_kernel_specifications(
+ project: &Model<Project>,
+ worktree_id: WorktreeId,
+ cx: &mut AppContext,
+) -> impl Future<Output = Result<Vec<KernelSpecification>>> {
+ 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<dyn Fs>) -> Result<Vec<LocalKernelSpecification>> {
let mut data_dirs = dirs::data_dirs();
@@ -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<Editor>, 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<Editor>, 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<Editor>,
+ cx: &mut WindowContext,
+) -> Option<WorktreeId> {
+ 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<Editor>, cx: &mut WindowContext) -> SessionSupport {
let store = ReplStore::global(cx);
let entity_id = editor.entity_id();
@@ -164,17 +191,24 @@ pub fn session(editor: WeakView<Editor>, 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
@@ -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::<Vec<_>>(),
+ store
+ .pure_jupyter_kernel_specifications()
+ .cloned()
+ .collect::<Vec<_>>(),
store.sessions().cloned().collect::<Vec<_>>(),
)
});
@@ -198,97 +217,18 @@ impl Render for ReplSessionsPage {
);
}
- let mut kernels_by_language: HashMap<SharedString, Vec<&KernelSpecification>> =
- 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)
}
}
@@ -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<ReplStore>);
@@ -22,6 +23,8 @@ pub struct ReplStore {
enabled: bool,
sessions: HashMap<EntityId, View<Session>>,
kernel_specifications: Vec<KernelSpecification>,
+ selected_kernel_for_worktree: HashMap<WorktreeId, KernelSpecification>,
+ kernel_specifications_for_worktree: HashMap<WorktreeId, Vec<KernelSpecification>>,
telemetry: Arc<Telemetry>,
_subscriptions: Vec<Subscription>,
}
@@ -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<Item = &KernelSpecification> {
+ pub fn kernel_specifications_for_worktree(
+ &self,
+ worktree_id: WorktreeId,
+ ) -> impl Iterator<Item = &KernelSpecification> {
+ 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<Item = &KernelSpecification> {
self.kernel_specifications.iter()
}
@@ -105,8 +121,29 @@ impl ReplStore {
cx.notify();
}
+ pub fn refresh_python_kernelspecs(
+ &mut self,
+ worktree_id: WorktreeId,
+ project: &Model<Project>,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<()>> {
+ 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<Self>) -> Task<Result<()>> {
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<KernelSpecification> {
+ pub fn set_active_kernelspec(
+ &mut self,
+ worktree_id: WorktreeId,
+ kernelspec: KernelSpecification,
+ _cx: &mut ModelContext<Self>,
+ ) {
+ self.selected_kernel_for_worktree
+ .insert(worktree_id, kernelspec);
+ }
+
+ pub fn active_kernelspec(
+ &self,
+ worktree_id: WorktreeId,
+ language_at_cursor: Option<Arc<Language>>,
+ cx: &AppContext,
+ ) -> Option<KernelSpecification> {
+ 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<Language>,
+ cx: &AppContext,
+ ) -> Option<KernelSpecification> {
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()
}