repl: Refresh Python kernelspecs on buffer language change (#54709)

- Subscribe to buffer LanguageChanged events so language detection that
completes later (e.g. in remote projects) triggers a kernelspec refresh
and ensures the REPL UI is populated.

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #54388

Release Notes:

- Python REPL UI stays responsive on remote connection

---------

Co-authored-by: MrSubidubi <finn@zed.dev>
This commit is contained in:
MostlyK 2026-05-15 17:37:32 +05:30 committed by GitHub
parent 2a74fb78d6
commit d88afd0e90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 189 additions and 17 deletions

2
Cargo.lock generated
View file

@ -14741,6 +14741,7 @@ dependencies = [
"anyhow",
"async-dispatcher",
"async-task",
"async-trait",
"async-tungstenite",
"base64 0.22.1",
"client",
@ -14773,6 +14774,7 @@ dependencies = [
"settings",
"shlex",
"smol",
"task",
"telemetry",
"terminal",
"terminal_view",

View file

@ -62,6 +62,7 @@ picker.workspace = true
zed_actions.workspace = true
[dev-dependencies]
async-trait.workspace = true
editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }
@ -70,6 +71,7 @@ language = { workspace = true, features = ["test-support"] }
languages = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
task.workspace = true
terminal_view = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] }
tree-sitter-md.workspace = true

View file

@ -3,7 +3,8 @@ use gpui::{
AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, TaskExt, actions,
prelude::*,
};
use project::ProjectItem as _;
use language::{Buffer, BufferEvent};
use project::{Project, ProjectItem as _};
use ui::{ButtonLike, ElevationIndex, KeyBinding, prelude::*};
use util::ResultExt as _;
use workspace::item::ItemEvent;
@ -12,6 +13,31 @@ 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,
[
@ -97,24 +123,21 @@ pub fn init(cx: &mut App) {
let buffer = editor.buffer().read(cx).as_singleton();
let language = buffer
.as_ref()
.and_then(|buffer| buffer.read(cx).language());
let project_path = buffer.and_then(|buffer| buffer.read(cx).project_path(cx));
let editor_handle = cx.entity().downgrade();
if let Some(language) = language
&& language.name() == "Python"
&& 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);
});
// 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
@ -285,3 +308,148 @@ impl RenderOnce for ReplSessionsContainer {
.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()
},
..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)));
}
}