Setup activation scripts in python toolchain

This commit is contained in:
Lukas Wirth 2025-08-21 13:23:56 +02:00
parent c9eb9a9e41
commit d2f2142127
8 changed files with 109 additions and 249 deletions

1
Cargo.lock generated
View file

@ -9269,6 +9269,7 @@ dependencies = [
"snippet_provider",
"task",
"tempfile",
"terminal",
"text",
"theme",
"toml 0.8.20",

View file

@ -71,6 +71,7 @@ settings.workspace = true
smol.workspace = true
snippet_provider.workspace = true
task.workspace = true
terminal.workspace = true
tempfile.workspace = true
toml.workspace = true
tree-sitter = { workspace = true, optional = true }

View file

@ -2,6 +2,7 @@ use anyhow::{Context as _, ensure};
use anyhow::{Result, anyhow};
use async_trait::async_trait;
use collections::HashMap;
use futures::AsyncBufReadExt;
use gpui::{App, Task};
use gpui::{AsyncApp, SharedString};
use language::Toolchain;
@ -30,8 +31,6 @@ use std::{
borrow::Cow,
ffi::OsString,
fmt::Write,
fs,
io::{self, BufRead},
path::{Path, PathBuf},
sync::Arc,
};
@ -741,14 +740,16 @@ fn env_priority(kind: Option<PythonEnvironmentKind>) -> usize {
/// Return the name of environment declared in <worktree-root/.venv.
///
/// https://virtualfish.readthedocs.io/en/latest/plugins.html#auto-activation-auto-activation
fn get_worktree_venv_declaration(worktree_root: &Path) -> Option<String> {
fs::File::open(worktree_root.join(".venv"))
.and_then(|file| {
let mut venv_name = String::new();
io::BufReader::new(file).read_line(&mut venv_name)?;
Ok(venv_name.trim().to_string())
})
.ok()
async fn get_worktree_venv_declaration(worktree_root: &Path) -> Option<String> {
let file = async_fs::File::open(worktree_root.join(".venv"))
.await
.ok()?;
let mut venv_name = String::new();
smol::io::BufReader::new(file)
.read_line(&mut venv_name)
.await
.ok()?;
Some(venv_name.trim().to_string())
}
#[async_trait]
@ -791,7 +792,7 @@ impl ToolchainLister for PythonToolchainProvider {
.map_or(Vec::new(), |mut guard| std::mem::take(&mut guard));
let wr = worktree_root;
let wr_venv = get_worktree_venv_declaration(&wr);
let wr_venv = get_worktree_venv_declaration(&wr).await;
// Sort detected environments by:
// environment name matching activation file (<workdir>/.venv)
// environment project dir matching worktree_root
@ -856,7 +857,7 @@ impl ToolchainLister for PythonToolchainProvider {
.into_iter()
.filter_map(|toolchain| {
let mut name = String::from("Python");
if let Some(ref version) = toolchain.version {
if let Some(version) = &toolchain.version {
_ = write!(name, " {version}");
}
@ -876,13 +877,46 @@ impl ToolchainLister for PythonToolchainProvider {
name: name.into(),
path: toolchain.executable.as_ref()?.to_str()?.to_owned().into(),
language_name: LanguageName::new("Python"),
as_json: serde_json::to_value(toolchain).ok()?,
activation_script: std::iter::once((
ShellKind::Fish,
"echo python recognized a venv and injected a fish startup command"
.to_owned(),
))
.collect(),
as_json: serde_json::to_value(toolchain.clone()).ok()?,
activation_script: match (toolchain.kind, toolchain.prefix) {
(Some(PythonEnvironmentKind::Venv), Some(prefix)) => [
(
ShellKind::Fish,
"source",
prefix.join(BINARY_DIR).join("activate.fish"),
),
(
ShellKind::Powershell,
".",
prefix.join(BINARY_DIR).join("activate.ps1"),
),
(
ShellKind::Nushell,
"overlay use",
prefix.join(BINARY_DIR).join("activate.nu"),
),
(
ShellKind::Posix,
".",
prefix.join(BINARY_DIR).join("activate"),
),
(
ShellKind::Cmd,
".",
prefix.join(BINARY_DIR).join("activate.bat"),
),
(
ShellKind::Csh,
".",
prefix.join(BINARY_DIR).join("activate.csh"),
),
]
.into_iter()
.filter(|(_, _, path)| path.exists() && path.is_file())
.map(|(kind, cmd, path)| (kind, format!("{cmd} {}", path.display())))
.collect(),
_ => Default::default(),
},
})
})
.collect();
@ -1694,180 +1728,3 @@ mod tests {
});
}
}
/*
fn python_venv_directory(
abs_path: Arc<Path>,
venv_settings: VenvSettings,
cx: &Context<Project>,
) -> Task<Option<PathBuf>> {
cx.spawn(async move |this, cx| {
if let Some((worktree, relative_path)) = this
.update(cx, |this, cx| this.find_worktree(&abs_path, cx))
.ok()?
{
let toolchain = this
.update(cx, |this, cx| {
this.active_toolchain(
ProjectPath {
worktree_id: worktree.read(cx).id(),
path: relative_path.into(),
},
LanguageName::new("Python"),
cx,
)
})
.ok()?
.await;
if let Some(toolchain) = toolchain {
let toolchain_path = Path::new(toolchain.path.as_ref());
return Some(toolchain_path.parent()?.parent()?.to_path_buf());
}
}
let venv_settings = venv_settings.as_option()?;
this.update(cx, move |this, cx| {
if let Some(path) = this.find_venv_in_worktree(&abs_path, &venv_settings, cx) {
return Some(path);
}
this.find_venv_on_filesystem(&abs_path, &venv_settings, cx)
})
.ok()
.flatten()
})
}
fn find_venv_in_worktree(
&self,
abs_path: &Path,
venv_settings: &terminal_settings::VenvSettingsContent,
cx: &App,
) -> Option<PathBuf> {
venv_settings
.directories
.iter()
.map(|name| abs_path.join(name))
.find(|venv_path| {
let bin_path = venv_path.join(PYTHON_VENV_BIN_DIR);
self.find_worktree(&bin_path, cx)
.and_then(|(worktree, relative_path)| {
worktree.read(cx).entry_for_path(&relative_path)
})
.is_some_and(|entry| entry.is_dir())
})
}
fn find_venv_on_filesystem(
&self,
abs_path: &Path,
venv_settings: &terminal_settings::VenvSettingsContent,
cx: &App,
) -> Option<PathBuf> {
let (worktree, _) = self.find_worktree(abs_path, cx)?;
let fs = worktree.read(cx).as_local()?.fs();
venv_settings
.directories
.iter()
.map(|name| abs_path.join(name))
.find(|venv_path| {
let bin_path = venv_path.join(PYTHON_VENV_BIN_DIR);
// One-time synchronous check is acceptable for terminal/task initialization
smol::block_on(fs.metadata(&bin_path))
.ok()
.flatten()
.map_or(false, |meta| meta.is_dir)
})
}
fn activate_script_kind(shell: Option<&str>) -> ActivateScript {
let shell_env = std::env::var("SHELL").ok();
let shell_path = shell.or_else(|| shell_env.as_deref());
let shell = std::path::Path::new(shell_path.unwrap_or(""))
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("");
match shell {
"fish" => ActivateScript::Fish,
"tcsh" => ActivateScript::Csh,
"nu" => ActivateScript::Nushell,
"powershell" | "pwsh" => ActivateScript::PowerShell,
_ => ActivateScript::Default,
}
}
fn python_activate_command(
&self,
venv_base_directory: &Path,
venv_settings: &VenvSettings,
shell: &Shell,
cx: &mut App,
) -> Task<Option<String>> {
let Some(venv_settings) = venv_settings.as_option() else {
return Task::ready(None);
};
let activate_keyword = match venv_settings.activate_script {
terminal_settings::ActivateScript::Default => match std::env::consts::OS {
"windows" => ".",
_ => ".",
},
terminal_settings::ActivateScript::Nushell => "overlay use",
terminal_settings::ActivateScript::PowerShell => ".",
terminal_settings::ActivateScript::Pyenv => "pyenv",
_ => "source",
};
let script_kind = if venv_settings.activate_script == terminal_settings::ActivateScript::Default
{
match shell {
Shell::Program(program) => Self::activate_script_kind(Some(program)),
Shell::WithArguments {
program,
args: _,
title_override: _,
} => Self::activate_script_kind(Some(program)),
Shell::System => Self::activate_script_kind(None),
}
} else {
venv_settings.activate_script
};
let activate_script_name = match script_kind {
terminal_settings::ActivateScript::Default | terminal_settings::ActivateScript::Pyenv => {
"activate"
}
terminal_settings::ActivateScript::Csh => "activate.csh",
terminal_settings::ActivateScript::Fish => "activate.fish",
terminal_settings::ActivateScript::Nushell => "activate.nu",
terminal_settings::ActivateScript::PowerShell => "activate.ps1",
};
let line_ending = match std::env::consts::OS {
"windows" => "\r",
_ => "\n",
};
if venv_settings.venv_name.is_empty() {
let path = venv_base_directory
.join(PYTHON_VENV_BIN_DIR)
.join(activate_script_name)
.to_string_lossy()
.to_string();
let is_valid_path = self.resolve_abs_path(path.as_ref(), cx);
cx.background_spawn(async move {
let quoted = shlex::try_quote(&path).ok()?;
if is_valid_path.await.is_some_and(|meta| meta.is_file()) {
Some(format!(
"{} {} ; clear{}",
activate_keyword, quoted, line_ending
))
} else {
None
}
})
} else {
Task::ready(Some(format!(
"{activate_keyword} {activate_script_name} {name}; clear{line_ending}",
name = venv_settings.venv_name
)))
}
}
*/

View file

@ -11,12 +11,12 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use task::{Shell, ShellBuilder, ShellKind, SpawnInTerminal, system_shell};
use task::{Shell, ShellBuilder, ShellKind, SpawnInTerminal};
use terminal::{
TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings,
};
use util::{
maybe,
get_system_shell, maybe,
paths::{PathStyle, RemotePathBuf},
};
@ -287,18 +287,17 @@ impl Project {
args: _,
title_override: _,
} => program.clone(),
Shell::System => system_shell(),
Shell::System => get_system_shell(),
},
};
let shell_kind = ShellKind::new(&shell);
let scripts = maybe!(async {
let toolchain = toolchain?.await?;
Some(toolchain.activation_script)
})
.await;
let activation_script = scripts
.as_ref()
.and_then(|it| it.get(&ShellKind::new(&shell)));
let activation_script = scripts.as_ref().and_then(|it| it.get(&shell_kind));
let shell = {
match ssh_details {
Some(SshDetails {
@ -336,19 +335,17 @@ impl Project {
title_override: Some(format!("{} — Terminal", host).into()),
}
}
None if activation_script.is_some() => Shell::WithArguments {
program: shell.clone(),
args: vec![
"-c".to_owned(),
format!(
"{}; exec {} -l",
activation_script.unwrap().to_string(),
shell
),
],
title_override: None,
None => match activation_script {
Some(activation_script) => Shell::WithArguments {
program: shell.clone(),
args: vec![
"-c".to_owned(),
format!("{activation_script}; exec {shell} -l",),
],
title_override: None,
},
None => settings.shell,
},
None => settings.shell,
}
};
project.update(cx, move |this, cx| {
@ -508,6 +505,7 @@ pub fn wrap_for_ssh(
path_style: PathStyle,
activation_script: Option<&str>,
) -> (String, Vec<String>) {
// todo make this shell aware
let to_run = if let Some((command, args)) = command {
let command: Option<Cow<str>> = shlex::try_quote(command).ok();
let args = args.iter().filter_map(|arg| shlex::try_quote(arg).ok());

View file

@ -1,5 +1,7 @@
use std::fmt;
use util::get_system_shell;
use crate::Shell;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
@ -28,7 +30,7 @@ impl fmt::Display for ShellKind {
impl ShellKind {
pub fn system() -> Self {
Self::new(&system_shell())
Self::new(&get_system_shell())
}
pub fn new(program: &str) -> Self {
@ -37,12 +39,12 @@ impl ShellKind {
#[cfg(not(windows))]
let (_, program) = program.rsplit_once('/').unwrap_or(("", program));
if program == "powershell"
|| program == "powershell.exe"
|| program.ends_with("powershell.exe")
|| program == "pwsh"
|| program == "pwsh.exe"
|| program.ends_with("pwsh.exe")
{
ShellKind::Powershell
} else if program == "cmd" || program == "cmd.exe" {
} else if program == "cmd" || program.ends_with("cmd.exe") {
ShellKind::Cmd
} else if program == "nu" {
ShellKind::Nushell
@ -193,18 +195,6 @@ impl ShellKind {
}
}
pub fn system_shell() -> String {
if cfg!(target_os = "windows") {
// `alacritty_terminal` uses this as default on Windows. See:
// https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
// We could use `util::get_windows_system_shell()` here, but we are running tasks here, so leave it to `powershell.exe`
// should be okay.
"powershell.exe".to_string()
} else {
std::env::var("SHELL").unwrap_or("/bin/sh".to_string())
}
}
/// ShellBuilder is used to turn a user-requested task into a
/// program that can be executed by the shell.
pub struct ShellBuilder {
@ -221,7 +211,7 @@ impl ShellBuilder {
let (program, args) = match shell {
Shell::System => match remote_system_shell {
Some(remote_shell) => (remote_shell.to_string(), Vec::new()),
None => (system_shell(), Vec::new()),
None => (get_system_shell(), Vec::new()),
},
Shell::Program(shell) => (shell.clone(), Vec::new()),
Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()),

View file

@ -22,7 +22,7 @@ pub use debug_format::{
AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest,
Request, TcpArgumentsTemplate, ZedDebugConfig,
};
pub use shell_builder::{ShellBuilder, ShellKind, system_shell};
pub use shell_builder::{ShellBuilder, ShellKind};
pub use task_template::{
DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates,
substitute_variables_in_map, substitute_variables_in_str,

View file

@ -3,9 +3,12 @@ use async_recursion::async_recursion;
use collections::HashSet;
use futures::{StreamExt as _, stream::FuturesUnordered};
use gpui::{AppContext as _, AsyncWindowContext, Axis, Entity, Task, WeakEntity};
use project::Project;
use project::{Project, ProjectPath};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use ui::{App, Context, Pixels, Window};
use util::ResultExt as _;
@ -242,8 +245,19 @@ async fn deserialize_pane_group(
.update(cx, |workspace, cx| default_working_directory(workspace, cx))
.ok()
.flatten();
let p = workspace
.update(cx, |workspace, cx| {
let worktree = workspace.worktrees(cx).next()?.read(cx);
worktree.root_dir()?;
Some(ProjectPath {
worktree_id: worktree.id(),
path: Arc::from(Path::new("")),
})
})
.ok()
.flatten();
let terminal = project.update(cx, |project, cx| {
project.create_terminal_shell(working_directory, cx, None)
project.create_terminal_shell(working_directory, cx, p)
});
Some(Some(terminal))
} else {

View file

@ -15,7 +15,7 @@ use gpui::{
deferred, div,
};
use persistence::TERMINAL_DB;
use project::{Project, search::SearchQuery};
use project::{Project, ProjectPath, search::SearchQuery};
use schemars::JsonSchema;
use task::TaskId;
use terminal::{
@ -1498,20 +1498,19 @@ impl SerializableItem for TerminalView {
.ok()
.flatten();
let p = workspace
.update(cx, |workspace, cx| {
let worktree = workspace.worktrees(cx).next()?.read(cx);
worktree.root_dir()?;
Some(ProjectPath {
worktree_id: worktree.id(),
path: Arc::from(Path::new("")),
})
})
.ok()
.flatten();
let terminal = project
.update(cx, |project, cx| {
project.create_terminal_shell(
cwd,
cx,
project
.active_entry()
.and_then(|entry_id| project.worktree_id_for_entry(entry_id, cx))
.map(|worktree_id| project::ProjectPath {
worktree_id,
path: Arc::from(Path::new("")),
}),
)
})?
.update(cx, |project, cx| project.create_terminal_shell(cwd, cx, p))?
.await?;
cx.update(|window, cx| {
cx.new(|cx| {