terminal: Allow configuring conda manager (#40577)

Casper van Elteren , Lukas Wirth , and Lukas Wirth created

Closes #40576
This PR makes Conda activation configurable and transparent by adding a
`terminal.detect_venv.on.conda_manager` setting (`"auto" | "conda" |
"mamba" | "micromamba"`, default `"auto"`), updating Python environment
activation to honor this preference (or the detected manager executable)
and fall back to `conda` when necessary.

The preference is passed via `ZED_CONDA_MANAGER` from the terminal
settings, and the activation command is built accordingly (with proper
quoting for paths). Changes span
`zed/crates/terminal/src/terminal_settings.rs` (new `CondaManager` and
setting), `zed/crates/project/src/terminals.rs` (inject env var),
`zed/crates/languages/src/python.rs` (activation logic), and
`zed/assets/settings/default.json` (document the setting). Default
behavior remains unchanged for most users while enabling explicit
selection of `mamba` or `micromamba`.

Release Notes:
- Added: terminal.detect_venv.on.conda_manager setting to choose the
Conda manager (auto, conda, mamba, micromamba). Default: auto.
- Changed: Python Conda environment activation now respects the
configured manager, otherwise uses the detected environment manager
executable, and falls back to conda.
- Reliability: Activation commands quote manager paths to handle spaces
across platforms.
- Compatibility: No breaking changes; non-Conda environments are
unaffected; remote terminals are supported.

---------

Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
Co-authored-by: Lukas Wirth <lukas@zed.dev>

Change summary

Cargo.lock                                       |  1 
assets/settings/default.json                     |  6 ++
crates/language/src/toolchain.rs                 |  4 +-
crates/languages/Cargo.toml                      |  1 
crates/languages/src/python.rs                   | 32 ++++++++++++++++-
crates/project/src/project_tests.rs              |  2 
crates/project/src/terminals.rs                  | 17 ++++++--
crates/settings/src/settings_content/terminal.rs | 23 ++++++++++++
8 files changed, 74 insertions(+), 12 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9032,6 +9032,7 @@ dependencies = [
  "settings",
  "smol",
  "task",
+ "terminal",
  "text",
  "theme",
  "toml 0.8.23",

assets/settings/default.json 🔗

@@ -1487,7 +1487,11 @@
         // in your project's settings, rather than globally.
         "directories": [".env", "env", ".venv", "venv"],
         // Can also be `csh`, `fish`, `nushell` and `power_shell`
-        "activate_script": "default"
+        "activate_script": "default",
+        // Preferred Conda manager to use when activating Conda environments.
+        // Values: "auto", "conda", "mamba", "micromamba"
+        // Default: "auto"
+        "conda_manager": "auto"
       }
     },
     "toolbar": {

crates/language/src/toolchain.rs 🔗

@@ -9,7 +9,7 @@ use std::{path::PathBuf, sync::Arc};
 use async_trait::async_trait;
 use collections::HashMap;
 use fs::Fs;
-use gpui::{AsyncApp, SharedString};
+use gpui::{App, AsyncApp, SharedString};
 use settings::WorktreeId;
 use task::ShellKind;
 use util::rel_path::RelPath;
@@ -110,7 +110,7 @@ pub trait ToolchainLister: Send + Sync + 'static {
         fs: &dyn Fs,
     ) -> anyhow::Result<Toolchain>;
 
-    fn activation_script(&self, toolchain: &Toolchain, shell: ShellKind) -> Vec<String>;
+    fn activation_script(&self, toolchain: &Toolchain, shell: ShellKind, cx: &App) -> Vec<String>;
 
     /// Returns various "static" bits of information about this toolchain lister. This function should be pure.
     fn meta(&self) -> ToolchainMetadata;

crates/languages/Cargo.toml 🔗

@@ -69,6 +69,7 @@ settings.workspace = true
 smol.workspace = true
 url.workspace = true
 task.workspace = true
+terminal.workspace = true
 theme.workspace = true
 toml.workspace = true
 tree-sitter = { workspace = true, optional = true }

crates/languages/src/python.rs 🔗

@@ -21,9 +21,11 @@ use project::Fs;
 use project::lsp_store::language_server_settings;
 use serde::{Deserialize, Serialize};
 use serde_json::{Value, json};
+use settings::Settings;
 use smol::lock::OnceCell;
 use std::cmp::Ordering;
 use std::env::consts;
+use terminal::terminal_settings::TerminalSettings;
 use util::command::new_smol_command;
 use util::fs::{make_file_executable, remove_matching};
 use util::rel_path::RelPath;
@@ -1171,7 +1173,7 @@ impl ToolchainLister for PythonToolchainProvider {
             .context("Could not convert a venv into a toolchain")
     }
 
