diff --git a/crates/ide-dap/src/adapter.rs b/crates/ide-dap/src/adapter.rs index 91901492c..458a38eef 100644 --- a/crates/ide-dap/src/adapter.rs +++ b/crates/ide-dap/src/adapter.rs @@ -196,10 +196,18 @@ fn managed_dirs(root: &Path) -> Vec { dirs } +/// The `PATH` an adapter runs with, unless its launch sets one: the search path language servers +/// get, so what it starts in turn (js-debug's wrapper runs `node`, Delve runs `go build`) is +/// found like Run finds it, even when the login shell's `PATH` didn't reach the app. +fn adapter_path(env: &[(String, String)]) -> Option { + let own = env.iter().any(|(k, _)| if cfg!(windows) { k.eq_ignore_ascii_case("PATH") } else { k == "PATH" }); + (!own).then(ide_core::tool_path::search_path) +} + /// Directories searched for adapter binaries: managed tools first, then `PATH` and the usual install spots. fn search_dirs() -> Vec { let mut dirs: Vec = tools_root().map(|r| managed_dirs(&r)).unwrap_or_default(); - dirs.extend(std::env::var_os("PATH").map(|p| std::env::split_paths(&p).collect::>()).unwrap_or_default()); + dirs.extend(std::env::split_paths(&ide_core::tool_path::search_path())); if let Some(home) = std::env::var_os("HOME").map(PathBuf::from) { for rel in [".cargo/bin", ".local/bin", "go/bin", ".dotnet/tools", ".bun/bin", ".npm-global/bin", ".volta/bin"] { @@ -464,6 +472,9 @@ impl AdapterLauncher for ProcessLauncher { None => args, }; let mut cmd = Command::new(&binary); + if let Some(path) = adapter_path(&spec.env) { + cmd.env("PATH", path); + } ide_core::process::prepare(cmd.as_std_mut()); cmd.args(&args) .envs(spec.env.iter().map(|(k, v)| (k, v))) @@ -592,6 +603,21 @@ async fn connect_with_retries( mod tests { use super::*; + #[test] + fn adapters_run_on_the_tool_search_path() { + let search = ide_core::tool_path::search_path(); + assert_eq!(adapter_path(&[]), Some(search.clone())); + assert_eq!(adapter_path(&[("GOFLAGS".into(), "-mod=mod".into())]), Some(search)); + // A launch that sets its own (a selected Go SDK's bin first) keeps it. + assert_eq!(adapter_path(&[("PATH".into(), "/sdk/go/bin:/usr/bin".into())]), None); + // The search path reaches the usual install spots even when this process's `PATH` doesn't. + #[cfg(unix)] + { + let dirs: Vec = std::env::split_paths(&ide_core::tool_path::search_path()).collect(); + assert!(dirs.contains(&PathBuf::from("/usr/local/bin")), "{dirs:?}"); + } + } + #[tokio::test] async fn program_output_is_forwarded_without_the_adapter_banner() { let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();