language_extension: Handle prefixed WASI windows paths in extension spawning (#44477)

Lukas Wirth created

Closes https://github.com/zed-industries/zed/issues/12013

Release Notes:

- Fixed some wasm language extensions failing to spawn on windows

Change summary

crates/language_extension/src/extension_lsp_adapter.rs | 56 +++++++++++
1 file changed, 53 insertions(+), 3 deletions(-)

Detailed changes

crates/language_extension/src/extension_lsp_adapter.rs 🔗

@@ -1,5 +1,5 @@
 use std::ops::Range;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
 use std::sync::Arc;
 
 use anyhow::{Context as _, Result};
@@ -174,7 +174,32 @@ impl DynLspInstaller for ExtensionLspAdapter {
                     )
                     .await?;
 
-                let path = self.extension.path_from_extension(command.command.as_ref());
+                // on windows, extensions might produce weird paths
+                // that start with a leading slash due to WASI
+                // requiring that for PWD and friends so account for
+                // that here and try to transform those paths back
+                // to windows paths
+                //
+                // if we don't do this, std will interpret the path as relative,
+                // which changes join behavior
+                let command_path: &Path = if cfg!(windows)
+                    && let Some(command) = command.command.to_str()
+                {
+                    let mut chars = command.chars();
+                    if chars.next().is_some_and(|c| c == '/')
+                        && chars.next().is_some_and(|c| c.is_ascii_alphabetic())
+                        && chars.next().is_some_and(|c| c == ':')
+                        && chars.next().is_some_and(|c| c == '\\' || c == '/')
+                    {
+                        // looks like a windows path with a leading slash, so strip it
+                        command.strip_prefix('/').unwrap().as_ref()
+                    } else {
+                        command.as_ref()
+                    }
+                } else {
+                    command.command.as_ref()
+                };
+                let path = self.extension.path_from_extension(command_path);
 
                 // TODO: This should now be done via the `zed::make_file_executable` function in
                 // Zed extension API, but we're leaving these existing usages in place temporarily
@@ -193,7 +218,32 @@ impl DynLspInstaller for ExtensionLspAdapter {
 
                 Ok(LanguageServerBinary {
                     path,
-                    arguments: command.args.into_iter().map(|arg| arg.into()).collect(),
+                    arguments: command
+                        .args
+                        .into_iter()
+                        .map(|arg| {
+                            // on windows, extensions might produce weird paths
+                            // that start with a leading slash due to WASI
+                            // requiring that for PWD and friends so account for
+                            // that here and try to transform those paths back
+                            // to windows paths
+                            if cfg!(windows) {
+                                let mut chars = arg.chars();
+                                if chars.next().is_some_and(|c| c == '/')
+                                    && chars.next().is_some_and(|c| c.is_ascii_alphabetic())
+                                    && chars.next().is_some_and(|c| c == ':')
+                                    && chars.next().is_some_and(|c| c == '\\' || c == '/')
+                                {
+                                    // looks like a windows path with a leading slash, so strip it
+                                    arg.strip_prefix('/').unwrap().into()
+                                } else {
+                                    arg.into()
+                                }
+                            } else {
+                                arg.into()
+                            }
+                        })
+                        .collect(),
                     env: Some(command.env.into_iter().collect()),
                 })
             })