-    fn activation_script(&self, toolchain: &Toolchain, shell: ShellKind) -> Vec<String> {
+    fn activation_script(&self, toolchain: &Toolchain, shell: ShellKind, cx: &App) -> Vec<String> {
         let Ok(toolchain) =
             serde_json::from_value::<PythonToolchainData>(toolchain.as_json.clone())
         else {
@@ -1184,10 +1186,34 @@ impl ToolchainLister for PythonToolchainProvider {
 
         match toolchain.environment.kind {
             Some(PythonEnvironmentKind::Conda) => {
+                let settings = TerminalSettings::get_global(cx);
+                let conda_manager = settings
+                    .detect_venv
+                    .as_option()
+                    .map(|venv| venv.conda_manager)
+                    .unwrap_or(settings::CondaManager::Auto);
+
+                let manager = match conda_manager {
+                    settings::CondaManager::Conda => "conda",
+                    settings::CondaManager::Mamba => "mamba",
+                    settings::CondaManager::Micromamba => "micromamba",
+                    settings::CondaManager::Auto => {
+                        // When auto, prefer the detected manager or fall back to conda
+                        toolchain
+                            .environment
+                            .manager
+                            .as_ref()
+                            .and_then(|m| m.executable.file_name())
+                            .and_then(|name| name.to_str())
+                            .filter(|name| matches!(*name, "conda" | "mamba" | "micromamba"))
+                            .unwrap_or("conda")
+                    }
+                };
+
                 if let Some(name) = &toolchain.environment.name {
-                    activation_script.push(format!("conda activate {name}"));
+                    activation_script.push(format!("{manager} activate {name}"));
                 } else {
-                    activation_script.push("conda activate".to_string());
+                    activation_script.push(format!("{manager} activate base"));
                 }
             }
             Some(PythonEnvironmentKind::Venv | PythonEnvironmentKind::VirtualEnv) => {

crates/project/src/project_tests.rs 🔗

@@ -10185,7 +10185,7 @@ fn python_lang(fs: Arc<FakeFs>) -> Arc<Language> {
                 manifest_name: ManifestName::from(SharedString::new_static("pyproject.toml")),
             }
         }
-        fn activation_script(&self, _: &Toolchain, _: ShellKind) -> Vec<String> {
+        fn activation_script(&self, _: &Toolchain, _: ShellKind, _: &gpui::App) -> Vec<String> {
             vec![]
         }
     }

crates/project/src/terminals.rs 🔗

@@ -127,8 +127,10 @@ impl Project {
                         .language_for_name(&toolchain.language_name.0)
                         .await
                         .ok();
-                    let lister = language?.toolchain_lister();
-                    return Some(lister?.activation_script(&toolchain, shell_kind));
+                    let lister = language?.toolchain_lister()?;
+                    return cx
+                        .update(|cx| lister.activation_script(&toolchain, shell_kind, cx))
+                        .ok();
                 }
                 None
             })
@@ -317,7 +319,8 @@ impl Project {
                 .unwrap_or_else(get_default_system_shell),
             None => settings.shell.program(),
         };
-        let shell_kind = ShellKind::new(&shell, self.path_style(cx).is_windows());
+
+        let is_windows = self.path_style(cx).is_windows();
 
         // Prepare a task for resolving the environment
         let env_task =
@@ -325,6 +328,7 @@ impl Project {
 
         let lang_registry = self.languages.clone();
         cx.spawn(async move |project, cx| {
+            let shell_kind = ShellKind::new(&shell, is_windows);
             let mut env = env_task.await.unwrap_or_default();
             env.extend(settings.env);
 
@@ -337,13 +341,16 @@ impl Project {
                         .language_for_name(&toolchain.language_name.0)
                         .await
                         .ok();
-                    let lister = language?.toolchain_lister();
-                    return Some(lister?.activation_script(&toolchain, shell_kind));
+                    let lister = language?.toolchain_lister()?;
+                    return cx
+                        .update(|cx| lister.activation_script(&toolchain, shell_kind, cx))
+                        .ok();
                 }
                 None
             })
             .await
             .unwrap_or_default();
+
             let builder = project
                 .update(cx, move |_, cx| {
                     let (shell, env) = {

crates/settings/src/settings_content/terminal.rs 🔗

@@ -348,6 +348,22 @@ pub struct TerminalToolbarContent {
     pub breadcrumbs: Option<bool>,
 }
 
+#[derive(
+    Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
+)]
+#[serde(rename_all = "snake_case")]
+pub enum CondaManager {
+    /// Automatically detect the conda manager
+    #[default]
+    Auto,
+    /// Use conda
+    Conda,
+    /// Use mamba
+    Mamba,
+    /// Use micromamba
+    Micromamba,
+}
+
 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum VenvSettings {
@@ -360,6 +376,10 @@ pub enum VenvSettings {
         activate_script: Option<ActivateScript>,
         venv_name: Option<String>,
         directories: Option<Vec<PathBuf>>,
+        /// Preferred Conda manager to use when activating Conda environments.
+        ///
+        /// Default: auto
+        conda_manager: Option<CondaManager>,
     },
 }
 #[skip_serializing_none]
@@ -367,6 +387,7 @@ pub struct VenvSettingsContent<'a> {
     pub activate_script: ActivateScript,
     pub venv_name: &'a str,
     pub directories: &'a [PathBuf],
+    pub conda_manager: CondaManager,
 }
 
 impl VenvSettings {
@@ -377,10 +398,12 @@ impl VenvSettings {
                 activate_script,
                 venv_name,
                 directories,
+                conda_manager,
             } => Some(VenvSettingsContent {
                 activate_script: activate_script.unwrap_or(ActivateScript::Default),
                 venv_name: venv_name.as_deref().unwrap_or(""),
                 directories: directories.as_deref().unwrap_or(&[]),
+                conda_manager: conda_manager.unwrap_or(CondaManager::Auto),
             }),
         }
     }