project: Fix terminal activation scripts failing on Windows for new shells (#37986)

Lukas Wirth created

Tasks are still disabled as there seem to be more issues with it

Release Notes:

- N/A

Change summary

Cargo.lock                                 |  1 
crates/agent_ui/src/acp/thread_view.rs     |  2 
crates/languages/Cargo.toml                |  1 
crates/languages/src/python.rs             |  9 +++-
crates/project/src/terminals.rs            | 22 ++++++++++++-
crates/terminal/src/terminal.rs            |  8 +++-
crates/terminal_view/src/terminal_panel.rs | 38 +----------------------
7 files changed, 38 insertions(+), 43 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9301,6 +9301,7 @@ dependencies = [
  "serde_json_lenient",
  "settings",
  "sha2",
+ "shlex",
  "smol",
  "snippet_provider",
  "task",

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -1607,7 +1607,7 @@ impl AcpThreadView {
             task.shell = shell;
 
             let terminal = terminal_panel.update_in(cx, |terminal_panel, window, cx| {
-                terminal_panel.spawn_task(&login, window, cx)
+                terminal_panel.spawn_task(login.clone(), window, cx)
             })?;
 
             let terminal = terminal.await?;

crates/languages/Cargo.toml 🔗

@@ -92,6 +92,7 @@ tree-sitter-typescript = { workspace = true, optional = true }
 tree-sitter-yaml = { workspace = true, optional = true }
 util.workspace = true
 workspace-hack.workspace = true
+shlex.workspace = true
 
 [dev-dependencies]
 pretty_assertions.workspace = true

crates/languages/src/python.rs 🔗

@@ -918,9 +918,12 @@ impl ToolchainLister for PythonToolchainProvider {
                         ShellKind::Cmd => "activate.bat",
                     };
                     let path = prefix.join(BINARY_DIR).join(activate_script_name);
-                    if fs.is_file(&path).await {
-                        activation_script
-                            .push(format!("{activate_keyword} \"{}\"", path.display()));
+
+                    if let Ok(quoted) =
+                        shlex::try_quote(&path.to_string_lossy()).map(Cow::into_owned)
+                        && fs.is_file(&path).await
+                    {
+                        activation_script.push(format!("{activate_keyword} {quoted}"));
                     }
                 }
             }

crates/project/src/terminals.rs 🔗

@@ -197,7 +197,7 @@ impl Project {
                             )?,
                         },
                         None => match activation_script.clone() {
-                            #[cfg(not(target_os = "windows"))]
+                            #[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 {
@@ -214,7 +214,25 @@ impl Project {
                                     program: shell,
                                     args: vec![
                                         "-c".to_owned(),
-                                        format!("{activation_script}; {to_run}",),
+                                        // 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}",)
+                                        },
                                     ],
                                     title_override: None,
                                 }

crates/terminal/src/terminal.rs 🔗

@@ -531,10 +531,14 @@ impl TerminalBuilder {
             },
         };
 
-        if cfg!(not(target_os = "windows")) && !activation_script.is_empty() && no_task {
+        if !activation_script.is_empty() && no_task {
             for activation_script in activation_script {
                 terminal.input(activation_script.into_bytes());
-                terminal.write_to_pty(b"\n");
+                terminal.write_to_pty(if cfg!(windows) {
+                    &b"\r\n"[..]
+                } else {
+                    &b"\n"[..]
+                });
             }
             terminal.clear();
         }

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -19,7 +19,7 @@ use itertools::Itertools;
 use project::{Fs, Project, ProjectEntryId};
 use search::{BufferSearchBar, buffer_search::DivRegistrar};
 use settings::Settings;
-use task::{RevealStrategy, RevealTarget, ShellBuilder, SpawnInTerminal, TaskId};
+use task::{RevealStrategy, RevealTarget, SpawnInTerminal, TaskId};
 use terminal::{
     Terminal,
     terminal_settings::{TerminalDockPosition, TerminalSettings},
@@ -496,42 +496,10 @@ impl TerminalPanel {
 
     pub fn spawn_task(
         &mut self,
-        task: &SpawnInTerminal,
+        task: SpawnInTerminal,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Task<Result<WeakEntity<Terminal>>> {
-        let remote_client = self
-            .workspace
-            .update(cx, |workspace, cx| {
-                let project = workspace.project().read(cx);
-                if project.is_via_collab() {
-                    Err(anyhow!("cannot spawn tasks as a guest"))
-                } else {
-                    Ok(project.remote_client())
-                }
-            })
-            .flatten();
-
-        let remote_client = match remote_client {
-            Ok(remote_client) => remote_client,
-            Err(e) => return Task::ready(Err(e)),
-        };
-
-        let remote_shell = remote_client
-            .as_ref()
-            .and_then(|remote_client| remote_client.read(cx).shell());
-
-        let builder = ShellBuilder::new(remote_shell.as_deref(), &task.shell);
-        let command_label = builder.command_label(&task.command_label);
-        let (command, args) = builder.build(task.command.clone(), &task.args);
-
-        let task = SpawnInTerminal {
-            command_label,
-            command: Some(command),
-            args,
-            ..task.clone()
-        };
-
         if task.allow_concurrent_runs && task.use_new_terminal {
             return self.spawn_in_new_terminal(task, window, cx);
         }
@@ -1565,7 +1533,7 @@ impl workspace::TerminalProvider for TerminalProvider {
         window.spawn(cx, async move |cx| {
             let terminal = terminal_panel
                 .update_in(cx, |terminal_panel, window, cx| {
-                    terminal_panel.spawn_task(&task, window, cx)
+                    terminal_panel.spawn_task(task, window, cx)
                 })
                 .ok()?
                 .await;