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

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
)))
}
}
*/