From 92e64f9cf0e0ee11b631a7e5e5b9aa18fa2f4e75 Mon Sep 17 00:00:00 2001 From: Mayank Verma Date: Thu, 13 Nov 2025 19:44:18 +0530 Subject: [PATCH] settings: Add tilde expansion support for LSP binary path (#41715) Closes #38227 Release Notes: - Added tilde expansion support for LSP binary path in `settings.json` --- crates/project/src/project_tests.rs | 61 +++++++++++++++++++++++++++++ crates/worktree/src/worktree.rs | 15 +++++-- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 02c0e42c10..eb953cb325 100644 --- a/crates/project/src/project_tests.rs +++ b/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); diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 7b412e187f..1e8c1648dc 100644 --- a/crates/worktree/src/worktree.rs +++ b/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()) {