rust: Test rust-analyzer binary after finding in PATH (#17951)

Thorsten Ball and Conrad Irwin created

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

crates/language/src/language.rs |  1 +
crates/languages/src/rust.rs    | 23 ++++++++++++++++++++++-
crates/project/src/lsp_store.rs | 24 ++++++++++++++++++++++++
3 files changed, 47 insertions(+), 1 deletion(-)

Detailed changes

crates/language/src/language.rs 🔗

@@ -285,6 +285,7 @@ pub trait LspAdapterDelegate: Send + Sync {
     async fn which(&self, command: &OsStr) -> Option<PathBuf>;
     async fn shell_env(&self) -> HashMap<String, String>;
     async fn read_text_file(&self, path: PathBuf) -> Result<String>;
+    async fn try_exec(&self, binary: LanguageServerBinary) -> Result<()>;
 }
 
 #[async_trait(?Send)]

crates/languages/src/rust.rs 🔗

@@ -61,7 +61,28 @@ impl LspAdapter for RustLspAdapter {
             }) => {
                 let path = delegate.which(Self::SERVER_NAME.as_ref()).await;
                 let env = delegate.shell_env().await;
-                (path, Some(env), None)
+
+                if let Some(path) = path {
+                    // It is surprisingly common for ~/.cargo/bin/rust-analyzer to be a symlink to
+                    // /usr/bin/rust-analyzer that fails when you run it; so we need to test it.
+                    log::info!("found rust-analyzer in PATH. trying to run `rust-analyzer --help`");
+                    match delegate
+                        .try_exec(LanguageServerBinary {
+                            path: path.clone(),
+                            arguments: vec!["--help".into()],
+                            env: Some(env.clone()),
+                        })
+                        .await
+                    {
+                        Ok(()) => (Some(path), Some(env), None),
+                        Err(err) => {
+                            log::error!("failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {:?}", path, err);
+                            (None, None, None)
+                        }
+                    }
+                } else {
+                    (None, None, None)
+                }
             }
             // Otherwise, we use the configured binary.
             Some(BinarySettings {

crates/project/src/lsp_store.rs 🔗

@@ -7133,6 +7133,30 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate {
         which::which(command).ok()
     }
 
+    async fn try_exec(&self, command: LanguageServerBinary) -> Result<()> {
+        if self.fs.is_none() {
+            return Ok(());
+        }
+
+        let working_dir = self.worktree_root_path();
+        let output = smol::process::Command::new(&command.path)
+            .args(command.arguments)
+            .envs(command.env.clone().unwrap_or_default())
+            .current_dir(working_dir)
+            .output()
+            .await?;
+
+        if output.status.success() {
+            return Ok(());
+        }
+        Err(anyhow!(
+            "{}, stdout: {:?}, stderr: {:?}",
+            output.status,
+            String::from_utf8_lossy(&output.stdout),
+            String::from_utf8_lossy(&output.stderr)
+        ))
+    }
+
     fn update_status(
         &self,
         server_name: LanguageServerName,