@@ -708,7 +708,7 @@ impl ContextProvider for PythonContextProvider {
// Execute a selection
TaskTemplate {
label: "execute selection".to_owned(),
- command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(),
+ command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
args: vec![
"-c".to_owned(),
VariableName::SelectedText.template_value_with_whitespace(),
@@ -719,7 +719,7 @@ impl ContextProvider for PythonContextProvider {
// Execute an entire file
TaskTemplate {
label: format!("run '{}'", VariableName::File.template_value()),
- command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(),
+ command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
args: vec![VariableName::File.template_value_with_whitespace()],
cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
@@ -727,10 +727,10 @@ impl ContextProvider for PythonContextProvider {
// Execute a file as module
TaskTemplate {
label: format!("run module '{}'", VariableName::File.template_value()),
- command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(),
+ command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
args: vec![
"-m".to_owned(),
- PYTHON_MODULE_NAME_TASK_VARIABLE.template_value_with_whitespace(),
+ PYTHON_MODULE_NAME_TASK_VARIABLE.template_value(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
tags: vec!["python-module-main-method".to_owned()],
@@ -744,7 +744,7 @@ impl ContextProvider for PythonContextProvider {
// Run tests for an entire file
TaskTemplate {
label: format!("unittest '{}'", VariableName::File.template_value()),
- command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(),
+ command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
args: vec![
"-m".to_owned(),
"unittest".to_owned(),
@@ -756,7 +756,7 @@ impl ContextProvider for PythonContextProvider {
// Run test(s) for a specific target within a file
TaskTemplate {
label: "unittest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
- command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(),
+ command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
args: vec![
"-m".to_owned(),
"unittest".to_owned(),
@@ -776,7 +776,7 @@ impl ContextProvider for PythonContextProvider {
// Run tests for an entire file
TaskTemplate {
label: format!("pytest '{}'", VariableName::File.template_value()),
- command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(),
+ command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
args: vec![
"-m".to_owned(),
"pytest".to_owned(),
@@ -788,7 +788,7 @@ impl ContextProvider for PythonContextProvider {
// Run test(s) for a specific target within a file
TaskTemplate {
label: "pytest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
- command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(),
+ command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
args: vec![
"-m".to_owned(),
"pytest".to_owned(),
@@ -135,6 +135,7 @@ impl Project {
let lang_registry = self.languages.clone();
let fs = self.fs.clone();
cx.spawn(async move |project, cx| {
+ let shell_kind = ShellKind::new(&shell);
let activation_script = maybe!(async {
for toolchain in toolchains {
let Some(toolchain) = toolchain.await else {
@@ -147,7 +148,7 @@ impl Project {
let lister = language?.toolchain_lister();
return Some(
lister?
- .activation_script(&toolchain, ShellKind::new(&shell), fs.as_ref())
+ .activation_script(&toolchain, shell_kind, fs.as_ref())
.await,
);
}
@@ -157,26 +158,36 @@ impl Project {
.unwrap_or_default();
project.update(cx, move |this, cx| {
+ let format_to_run = || {
+ if let Some(command) = &spawn_task.command {
+ let mut command: Option<Cow<str>> = shlex::try_quote(command).ok();
+ if let Some(command) = &mut command
+ && command.starts_with('"')
+ && let Some(prefix) = shell_kind.command_prefix()
+ {
+ *command = Cow::Owned(format!("{prefix}{command}"));
+ }
+
+ let args = spawn_task
+ .args
+ .iter()
+ .filter_map(|arg| shlex::try_quote(arg).ok());
+ command.into_iter().chain(args).join(" ")
+ } else {
+ // todo: this breaks for remotes to windows
+ format!("exec {shell} -l")
+ }
+ };
+
let shell = {
env.extend(spawn_task.env);
match remote_client {
Some(remote_client) => match activation_script.clone() {
activation_script if !activation_script.is_empty() => {
let activation_script = activation_script.join("; ");
- let to_run = if let Some(command) = spawn_task.command {
- let command: Option<Cow<str>> = shlex::try_quote(&command).ok();
- let args = spawn_task
- .args
- .iter()
- .filter_map(|arg| shlex::try_quote(arg).ok());
- command.into_iter().chain(args).join(" ")
- } else {
- format!("exec {shell} -l")
- };
- let args = vec![
- "-c".to_owned(),
- format!("{activation_script}; {to_run}",),
- ];
+ let to_run = format_to_run();
+ let args =
+ vec!["-c".to_owned(), format!("{activation_script}; {to_run}")];
create_remote_shell(
Some((&shell, &args)),
&mut env,
@@ -197,43 +208,21 @@ impl Project {
)?,
},
None => match activation_script.clone() {
- #[cfg(not(windows))]
activation_script if !activation_script.is_empty() => {
let activation_script = activation_script.join("; ");
- let to_run = if let Some(command) = spawn_task.command {
- let command: Option<Cow<str>> = shlex::try_quote(&command).ok();
- let args = spawn_task
- .args
- .iter()
- .filter_map(|arg| shlex::try_quote(arg).ok());
- command.into_iter().chain(args).join(" ")
- } else {
- format!("exec {shell} -l")
- };
+ let to_run = format_to_run();
+
+ // todo(lw): Alacritty uses `CreateProcessW` on windows with the entire command and arg sequence merged into a single string,
+ // without quoting the arguments
+ #[cfg(windows)]
+ let arg =
+ quote_arg(&format!("{activation_script}; {to_run}"), true);
+ #[cfg(not(windows))]
+ let arg = format!("{activation_script}; {to_run}");
+
Shell::WithArguments {
program: shell,
- args: vec![
- "-c".to_owned(),
- // alacritty formats all args into a single string literally without extra quoting before handing it off to powershell
- // so we work around this here
- if cfg!(windows) {
- println!(
- "{}",
- shlex::try_quote(&format!(
- "{activation_script}; {to_run}",
- ))
- .unwrap()
- .into_owned()
- );
- shlex::try_quote(&format!(
- "{activation_script}; {to_run}",
- ))
- .unwrap()
- .into_owned()
- } else {
- format!("{activation_script}; {to_run}",)
- },
- ],
+ args: vec!["-c".to_owned(), arg],
title_override: None,
}
}
@@ -523,6 +512,37 @@ impl Project {
}
}
+/// We're not using shlex for windows as it is overly eager with escaping some of the special characters (^) we need for nu. Hence, we took
+/// that quote impl straight from Rust stdlib (Command API).
+#[cfg(windows)]
+fn quote_arg(argument: &str, quote: bool) -> String {
+ let mut arg = String::new();
+ if quote {
+ arg.push('"');
+ }
+
+ let mut backslashes: usize = 0;
+ for x in argument.chars() {
+ if x == '\\' {
+ backslashes += 1;
+ } else {
+ if x == '"' {
+ // Add n+1 backslashes to total 2n+1 before internal '"'.
+ arg.extend((0..=backslashes).map(|_| '\\'));
+ }
+ backslashes = 0;
+ }
+ arg.push(x);
+ }
+
+ if quote {
+ // Add n backslashes to total 2n before ending '"'.
+ arg.extend((0..backslashes).map(|_| '\\'));
+ arg.push('"');
+ }
+ arg
+}
+
fn create_remote_shell(
spawn_command: Option<(&String, &Vec<String>)>,
env: &mut HashMap<String, String>,
@@ -193,6 +193,14 @@ impl ShellKind {
.collect(),
}
}
+
+ pub fn command_prefix(&self) -> Option<char> {
+ match self {
+ ShellKind::Powershell => Some('&'),
+ ShellKind::Nushell => Some('^'),
+ _ => None,
+ }
+ }
}
/// ShellBuilder is used to turn a user-requested task into a