diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index d0804834ae..ead3866e7f 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -147,7 +147,7 @@ use util::{ paths::{PathStyle, SanitizedPath, UrlExt}, post_inc, redact::redact_command, - rel_path::RelPath, + rel_path::{RelPath, RelPathBuf}, union_json_value_into, }; @@ -10272,15 +10272,6 @@ impl LspStore { let abs_path = abs_path .to_file_path_ext(path_style) .map_err(|()| anyhow!("can't convert URI to path"))?; - - let fs = lsp_store.update(cx, |lsp_store, _cx| { - lsp_store.as_local().map(|local| local.fs.clone()) - })?; - let abs_path = if let Some(fs) = fs { - fs.canonicalize(&abs_path).await.unwrap_or(abs_path) - } else { - abs_path - }; let p = abs_path.clone(); let yarn_worktree = lsp_store .update(cx, move |lsp_store, cx| match lsp_store.as_local() { @@ -10301,11 +10292,16 @@ impl LspStore { } else { (Arc::::from(abs_path.as_path()), None) }; - let worktree = lsp_store.update(cx, |lsp_store, cx| { - lsp_store.worktree_store.update(cx, |worktree_store, cx| { - worktree_store.find_worktree(&worktree_root_target, cx) - }) - })?; + let worktree = if known_relative_path.is_some() { + lsp_store.read_with(cx, |lsp_store, cx| { + lsp_store + .worktree_store + .read(cx) + .find_worktree(&worktree_root_target, cx) + })? + } else { + find_worktree_for_lsp_path(&lsp_store, &abs_path, cx).await? + }; let (worktree, relative_path, source_ws) = if let Some(result) = worktree { let relative_path = known_relative_path.unwrap_or_else(|| result.1.clone()); (result.0, relative_path, None) @@ -14948,6 +14944,101 @@ fn lsp_ranges_overlap(a: &lsp::Range, b: &lsp::Range) -> bool { a.start < b.end && b.start < a.end } +async fn find_worktree_for_lsp_path( + lsp_store: &WeakEntity, + abs_path: &Path, + cx: &mut AsyncApp, +) -> Result, Arc)>> { + let (fs, worktree) = lsp_store.read_with(cx, |lsp_store, cx| { + ( + lsp_store.as_local().map(|local| local.fs.clone()), + lsp_store + .worktree_store + .read(cx) + .find_worktree(abs_path, cx), + ) + })?; + match worktree { + Some((worktree, relative_path)) => { + let relative_path = + normalize_lsp_relative_path(&worktree, abs_path, relative_path, cx).await?; + Ok(Some((worktree, relative_path))) + } + None => { + let Some(fs) = fs else { + return Ok(None); + }; + let Ok(canonical_path) = fs.canonicalize(abs_path).await else { + return Ok(None); + }; + lsp_store.read_with(cx, |lsp_store, cx| { + lsp_store + .worktree_store + .read(cx) + .find_worktree(&canonical_path, cx) + }) + } + } +} + +async fn normalize_lsp_relative_path( + worktree: &Entity, + abs_path: &Path, + relative_path: Arc, + cx: &mut AsyncApp, +) -> Result> { + let Some((fs, snapshot, worktree_root)) = worktree.read_with(cx, |worktree, _| { + let local_worktree = worktree.as_local()?; + if local_worktree.fs_is_case_sensitive() { + return None; + } + Some(( + local_worktree.fs().clone(), + worktree.snapshot(), + worktree.abs_path(), + )) + }) else { + return Ok(relative_path); + }; + if snapshot.entry_for_path(&relative_path).is_some() { + return Ok(relative_path); + } + + let mut normalized_path = RelPathBuf::new(); + let mut components = relative_path.components(); + while let Some(component) = components.next() { + let mut exact_path = normalized_path.clone(); + exact_path.push_component(component)?; + if snapshot.entry_for_path(&exact_path).is_some() { + normalized_path = exact_path; + continue; + } + let case_insensitive_match = snapshot + .child_entries(&normalized_path) + .find(|entry| { + entry + .path + .file_name() + .is_some_and(|name| name.eq_ignore_ascii_case(component)) + }) + .map(|entry| entry.path.to_rel_path_buf()); + let Some(matched_path) = case_insensitive_match else { + if let Ok(canonical_path) = fs.canonicalize(abs_path).await + && let Ok(canonical_root) = fs.canonicalize(&worktree_root).await + && let Ok(canonical_relative_path) = canonical_path.strip_prefix(&canonical_root) + && let Ok(canonical_relative_path) = + RelPath::new(canonical_relative_path, PathStyle::local()) + { + return Ok(canonical_relative_path.into_arc()); + } + exact_path.push(components.rest()); + return Ok(Arc::from(exact_path)); + }; + normalized_path = matched_path; + } + Ok(Arc::from(normalized_path)) +} + fn subscribe_to_binary_statuses( languages: &Arc, cx: &mut Context<'_, LspStore>, diff --git a/crates/project/tests/integration/lsp_store.rs b/crates/project/tests/integration/lsp_store.rs index 2ce689b429..3db3f11211 100644 --- a/crates/project/tests/integration/lsp_store.rs +++ b/crates/project/tests/integration/lsp_store.rs @@ -1,15 +1,15 @@ use std::{ - path::Path, + path::{Path, PathBuf}, sync::Arc, time::{Duration, Instant}, }; use collections::HashMap; -use fs::FakeFs; +use fs::{FakeFs, Fs}; use futures::StreamExt; -use gpui::TestAppContext; -use language::{CodeLabel, FakeLspAdapter, HighlightId, rust_lang}; -use lsp::{LanguageServerName, Uri}; +use gpui::{Entity, TestAppContext}; +use language::{Buffer, CodeLabel, FakeLspAdapter, HighlightId, LocalFile, rust_lang}; +use lsp::{LanguageServerId, LanguageServerName, Uri}; use parking_lot::Mutex; use project::{ Project, @@ -163,6 +163,117 @@ async fn test_open_buffer_via_lsp_case_variant_no_duplicate(cx: &mut TestAppCont }); } +#[gpui::test] +async fn test_open_buffer_via_lsp_preserves_external_symlink_path(cx: &mut TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/shared"), + json!({ "pkg": { "def.rs": "pub fn def() {}" } }), + ) + .await; + fs.insert_tree( + path!("/project"), + json!({ "src": { "main.rs": "fn main() {}" } }), + ) + .await; + fs.create_symlink( + path!("/project/pkg").as_ref(), + PathBuf::from(path!("/shared/pkg")), + ) + .await + .unwrap(); + + let (project, server_id) = + project_with_rust_server(fs, path!("/project"), path!("/project/src/main.rs"), cx).await; + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer_via_lsp( + Uri::from_file_path(path!("/project/pkg/def.rs")).unwrap(), + server_id, + cx, + ) + }) + .await + .unwrap(); + cx.run_until_parked(); + + assert_eq!( + buffer_paths(&buffer, cx), + ( + "pkg/def.rs".to_string(), + PathBuf::from(path!("/project/pkg/def.rs")) + ) + ); + assert_eq!( + worktree_roots(&project, cx), + vec![PathBuf::from(path!("/project"))] + ); +} + +#[gpui::test] +async fn test_open_buffer_via_lsp_case_variant_in_unscanned_dir(cx: &mut TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + + let fs = FakeFs::new(cx.executor()); + fs.set_case_sensitive(false); + fs.insert_tree( + path!("/root"), + json!({ + ".gitignore": "ignored\n", + "src": { "main.rs": "fn main() {}" }, + "ignored": { "lib.rs": "pub fn lib() {}" }, + }), + ) + .await; + + let (project, server_id) = + project_with_rust_server(fs, path!("/root"), path!("/root/src/main.rs"), cx).await; + assert_eq!( + worktree_entries(&project, cx), + vec!["", ".gitignore", "ignored", "src", "src/main.rs"] + ); + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer_via_lsp( + Uri::from_file_path(path!("/root/IGNORED/LIB.rs")).unwrap(), + server_id, + cx, + ) + }) + .await + .unwrap(); + cx.run_until_parked(); + + assert_eq!( + buffer_paths(&buffer, cx), + ( + "ignored/lib.rs".to_string(), + PathBuf::from(path!("/root/ignored/lib.rs")) + ) + ); + assert_eq!( + worktree_roots(&project, cx), + vec![PathBuf::from(path!("/root"))] + ); + assert_eq!( + worktree_entries(&project, cx), + vec![ + "", + ".gitignore", + "ignored", + "ignored/lib.rs", + "src", + "src/main.rs" + ] + ); +} + #[test] fn test_rpc_log_grouping_separates_timed_messages() { for (received, direction) in [(false, "Send"), (true, "Receive")] { @@ -673,3 +784,63 @@ async fn test_initialization_options_contributions_without_own_options(cx: &mut Some(Some(contribution)), ); } + +async fn project_with_rust_server( + fs: Arc, + root: &str, + first_file: &str, + cx: &mut TestAppContext, +) -> (Entity, LanguageServerId) { + let project = Project::test(fs, [root.as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default()); + + project + .update(cx, |project, cx| { + project.open_local_buffer_with_lsp(first_file, cx) + }) + .await + .unwrap(); + fake_servers.next().await.unwrap(); + cx.run_until_parked(); + + let server_id = project.read_with(cx, |project, cx| { + project + .lsp_store() + .read(cx) + .language_server_statuses() + .next() + .unwrap() + .0 + }); + (project, server_id) +} + +fn buffer_paths(buffer: &Entity, cx: &TestAppContext) -> (String, PathBuf) { + buffer.read_with(cx, |buffer, cx| { + let file = File::from_dyn(buffer.file()).unwrap(); + (file.path.as_unix_str().to_string(), file.abs_path(cx)) + }) +} + +fn worktree_roots(project: &Entity, cx: &TestAppContext) -> Vec { + project.read_with(cx, |project, cx| { + project + .worktrees(cx) + .map(|worktree| worktree.read(cx).abs_path().to_path_buf()) + .collect() + }) +} + +fn worktree_entries(project: &Entity, cx: &TestAppContext) -> Vec { + project.read_with(cx, |project, cx| { + let worktree = project.worktrees(cx).next().unwrap(); + worktree + .read(cx) + .snapshot() + .entries(true, 0) + .map(|entry| entry.path.as_unix_str().to_string()) + .collect() + }) +}