acp_thread: Respect terminal settings shell for terminal tool environment (#39349)
When sourcing the project environment for the terminal tool, we will now do so by spawning the shell specified by the users `terminal.shell` setting (or as usual fall back to the login shell). Closes #37687 Release Notes: - N/A
This commit is contained in:
@@ -3,6 +3,7 @@ mod diff;
|
||||
mod mention;
|
||||
mod terminal;
|
||||
|
||||
use ::terminal::terminal_settings::TerminalSettings;
|
||||
use agent_settings::AgentSettings;
|
||||
use collections::HashSet;
|
||||
pub use connection::*;
|
||||
@@ -1961,11 +1962,11 @@ impl AcpThread {
|
||||
) -> Task<Result<Entity<Terminal>>> {
|
||||
let env = match &cwd {
|
||||
Some(dir) => self.project.update(cx, |project, cx| {
|
||||
project.directory_environment(dir.as_path().into(), cx)
|
||||
let shell = TerminalSettings::get_global(cx).shell.clone();
|
||||
project.directory_environment(&shell, dir.as_path().into(), cx)
|
||||
}),
|
||||
None => Task::ready(None).shared(),
|
||||
};
|
||||
|
||||
let env = cx.spawn(async move |_, _| {
|
||||
let mut env = env.await.unwrap_or_default();
|
||||
// Disables paging for `git` and hopefully other commands
|
||||
|
||||
@@ -27,6 +27,7 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use task::{Shell, ShellBuilder};
|
||||
use terminal::terminal_settings::TerminalSettings;
|
||||
use terminal_view::TerminalView;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
|
||||
@@ -119,9 +120,10 @@ impl Tool for TerminalTool {
|
||||
};
|
||||
|
||||
let cwd = working_dir.clone();
|
||||
let env = match &working_dir {
|
||||
let env = match &cwd {
|
||||
Some(dir) => project.update(cx, |project, cx| {
|
||||
project.directory_environment(dir.as_path().into(), cx)
|
||||
let shell = TerminalSettings::get_global(cx).shell.clone();
|
||||
project.directory_environment(&shell, dir.as_path().into(), cx)
|
||||
}),
|
||||
None => Task::ready(None).shared(),
|
||||
};
|
||||
|
||||
@@ -1187,11 +1187,13 @@ impl ToolchainLister for PythonToolchainProvider {
|
||||
ShellKind::PowerShell => ".",
|
||||
ShellKind::Fish => "source",
|
||||
ShellKind::Csh => "source",
|
||||
ShellKind::Posix => "source",
|
||||
ShellKind::Tcsh => "source",
|
||||
ShellKind::Posix | ShellKind::Rc => "source",
|
||||
};
|
||||
let activate_script_name = match shell {
|
||||
ShellKind::Posix => "activate",
|
||||
ShellKind::Posix | ShellKind::Rc => "activate",
|
||||
ShellKind::Csh => "activate.csh",
|
||||
ShellKind::Tcsh => "activate.csh",
|
||||
ShellKind::Fish => "activate.fish",
|
||||
ShellKind::Nushell => "activate.nu",
|
||||
ShellKind::PowerShell => "activate.ps1",
|
||||
@@ -1220,7 +1222,9 @@ impl ToolchainLister for PythonToolchainProvider {
|
||||
ShellKind::Nushell => Some(format!("\"{pyenv}\" shell - nu {version}")),
|
||||
ShellKind::PowerShell => None,
|
||||
ShellKind::Csh => None,
|
||||
ShellKind::Tcsh => None,
|
||||
ShellKind::Cmd => None,
|
||||
ShellKind::Rc => None,
|
||||
})
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::environment::EnvironmentErrorMessage;
|
||||
use std::process::ExitStatus;
|
||||
|
||||
#[cfg(not(any(target_os = "windows", test, feature = "test-support")))]
|
||||
use {collections::HashMap, std::path::Path, util::ResultExt};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -28,7 +27,6 @@ impl From<DirenvError> for Option<EnvironmentErrorMessage> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", test, feature = "test-support")))]
|
||||
pub async fn load_direnv_environment(
|
||||
env: &HashMap<String, String>,
|
||||
dir: &Path,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use futures::{FutureExt, future::Shared};
|
||||
use language::Buffer;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use task::Shell;
|
||||
use util::ResultExt;
|
||||
use worktree::Worktree;
|
||||
|
||||
@@ -16,6 +17,8 @@ use crate::{
|
||||
pub struct ProjectEnvironment {
|
||||
cli_environment: Option<HashMap<String, String>>,
|
||||
environments: HashMap<Arc<Path>, Shared<Task<Option<HashMap<String, String>>>>>,
|
||||
shell_based_environments:
|
||||
HashMap<(Shell, Arc<Path>), Shared<Task<Option<HashMap<String, String>>>>>,
|
||||
environment_error_messages: HashMap<Arc<Path>, EnvironmentErrorMessage>,
|
||||
}
|
||||
|
||||
@@ -30,6 +33,7 @@ impl ProjectEnvironment {
|
||||
Self {
|
||||
cli_environment,
|
||||
environments: Default::default(),
|
||||
shell_based_environments: Default::default(),
|
||||
environment_error_messages: Default::default(),
|
||||
}
|
||||
}
|
||||
@@ -134,7 +138,22 @@ impl ProjectEnvironment {
|
||||
|
||||
self.environments
|
||||
.entry(abs_path.clone())
|
||||
.or_insert_with(|| get_directory_env_impl(abs_path.clone(), cx).shared())
|
||||
.or_insert_with(|| {
|
||||
get_directory_env_impl(&Shell::System, abs_path.clone(), cx).shared()
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Returns the project environment, if possible, with the given shell.
|
||||
pub fn get_directory_environment_for_shell(
|
||||
&mut self,
|
||||
shell: &Shell,
|
||||
abs_path: Arc<Path>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Shared<Task<Option<HashMap<String, String>>>> {
|
||||
self.shell_based_environments
|
||||
.entry((shell.clone(), abs_path.clone()))
|
||||
.or_insert_with(|| get_directory_env_impl(shell, abs_path.clone(), cx).shared())
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
@@ -176,6 +195,7 @@ impl EnvironmentErrorMessage {
|
||||
}
|
||||
|
||||
async fn load_directory_shell_environment(
|
||||
shell: &Shell,
|
||||
abs_path: &Path,
|
||||
load_direnv: &DirenvSettings,
|
||||
) -> (
|
||||
@@ -198,7 +218,7 @@ async fn load_directory_shell_environment(
|
||||
);
|
||||
};
|
||||
|
||||
load_shell_environment(dir, load_direnv).await
|
||||
load_shell_environment(shell, dir, load_direnv).await
|
||||
}
|
||||
Err(err) => (
|
||||
None,
|
||||
@@ -211,51 +231,8 @@ async fn load_directory_shell_environment(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
async fn load_shell_environment(
|
||||
_dir: &Path,
|
||||
_load_direnv: &DirenvSettings,
|
||||
) -> (
|
||||
Option<HashMap<String, String>>,
|
||||
Option<EnvironmentErrorMessage>,
|
||||
) {
|
||||
let fake_env = [("ZED_FAKE_TEST_ENV".into(), "true".into())]
|
||||
.into_iter()
|
||||
.collect();
|
||||
(Some(fake_env), None)
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "windows", not(any(test, feature = "test-support"))))]
|
||||
async fn load_shell_environment(
|
||||
dir: &Path,
|
||||
_load_direnv: &DirenvSettings,
|
||||
) -> (
|
||||
Option<HashMap<String, String>>,
|
||||
Option<EnvironmentErrorMessage>,
|
||||
) {
|
||||
use util::shell_env;
|
||||
|
||||
let envs = match shell_env::capture(dir).await {
|
||||
Ok(envs) => envs,
|
||||
Err(err) => {
|
||||
util::log_err(&err);
|
||||
return (
|
||||
None,
|
||||
Some(EnvironmentErrorMessage(format!(
|
||||
"Failed to load environment variables: {}",
|
||||
err
|
||||
))),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Note: direnv is not available on Windows, so we skip direnv processing
|
||||
// and just return the shell environment
|
||||
(Some(envs), None)
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", test, feature = "test-support")))]
|
||||
async fn load_shell_environment(
|
||||
shell: &Shell,
|
||||
dir: &Path,
|
||||
load_direnv: &DirenvSettings,
|
||||
) -> (
|
||||
@@ -265,55 +242,86 @@ async fn load_shell_environment(
|
||||
use crate::direnv::load_direnv_environment;
|
||||
use util::shell_env;
|
||||
|
||||
let dir_ = dir.to_owned();
|
||||
let mut envs = match shell_env::capture(&dir_).await {
|
||||
Ok(envs) => envs,
|
||||
Err(err) => {
|
||||
util::log_err(&err);
|
||||
return (
|
||||
None,
|
||||
Some(EnvironmentErrorMessage::from_str(
|
||||
"Failed to load environment variables. See log for details",
|
||||
)),
|
||||
);
|
||||
}
|
||||
};
|
||||
if cfg!(any(test, feature = "test-support")) {
|
||||
let fake_env = [("ZED_FAKE_TEST_ENV".into(), "true".into())]
|
||||
.into_iter()
|
||||
.collect();
|
||||
(Some(fake_env), None)
|
||||
} else if cfg!(target_os = "windows",) {
|
||||
let (shell, args) = shell.program_and_args();
|
||||
let envs = match shell_env::capture(shell, args, dir).await {
|
||||
Ok(envs) => envs,
|
||||
Err(err) => {
|
||||
util::log_err(&err);
|
||||
return (
|
||||
None,
|
||||
Some(EnvironmentErrorMessage(format!(
|
||||
"Failed to load environment variables: {}",
|
||||
err
|
||||
))),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// If the user selects `Direct` for direnv, it would set an environment
|
||||
// variable that later uses to know that it should not run the hook.
|
||||
// We would include in `.envs` call so it is okay to run the hook
|
||||
// even if direnv direct mode is enabled.
|
||||
let (direnv_environment, direnv_error) = match load_direnv {
|
||||
DirenvSettings::ShellHook => (None, None),
|
||||
DirenvSettings::Direct => match load_direnv_environment(&envs, dir).await {
|
||||
Ok(env) => (Some(env), None),
|
||||
Err(err) => (None, err.into()),
|
||||
},
|
||||
};
|
||||
if let Some(direnv_environment) = direnv_environment {
|
||||
for (key, value) in direnv_environment {
|
||||
if let Some(value) = value {
|
||||
envs.insert(key, value);
|
||||
} else {
|
||||
envs.remove(&key);
|
||||
// Note: direnv is not available on Windows, so we skip direnv processing
|
||||
// and just return the shell environment
|
||||
(Some(envs), None)
|
||||
} else {
|
||||
let dir_ = dir.to_owned();
|
||||
let (shell, args) = shell.program_and_args();
|
||||
let mut envs = match shell_env::capture(shell, args, &dir_).await {
|
||||
Ok(envs) => envs,
|
||||
Err(err) => {
|
||||
util::log_err(&err);
|
||||
return (
|
||||
None,
|
||||
Some(EnvironmentErrorMessage::from_str(
|
||||
"Failed to load environment variables. See log for details",
|
||||
)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// If the user selects `Direct` for direnv, it would set an environment
|
||||
// variable that later uses to know that it should not run the hook.
|
||||
// We would include in `.envs` call so it is okay to run the hook
|
||||
// even if direnv direct mode is enabled.
|
||||
let (direnv_environment, direnv_error) = match load_direnv {
|
||||
DirenvSettings::ShellHook => (None, None),
|
||||
DirenvSettings::Direct => match load_direnv_environment(&envs, dir).await {
|
||||
Ok(env) => (Some(env), None),
|
||||
Err(err) => (None, err.into()),
|
||||
},
|
||||
};
|
||||
if let Some(direnv_environment) = direnv_environment {
|
||||
for (key, value) in direnv_environment {
|
||||
if let Some(value) = value {
|
||||
envs.insert(key, value);
|
||||
} else {
|
||||
envs.remove(&key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(Some(envs), direnv_error)
|
||||
(Some(envs), direnv_error)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_directory_env_impl(
|
||||
shell: &Shell,
|
||||
abs_path: Arc<Path>,
|
||||
cx: &Context<ProjectEnvironment>,
|
||||
) -> Task<Option<HashMap<String, String>>> {
|
||||
let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone();
|
||||
|
||||
let shell = shell.clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
let (mut shell_env, error_message) = cx
|
||||
.background_spawn({
|
||||
let abs_path = abs_path.clone();
|
||||
async move { load_directory_shell_environment(&abs_path, &load_direnv).await }
|
||||
async move {
|
||||
load_directory_shell_environment(&shell, &abs_path, &load_direnv).await
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ pub mod search_history;
|
||||
mod yarn;
|
||||
|
||||
use dap::inline_value::{InlineValueLocation, VariableLookupKind, VariableScope};
|
||||
use task::Shell;
|
||||
|
||||
use crate::{
|
||||
agent_server_store::{AgentServerStore, AllAgentServersSettings},
|
||||
@@ -1894,11 +1895,12 @@ impl Project {
|
||||
|
||||
pub fn directory_environment(
|
||||
&self,
|
||||
shell: &Shell,
|
||||
abs_path: Arc<Path>,
|
||||
cx: &mut App,
|
||||
) -> Shared<Task<Option<HashMap<String, String>>>> {
|
||||
self.environment.update(cx, |environment, cx| {
|
||||
environment.get_directory_environment(abs_path, cx)
|
||||
environment.get_directory_environment_for_shell(shell, abs_path, cx)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ use task::{Shell, ShellBuilder, ShellKind, SpawnInTerminal};
|
||||
use terminal::{
|
||||
TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings,
|
||||
};
|
||||
use util::{get_default_system_shell, get_system_shell, maybe, rel_path::RelPath};
|
||||
use util::{get_default_system_shell, maybe, rel_path::RelPath};
|
||||
|
||||
use crate::{Project, ProjectPath};
|
||||
|
||||
@@ -98,15 +98,7 @@ impl Project {
|
||||
.read(cx)
|
||||
.shell()
|
||||
.unwrap_or_else(get_default_system_shell),
|
||||
None => match &settings.shell {
|
||||
Shell::Program(program) => program.clone(),
|
||||
Shell::WithArguments {
|
||||
program,
|
||||
args: _,
|
||||
title_override: _,
|
||||
} => program.clone(),
|
||||
Shell::System => get_system_shell(),
|
||||
},
|
||||
None => settings.shell.program(),
|
||||
};
|
||||
|
||||
let project_path_contexts = self
|
||||
@@ -332,15 +324,7 @@ impl Project {
|
||||
.read(cx)
|
||||
.shell()
|
||||
.unwrap_or_else(get_default_system_shell),
|
||||
None => match &settings.shell {
|
||||
Shell::Program(program) => program.clone(),
|
||||
Shell::WithArguments {
|
||||
program,
|
||||
args: _,
|
||||
title_override: _,
|
||||
} => program.clone(),
|
||||
Shell::System => get_system_shell(),
|
||||
},
|
||||
None => settings.shell.program(),
|
||||
});
|
||||
|
||||
let lang_registry = self.languages.clone();
|
||||
|
||||
@@ -1,207 +1,8 @@
|
||||
use std::fmt;
|
||||
|
||||
use util::get_system_shell;
|
||||
use util::shell::get_system_shell;
|
||||
|
||||
use crate::Shell;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ShellKind {
|
||||
#[default]
|
||||
Posix,
|
||||
Csh,
|
||||
Fish,
|
||||
PowerShell,
|
||||
Nushell,
|
||||
Cmd,
|
||||
}
|
||||
|
||||
impl fmt::Display for ShellKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ShellKind::Posix => write!(f, "sh"),
|
||||
ShellKind::Csh => write!(f, "csh"),
|
||||
ShellKind::Fish => write!(f, "fish"),
|
||||
ShellKind::PowerShell => write!(f, "powershell"),
|
||||
ShellKind::Nushell => write!(f, "nu"),
|
||||
ShellKind::Cmd => write!(f, "cmd"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShellKind {
|
||||
pub fn system() -> Self {
|
||||
Self::new(&get_system_shell())
|
||||
}
|
||||
|
||||
pub fn new(program: &str) -> Self {
|
||||
#[cfg(windows)]
|
||||
let (_, program) = program.rsplit_once('\\').unwrap_or(("", program));
|
||||
#[cfg(not(windows))]
|
||||
let (_, program) = program.rsplit_once('/').unwrap_or(("", program));
|
||||
if program == "powershell"
|
||||
|| program.ends_with("powershell.exe")
|
||||
|| program == "pwsh"
|
||||
|| program.ends_with("pwsh.exe")
|
||||
{
|
||||
ShellKind::PowerShell
|
||||
} else if program == "cmd" || program.ends_with("cmd.exe") {
|
||||
ShellKind::Cmd
|
||||
} else if program == "nu" {
|
||||
ShellKind::Nushell
|
||||
} else if program == "fish" {
|
||||
ShellKind::Fish
|
||||
} else if program == "csh" {
|
||||
ShellKind::Csh
|
||||
} else {
|
||||
// Some other shell detected, the user might install and use a
|
||||
// unix-like shell.
|
||||
ShellKind::Posix
|
||||
}
|
||||
}
|
||||
|
||||
fn to_shell_variable(self, input: &str) -> String {
|
||||
match self {
|
||||
Self::PowerShell => Self::to_powershell_variable(input),
|
||||
Self::Cmd => Self::to_cmd_variable(input),
|
||||
Self::Posix => input.to_owned(),
|
||||
Self::Fish => input.to_owned(),
|
||||
Self::Csh => input.to_owned(),
|
||||
Self::Nushell => Self::to_nushell_variable(input),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cmd_variable(input: &str) -> String {
|
||||
if let Some(var_str) = input.strip_prefix("${") {
|
||||
if var_str.find(':').is_none() {
|
||||
// If the input starts with "${", remove the trailing "}"
|
||||
format!("%{}%", &var_str[..var_str.len() - 1])
|
||||
} else {
|
||||
// `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
|
||||
// which will result in the task failing to run in such cases.
|
||||
input.into()
|
||||
}
|
||||
} else if let Some(var_str) = input.strip_prefix('$') {
|
||||
// If the input starts with "$", directly append to "$env:"
|
||||
format!("%{}%", var_str)
|
||||
} else {
|
||||
// If no prefix is found, return the input as is
|
||||
input.into()
|
||||
}
|
||||
}
|
||||
fn to_powershell_variable(input: &str) -> String {
|
||||
if let Some(var_str) = input.strip_prefix("${") {
|
||||
if var_str.find(':').is_none() {
|
||||
// If the input starts with "${", remove the trailing "}"
|
||||
format!("$env:{}", &var_str[..var_str.len() - 1])
|
||||
} else {
|
||||
// `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
|
||||
// which will result in the task failing to run in such cases.
|
||||
input.into()
|
||||
}
|
||||
} else if let Some(var_str) = input.strip_prefix('$') {
|
||||
// If the input starts with "$", directly append to "$env:"
|
||||
format!("$env:{}", var_str)
|
||||
} else {
|
||||
// If no prefix is found, return the input as is
|
||||
input.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn to_nushell_variable(input: &str) -> String {
|
||||
let mut result = String::new();
|
||||
let mut source = input;
|
||||
let mut is_start = true;
|
||||
|
||||
loop {
|
||||
match source.chars().next() {
|
||||
None => return result,
|
||||
Some('$') => {
|
||||
source = Self::parse_nushell_var(&source[1..], &mut result, is_start);
|
||||
is_start = false;
|
||||
}
|
||||
Some(_) => {
|
||||
is_start = false;
|
||||
let chunk_end = source.find('$').unwrap_or(source.len());
|
||||
let (chunk, rest) = source.split_at(chunk_end);
|
||||
result.push_str(chunk);
|
||||
source = rest;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_nushell_var<'a>(source: &'a str, text: &mut String, is_start: bool) -> &'a str {
|
||||
if source.starts_with("env.") {
|
||||
text.push('$');
|
||||
return source;
|
||||
}
|
||||
|
||||
match source.chars().next() {
|
||||
Some('{') => {
|
||||
let source = &source[1..];
|
||||
if let Some(end) = source.find('}') {
|
||||
let var_name = &source[..end];
|
||||
if !var_name.is_empty() {
|
||||
if !is_start {
|
||||
text.push_str("(");
|
||||
}
|
||||
text.push_str("$env.");
|
||||
text.push_str(var_name);
|
||||
if !is_start {
|
||||
text.push_str(")");
|
||||
}
|
||||
&source[end + 1..]
|
||||
} else {
|
||||
text.push_str("${}");
|
||||
&source[end + 1..]
|
||||
}
|
||||
} else {
|
||||
text.push_str("${");
|
||||
source
|
||||
}
|
||||
}
|
||||
Some(c) if c.is_alphabetic() || c == '_' => {
|
||||
let end = source
|
||||
.find(|c: char| !c.is_alphanumeric() && c != '_')
|
||||
.unwrap_or(source.len());
|
||||
let var_name = &source[..end];
|
||||
if !is_start {
|
||||
text.push_str("(");
|
||||
}
|
||||
text.push_str("$env.");
|
||||
text.push_str(var_name);
|
||||
if !is_start {
|
||||
text.push_str(")");
|
||||
}
|
||||
&source[end..]
|
||||
}
|
||||
_ => {
|
||||
text.push('$');
|
||||
source
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn args_for_shell(&self, interactive: bool, combined_command: String) -> Vec<String> {
|
||||
match self {
|
||||
ShellKind::PowerShell => vec!["-C".to_owned(), combined_command],
|
||||
ShellKind::Cmd => vec!["/C".to_owned(), combined_command],
|
||||
ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => interactive
|
||||
.then(|| "-i".to_owned())
|
||||
.into_iter()
|
||||
.chain(["-c".to_owned(), combined_command])
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn command_prefix(&self) -> Option<char> {
|
||||
match self {
|
||||
ShellKind::PowerShell => Some('&'),
|
||||
ShellKind::Nushell => Some('^'),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use util::shell::ShellKind;
|
||||
|
||||
/// ShellBuilder is used to turn a user-requested task into a
|
||||
/// program that can be executed by the shell.
|
||||
@@ -253,7 +54,12 @@ impl ShellBuilder {
|
||||
ShellKind::Cmd => {
|
||||
format!("{} /C '{}'", self.program, command_to_use_in_label)
|
||||
}
|
||||
ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => {
|
||||
ShellKind::Posix
|
||||
| ShellKind::Nushell
|
||||
| ShellKind::Fish
|
||||
| ShellKind::Csh
|
||||
| ShellKind::Tcsh
|
||||
| ShellKind::Rc => {
|
||||
let interactivity = self.interactive.then_some("-i ").unwrap_or_default();
|
||||
format!(
|
||||
"{PROGRAM} {interactivity}-c '{command_to_use_in_label}'",
|
||||
@@ -283,7 +89,12 @@ impl ShellBuilder {
|
||||
});
|
||||
if self.redirect_stdin {
|
||||
match self.kind {
|
||||
ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => {
|
||||
ShellKind::Posix
|
||||
| ShellKind::Nushell
|
||||
| ShellKind::Fish
|
||||
| ShellKind::Csh
|
||||
| ShellKind::Tcsh
|
||||
| ShellKind::Rc => {
|
||||
combined_command.insert(0, '(');
|
||||
combined_command.push_str(") </dev/null");
|
||||
}
|
||||
|
||||
+19
-1
@@ -16,6 +16,7 @@ use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use util::get_system_shell;
|
||||
|
||||
pub use adapter_schema::{AdapterSchema, AdapterSchemas};
|
||||
pub use debug_format::{
|
||||
@@ -317,7 +318,7 @@ pub struct TaskContext {
|
||||
pub struct RunnableTag(pub SharedString);
|
||||
|
||||
/// Shell configuration to open the terminal with.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Shell {
|
||||
/// Use the system's default terminal configuration in /etc/passwd
|
||||
@@ -336,6 +337,23 @@ pub enum Shell {
|
||||
},
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
pub fn program(&self) -> String {
|
||||
match self {
|
||||
Shell::Program(program) => program.clone(),
|
||||
Shell::WithArguments { program, .. } => program.clone(),
|
||||
Shell::System => get_system_shell(),
|
||||
}
|
||||
}
|
||||
pub fn program_and_args(&self) -> (String, &[String]) {
|
||||
match self {
|
||||
Shell::Program(program) => (program.clone(), &[]),
|
||||
Shell::WithArguments { program, args, .. } => (program.clone(), args),
|
||||
Shell::System => (get_system_shell(), &[]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type VsCodeEnvVariable = String;
|
||||
type ZedEnvVariable = String;
|
||||
|
||||
|
||||
@@ -398,7 +398,7 @@ impl TerminalBuilder {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Some(ShellParams::new(
|
||||
util::get_windows_system_shell(),
|
||||
util::shell::get_windows_system_shell(),
|
||||
None,
|
||||
None,
|
||||
))
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
use std::{fmt, path::Path, sync::LazyLock};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ShellKind {
|
||||
#[default]
|
||||
Posix,
|
||||
Csh,
|
||||
Tcsh,
|
||||
Rc,
|
||||
Fish,
|
||||
PowerShell,
|
||||
Nushell,
|
||||
Cmd,
|
||||
}
|
||||
|
||||
pub fn get_system_shell() -> String {
|
||||
if cfg!(windows) {
|
||||
get_windows_system_shell()
|
||||
} else {
|
||||
std::env::var("SHELL").unwrap_or("/bin/sh".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_default_system_shell() -> String {
|
||||
if cfg!(windows) {
|
||||
get_windows_system_shell()
|
||||
} else {
|
||||
"/bin/sh".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_windows_system_shell() -> String {
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn find_pwsh_in_programfiles(find_alternate: bool, find_preview: bool) -> Option<PathBuf> {
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
let env_var = if find_alternate {
|
||||
"ProgramFiles(x86)"
|
||||
} else {
|
||||
"ProgramFiles"
|
||||
};
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
let env_var = if find_alternate {
|
||||
"ProgramW6432"
|
||||
} else {
|
||||
"ProgramFiles"
|
||||
};
|
||||
|
||||
let install_base_dir = PathBuf::from(std::env::var_os(env_var)?).join("PowerShell");
|
||||
install_base_dir
|
||||
.read_dir()
|
||||
.ok()?
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| matches!(entry.file_type(), Ok(ft) if ft.is_dir()))
|
||||
.filter_map(|entry| {
|
||||
let dir_name = entry.file_name();
|
||||
let dir_name = dir_name.to_string_lossy();
|
||||
|
||||
let version = if find_preview {
|
||||
let dash_index = dir_name.find('-')?;
|
||||
if &dir_name[dash_index + 1..] != "preview" {
|
||||
return None;
|
||||
};
|
||||
dir_name[..dash_index].parse::<u32>().ok()?
|
||||
} else {
|
||||
dir_name.parse::<u32>().ok()?
|
||||
};
|
||||
|
||||
let exe_path = entry.path().join("pwsh.exe");
|
||||
if exe_path.exists() {
|
||||
Some((version, exe_path))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.max_by_key(|(version, _)| *version)
|
||||
.map(|(_, path)| path)
|
||||
}
|
||||
|
||||
fn find_pwsh_in_msix(find_preview: bool) -> Option<PathBuf> {
|
||||
let msix_app_dir =
|
||||
PathBuf::from(std::env::var_os("LOCALAPPDATA")?).join("Microsoft\\WindowsApps");
|
||||
if !msix_app_dir.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let prefix = if find_preview {
|
||||
"Microsoft.PowerShellPreview_"
|
||||
} else {
|
||||
"Microsoft.PowerShell_"
|
||||
};
|
||||
msix_app_dir
|
||||
.read_dir()
|
||||
.ok()?
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
if !matches!(entry.file_type(), Ok(ft) if ft.is_dir()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !entry.file_name().to_string_lossy().starts_with(prefix) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let exe_path = entry.path().join("pwsh.exe");
|
||||
exe_path.exists().then_some(exe_path)
|
||||
})
|
||||
.next()
|
||||
}
|
||||
|
||||
fn find_pwsh_in_scoop() -> Option<PathBuf> {
|
||||
let pwsh_exe =
|
||||
PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\pwsh.exe");
|
||||
pwsh_exe.exists().then_some(pwsh_exe)
|
||||
}
|
||||
|
||||
static SYSTEM_SHELL: LazyLock<String> = LazyLock::new(|| {
|
||||
find_pwsh_in_programfiles(false, false)
|
||||
.or_else(|| find_pwsh_in_programfiles(true, false))
|
||||
.or_else(|| find_pwsh_in_msix(false))
|
||||
.or_else(|| find_pwsh_in_programfiles(false, true))
|
||||
.or_else(|| find_pwsh_in_msix(true))
|
||||
.or_else(|| find_pwsh_in_programfiles(true, true))
|
||||
.or_else(find_pwsh_in_scoop)
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.unwrap_or("powershell.exe".to_string())
|
||||
});
|
||||
|
||||
(*SYSTEM_SHELL).clone()
|
||||
}
|
||||
|
||||
impl fmt::Display for ShellKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ShellKind::Posix => write!(f, "sh"),
|
||||
ShellKind::Csh => write!(f, "csh"),
|
||||
ShellKind::Tcsh => write!(f, "tcsh"),
|
||||
ShellKind::Fish => write!(f, "fish"),
|
||||
ShellKind::PowerShell => write!(f, "powershell"),
|
||||
ShellKind::Nushell => write!(f, "nu"),
|
||||
ShellKind::Cmd => write!(f, "cmd"),
|
||||
ShellKind::Rc => write!(f, "rc"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShellKind {
|
||||
pub fn system() -> Self {
|
||||
Self::new(&get_system_shell())
|
||||
}
|
||||
|
||||
pub fn new(program: impl AsRef<Path>) -> Self {
|
||||
let program = program.as_ref();
|
||||
let Some(program) = program.file_name().and_then(|s| s.to_str()) else {
|
||||
return if cfg!(windows) {
|
||||
ShellKind::PowerShell
|
||||
} else {
|
||||
ShellKind::Posix
|
||||
};
|
||||
};
|
||||
if program == "powershell"
|
||||
|| program.ends_with("powershell.exe")
|
||||
|| program == "pwsh"
|
||||
|| program.ends_with("pwsh.exe")
|
||||
{
|
||||
ShellKind::PowerShell
|
||||
} else if program == "cmd" || program.ends_with("cmd.exe") {
|
||||
ShellKind::Cmd
|
||||
} else if program == "nu" {
|
||||
ShellKind::Nushell
|
||||
} else if program == "fish" {
|
||||
ShellKind::Fish
|
||||
} else if program == "csh" {
|
||||
ShellKind::Csh
|
||||
} else if program == "tcsh" {
|
||||
ShellKind::Tcsh
|
||||
} else if program == "rc" {
|
||||
ShellKind::Rc
|
||||
} else {
|
||||
if cfg!(windows) {
|
||||
ShellKind::PowerShell
|
||||
} else {
|
||||
// Some other shell detected, the user might install and use a
|
||||
// unix-like shell.
|
||||
ShellKind::Posix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_shell_variable(self, input: &str) -> String {
|
||||
match self {
|
||||
Self::PowerShell => Self::to_powershell_variable(input),
|
||||
Self::Cmd => Self::to_cmd_variable(input),
|
||||
Self::Posix => input.to_owned(),
|
||||
Self::Fish => input.to_owned(),
|
||||
Self::Csh => input.to_owned(),
|
||||
Self::Tcsh => input.to_owned(),
|
||||
Self::Rc => input.to_owned(),
|
||||
Self::Nushell => Self::to_nushell_variable(input),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cmd_variable(input: &str) -> String {
|
||||
if let Some(var_str) = input.strip_prefix("${") {
|
||||
if var_str.find(':').is_none() {
|
||||
// If the input starts with "${", remove the trailing "}"
|
||||
format!("%{}%", &var_str[..var_str.len() - 1])
|
||||
} else {
|
||||
// `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
|
||||
// which will result in the task failing to run in such cases.
|
||||
input.into()
|
||||
}
|
||||
} else if let Some(var_str) = input.strip_prefix('$') {
|
||||
// If the input starts with "$", directly append to "$env:"
|
||||
format!("%{}%", var_str)
|
||||
} else {
|
||||
// If no prefix is found, return the input as is
|
||||
input.into()
|
||||
}
|
||||
}
|
||||
fn to_powershell_variable(input: &str) -> String {
|
||||
if let Some(var_str) = input.strip_prefix("${") {
|
||||
if var_str.find(':').is_none() {
|
||||
// If the input starts with "${", remove the trailing "}"
|
||||
format!("$env:{}", &var_str[..var_str.len() - 1])
|
||||
} else {
|
||||
// `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
|
||||
// which will result in the task failing to run in such cases.
|
||||
input.into()
|
||||
}
|
||||
} else if let Some(var_str) = input.strip_prefix('$') {
|
||||
// If the input starts with "$", directly append to "$env:"
|
||||
format!("$env:{}", var_str)
|
||||
} else {
|
||||
// If no prefix is found, return the input as is
|
||||
input.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn to_nushell_variable(input: &str) -> String {
|
||||
let mut result = String::new();
|
||||
let mut source = input;
|
||||
let mut is_start = true;
|
||||
|
||||
loop {
|
||||
match source.chars().next() {
|
||||
None => return result,
|
||||
Some('$') => {
|
||||
source = Self::parse_nushell_var(&source[1..], &mut result, is_start);
|
||||
is_start = false;
|
||||
}
|
||||
Some(_) => {
|
||||
is_start = false;
|
||||
let chunk_end = source.find('$').unwrap_or(source.len());
|
||||
let (chunk, rest) = source.split_at(chunk_end);
|
||||
result.push_str(chunk);
|
||||
source = rest;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_nushell_var<'a>(source: &'a str, text: &mut String, is_start: bool) -> &'a str {
|
||||
if source.starts_with("env.") {
|
||||
text.push('$');
|
||||
return source;
|
||||
}
|
||||
|
||||
match source.chars().next() {
|
||||
Some('{') => {
|
||||
let source = &source[1..];
|
||||
if let Some(end) = source.find('}') {
|
||||
let var_name = &source[..end];
|
||||
if !var_name.is_empty() {
|
||||
if !is_start {
|
||||
text.push_str("(");
|
||||
}
|
||||
text.push_str("$env.");
|
||||
text.push_str(var_name);
|
||||
if !is_start {
|
||||
text.push_str(")");
|
||||
}
|
||||
&source[end + 1..]
|
||||
} else {
|
||||
text.push_str("${}");
|
||||
&source[end + 1..]
|
||||
}
|
||||
} else {
|
||||
text.push_str("${");
|
||||
source
|
||||
}
|
||||
}
|
||||
Some(c) if c.is_alphabetic() || c == '_' => {
|
||||
let end = source
|
||||
.find(|c: char| !c.is_alphanumeric() && c != '_')
|
||||
.unwrap_or(source.len());
|
||||
let var_name = &source[..end];
|
||||
if !is_start {
|
||||
text.push_str("(");
|
||||
}
|
||||
text.push_str("$env.");
|
||||
text.push_str(var_name);
|
||||
if !is_start {
|
||||
text.push_str(")");
|
||||
}
|
||||
&source[end..]
|
||||
}
|
||||
_ => {
|
||||
text.push('$');
|
||||
source
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn args_for_shell(&self, interactive: bool, combined_command: String) -> Vec<String> {
|
||||
match self {
|
||||
ShellKind::PowerShell => vec!["-C".to_owned(), combined_command],
|
||||
ShellKind::Cmd => vec!["/C".to_owned(), combined_command],
|
||||
ShellKind::Posix
|
||||
| ShellKind::Nushell
|
||||
| ShellKind::Fish
|
||||
| ShellKind::Csh
|
||||
| ShellKind::Tcsh
|
||||
| ShellKind::Rc => interactive
|
||||
.then(|| "-i".to_owned())
|
||||
.into_iter()
|
||||
.chain(["-c".to_owned(), combined_command])
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn command_prefix(&self) -> Option<char> {
|
||||
match self {
|
||||
ShellKind::PowerShell => Some('&'),
|
||||
ShellKind::Nushell => Some('^'),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
+132
-61
@@ -1,60 +1,81 @@
|
||||
#![cfg_attr(not(unix), allow(unused))]
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
|
||||
/// Capture all environment variables from the login shell.
|
||||
use crate::shell::ShellKind;
|
||||
|
||||
pub fn print_env() {
|
||||
let env_vars: HashMap<String, String> = std::env::vars().collect();
|
||||
let json = serde_json::to_string_pretty(&env_vars).unwrap_or_else(|err| {
|
||||
eprintln!("Error serializing environment variables: {}", err);
|
||||
std::process::exit(1);
|
||||
});
|
||||
println!("{}", json);
|
||||
}
|
||||
|
||||
/// Capture all environment variables from the login shell in the given directory.
|
||||
pub async fn capture(
|
||||
shell_path: impl AsRef<Path>,
|
||||
args: &[String],
|
||||
directory: impl AsRef<Path>,
|
||||
) -> Result<collections::HashMap<String, String>> {
|
||||
#[cfg(windows)]
|
||||
return capture_windows(shell_path.as_ref(), args, directory.as_ref()).await;
|
||||
#[cfg(unix)]
|
||||
return capture_unix(shell_path.as_ref(), args, directory.as_ref()).await;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub async fn capture(directory: &std::path::Path) -> Result<collections::HashMap<String, String>> {
|
||||
async fn capture_unix(
|
||||
shell_path: &Path,
|
||||
args: &[String],
|
||||
directory: &Path,
|
||||
) -> Result<collections::HashMap<String, String>> {
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::Stdio;
|
||||
|
||||
let zed_path = super::get_shell_safe_zed_path()?;
|
||||
let shell_path = std::env::var("SHELL").map(std::path::PathBuf::from)?;
|
||||
let shell_name = shell_path.file_name().and_then(std::ffi::OsStr::to_str);
|
||||
let shell_kind = ShellKind::new(shell_path);
|
||||
|
||||
let mut command_string = String::new();
|
||||
let mut command = std::process::Command::new(&shell_path);
|
||||
let mut command = std::process::Command::new(shell_path);
|
||||
command.args(args);
|
||||
// In some shells, file descriptors greater than 2 cannot be used in interactive mode,
|
||||
// so file descriptor 0 (stdin) is used instead. This impacts zsh, old bash; perhaps others.
|
||||
// See: https://github.com/zed-industries/zed/pull/32136#issuecomment-2999645482
|
||||
const FD_STDIN: std::os::fd::RawFd = 0;
|
||||
const FD_STDOUT: std::os::fd::RawFd = 1;
|
||||
|
||||
let (fd_num, redir) = match shell_name {
|
||||
Some("rc") => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]`
|
||||
Some("nu") | Some("tcsh") => (FD_STDOUT, "".to_string()),
|
||||
let (fd_num, redir) = match shell_kind {
|
||||
ShellKind::Rc => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]`
|
||||
ShellKind::Nushell | ShellKind::Tcsh => (FD_STDOUT, "".to_string()),
|
||||
_ => (FD_STDIN, format!(">&{}", FD_STDIN)), // `>&0`
|
||||
};
|
||||
command.stdin(Stdio::null());
|
||||
command.stdout(Stdio::piped());
|
||||
command.stderr(Stdio::piped());
|
||||
|
||||
let mut command_prefix = String::new();
|
||||
match shell_name {
|
||||
Some("tcsh" | "csh") => {
|
||||
match shell_kind {
|
||||
ShellKind::Csh | ShellKind::Tcsh => {
|
||||
// For csh/tcsh, login shell requires passing `-` as 0th argument (instead of `-l`)
|
||||
command.arg0("-");
|
||||
}
|
||||
Some("fish") => {
|
||||
ShellKind::Fish => {
|
||||
// in fish, asdf, direnv attach to the `fish_prompt` event
|
||||
command_string.push_str("emit fish_prompt;");
|
||||
command.arg("-l");
|
||||
}
|
||||
Some("nu") => {
|
||||
// nu needs special handling for -- options.
|
||||
command_prefix = String::from("^");
|
||||
}
|
||||
_ => {
|
||||
command.arg("-l");
|
||||
}
|
||||
}
|
||||
// cd into the directory, triggering directory specific side-effects (asdf, direnv, etc)
|
||||
command_string.push_str(&format!("cd '{}';", directory.display()));
|
||||
command_string.push_str(&format!(
|
||||
"{}{} --printenv {}",
|
||||
command_prefix, zed_path, redir
|
||||
));
|
||||
if let Some(prefix) = shell_kind.command_prefix() {
|
||||
command_string.push(prefix);
|
||||
}
|
||||
command_string.push_str(&format!("{} --printenv {}", zed_path, redir));
|
||||
command.args(["-i", "-c", &command_string]);
|
||||
|
||||
super::set_pre_exec_to_start_new_session(&mut command);
|
||||
@@ -99,54 +120,104 @@ async fn spawn_and_read_fd(
|
||||
Ok((buffer, process.output().await?))
|
||||
}
|
||||
|
||||
/// Capture all environment variables from the shell on Windows.
|
||||
#[cfg(windows)]
|
||||
pub async fn capture(directory: &std::path::Path) -> Result<collections::HashMap<String, String>> {
|
||||
async fn capture_windows(
|
||||
shell_path: &Path,
|
||||
_args: &[String],
|
||||
directory: &Path,
|
||||
) -> Result<collections::HashMap<String, String>> {
|
||||
use std::process::Stdio;
|
||||
|
||||
let zed_path =
|
||||
std::env::current_exe().context("Failed to determine current zed executable path.")?;
|
||||
|
||||
// Use PowerShell to get environment variables in the directory context
|
||||
let output = crate::command::new_smol_command(crate::get_windows_system_shell())
|
||||
.args([
|
||||
"-NonInteractive",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
&format!(
|
||||
"Set-Location '{}'; & '{}' --printenv",
|
||||
directory.display(),
|
||||
zed_path.display()
|
||||
),
|
||||
])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await?;
|
||||
let shell_kind = ShellKind::new(shell_path);
|
||||
let env_output = match shell_kind {
|
||||
ShellKind::Posix | ShellKind::Csh | ShellKind::Tcsh | ShellKind::Rc | ShellKind::Fish => {
|
||||
return Err(anyhow::anyhow!("unsupported shell kind"));
|
||||
}
|
||||
ShellKind::PowerShell => {
|
||||
let output = crate::command::new_smol_command(shell_path)
|
||||
.args([
|
||||
"-NonInteractive",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
&format!(
|
||||
"Set-Location '{}'; & '{}' --printenv",
|
||||
directory.display(),
|
||||
zed_path.display()
|
||||
),
|
||||
])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"PowerShell command failed with {}. stdout: {:?}, stderr: {:?}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"PowerShell command failed with {}. stdout: {:?}, stderr: {:?}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
output
|
||||
}
|
||||
ShellKind::Nushell => {
|
||||
let output = crate::command::new_smol_command(shell_path)
|
||||
.args([
|
||||
"-c",
|
||||
&format!(
|
||||
"cd '{}'; {} --printenv",
|
||||
directory.display(),
|
||||
zed_path.display()
|
||||
),
|
||||
])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
let env_output = String::from_utf8_lossy(&output.stdout);
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"Nushell command failed with {}. stdout: {:?}, stderr: {:?}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
output
|
||||
}
|
||||
ShellKind::Cmd => {
|
||||
let output = crate::command::new_smol_command(shell_path)
|
||||
.args([
|
||||
"/c",
|
||||
&format!(
|
||||
"cd '{}'; {} --printenv",
|
||||
directory.display(),
|
||||
zed_path.display()
|
||||
),
|
||||
])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"Cmd command failed with {}. stdout: {:?}, stderr: {:?}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
output
|
||||
}
|
||||
};
|
||||
|
||||
let env_output = String::from_utf8_lossy(&env_output.stdout);
|
||||
|
||||
// Parse the JSON output from zed --printenv
|
||||
let env_map: collections::HashMap<String, String> = serde_json::from_str(&env_output)
|
||||
.with_context(|| "Failed to deserialize environment variables from json")?;
|
||||
Ok(env_map)
|
||||
}
|
||||
|
||||
pub fn print_env() {
|
||||
let env_vars: HashMap<String, String> = std::env::vars().collect();
|
||||
let json = serde_json::to_string_pretty(&env_vars).unwrap_or_else(|err| {
|
||||
eprintln!("Error serializing environment variables: {}", err);
|
||||
std::process::exit(1);
|
||||
});
|
||||
println!("{}", json);
|
||||
std::process::exit(0);
|
||||
serde_json::from_str(&env_output)
|
||||
.with_context(|| "Failed to deserialize environment variables from json")
|
||||
}
|
||||
|
||||
+3
-126
@@ -8,6 +8,7 @@ pub mod redact;
|
||||
pub mod rel_path;
|
||||
pub mod schemars;
|
||||
pub mod serde;
|
||||
pub mod shell;
|
||||
pub mod shell_env;
|
||||
pub mod size;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -367,7 +368,7 @@ pub async fn load_login_shell_environment() -> Result<()> {
|
||||
// into shell's `cd` command (and hooks) to manipulate env.
|
||||
// We do this so that we get the env a user would have when spawning a shell
|
||||
// in home directory.
|
||||
for (name, value) in shell_env::capture(paths::home_dir()).await? {
|
||||
for (name, value) in shell_env::capture(get_system_shell(), &[], paths::home_dir()).await? {
|
||||
unsafe { env::set_var(&name, &value) };
|
||||
}
|
||||
|
||||
@@ -555,108 +556,6 @@ pub fn wrapped_usize_outward_from(
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn get_windows_system_shell() -> String {
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn find_pwsh_in_programfiles(find_alternate: bool, find_preview: bool) -> Option<PathBuf> {
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
let env_var = if find_alternate {
|
||||
"ProgramFiles(x86)"
|
||||
} else {
|
||||
"ProgramFiles"
|
||||
};
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
let env_var = if find_alternate {
|
||||
"ProgramW6432"
|
||||
} else {
|
||||
"ProgramFiles"
|
||||
};
|
||||
|
||||
let install_base_dir = PathBuf::from(std::env::var_os(env_var)?).join("PowerShell");
|
||||
install_base_dir
|
||||
.read_dir()
|
||||
.ok()?
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| matches!(entry.file_type(), Ok(ft) if ft.is_dir()))
|
||||
.filter_map(|entry| {
|
||||
let dir_name = entry.file_name();
|
||||
let dir_name = dir_name.to_string_lossy();
|
||||
|
||||
let version = if find_preview {
|
||||
let dash_index = dir_name.find('-')?;
|
||||
if &dir_name[dash_index + 1..] != "preview" {
|
||||
return None;
|
||||
};
|
||||
dir_name[..dash_index].parse::<u32>().ok()?
|
||||
} else {
|
||||
dir_name.parse::<u32>().ok()?
|
||||
};
|
||||
|
||||
let exe_path = entry.path().join("pwsh.exe");
|
||||
if exe_path.exists() {
|
||||
Some((version, exe_path))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.max_by_key(|(version, _)| *version)
|
||||
.map(|(_, path)| path)
|
||||
}
|
||||
|
||||
fn find_pwsh_in_msix(find_preview: bool) -> Option<PathBuf> {
|
||||
let msix_app_dir =
|
||||
PathBuf::from(std::env::var_os("LOCALAPPDATA")?).join("Microsoft\\WindowsApps");
|
||||
if !msix_app_dir.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let prefix = if find_preview {
|
||||
"Microsoft.PowerShellPreview_"
|
||||
} else {
|
||||
"Microsoft.PowerShell_"
|
||||
};
|
||||
msix_app_dir
|
||||
.read_dir()
|
||||
.ok()?
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
if !matches!(entry.file_type(), Ok(ft) if ft.is_dir()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !entry.file_name().to_string_lossy().starts_with(prefix) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let exe_path = entry.path().join("pwsh.exe");
|
||||
exe_path.exists().then_some(exe_path)
|
||||
})
|
||||
.next()
|
||||
}
|
||||
|
||||
fn find_pwsh_in_scoop() -> Option<PathBuf> {
|
||||
let pwsh_exe =
|
||||
PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\pwsh.exe");
|
||||
pwsh_exe.exists().then_some(pwsh_exe)
|
||||
}
|
||||
|
||||
static SYSTEM_SHELL: LazyLock<String> = LazyLock::new(|| {
|
||||
find_pwsh_in_programfiles(false, false)
|
||||
.or_else(|| find_pwsh_in_programfiles(true, false))
|
||||
.or_else(|| find_pwsh_in_msix(false))
|
||||
.or_else(|| find_pwsh_in_programfiles(false, true))
|
||||
.or_else(|| find_pwsh_in_msix(true))
|
||||
.or_else(|| find_pwsh_in_programfiles(true, true))
|
||||
.or_else(find_pwsh_in_scoop)
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.unwrap_or("powershell.exe".to_string())
|
||||
});
|
||||
|
||||
(*SYSTEM_SHELL).clone()
|
||||
}
|
||||
|
||||
pub trait ResultExt<E> {
|
||||
type Ok;
|
||||
|
||||
@@ -1100,29 +999,7 @@ pub fn default<D: Default>() -> D {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn get_system_shell() -> String {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
get_windows_system_shell()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
std::env::var("SHELL").unwrap_or("/bin/sh".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_default_system_shell() -> String {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
get_windows_system_shell()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
"/bin/sh".to_string()
|
||||
}
|
||||
}
|
||||
pub use self::shell::{get_default_system_shell, get_system_shell};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ConnectionResult<O> {
|
||||
|
||||
Reference in New Issue
Block a user