windows: Fix `rust` tasks (#13413)

张小白 created

https://github.com/zed-industries/zed/assets/14981363/56c208da-132c-438a-92b3-e31505859262


Release Notes:

- N/A

Change summary

crates/languages/src/rust.rs               |  5 
crates/terminal/src/terminal_settings.rs   | 94 ++++++++++++++++++++++++
crates/terminal_view/src/terminal_panel.rs | 46 +++++++++++
3 files changed, 140 insertions(+), 5 deletions(-)

Detailed changes

crates/languages/src/rust.rs 🔗

@@ -575,12 +575,11 @@ fn retrieve_package_id_and_bin_name_from_metadata(
     metadata: CargoMetadata,
     abs_path: &Path,
 ) -> Option<(String, String)> {
-    let abs_path = abs_path.to_str()?;
-
     for package in metadata.packages {
         for target in package.targets {
             let is_bin = target.kind.iter().any(|kind| kind == "bin");
-            if target.src_path == abs_path && is_bin {
+            let target_path = PathBuf::from(target.src_path);
+            if target_path == abs_path && is_bin {
                 return Some((package.id, target.name));
             }
         }

crates/terminal/src/terminal_settings.rs 🔗

@@ -268,6 +268,48 @@ pub enum Shell {
     },
 }
 
+impl Shell {
+    pub fn retrieve_system_shell() -> Option<String> {
+        #[cfg(not(target_os = "windows"))]
+        {
+            use anyhow::Context;
+            use util::ResultExt;
+
+            return std::env::var("SHELL")
+                .context("Error finding SHELL in env.")
+                .log_err();
+        }
+        // `alacritty_terminal` uses this as default on Windows. See:
+        // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
+        #[cfg(target_os = "windows")]
+        return Some("powershell".to_owned());
+    }
+
+    /// Convert unix-shell variable syntax to windows-shell syntax.
+    /// `powershell` and `cmd` are considered valid here.
+    #[cfg(target_os = "windows")]
+    pub fn to_windows_shell_variable(shell_type: WindowsShellType, input: String) -> String {
+        match shell_type {
+            WindowsShellType::Powershell => to_powershell_variable(input),
+            WindowsShellType::Cmd => to_cmd_variable(input),
+            WindowsShellType::Other => input,
+        }
+    }
+
+    #[cfg(target_os = "windows")]
+    pub fn to_windows_shell_type(shell: &str) -> WindowsShellType {
+        if shell == "powershell" || shell.ends_with("powershell.exe") {
+            WindowsShellType::Powershell
+        } else if shell == "cmd" || shell.ends_with("cmd.exe") {
+            WindowsShellType::Cmd
+        } else {
+            // Someother shell detected, the user might install and use a
+            // unix-like shell.
+            WindowsShellType::Other
+        }
+    }
+}
+
 #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum AlternateScroll {
@@ -299,3 +341,55 @@ pub struct ToolbarContent {
     /// Default: true
     pub title: Option<bool>,
 }
+
+#[cfg(target_os = "windows")]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum WindowsShellType {
+    Powershell,
+    Cmd,
+    Other,
+}
+
+/// Convert `${SOME_VAR}`, `$SOME_VAR` to `%SOME_VAR%`.
+#[inline]
+#[cfg(target_os = "windows")]
+fn to_cmd_variable(input: String) -> 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
+        }
+    } 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
+    }
+}
+
+/// Convert `${SOME_VAR}`, `$SOME_VAR` to `$env:SOME_VAR`.
+#[inline]
+#[cfg(target_os = "windows")]
+fn to_powershell_variable(input: String) -> 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
+        }
+    } 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
+    }
+}

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -350,24 +350,66 @@ impl TerminalPanel {
         let mut spawn_task = spawn_in_terminal.clone();
         // Set up shell args unconditionally, as tasks are always spawned inside of a shell.
         let Some((shell, mut user_args)) = (match TerminalSettings::get_global(cx).shell.clone() {
-            Shell::System => std::env::var("SHELL").ok().map(|shell| (shell, Vec::new())),
+            Shell::System => Shell::retrieve_system_shell().map(|shell| (shell, Vec::new())),
             Shell::Program(shell) => Some((shell, Vec::new())),
             Shell::WithArguments { program, args } => Some((program, args)),
         }) else {
             return;
         };
+        #[cfg(target_os = "windows")]
+        let windows_shell_type = Shell::to_windows_shell_type(&shell);
+
+        #[cfg(not(target_os = "windows"))]
+        {
+            spawn_task.command_label = format!("{shell} -i -c `{}`", spawn_task.command_label);
+        }
+        #[cfg(target_os = "windows")]
+        {
+            use terminal::terminal_settings::WindowsShellType;
+
+            match windows_shell_type {
+                WindowsShellType::Powershell => {
+                    spawn_task.command_label = format!("{shell} -C `{}`", spawn_task.command_label)
+                }
+                WindowsShellType::Cmd => {
+                    spawn_task.command_label = format!("{shell} /C `{}`", spawn_task.command_label)
+                }
+                WindowsShellType::Other => {
+                    spawn_task.command_label =
+                        format!("{shell} -i -c `{}`", spawn_task.command_label)
+                }
+            }
+        }
 
-        spawn_task.command_label = format!("{shell} -i -c `{}`", spawn_task.command_label);
         let task_command = std::mem::replace(&mut spawn_task.command, shell);
         let task_args = std::mem::take(&mut spawn_task.args);
         let combined_command = task_args
             .into_iter()
             .fold(task_command, |mut command, arg| {
                 command.push(' ');
+                #[cfg(not(target_os = "windows"))]
                 command.push_str(&arg);
+                #[cfg(target_os = "windows")]
+                command.push_str(&Shell::to_windows_shell_variable(windows_shell_type, arg));
                 command
             });
+
+        #[cfg(not(target_os = "windows"))]
         user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command]);
+        #[cfg(target_os = "windows")]
+        {
+            use terminal::terminal_settings::WindowsShellType;
+
+            match windows_shell_type {
+                WindowsShellType::Powershell => {
+                    user_args.extend(["-C".to_owned(), combined_command])
+                }
+                WindowsShellType::Cmd => user_args.extend(["/C".to_owned(), combined_command]),
+                WindowsShellType::Other => {
+                    user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command])
+                }
+            }
+        }
         spawn_task.args = user_args;
         let spawn_task = spawn_task;