Support relative paths in LSP & DAP binaries (#42135)

Andrew Farkas , Cole Miller , and Julia Ryan created

Closes #41214

Release Notes:

- Added support for relative paths in LSP and DAP binaries

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>

Change summary

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(-)

Detailed changes

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());

crates/project/src/lsp_store.rs 🔗

@@ -563,8 +563,8 @@ impl LocalLspStore {
         allow_binary_download: bool,
         cx: &mut App,
     ) -> Task<Result<LanguageServerBinary>> {
-        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

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);

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<Arc<Path>> {
         self.root_entry()
             .filter(|entry| entry.is_dir())