From 3fbfea491d81664b40e3b73abcf0e8bba491ba82 Mon Sep 17 00:00:00 2001 From: Andrew Farkas <6060305+HactarCE@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:33:00 -0500 Subject: [PATCH] Support relative paths in LSP & DAP binaries (#42135) Closes #41214 Release Notes: - Added support for relative paths in LSP and DAP binaries --------- Co-authored-by: Cole Miller Co-authored-by: Julia Ryan --- crates/project/src/debugger/dap_store.rs | 5 +- crates/project/src/lsp_store.rs | 7 +-- crates/project/src/project_tests.rs | 67 ++++++++++++++++++++++++ crates/worktree/src/worktree.rs | 4 +- 4 files changed, 77 insertions(+), 6 deletions(-) diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 0b733aac29843090361cd5868799f6cb1db630f6..04901a5fef60cfc1692f712f3cdd4a3ec1071632 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -261,7 +261,10 @@ impl DapStore { .get(&adapter.name()); let user_installed_path = dap_settings.and_then(|s| match &s.binary { DapBinary::Default => None, - DapBinary::Custom(binary) => Some(PathBuf::from(binary)), + DapBinary::Custom(binary) => { + // if `binary` is absolute, `.join()` will keep it unmodified + Some(worktree.read(cx).abs_path().join(PathBuf::from(binary))) + } }); let user_args = dap_settings.map(|s| s.args.clone()); let user_env = dap_settings.map(|s| s.env.clone()); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 50f8c6695c188b065e89b4694e004470aa997abc..ecfe169b47b7daa1b1c8c0794d9cdde8f0b06ad4 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -563,8 +563,8 @@ impl LocalLspStore { allow_binary_download: bool, cx: &mut App, ) -> Task> { - if let Some(settings) = settings.binary.as_ref() - && settings.path.is_some() + if let Some(settings) = &settings.binary + && let Some(path) = settings.path.as_ref().map(PathBuf::from) { let settings = settings.clone(); @@ -573,7 +573,8 @@ impl LocalLspStore { env.extend(settings.env.unwrap_or_default()); Ok(LanguageServerBinary { - path: PathBuf::from(&settings.path.unwrap()), + // if `path` is absolute, `.join()` will keep it unmodified + path: delegate.worktree_root_path().join(path), env: Some(env), arguments: settings .arguments diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index c07ca96cd80a42500768a42a696b871f8c54bf04..332fdb3e0ffd158cfb0d4df199752b3ccddfb743 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1208,6 +1208,73 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_language_server_relative_path(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let settings_json_contents = json!({ + "languages": { + "Rust": { + "language_servers": ["my_fake_lsp"] + } + }, + "lsp": { + "my_fake_lsp": { + "binary": { + "path": path!("relative_path/to/my_fake_lsp_binary.exe").to_string(), + } + } + }, + }); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/the-root"), + json!({ + ".zed": { + "settings.json": settings_json_contents.to_string(), + }, + "relative_path": { + "to": { + "my_fake_lsp.exe": "", + }, + }, + "src": { + "main.rs": "", + } + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/the-root").as_ref()], cx).await; + 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( + "Rust", + FakeLspAdapter { + name: "my_fake_lsp", + ..Default::default() + }, + ); + + cx.run_until_parked(); + + // Start the language server by opening a buffer with a compatible file extension. + project + .update(cx, |project, cx| { + project.open_local_buffer_with_lsp(path!("/the-root/src/main.rs"), cx) + }) + .await + .unwrap(); + + let lsp_path = fake_rust_servers.next().await.unwrap().binary.path; + assert_eq!( + lsp_path.to_string_lossy(), + path!("/the-root/relative_path/to/my_fake_lsp_binary.exe"), + ); +} + #[gpui::test] async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) { init_test(cx); diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 8f6a1d23b82a272452ed90e635c3936f169d1404..69fee07583a33106689c463732fe6defbdcfbb40 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -2352,8 +2352,8 @@ impl Snapshot { self.entries_by_path.first() } - /// TODO: what's the difference between `root_dir` and `abs_path`? - /// is there any? if so, document it. + /// Returns `None` for a single file worktree, or `Some(self.abs_path())` if + /// it is a directory. pub fn root_dir(&self) -> Option> { self.root_entry() .filter(|entry| entry.is_dir())