Auto merge of #15600 - davidbarsky:davidbarsky/broken-rustfmt-in-ra, r=Veykril

fix: ensure `rustfmt` runs when configured with `./`

(Hopefully) resolves https://github.com/rust-lang/rust-analyzer/issues/15595. This change kinda approaches canonicalization—which I am not a fan of—but only in service of making `./`-configured commands run correctly.

Longer-term, I feel like this code should be removed once `rustfmt` supports recursive searches of configuration files or interpolation of values like `${workspace_folder}` lands in rust-analyzer.

## Testing

I cloned `rustc`, setup rust-analyzer as suggested in the [`rustc` dev guide](https://rustc-dev-guide.rust-lang.org/building/suggested.html#configuring-rust-analyzer-for-rustc), saved and formatted files in `src/tools/miri` and `compiler`, and saw `rustfmt` (seemingly) correctly.
diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs
index b8a1a39..6c2f1ec 100644
--- a/crates/rust-analyzer/src/handlers/request.rs
+++ b/crates/rust-analyzer/src/handlers/request.rs
@@ -4,6 +4,7 @@
 use std::{
     fs,
     io::Write as _,
+    path::PathBuf,
     process::{self, Stdio},
 };
 
@@ -1995,7 +1996,25 @@
             cmd
         }
         RustfmtConfig::CustomCommand { command, args } => {
-            let mut cmd = process::Command::new(command);
+            let cmd = PathBuf::from(&command);
+            let workspace = CargoTargetSpec::for_file(&snap, file_id)?;
+            let mut cmd = match workspace {
+                Some(spec) => {
+                    // approach: if the command name contains a path seperator, join it with the workspace root.
+                    // however, if the path is absolute, joining will result in the absolute path being preserved.
+                    // as a fallback, rely on $PATH-based discovery.
+                    let cmd_path =
+                        if cfg!(windows) && command.contains(&[std::path::MAIN_SEPARATOR, '/']) {
+                            spec.workspace_root.join(cmd).into()
+                        } else if command.contains(std::path::MAIN_SEPARATOR) {
+                            spec.workspace_root.join(cmd).into()
+                        } else {
+                            cmd
+                        };
+                    process::Command::new(cmd_path)
+                }
+                None => process::Command::new(cmd),
+            };
 
             cmd.envs(snap.config.extra_env());
             cmd.args(args);
@@ -2003,6 +2022,8 @@
         }
     };
 
+    tracing::debug!(?command, "created format command");
+
     // try to chdir to the file so we can respect `rustfmt.toml`
     // FIXME: use `rustfmt --config-path` once
     // https://github.com/rust-lang/rustfmt/issues/4660 gets fixed