use editor::Editor;use gpui::{ AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, TaskExt, actions, prelude::*,};use language::{Buffer, BufferEvent};use project::{Project, ProjectItem as _};use ui::{ButtonLike, ElevationIndex, KeyBinding, prelude::*};use util::ResultExt as _;use workspace::item::ItemEvent;use workspace::{Workspace, item::Item}; use crate::jupyter_settings::JupyterSettings;use crate::repl_store::ReplStore; fn refresh_python_kernelspecs_for_buffer( buffer: &Entity<Buffer>, project: &Entity<Project>, cx: &mut App,) { let buffer = buffer.read(cx); if buffer .language() .is_none_or(|language| language.name() != "Python") { return; }; let Some(project_path) = buffer.project_path(cx) else { return; }; 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); });} actions!( repl, [ /// Runs the current cell and advances to the next one. Run, /// Runs the current cell without advancing. RunInPlace, /// Clears all outputs in the REPL. ClearOutputs, /// Clears the output of the cell at the current cursor position. ClearCurrentOutput, /// Opens the REPL sessions panel. Sessions, /// Interrupts the currently running kernel. Interrupt, /// Shuts down the current kernel. Shutdown, /// Restarts the current kernel. Restart, /// Refreshes the list of available kernelspecs. RefreshKernelspecs ]); pub fn init(cx: &mut App) { cx.observe_new( |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| { workspace.register_action(|workspace, _: &Sessions, window, cx| { let existing = workspace .active_pane() .read(cx) .items() .find_map(|item| item.downcast::<ReplSessionsPage>()); if let Some(existing) = existing { workspace.activate_item(&existing, true, true, window, cx); } else { let repl_sessions_page = ReplSessionsPage::new(window, cx); workspace.add_item_to_active_pane( Box::new(repl_sessions_page), None, true, window, 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( move |editor: &mut Editor, window, cx: &mut Context<Editor>| { let Some(window) = window else { return; }; if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() { return; } cx.defer_in(window, |editor, _window, cx| { let project = editor.project().cloned(); let is_valid_project = project .as_ref() .map(|project| { let p = project.read(cx); !p.is_via_collab() }) .unwrap_or(false); if !is_valid_project { return; } let buffer = editor.buffer().read(cx).as_singleton(); let editor_handle = cx.entity().downgrade(); // Subscribe to the buffer's `LanguageChanged` events so remote projects, // where language detection can complete after the editor is observed, // still trigger a kernelspec refresh. Without this the REPL UI stays // hidden until something else populates the global kernel list. if let Some((buffer, project)) = buffer.zip(project) { refresh_python_kernelspecs_for_buffer(&buffer, &project, cx); cx.subscribe(&buffer, move |_editor, buffer, event, cx| { if let BufferEvent::LanguageChanged(_) = event { refresh_python_kernelspecs_for_buffer(&buffer, &project, cx); } }) .detach(); } editor .register_action({ let editor_handle = editor_handle.clone(); move |_: &Run, window, cx| { if !JupyterSettings::enabled(cx) { return; } crate::run(editor_handle.clone(), true, window, cx).log_err(); } }) .detach(); editor .register_action({ move |_: &RunInPlace, window, cx| { if !JupyterSettings::enabled(cx) { return; } crate::run(editor_handle.clone(), false, window, cx).log_err(); } }) .detach(); }); }, ) .detach();} pub struct ReplSessionsPage { focus_handle: FocusHandle, _subscriptions: Vec<Subscription>,} impl ReplSessionsPage { pub fn new(window: &mut Window, cx: &mut Context<Workspace>) -> Entity<Self> { cx.new(|cx| { let focus_handle = cx.focus_handle(); let subscriptions = vec![ cx.on_focus_in(&focus_handle, window, |_this, _window, cx| cx.notify()), cx.on_focus_out(&focus_handle, window, |_this, _event, _window, cx| { cx.notify() }), ]; Self { focus_handle, _subscriptions: subscriptions, } }) }} impl EventEmitter<ItemEvent> for ReplSessionsPage {} impl Focusable for ReplSessionsPage { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() }} impl Item for ReplSessionsPage { type Event = ItemEvent; fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { "REPL Sessions".into() } fn telemetry_event_text(&self) -> Option<&'static str> { Some("REPL Session Started") } fn show_toolbar(&self) -> bool { false } fn to_item_events(event: &Self::Event, f: &mut dyn FnMut(workspace::item::ItemEvent)) { f(*event) }} impl Render for ReplSessionsPage { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let store = ReplStore::global(cx); let (kernel_specifications, sessions) = store.update(cx, |store, cx| { store.ensure_kernelspecs(cx); ( store .pure_jupyter_kernel_specifications() .cloned() .collect::<Vec<_>>(), store.sessions().cloned().collect::<Vec<_>>(), ) }); // 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() { let instructions = "To start interactively running code in your editor, you need to install and configure Jupyter kernels."; return ReplSessionsContainer::new("No Jupyter Kernels Available") .child(Label::new(instructions)) .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://zed.dev/docs/repl#language-specific-instructions", ) }), ), ); } // 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)) .child(KeyBinding::for_action(&Run, cx)), ); } ReplSessionsContainer::new("Jupyter Kernel Sessions").children(sessions) }} #[derive(IntoElement)]struct ReplSessionsContainer { title: SharedString, children: Vec<AnyElement>,} impl ReplSessionsContainer { pub fn new(title: impl Into<SharedString>) -> Self { Self { title: title.into(), children: Vec::new(), } }} impl ParentElement for ReplSessionsContainer { fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) { self.children.extend(elements) }} impl RenderOnce for ReplSessionsContainer { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { v_flex() .p_4() .gap_2() .size_full() .child(Label::new(self.title).size(LabelSize::Large)) .children(self.children) }} #[cfg(test)]mod tests { use super::*; use std::{path::PathBuf, sync::Arc}; use async_trait::async_trait; use collections::HashMap; use editor::EditorMode; use gpui::TestAppContext; use language::{ Language, LanguageConfig, LanguageMatcher, LanguageName, ManifestName, Toolchain, ToolchainList, ToolchainLister, ToolchainMetadata, }; use multi_buffer::MultiBuffer; use task::ShellKind; use util::path; struct TestPythonToolchainLister; #[async_trait] impl ToolchainLister for TestPythonToolchainLister { async fn list( &self, _worktree_root: PathBuf, _subroot_relative_path: Arc<util::rel_path::RelPath>, _project_env: Option<HashMap<String, String>>, ) -> ToolchainList { ToolchainList { toolchains: vec![Toolchain { name: SharedString::new_static("Test Python"), path: SharedString::new_static("/test/python"), language_name: LanguageName::new_static("Python"), as_json: serde_json::Value::Null, }], ..Default::default() } } async fn resolve( &self, _path: PathBuf, _project_env: Option<HashMap<String, String>>, ) -> anyhow::Result<Toolchain> { anyhow::bail!("not implemented") } fn activation_script( &self, _toolchain: &Toolchain, _shell: ShellKind, _cx: &App, ) -> futures::future::BoxFuture<'static, Vec<String>> { Box::pin(async { Vec::new() }) } fn meta(&self) -> ToolchainMetadata { ToolchainMetadata { term: SharedString::new_static("Python"), new_toolchain_placeholder: SharedString::default(), manifest_name: ManifestName::from(SharedString::new_static("pyproject.toml")), } } } #[gpui::test] async fn test_refreshes_python_kernelspecs_when_buffer_language_changes( cx: &mut TestAppContext, ) { cx.update(|cx| { settings::init(cx); theme_settings::init(theme::LoadThemes::JustBase, cx); editor::init(cx); }); let fs = project::FakeFs::new(cx.background_executor.clone()); fs.insert_tree( path!("/project"), serde_json::json!({ "main.txt": "print('hi')", }), ) .await; let project = project::Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let python = Arc::new( Language::new( LanguageConfig { name: "Python".into(), matcher: (LanguageMatcher { path_suffixes: vec!["py".to_string()], ..Default::default() }) .into(), ..Default::default() }, None, ) .with_manifest(Some(ManifestName::from(SharedString::new_static( "pyproject.toml", )))) .with_toolchain_lister(Some(Arc::new(TestPythonToolchainLister))), ); project.read_with(cx, |project, _cx| { project.languages().add(python.clone()); }); cx.update(|cx| crate::init(fs, cx)); let buffer = project .update(cx, |project, cx| { project.open_local_buffer(path!("/project/main.txt"), cx) }) .await .expect("failed to open buffer"); let worktree_id = buffer .read_with(cx, |buffer, cx| { buffer.project_path(cx).map(|path| path.worktree_id) }) .expect("buffer should have a project path"); cx.add_window(|window, cx| { let multi_buffer = MultiBuffer::build_from_buffer(buffer.clone(), cx); Editor::new( EditorMode::full(), multi_buffer, Some(project.clone()), window, cx, ) }); cx.run_until_parked(); let store = cx.update(|cx| ReplStore::global(cx)); assert!(!cx.update(|cx| store.read(cx).has_python_kernelspecs(worktree_id))); buffer.update(cx, |buffer, cx| { buffer.set_language(Some(python), cx); }); cx.run_until_parked(); assert!(cx.update(|cx| store.read(cx).has_python_kernelspecs(worktree_id))); }}
use editor::Editor;
use gpui::{
AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, TaskExt, actions,
prelude::*,
};
use language::{Buffer, BufferEvent};
use project::{Project, ProjectItem as _};
use ui::{ButtonLike, ElevationIndex, KeyBinding, prelude::*};
use util::ResultExt as _;
use workspace::item::ItemEvent;
use workspace::{Workspace, item::Item};
use crate::jupyter_settings::JupyterSettings;
use crate::repl_store::ReplStore;
fn refresh_python_kernelspecs_for_buffer(
buffer: &Entity<Buffer>,
project: &Entity<Project>,
cx: &mut App,
) {
let buffer = buffer.read(cx);
if buffer
.language()
.is_none_or(|language| language.name() != "Python")
{
return;
};
let Some(project_path) = buffer.project_path(cx) else {
return;
};
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);
});
}
actions!(
repl,
[
/// Runs the current cell and advances to the next one.
Run,
/// Runs the current cell without advancing.
RunInPlace,
/// Clears all outputs in the REPL.
ClearOutputs,
/// Clears the output of the cell at the current cursor position.
ClearCurrentOutput,
/// Opens the REPL sessions panel.
Sessions,
/// Interrupts the currently running kernel.
Interrupt,
/// Shuts down the current kernel.
Shutdown,
/// Restarts the current kernel.
Restart,
/// Refreshes the list of available kernelspecs.
RefreshKernelspecs
]
);
pub fn init(cx: &mut App) {
cx.observe_new(
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
workspace.register_action(|workspace, _: &Sessions, window, cx| {
let existing = workspace
.active_pane()
.read(cx)
.items()
.find_map(|item| item.downcast::<ReplSessionsPage>());
if let Some(existing) = existing {
workspace.activate_item(&existing, true, true, window, cx);
} else {
let repl_sessions_page = ReplSessionsPage::new(window, cx);
workspace.add_item_to_active_pane(
Box::new(repl_sessions_page),
None,
true,
window,
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(
move |editor: &mut Editor, window, cx: &mut Context<Editor>| {
let Some(window) = window else {
return;
};
if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() {
return;
}
cx.defer_in(window, |editor, _window, cx| {
let project = editor.project().cloned();
let is_valid_project = project
.as_ref()
.map(|project| {
let p = project.read(cx);
!p.is_via_collab()
})
.unwrap_or(false);
if !is_valid_project {
return;
}
let buffer = editor.buffer().read(cx).as_singleton();
let editor_handle = cx.entity().downgrade();
// Subscribe to the buffer's `LanguageChanged` events so remote projects,
// where language detection can complete after the editor is observed,
// still trigger a kernelspec refresh. Without this the REPL UI stays
// hidden until something else populates the global kernel list.
if let Some((buffer, project)) = buffer.zip(project) {
refresh_python_kernelspecs_for_buffer(&buffer, &project, cx);
cx.subscribe(&buffer, move |_editor, buffer, event, cx| {
if let BufferEvent::LanguageChanged(_) = event {
refresh_python_kernelspecs_for_buffer(&buffer, &project, cx);
}
})
.detach();
}
editor
.register_action({
let editor_handle = editor_handle.clone();
move |_: &Run, window, cx| {
if !JupyterSettings::enabled(cx) {
return;
}
crate::run(editor_handle.clone(), true, window, cx).log_err();
}
})
.detach();
editor
.register_action({
move |_: &RunInPlace, window, cx| {
if !JupyterSettings::enabled(cx) {
return;
}
crate::run(editor_handle.clone(), false, window, cx).log_err();
}
})
.detach();
});
},
)
.detach();
}
pub struct ReplSessionsPage {
focus_handle: FocusHandle,
_subscriptions: Vec<Subscription>,
}
impl ReplSessionsPage {
pub fn new(window: &mut Window, cx: &mut Context<Workspace>) -> Entity<Self> {
cx.new(|cx| {
let focus_handle = cx.focus_handle();
let subscriptions = vec![
cx.on_focus_in(&focus_handle, window, |_this, _window, cx| cx.notify()),
cx.on_focus_out(&focus_handle, window, |_this, _event, _window, cx| {
cx.notify()
}),
];
Self {
focus_handle,
_subscriptions: subscriptions,
}
})
}
}
impl EventEmitter<ItemEvent> for ReplSessionsPage {}
impl Focusable for ReplSessionsPage {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Item for ReplSessionsPage {
type Event = ItemEvent;
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
"REPL Sessions".into()
}
fn telemetry_event_text(&self) -> Option<&'static str> {
Some("REPL Session Started")
}
fn show_toolbar(&self) -> bool {
false
}
fn to_item_events(event: &Self::Event, f: &mut dyn FnMut(workspace::item::ItemEvent)) {
f(*event)
}
}
impl Render for ReplSessionsPage {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let store = ReplStore::global(cx);
let (kernel_specifications, sessions) = store.update(cx, |store, cx| {
store.ensure_kernelspecs(cx);
(
store
.pure_jupyter_kernel_specifications()
.cloned()
.collect::<Vec<_>>(),
store.sessions().cloned().collect::<Vec<_>>(),
)
});
// 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() {
let instructions = "To start interactively running code in your editor, you need to install and configure Jupyter kernels.";
return ReplSessionsContainer::new("No Jupyter Kernels Available")
.child(Label::new(instructions))
.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://zed.dev/docs/repl#language-specific-instructions",
)
}),
),
);
}
// 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))
.child(KeyBinding::for_action(&Run, cx)),
);
}
ReplSessionsContainer::new("Jupyter Kernel Sessions").children(sessions)
}
}
#[derive(IntoElement)]
struct ReplSessionsContainer {
title: SharedString,
children: Vec<AnyElement>,
}
impl ReplSessionsContainer {
pub fn new(title: impl Into<SharedString>) -> Self {
Self {
title: title.into(),
children: Vec::new(),
}
}
}
impl ParentElement for ReplSessionsContainer {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
impl RenderOnce for ReplSessionsContainer {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
v_flex()
.p_4()
.gap_2()
.size_full()
.child(Label::new(self.title).size(LabelSize::Large))
.children(self.children)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use collections::HashMap;
use editor::EditorMode;
use gpui::TestAppContext;
use language::{
Language, LanguageConfig, LanguageMatcher, LanguageName, ManifestName, Toolchain,
ToolchainList, ToolchainLister, ToolchainMetadata,
};
use multi_buffer::MultiBuffer;
use task::ShellKind;
use util::path;
struct TestPythonToolchainLister;
#[async_trait]
impl ToolchainLister for TestPythonToolchainLister {
async fn list(
&self,
_worktree_root: PathBuf,
_subroot_relative_path: Arc<util::rel_path::RelPath>,
_project_env: Option<HashMap<String, String>>,
) -> ToolchainList {
ToolchainList {
toolchains: vec![Toolchain {
name: SharedString::new_static("Test Python"),
path: SharedString::new_static("/test/python"),
language_name: LanguageName::new_static("Python"),
as_json: serde_json::Value::Null,
}],
..Default::default()
}
}
async fn resolve(
&self,
_path: PathBuf,
_project_env: Option<HashMap<String, String>>,
) -> anyhow::Result<Toolchain> {
anyhow::bail!("not implemented")
}
fn activation_script(
&self,
_toolchain: &Toolchain,
_shell: ShellKind,
_cx: &App,
) -> futures::future::BoxFuture<'static, Vec<String>> {
Box::pin(async { Vec::new() })
}
fn meta(&self) -> ToolchainMetadata {
ToolchainMetadata {
term: SharedString::new_static("Python"),
new_toolchain_placeholder: SharedString::default(),
manifest_name: ManifestName::from(SharedString::new_static("pyproject.toml")),
}
}
}
#[gpui::test]
async fn test_refreshes_python_kernelspecs_when_buffer_language_changes(
cx: &mut TestAppContext,
) {
cx.update(|cx| {
settings::init(cx);
theme_settings::init(theme::LoadThemes::JustBase, cx);
editor::init(cx);
});
let fs = project::FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
path!("/project"),
serde_json::json!({
"main.txt": "print('hi')",
}),
)
.await;
let project = project::Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let python = Arc::new(
Language::new(
LanguageConfig {
name: "Python".into(),
matcher: (LanguageMatcher {
path_suffixes: vec!["py".to_string()],
..Default::default()
})
.into(),
..Default::default()
},
None,
)
.with_manifest(Some(ManifestName::from(SharedString::new_static(
"pyproject.toml",
))))
.with_toolchain_lister(Some(Arc::new(TestPythonToolchainLister))),
);
project.read_with(cx, |project, _cx| {
project.languages().add(python.clone());
});
cx.update(|cx| crate::init(fs, cx));
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer(path!("/project/main.txt"), cx)
})
.await
.expect("failed to open buffer");
let worktree_id = buffer
.read_with(cx, |buffer, cx| {
buffer.project_path(cx).map(|path| path.worktree_id)
})
.expect("buffer should have a project path");
cx.add_window(|window, cx| {
let multi_buffer = MultiBuffer::build_from_buffer(buffer.clone(), cx);
Editor::new(
EditorMode::full(),
multi_buffer,
Some(project.clone()),
window,
cx,
)
});
cx.run_until_parked();
let store = cx.update(|cx| ReplStore::global(cx));
assert!(!cx.update(|cx| store.read(cx).has_python_kernelspecs(worktree_id)));
buffer.update(cx, |buffer, cx| {
buffer.set_language(Some(python), cx);
});
cx.run_until_parked();
assert!(cx.update(|cx| store.read(cx).has_python_kernelspecs(worktree_id)));
}
}