diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 2a2f870d6b55abc57a14e623375f77b9fb2d5dbc..ac94378c9cc1ae300f9dcbd5a088f25761f309b4 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -291,6 +291,7 @@ pub trait LspAdapterDelegate: Send + Sync { fn http_client(&self) -> Arc; fn worktree_id(&self) -> WorktreeId; fn worktree_root_path(&self) -> &Path; + fn resolve_executable_path(&self, path: PathBuf) -> PathBuf; fn update_status(&self, language: LanguageServerName, status: BinaryStatus); fn registered_lsp_adapters(&self) -> Vec>; async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option>; diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 04901a5fef60cfc1692f712f3cdd4a3ec1071632..a82286441d625561009f4f9259f5c06fe424ff10 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -262,8 +262,8 @@ impl DapStore { let user_installed_path = dap_settings.and_then(|s| match &s.binary { DapBinary::Default => None, DapBinary::Custom(binary) => { - // if `binary` is absolute, `.join()` will keep it unmodified - Some(worktree.read(cx).abs_path().join(PathBuf::from(binary))) + let path = PathBuf::from(binary); + Some(worktree.read(cx).resolve_executable_path(path)) } }); let user_args = dap_settings.map(|s| s.args.clone()); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 358bf164d9a26c58f1bbf1bd5829184f6d86e7e4..cae4d64c67d3261f59d87273a38865992da18284 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -573,8 +573,7 @@ impl LocalLspStore { env.extend(settings.env.unwrap_or_default()); Ok(LanguageServerBinary { - // if `path` is absolute, `.join()` will keep it unmodified - path: delegate.worktree_root_path().join(path), + path: delegate.resolve_executable_path(path), env: Some(env), arguments: settings .arguments @@ -13516,6 +13515,10 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate { self.worktree.abs_path().as_ref() } + fn resolve_executable_path(&self, path: PathBuf) -> PathBuf { + self.worktree.resolve_executable_path(path) + } + async fn shell_env(&self) -> HashMap { let task = self.load_shell_env_task.clone(); task.await.unwrap_or_default() diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 332fdb3e0ffd158cfb0d4df199752b3ccddfb743..02c0e42c10f06006fa5b61a549684e2bb336f509 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1215,13 +1215,20 @@ async fn test_language_server_relative_path(cx: &mut gpui::TestAppContext) { let settings_json_contents = json!({ "languages": { "Rust": { - "language_servers": ["my_fake_lsp"] + "language_servers": ["my_fake_lsp", "lsp_on_path"] } }, "lsp": { "my_fake_lsp": { "binary": { - "path": path!("relative_path/to/my_fake_lsp_binary.exe").to_string(), + // file exists, so this is treated as a relative path + "path": path!(".relative_path/to/my_fake_lsp_binary.exe").to_string(), + } + }, + "lsp_on_path": { + "binary": { + // file doesn't exist, so it will fall back on PATH env var + "path": path!("lsp_on_path.exe").to_string(), } } }, @@ -1234,7 +1241,7 @@ async fn test_language_server_relative_path(cx: &mut gpui::TestAppContext) { ".zed": { "settings.json": settings_json_contents.to_string(), }, - "relative_path": { + ".relative_path": { "to": { "my_fake_lsp.exe": "", }, @@ -1250,13 +1257,20 @@ async fn test_language_server_relative_path(cx: &mut gpui::TestAppContext) { let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); - let mut fake_rust_servers = language_registry.register_fake_lsp( + let mut my_fake_lsp = language_registry.register_fake_lsp( "Rust", FakeLspAdapter { name: "my_fake_lsp", ..Default::default() }, ); + let mut lsp_on_path = language_registry.register_fake_lsp( + "Rust", + FakeLspAdapter { + name: "lsp_on_path", + ..Default::default() + }, + ); cx.run_until_parked(); @@ -1268,11 +1282,14 @@ async fn test_language_server_relative_path(cx: &mut gpui::TestAppContext) { .await .unwrap(); - let lsp_path = fake_rust_servers.next().await.unwrap().binary.path; + let lsp_path = my_fake_lsp.next().await.unwrap().binary.path; assert_eq!( lsp_path.to_string_lossy(), - path!("/the-root/relative_path/to/my_fake_lsp_binary.exe"), + path!("/the-root/.relative_path/to/my_fake_lsp_binary.exe"), ); + + let lsp_path = lsp_on_path.next().await.unwrap().binary.path; + assert_eq!(lsp_path.to_string_lossy(), path!("lsp_on_path.exe")); } #[gpui::test] diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 69fee07583a33106689c463732fe6defbdcfbb40..7b412e187f0d2cab5c34800309525a16201a83c0 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -2384,6 +2384,27 @@ impl Snapshot { }) } + /// Resolves a path to an executable using the following heuristics: + /// + /// 1. If the path is relative and contains more than one component, + /// it is joined to the worktree root path. + /// 2. If the path is relative and exists in the worktree + /// (even if falls under an exclusion filter), + /// it is joined to the worktree root path. + /// 3. Otherwise the path is returned unmodified. + /// + /// Relative paths that do not exist in the worktree may + /// still be found using the `PATH` environment variable. + pub fn resolve_executable_path(&self, path: PathBuf) -> PathBuf { + if let Ok(rel_path) = RelPath::new(&path, self.path_style) + && (path.components().count() > 1 || self.entry_for_path(&rel_path).is_some()) + { + self.abs_path().join(path) + } else { + path + } + } + pub fn entry_for_id(&self, id: ProjectEntryId) -> Option<&Entry> { let entry = self.entries_by_id.get(&id, ())?; self.entry_for_path(&entry.path)