Setup activation scripts in python toolchain
This commit is contained in:
parent
c9eb9a9e41
commit
d2f2142127
8 changed files with 109 additions and 249 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -9269,6 +9269,7 @@ dependencies = [
|
|||
"snippet_provider",
|
||||
"task",
|
||||
"tempfile",
|
||||
"terminal",
|
||||
"text",
|
||||
"theme",
|
||||
"toml 0.8.20",
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
)))
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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| {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue