diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 5a3f693983..97483cf3da 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -1495,18 +1495,23 @@ impl ToolchainLister for PythonToolchainProvider { } } + // Only inject `{manager} activate ` when we have a + // safely-quotable name. Never silently fall back to + // `activate base`: a user with miniforge installed but a + // local uv/venv project should not have their terminal + // hijacked just because we couldn't resolve a name. if let Some(name) = &toolchain.environment.name { if let Some(quoted_name) = shell.try_quote(name) { activation_script.push(format!("{manager} activate {quoted_name}")); } else { log::warn!( - "Could not safely quote environment name {:?}, falling back to base", + "Conda environment name {:?} could not be safely quoted; \ + skipping terminal activation", name ); - activation_script.push(format!("{manager} activate base")); } } else { - activation_script.push(format!("{manager} activate base")); + log::warn!("Conda toolchain has no name; skipping terminal activation"); } } Some( @@ -2793,6 +2798,143 @@ mod tests { ); } + #[gpui::test] + async fn test_conda_activation_skips_when_name_missing(cx: &mut TestAppContext) { + use language::{LanguageName, Toolchain, ToolchainLister}; + use settings::{CondaManager, VenvSettings}; + use task::ShellKind; + + use crate::python::PythonToolchainProvider; + + cx.executor().allow_parking(); + + cx.update(|cx| { + let test_settings = SettingsStore::test(cx); + cx.set_global(test_settings); + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |s| { + s.terminal + .get_or_insert_with(Default::default) + .project + .detect_venv = Some(VenvSettings::On { + activate_script: None, + venv_name: None, + directories: None, + conda_manager: Some(CondaManager::Conda), + }); + }); + }); + }); + + let fs = project::FakeFs::new(cx.executor()); + let provider = PythonToolchainProvider::new(fs); + let manager_executable = std::env::current_exe().unwrap(); + + let data = serde_json::json!({ + "name": serde_json::Value::Null, + "kind": "Conda", + "executable": "/tmp/conda/bin/python", + "version": serde_json::Value::Null, + "prefix": serde_json::Value::Null, + "arch": serde_json::Value::Null, + "displayName": serde_json::Value::Null, + "project": serde_json::Value::Null, + "symlinks": serde_json::Value::Null, + "manager": { + "executable": manager_executable, + "version": serde_json::Value::Null, + "tool": "Conda", + }, + }); + + let toolchain = Toolchain { + name: "test".into(), + path: "/tmp/conda".into(), + language_name: LanguageName::new_static("Python"), + as_json: data, + }; + + let script = cx + .update(|cx| provider.activation_script(&toolchain, ShellKind::Posix, cx)) + .await; + + assert!( + script.is_empty(), + "Nameless conda toolchains must not fall back to `conda activate base`, actual: {:?}", + script + ); + } + + #[gpui::test] + async fn test_conda_activation_skips_unquotable_name(cx: &mut TestAppContext) { + use language::{LanguageName, Toolchain, ToolchainLister}; + use settings::{CondaManager, VenvSettings}; + use task::ShellKind; + + use crate::python::PythonToolchainProvider; + + cx.executor().allow_parking(); + + cx.update(|cx| { + let test_settings = SettingsStore::test(cx); + cx.set_global(test_settings); + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |s| { + s.terminal + .get_or_insert_with(Default::default) + .project + .detect_venv = Some(VenvSettings::On { + activate_script: None, + venv_name: None, + directories: None, + conda_manager: Some(CondaManager::Conda), + }); + }); + }); + }); + + let fs = project::FakeFs::new(cx.executor()); + let provider = PythonToolchainProvider::new(fs); + // shlex::try_quote rejects strings containing a NUL byte, so this name + // is guaranteed to fail the Posix quoting path. + let unquotable_name = "foo\0bar"; + let manager_executable = std::env::current_exe().unwrap(); + + let data = serde_json::json!({ + "name": unquotable_name, + "kind": "Conda", + "executable": "/tmp/conda/bin/python", + "version": serde_json::Value::Null, + "prefix": serde_json::Value::Null, + "arch": serde_json::Value::Null, + "displayName": serde_json::Value::Null, + "project": serde_json::Value::Null, + "symlinks": serde_json::Value::Null, + "manager": { + "executable": manager_executable, + "version": serde_json::Value::Null, + "tool": "Conda", + }, + }); + + let toolchain = Toolchain { + name: "test".into(), + path: "/tmp/conda".into(), + language_name: LanguageName::new_static("Python"), + as_json: data, + }; + + let script = cx + .update(|cx| provider.activation_script(&toolchain, ShellKind::Posix, cx)) + .await; + + assert!( + !script.iter().any(|s| s.contains("conda activate")), + "Unquotable conda env names must not emit any `conda activate` line, actual: {:?}", + script + ); + } + #[gpui::test] async fn test_python_autoindent(cx: &mut TestAppContext) { cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);