From d2f2142127fdc265b27ccc19d6d8747d88300dab Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 21 Aug 2025 13:23:56 +0200 Subject: [PATCH] Setup activation scripts in python toolchain --- Cargo.lock | 1 + crates/languages/Cargo.toml | 1 + crates/languages/src/python.rs | 249 +++++----------------- crates/project/src/terminals.rs | 34 ++- crates/task/src/shell_builder.rs | 24 +-- crates/task/src/task.rs | 2 +- crates/terminal_view/src/persistence.rs | 20 +- crates/terminal_view/src/terminal_view.rs | 27 ++- 8 files changed, 109 insertions(+), 249 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c835b503ad..ef06443b5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9269,6 +9269,7 @@ dependencies = [ "snippet_provider", "task", "tempfile", + "terminal", "text", "theme", "toml 0.8.20", diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index 8e25818070..e431573088 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -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 } diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 9073960654..ab36f47704 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -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) -> usize { /// Return the name of environment declared in Option { - 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 { + 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 (/.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, - venv_settings: VenvSettings, - cx: &Context, -) -> Task> { - 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 { - 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 { - 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> { - 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 - ))) - } -} -*/ diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 9ed65f0377..a0ab1a4bff 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -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) { + // todo make this shell aware let to_run = if let Some((command, args)) = command { let command: Option> = shlex::try_quote(command).ok(); let args = args.iter().filter_map(|arg| shlex::try_quote(arg).ok()); diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index 1195e59c93..2d30b3f769 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -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()), diff --git a/crates/task/src/task.rs b/crates/task/src/task.rs index 2dd771b9e5..eb9e59f087 100644 --- a/crates/task/src/task.rs +++ b/crates/task/src/task.rs @@ -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, diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index 328468dfb8..b90f0fc7d3 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -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 { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index a719c4055a..09daab1bc1 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -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| {