From 7403a4ba17d05e8ea02f80b5f4ea25d1d3c1cb71 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 29 Aug 2025 12:19:27 +0200 Subject: [PATCH] Add basic PyEnv and pixi support for python environments (#37156) cc https://github.com/zed-industries/zed/issues/29807 Release Notes: - Fixed terminals and tasks not respecting python pyenv and pixi environments --- crates/language/src/toolchain.rs | 8 +- crates/languages/src/python.rs | 71 ++++++++++--- crates/project/src/debugger/dap_store.rs | 1 - crates/project/src/project_tests.rs | 6 +- crates/project/src/terminals.rs | 125 +++++++++++++---------- crates/remote/src/remote_client.rs | 12 +-- crates/remote/src/transport/ssh.rs | 12 +-- crates/terminal/src/terminal.rs | 20 +++- 8 files changed, 155 insertions(+), 100 deletions(-) diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index 2a8dfd58418812b94c625845dce9724e145c7388..84b10c7961eddb130f88b24c9e3438ff2882f8d3 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -14,6 +14,7 @@ use collections::HashMap; use fs::Fs; use gpui::{AsyncApp, SharedString}; use settings::WorktreeId; +use task::ShellKind; use crate::{LanguageName, ManifestName}; @@ -68,7 +69,12 @@ pub trait ToolchainLister: Send + Sync { fn term(&self) -> SharedString; /// Returns the name of the manifest file for this toolchain. fn manifest_name(&self) -> ManifestName; - async fn activation_script(&self, toolchain: &Toolchain, fs: &dyn Fs) -> Option; + async fn activation_script( + &self, + toolchain: &Toolchain, + shell: ShellKind, + fs: &dyn Fs, + ) -> Vec; } #[async_trait(?Send)] diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 37d38de9dab6bb5968b446e7009a42c5f2e86e86..f76bd8e793d8e391654cb6391086ade528d56264 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -34,7 +34,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use task::{TaskTemplate, TaskTemplates, VariableName}; +use task::{ShellKind, TaskTemplate, TaskTemplates, VariableName}; use util::ResultExt; pub(crate) struct PyprojectTomlManifestProvider; @@ -894,20 +894,65 @@ impl ToolchainLister for PythonToolchainProvider { fn term(&self) -> SharedString { self.term.clone() } - async fn activation_script(&self, toolchain: &Toolchain, fs: &dyn Fs) -> Option { - let toolchain = serde_json::from_value::( + async fn activation_script( + &self, + toolchain: &Toolchain, + shell: ShellKind, + fs: &dyn Fs, + ) -> Vec { + let Ok(toolchain) = serde_json::from_value::( toolchain.as_json.clone(), - ) - .ok()?; - let mut activation_script = None; - if let Some(prefix) = &toolchain.prefix { - #[cfg(not(target_os = "windows"))] - let path = prefix.join(BINARY_DIR).join("activate"); - #[cfg(target_os = "windows")] - let path = prefix.join(BINARY_DIR).join("activate.ps1"); - if fs.is_file(&path).await { - activation_script = Some(format!(". {}", path.display())); + ) else { + return vec![]; + }; + let mut activation_script = vec![]; + + match toolchain.kind { + Some(PythonEnvironmentKind::Pixi) => { + let env = toolchain.name.as_deref().unwrap_or("default"); + activation_script.push(format!("pixi shell -e {env}")) + } + Some(PythonEnvironmentKind::Venv | PythonEnvironmentKind::VirtualEnv) => { + if let Some(prefix) = &toolchain.prefix { + let activate_keyword = match shell { + ShellKind::Cmd => ".", + ShellKind::Nushell => "overlay use", + ShellKind::Powershell => ".", + ShellKind::Fish => "source", + ShellKind::Csh => "source", + ShellKind::Posix => "source", + }; + let activate_script_name = match shell { + ShellKind::Posix => "activate", + ShellKind::Csh => "activate.csh", + ShellKind::Fish => "activate.fish", + ShellKind::Nushell => "activate.nu", + ShellKind::Powershell => "activate.ps1", + ShellKind::Cmd => "activate.bat", + }; + let path = prefix.join(BINARY_DIR).join(activate_script_name); + if fs.is_file(&path).await { + activation_script.push(format!("{activate_keyword} {}", path.display())); + } + } + } + Some(PythonEnvironmentKind::Pyenv) => { + let Some(manager) = toolchain.manager else { + return vec![]; + }; + let version = toolchain.version.as_deref().unwrap_or("system"); + let pyenv = manager.executable; + let pyenv = pyenv.display(); + activation_script.extend(match shell { + ShellKind::Fish => Some(format!("{pyenv} shell - fish {version}")), + ShellKind::Posix => Some(format!("{pyenv} shell - sh {version}")), + ShellKind::Nushell => Some(format!("{pyenv} shell - nu {version}")), + ShellKind::Powershell => None, + ShellKind::Csh => None, + ShellKind::Cmd => None, + }) } + _ => {} } activation_script } diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 859574c82a5b4470d477df555b314498cbfcd0e0..d8c6d3acc1116e9a97b2f6ca3fc54ec098029cbe 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -276,7 +276,6 @@ impl DapStore { &binary.arguments, &binary.envs, binary.cwd.map(|path| path.display().to_string()), - None, port_forwarding, ) })??; diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index c814d6207e92608c13502a4da3a0781836acce0e..96f891d9c380fe6feec490627cd782955c833eda 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -40,7 +40,7 @@ use serde_json::json; #[cfg(not(windows))] use std::os; use std::{env, mem, num::NonZeroU32, ops::Range, str::FromStr, sync::OnceLock, task::Poll}; -use task::{ResolvedTask, TaskContext}; +use task::{ResolvedTask, ShellKind, TaskContext}; use unindent::Unindent as _; use util::{ TryFutureExt as _, assert_set_eq, maybe, path, @@ -9222,8 +9222,8 @@ fn python_lang(fs: Arc) -> Arc { fn manifest_name(&self) -> ManifestName { SharedString::new_static("pyproject.toml").into() } - async fn activation_script(&self, _: &Toolchain, _: &dyn Fs) -> Option { - None + async fn activation_script(&self, _: &Toolchain, _: ShellKind, _: &dyn Fs) -> Vec { + vec![] } } Arc::new( diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index aad5ce941125c2c747df3a76473a9dbffba0b80e..c189242fadc2948593186edb5dcd2c56879f07af 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -1,7 +1,8 @@ use anyhow::Result; use collections::HashMap; use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity}; -use itertools::Itertools; + +use itertools::Itertools as _; use language::LanguageName; use remote::RemoteClient; use settings::{Settings, SettingsLocation}; @@ -11,7 +12,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use task::{Shell, ShellBuilder, SpawnInTerminal}; +use task::{Shell, ShellBuilder, ShellKind, SpawnInTerminal}; use terminal::{ TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings, }; @@ -131,33 +132,62 @@ impl Project { cx.spawn(async move |project, cx| { let activation_script = maybe!(async { let toolchain = toolchain?.await?; - lang_registry - .language_for_name(&toolchain.language_name.0) - .await - .ok()? - .toolchain_lister()? - .activation_script(&toolchain, fs.as_ref()) - .await + Some( + lang_registry + .language_for_name(&toolchain.language_name.0) + .await + .ok()? + .toolchain_lister()? + .activation_script(&toolchain, ShellKind::new(&shell), fs.as_ref()) + .await, + ) }) - .await; + .await + .unwrap_or_default(); project.update(cx, move |this, cx| { let shell = { env.extend(spawn_task.env); match remote_client { - Some(remote_client) => create_remote_shell( - spawn_task - .command - .as_ref() - .map(|command| (command, &spawn_task.args)), - &mut env, - path, - remote_client, - activation_script.clone(), - cx, - )?, + Some(remote_client) => match activation_script.clone() { + activation_script if !activation_script.is_empty() => { + let activation_script = activation_script.join("; "); + let to_run = if let Some(command) = spawn_task.command { + let command: Option> = shlex::try_quote(&command).ok(); + let args = spawn_task + .args + .iter() + .filter_map(|arg| shlex::try_quote(arg).ok()); + command.into_iter().chain(args).join(" ") + } else { + format!("exec {shell} -l") + }; + let args = vec![ + "-c".to_owned(), + format!("{activation_script}; {to_run}",), + ]; + create_remote_shell( + Some((&shell, &args)), + &mut env, + path, + remote_client, + cx, + )? + } + _ => create_remote_shell( + spawn_task + .command + .as_ref() + .map(|command| (command, &spawn_task.args)), + &mut env, + path, + remote_client, + cx, + )?, + }, None => match activation_script.clone() { - Some(activation_script) => { + activation_script if !activation_script.is_empty() => { + let activation_script = activation_script.join("; "); let to_run = if let Some(command) = spawn_task.command { let command: Option> = shlex::try_quote(&command).ok(); let args = spawn_task @@ -169,7 +199,7 @@ impl Project { format!("exec {shell} -l") }; Shell::WithArguments { - program: get_default_system_shell(), + program: shell, args: vec![ "-c".to_owned(), format!("{activation_script}; {to_run}",), @@ -177,7 +207,7 @@ impl Project { title_override: None, } } - None => { + _ => { if let Some(program) = spawn_task.command { Shell::WithArguments { program, @@ -302,31 +332,21 @@ impl Project { .await .ok(); let lister = language?.toolchain_lister(); - lister?.activation_script(&toolchain, fs.as_ref()).await + Some( + lister? + .activation_script(&toolchain, ShellKind::new(&shell), fs.as_ref()) + .await, + ) }) - .await; + .await + .unwrap_or_default(); project.update(cx, move |this, cx| { let shell = { match remote_client { - Some(remote_client) => create_remote_shell( - None, - &mut env, - path, - remote_client, - activation_script.clone(), - cx, - )?, - None => match activation_script.clone() { - Some(activation_script) => Shell::WithArguments { - program: get_default_system_shell(), - args: vec![ - "-c".to_owned(), - format!("{activation_script}; exec {shell} -l",), - ], - title_override: Some(shell.into()), - }, - None => settings.shell, - }, + Some(remote_client) => { + create_remote_shell(None, &mut env, path, remote_client, cx)? + } + None => settings.shell, } }; TerminalBuilder::new( @@ -437,15 +457,10 @@ impl Project { match remote_client { Some(remote_client) => { - let command_template = remote_client.read(cx).build_command( - Some(command), - &args, - &env, - None, - // todo - None, - None, - )?; + let command_template = + remote_client + .read(cx) + .build_command(Some(command), &args, &env, None, None)?; let mut command = std::process::Command::new(command_template.program); command.args(command_template.args); command.envs(command_template.env); @@ -473,7 +488,6 @@ fn create_remote_shell( env: &mut HashMap, working_directory: Option>, remote_client: Entity, - activation_script: Option, cx: &mut App, ) -> Result { // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed @@ -493,7 +507,6 @@ fn create_remote_shell( args.as_slice(), env, working_directory.map(|path| path.display().to_string()), - activation_script, None, )?; *env = command.env; diff --git a/crates/remote/src/remote_client.rs b/crates/remote/src/remote_client.rs index 2b8d9e4a94fb9988e801c5ef9202ee603959d36b..dd529ca87499b0daf2061fd990f7149828e3fce4 100644 --- a/crates/remote/src/remote_client.rs +++ b/crates/remote/src/remote_client.rs @@ -757,7 +757,6 @@ impl RemoteClient { args: &[String], env: &HashMap, working_dir: Option, - activation_script: Option, port_forward: Option<(u16, String, u16)>, ) -> Result { let Some(connection) = self @@ -767,14 +766,7 @@ impl RemoteClient { else { return Err(anyhow!("no connection")); }; - connection.build_command( - program, - args, - env, - working_dir, - activation_script, - port_forward, - ) + connection.build_command(program, args, env, working_dir, port_forward) } pub fn upload_directory( @@ -1006,7 +998,6 @@ pub(crate) trait RemoteConnection: Send + Sync { args: &[String], env: &HashMap, working_dir: Option, - activation_script: Option, port_forward: Option<(u16, String, u16)>, ) -> Result; fn connection_options(&self) -> SshConnectionOptions; @@ -1373,7 +1364,6 @@ mod fake { args: &[String], env: &HashMap, _: Option, - _: Option, _: Option<(u16, String, u16)>, ) -> Result { let ssh_program = program.unwrap_or_else(|| "sh".to_string()); diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index 0036a687a6f73b57723e8c3c9fcffc56cab626c2..b6698014024ab48d171631a190b421dcb614edae 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -30,10 +30,7 @@ use std::{ time::Instant, }; use tempfile::TempDir; -use util::{ - get_default_system_shell, - paths::{PathStyle, RemotePathBuf}, -}; +use util::paths::{PathStyle, RemotePathBuf}; pub(crate) struct SshRemoteConnection { socket: SshSocket, @@ -116,7 +113,6 @@ impl RemoteConnection for SshRemoteConnection { input_args: &[String], input_env: &HashMap, working_dir: Option, - activation_script: Option, port_forward: Option<(u16, String, u16)>, ) -> Result { use std::fmt::Write as _; @@ -138,9 +134,6 @@ impl RemoteConnection for SshRemoteConnection { } else { write!(&mut script, "cd; ").unwrap(); }; - if let Some(activation_script) = activation_script { - write!(&mut script, " {activation_script};").unwrap(); - } for (k, v) in input_env.iter() { if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) { @@ -162,8 +155,7 @@ impl RemoteConnection for SshRemoteConnection { write!(&mut script, "exec {shell} -l").unwrap(); }; - let sys_shell = get_default_system_shell(); - let shell_invocation = format!("{sys_shell} -c {}", shlex::try_quote(&script).unwrap()); + let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&script).unwrap()); let mut args = Vec::new(); args.extend(self.socket.ssh_args()); diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index a5e0227533cf0e3ecbc9a8f2c6c55fa1254473e3..0f4f2ae97b67b9fd43a63b54088f66c74ca1c855 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -354,7 +354,7 @@ impl TerminalBuilder { window_id: u64, completion_tx: Option>>, cx: &App, - activation_script: Option, + activation_script: Vec, ) -> Result { // If the parent environment doesn't have a locale set // (As is the case when launched from a .app on MacOS), @@ -493,7 +493,9 @@ impl TerminalBuilder { let pty_tx = event_loop.channel(); let _io_thread = event_loop.spawn(); // DANGER - let terminal = Terminal { + let no_task = task.is_none(); + + let mut terminal = Terminal { task, pty_tx: Notifier(pty_tx), completion_tx, @@ -518,7 +520,7 @@ impl TerminalBuilder { last_hyperlink_search_position: None, #[cfg(windows)] shell_program, - activation_script, + activation_script: activation_script.clone(), template: CopyTemplate { shell, env, @@ -529,6 +531,14 @@ impl TerminalBuilder { }, }; + if !activation_script.is_empty() && no_task { + for activation_script in activation_script { + terminal.input(activation_script.into_bytes()); + terminal.write_to_pty(b"\n"); + } + terminal.clear(); + } + Ok(TerminalBuilder { terminal, events_rx, @@ -712,7 +722,7 @@ pub struct Terminal { #[cfg(windows)] shell_program: Option, template: CopyTemplate, - activation_script: Option, + activation_script: Vec, } struct CopyTemplate { @@ -2218,7 +2228,7 @@ mod tests { 0, Some(completion_tx), cx, - None, + vec![], ) .unwrap() .subscribe(cx)