From 74e747a6c77ee7c5a6eb5dd70e4aae23002a2947 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Wed, 4 Mar 2026 09:58:51 -0800 Subject: [PATCH] repl: Support kernel language aliases in REPL (#49762) Add a `kernel_language_names` field to `LanguageConfig` that allows languages to declare alternative names that Jupyter kernels may use. This fixes REPL matching for cases where a kernel reports a different language identifier than Zed's language name. For example, the Nu extension would set `kernel_language_names = ["nushell", "nu"]` in its config.toml, enabling REPL support for nu-jupyter-kernel which reports `"language": "nushell"` in its kernelspec. The change consolidates kernel language matching logic into a single `Language::matches_kernel_language()` method that checks the code fence block name, language name, and the new aliases list (all case-insensitive). - [x] Done a self-review taking into account security and performance aspects Release Notes: - Added `kernel_language_names` field for extensions to self identify REPL mappings --- crates/language/src/language.rs | 23 +++++++++++++++++++++++ crates/repl/src/repl_editor.rs | 9 +++------ crates/repl/src/repl_store.rs | 9 ++++----- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index fe5c5d09aa0765e2c305d88c65e86d6832443b1e..435d3d4e27998cb135dc3145ad7800ed8da97c9e 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -835,6 +835,11 @@ pub struct LanguageConfig { pub name: LanguageName, /// The name of this language for a Markdown code fence block pub code_fence_block_name: Option>, + /// Alternative language names that Jupyter kernels may report for this language. + /// Used when a kernel's `language` field differs from Zed's language name. + /// For example, the Nu extension would set this to `["nushell"]`. + #[serde(default)] + pub kernel_language_names: Vec>, // The name of the grammar in a WASM bundle (experimental). pub grammar: Option>, /// The criteria for matching this language to a given file. @@ -1141,6 +1146,7 @@ impl Default for LanguageConfig { Self { name: LanguageName::new_static(""), code_fence_block_name: None, + kernel_language_names: Default::default(), grammar: None, matcher: LanguageMatcher::default(), brackets: Default::default(), @@ -2075,6 +2081,23 @@ impl Language { .unwrap_or_else(|| self.config.name.as_ref().to_lowercase().into()) } + pub fn matches_kernel_language(&self, kernel_language: &str) -> bool { + let kernel_language_lower = kernel_language.to_lowercase(); + + if self.code_fence_block_name().to_lowercase() == kernel_language_lower { + return true; + } + + if self.config.name.as_ref().to_lowercase() == kernel_language_lower { + return true; + } + + self.config + .kernel_language_names + .iter() + .any(|name| name.to_lowercase() == kernel_language_lower) + } + pub fn context_provider(&self) -> Option> { self.context_provider.clone() } diff --git a/crates/repl/src/repl_editor.rs b/crates/repl/src/repl_editor.rs index 6e061c3e2e37aa94074f17f94791ad147f56f344..56b79e20ffca74ab3f9f9c7948a7caeffc4ad4ce 100644 --- a/crates/repl/src/repl_editor.rs +++ b/crates/repl/src/repl_editor.rs @@ -636,12 +636,9 @@ fn language_supported(language: &Arc, cx: &mut App) -> bool { let store = ReplStore::global(cx); let store_read = store.read(cx); - // Since we're just checking for general language support, we only need to look at - // the pure Jupyter kernels - these are all the globally available ones - store_read.pure_jupyter_kernel_specifications().any(|spec| { - // Convert to lowercase for case-insensitive comparison since kernels might report "python" while our language is "Python" - spec.language().as_ref().to_lowercase() == language.name().as_ref().to_lowercase() - }) + store_read + .pure_jupyter_kernel_specifications() + .any(|spec| language.matches_kernel_language(spec.language().as_ref())) } fn get_language(editor: WeakEntity, cx: &mut App) -> Option> { diff --git a/crates/repl/src/repl_store.rs b/crates/repl/src/repl_store.rs index 1c6ce99c2177260c1b9aaf1733326ddbda85a64f..8da94eaa7fe40e28a1d6336a648d7eae5c6767ae 100644 --- a/crates/repl/src/repl_store.rs +++ b/crates/repl/src/repl_store.rs @@ -289,7 +289,6 @@ impl ReplStore { } let language_at_cursor = language_at_cursor?; - let language_name = language_at_cursor.code_fence_block_name().to_lowercase(); // Prefer the recommended (active toolchain) kernel if it has ipykernel if let Some(active_path) = self.active_python_toolchain_path(worktree_id) { @@ -297,7 +296,7 @@ impl ReplStore { .kernel_specifications_for_worktree(worktree_id) .find(|spec| { spec.has_ipykernel() - && spec.language().as_ref().to_lowercase() == language_name + && language_at_cursor.matches_kernel_language(spec.language().as_ref()) && spec.path().as_ref() == active_path.as_ref() }) .cloned(); @@ -312,7 +311,7 @@ impl ReplStore { .find(|spec| { matches!(spec, KernelSpecification::PythonEnv(_)) && spec.has_ipykernel() - && spec.language().as_ref().to_lowercase() == language_name + && language_at_cursor.matches_kernel_language(spec.language().as_ref()) }) .cloned(); if python_env.is_some() { @@ -350,10 +349,10 @@ impl ReplStore { return Some(found_by_name); } - let language_name = language_at_cursor.code_fence_block_name().to_lowercase(); self.kernel_specifications_for_worktree(worktree_id) .find(|spec| { - spec.has_ipykernel() && spec.language().as_ref().to_lowercase() == language_name + spec.has_ipykernel() + && language_at_cursor.matches_kernel_language(spec.language().as_ref()) }) .cloned() }