settings: Add tilde expansion support for LSP binary path (#41715)

Mayank Verma created

Closes #38227

Release Notes:

- Added tilde expansion support for LSP binary path in `settings.json`

Change summary

crates/project/src/project_tests.rs | 61 +++++++++++++++++++++++++++++++
crates/worktree/src/worktree.rs     | 15 ++++++-
2 files changed, 73 insertions(+), 3 deletions(-)

Detailed changes

crates/project/src/project_tests.rs 🔗

@@ -1292,6 +1292,67 @@ async fn test_language_server_relative_path(cx: &mut gpui::TestAppContext) {
     assert_eq!(lsp_path.to_string_lossy(), path!("lsp_on_path.exe"));
 }
 
+#[gpui::test]
+async fn test_language_server_tilde_path(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
+    let settings_json_contents = json!({
+        "languages": {
+            "Rust": {
+                "language_servers": ["tilde_lsp"]
+            }
+        },
+        "lsp": {
+            "tilde_lsp": {
+                "binary": {
+                    "path": "~/.local/bin/rust-analyzer",
+                }
+            }
+        },
+    });
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        path!("/root"),
+        json!({
+            ".zed": {
+                "settings.json": settings_json_contents.to_string(),
+            },
+            "src": {
+                "main.rs": "fn main() {}",
+            }
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+    language_registry.add(rust_lang());
+
+    let mut tilde_lsp = language_registry.register_fake_lsp(
+        "Rust",
+        FakeLspAdapter {
+            name: "tilde_lsp",
+            ..Default::default()
+        },
+    );
+    cx.run_until_parked();
+
+    project
+        .update(cx, |project, cx| {
+            project.open_local_buffer_with_lsp(path!("/root/src/main.rs"), cx)
+        })
+        .await
+        .unwrap();
+
+    let lsp_path = tilde_lsp.next().await.unwrap().binary.path;
+    let expected_path = paths::home_dir().join(".local/bin/rust-analyzer");
+    assert_eq!(
+        lsp_path, expected_path,
+        "Tilde path should expand to home directory"
+    );
+}
+
 #[gpui::test]
 async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) {
     init_test(cx);

crates/worktree/src/worktree.rs 🔗

@@ -2386,16 +2386,25 @@ impl Snapshot {
 
     /// Resolves a path to an executable using the following heuristics:
     ///
-    /// 1. If the path is relative and contains more than one component,
+    /// 1. If the path starts with `~`, it is expanded to the user's home directory.
+    /// 2. 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
+    /// 3. 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.
+    /// 4. 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 Some(path_str) = path.to_str() {
+            if let Some(remaining_path) = path_str.strip_prefix("~/") {
+                return home_dir().join(remaining_path);
+            } else if path_str == "~" {
+                return home_dir().to_path_buf();
+            }
+        }
+
         if let Ok(rel_path) = RelPath::new(&path, self.path_style)
             && (path.components().count() > 1 || self.entry_for_path(&rel_path).is_some())
         {