Stop canonicalizing LSP locations before worktree lookup (#63754)
Closes https://github.com/zed-industries/zed/pull/61392#issuecomment-5538772188 Follow-up to https://github.com/zed-industries/zed/pull/61392 https://github.com/user-attachments/assets/6c745474-6b6e-4cfa-b1b6-87e9631973bf Not every language server wants to deal with the canonicalized paths, some (e.g. gopls) can resolve them themselves, and keep sending symlinked paths over the LSP — canonicalizing them means we will break out of the current worktree and open odd singleton files instead. Release Notes: - Fixed goToDefinition opening out-of-worktree entries
This commit is contained in:
1 parent
5889a27ff2
commit
5d1147a6e4
2 files changed
+282
-20
No files matched your search
+106
-15
@@ -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::<Path>::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<LspStore>,
|
||||
abs_path: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Option<(Entity<Worktree>, Arc<RelPath>)>> {
|
||||
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<Worktree>,
|
||||
abs_path: &Path,
|
||||
relative_path: Arc<RelPath>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Arc<RelPath>> {
|
||||
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<LanguageRegistry>,
|
||||
cx: &mut Context<'_, LspStore>,
|
||||
|
||||
@@ -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<FakeFs>,
|
||||
root: &str,
|
||||
first_file: &str,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<Project>, 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<Buffer>, 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<Project>, cx: &TestAppContext) -> Vec<PathBuf> {
|
||||
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<Project>, cx: &TestAppContext) -> Vec<String> {
|
||||
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()
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user