repl: Support kernel language aliases in REPL (#49762)

Kyle Kelley created

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

Change summary

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(-)

Detailed changes

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<Arc<str>>,
+    /// 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<Arc<str>>,
     // The name of the grammar in a WASM bundle (experimental).
     pub grammar: Option<Arc<str>>,
     /// 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<Arc<dyn ContextProvider>> {
         self.context_provider.clone()
     }

crates/repl/src/repl_editor.rs 🔗

@@ -636,12 +636,9 @@ fn language_supported(language: &Arc<Language>, 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<Editor>, cx: &mut App) -> Option<Arc<Language>> {

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()
     }