python: Stop falling back to conda activate base for nameless toolchains (#56785)

Stops the `conda activate base` fallback in
`PythonToolchainProvider::activation_script` for two cases:

1. A Conda toolchain whose `environment.name` is `None`.
2. A Conda toolchain whose name is `Some(...)` but `shell.try_quote`
rejects it.

Previously both cases pushed `conda activate base` into the activation
script. With miniforge installed and no env explicitly selected, every
terminal silently activated `(base)`. The user didn't pick `base`;
injecting it is contamination.

Both branches now log a warning and emit no activation line. Users who
want `(base)` can select it as the toolchain explicitly.

Two unit tests cover the new behavior:
`test_conda_activation_skips_when_name_missing` (name=None) and
`test_conda_activation_skips_unquotable_name` (name=Some but unquotable,
exercised with an embedded NUL byte that `shlex::try_quote` rejects).
The existing `test_conda_activation_script_injection` still verifies
that quotable names get activated.

Release Notes:

- Fixed Zed silently injecting `conda activate base` into terminals when
a Conda manager (miniforge/miniconda) was installed but no specific
environment was selected.
This commit is contained in:
Kyle Kelley authored and GitHub committed 2026-06-01 06:45:53 +00:00
1 parent ee591c04ae
commit 7cb01e42ef
1 file changed
+145 -3
+145 -3
View File
@@ -1495,18 +1495,23 @@ impl ToolchainLister for PythonToolchainProvider {
}
}
// Only inject `{manager} activate <name>` 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::<SettingsStore, _>(|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::<SettingsStore, _>(|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);