lsp: Use Path instead of String for path handling (#22762)
Some checks are pending
CI / Check Postgres and Protobuf migrations, mergability (push) Waiting to run
CI / Check formatting and spelling (push) Waiting to run
CI / (macOS) Run Clippy and tests (push) Waiting to run
CI / (Linux) Run Clippy and tests (push) Waiting to run
CI / (Linux) Build Remote Server (push) Waiting to run
CI / (Windows) Run Clippy and tests (push) Waiting to run
CI / Create a macOS bundle (push) Blocked by required conditions
CI / Linux x86_x64 release bundle (push) Blocked by required conditions
CI / Linux arm64 release bundle (push) Blocked by required conditions
CI / Auto release preview (push) Blocked by required conditions

During my work on PR #22616, while trying to fix the
`test_reporting_fs_changes_to_language_servers` test case, I noticed
that we are currently handling paths using `String` in some places.
However, this approach causes issues on Windows.

This draft PR modifies `rebuild_watched_paths_inner` and
`glob_literal_prefix`. For example, take the `glob_literal_prefix`
function modified in this PR:

```rust
assert_eq!(
    glob_literal_prefix("node_modules/**/*.js"), 
    "node_modules"
);    // This works on Unix, fails on Windows

assert_eq!(
    glob_literal_prefix("node_modules\\**\\*.js"), 
    "node_modules"
);    // This works on Windows

assert_eq!(
    glob_literal_prefix("node_modules\\**/*.js"), 
    "node_modules"
);    // This fails on Windows
```

The current implementation treats path as `String` and relies on `\` as
the path separator on Windows, but on Windows, both `/` and `\` can be
used as separators. This means that `node_modules\**/*.js` is also a
valid path representation.

There are two potential solutions to this issue:

1. **Continue handling paths with `String`**, and on Windows, replace
all `/` with `\`.
2. **Use `Path` for path handling**, which is the solution implemented
in this PR.

### Advantages of Solution 1:
- Simple and direct.

### Advantages of Solution 2:
- More robust, especially in handling `strip_prefix`.

Currently, the logic for removing a path prefix looks like this:

```rust
let path = "/some/path/to/file.rs";
let parent = "/some/path/to";
// remove prefix
let file = path.strip_prefix(parent).unwrap();    // which is `/file.rs`
let file = file.strip_prefix("/").unwrap();
```

However, using `Path` simplifies this process and makes it more robust:

```rust
let path = Path::new("C:/path/to/src/main.rs");
let parent = Path::new("C:/path/to/src"); 
let file = path.strip_prefix(&parent).unwrap(); // which is `main.rs`

let path = Path::new("C:\\path\\to/src/main.rs");
let parent = Path::new("C:/path/to\\src\\"); 
let file = path.strip_prefix(&parent).unwrap(); // which is `main.rs`
```

Release Notes:

- N/A

---------

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
This commit is contained in:
张小白 2025-01-20 03:07:39 +08:00 committed by GitHub
parent 7578834d77
commit 7302be8ebd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 198 additions and 150 deletions

View file

@ -79,7 +79,8 @@ use std::{
}; };
use text::{Anchor, BufferId, LineEnding, OffsetRangeExt}; use text::{Anchor, BufferId, LineEnding, OffsetRangeExt};
use util::{ use util::{
debug_panic, defer, maybe, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _, debug_panic, defer, maybe, merge_json_value_into, paths::SanitizedPath, post_inc, ResultExt,
TryFutureExt as _,
}; };
pub use fs::*; pub use fs::*;
@ -2546,112 +2547,119 @@ impl LocalLspStore {
let mut found_host = false; let mut found_host = false;
for worktree in &worktrees { for worktree in &worktrees {
let glob_is_inside_worktree = worktree.update(cx, |tree, _| { let glob_is_inside_worktree = worktree.update(cx, |tree, _| {
if let Some(worktree_root_path) = tree.abs_path().to_str() { let worktree_root_path = tree.abs_path();
let path_to_watch = match &watcher.glob_pattern { let path_to_watch = match &watcher.glob_pattern {
lsp::GlobPattern::String(s) => { lsp::GlobPattern::String(s) => {
match s.strip_prefix(worktree_root_path) { let watcher_path = SanitizedPath::from(s);
Some(relative) => { match watcher_path.as_path().strip_prefix(&worktree_root_path) {
let pattern = relative Ok(relative) => {
.strip_prefix(std::path::MAIN_SEPARATOR) let pattern = relative.to_string_lossy().to_string();
.unwrap_or(relative) let literal_prefix = glob_literal_prefix(relative).into();
.to_owned();
let literal_prefix = glob_literal_prefix(&pattern);
let literal_prefix = Arc::from(PathBuf::from( PathToWatch::Worktree {
literal_prefix literal_prefix,
.strip_prefix(std::path::MAIN_SEPARATOR) pattern,
.unwrap_or(literal_prefix),
));
PathToWatch::Worktree {
literal_prefix,
pattern,
}
}
None => {
let path = glob_literal_prefix(s);
let glob = &s[path.len()..];
let pattern = glob
.strip_prefix(std::path::MAIN_SEPARATOR)
.unwrap_or(glob)
.to_owned();
let path = if Path::new(path).components().next().is_none()
{
Arc::from(Path::new(worktree_root_path))
} else {
PathBuf::from(path).into()
};
PathToWatch::Absolute { path, pattern }
} }
} }
} Err(_) => {
lsp::GlobPattern::Relative(rp) => { let path = glob_literal_prefix(watcher_path.as_path());
let Ok(mut base_uri) = match &rp.base_uri { let pattern = watcher_path
lsp::OneOf::Left(workspace_folder) => &workspace_folder.uri, .as_path()
lsp::OneOf::Right(base_uri) => base_uri, .strip_prefix(&path)
} .map(|p| p.to_string_lossy().to_string())
.to_file_path() else { .unwrap_or_else(|e| {
return false; debug_panic!(
}; "Failed to strip prefix for string pattern: {}, with prefix: {}, with error: {}",
s,
path.display(),
e
);
watcher_path.as_path().to_string_lossy().to_string()
});
let path = if path.components().next().is_none() {
worktree_root_path.clone()
} else {
path.into()
};
match base_uri.strip_prefix(worktree_root_path) { PathToWatch::Absolute { path, pattern }
Ok(relative) => {
let mut literal_prefix = relative.to_owned();
literal_prefix.push(glob_literal_prefix(&rp.pattern));
PathToWatch::Worktree {
literal_prefix: literal_prefix.into(),
pattern: rp.pattern.clone(),
}
}
Err(_) => {
let path = glob_literal_prefix(&rp.pattern);
let glob = &rp.pattern[path.len()..];
let pattern = glob
.strip_prefix(std::path::MAIN_SEPARATOR)
.unwrap_or(glob)
.to_owned();
base_uri.push(path);
let path = if base_uri.components().next().is_none() {
Arc::from(Path::new("/"))
} else {
base_uri.into()
};
PathToWatch::Absolute { path, pattern }
}
}
}
};
match path_to_watch {
PathToWatch::Worktree {
literal_prefix,
pattern,
} => {
if let Some((tree, glob)) =
tree.as_local_mut().zip(Glob::new(&pattern).log_err())
{
tree.add_path_prefix_to_scan(literal_prefix);
worktree_globs
.entry(tree.id())
.or_insert_with(GlobSetBuilder::new)
.add(glob);
} else {
return false;
}
}
PathToWatch::Absolute { path, pattern } => {
if let Some(glob) = Glob::new(&pattern).log_err() {
abs_globs
.entry(path)
.or_insert_with(GlobSetBuilder::new)
.add(glob);
} }
} }
} }
return true; lsp::GlobPattern::Relative(rp) => {
let Ok(mut base_uri) = match &rp.base_uri {
lsp::OneOf::Left(workspace_folder) => &workspace_folder.uri,
lsp::OneOf::Right(base_uri) => base_uri,
}
.to_file_path() else {
return false;
};
match base_uri.strip_prefix(&worktree_root_path) {
Ok(relative) => {
let mut literal_prefix = relative.to_owned();
literal_prefix
.push(glob_literal_prefix(Path::new(&rp.pattern)));
PathToWatch::Worktree {
literal_prefix: literal_prefix.into(),
pattern: rp.pattern.clone(),
}
}
Err(_) => {
let path = glob_literal_prefix(Path::new(&rp.pattern));
let pattern = Path::new(&rp.pattern)
.strip_prefix(&path)
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|e| {
debug_panic!(
"Failed to strip prefix for relative pattern: {}, with prefix: {}, with error: {}",
rp.pattern,
path.display(),
e
);
rp.pattern.clone()
});
base_uri.push(path);
let path = if base_uri.components().next().is_none() {
debug_panic!("base_uri is empty, {}", base_uri.display());
worktree_root_path.clone()
} else {
base_uri.into()
};
PathToWatch::Absolute { path, pattern }
}
}
}
};
match path_to_watch {
PathToWatch::Worktree {
literal_prefix,
pattern,
} => {
if let Some((tree, glob)) =
tree.as_local_mut().zip(Glob::new(&pattern).log_err())
{
tree.add_path_prefix_to_scan(literal_prefix);
worktree_globs
.entry(tree.id())
.or_insert_with(GlobSetBuilder::new)
.add(glob);
} else {
return false;
}
}
PathToWatch::Absolute { path, pattern } => {
if let Some(glob) = Glob::new(&pattern).log_err() {
abs_globs
.entry(path)
.or_insert_with(GlobSetBuilder::new)
.add(glob);
}
}
} }
false true
}); });
if glob_is_inside_worktree { if glob_is_inside_worktree {
log::trace!( log::trace!(
@ -8319,23 +8327,13 @@ impl DiagnosticSummary {
} }
} }
fn glob_literal_prefix(glob: &str) -> &str { fn glob_literal_prefix(glob: &Path) -> PathBuf {
let is_absolute = glob.starts_with(path::MAIN_SEPARATOR); glob.components()
.take_while(|component| match component {
let mut literal_end = is_absolute as usize; path::Component::Normal(part) => !part.to_string_lossy().contains(['*', '?', '{', '}']),
for (i, part) in glob.split(path::MAIN_SEPARATOR).enumerate() { _ => true,
if part.contains(['*', '?', '{', '}']) { })
break; .collect()
} else {
if i > 0 {
// Account for separator prior to this part
literal_end += path::MAIN_SEPARATOR.len_utf8();
}
literal_end += part.len();
}
}
let literal_end = literal_end.min(glob.len());
&glob[..literal_end]
} }
pub struct SshLspAdapter { pub struct SshLspAdapter {
@ -8715,8 +8713,34 @@ fn include_text(server: &lsp::LanguageServer) -> Option<bool> {
#[cfg(test)] #[cfg(test)]
#[test] #[test]
fn test_glob_literal_prefix() { fn test_glob_literal_prefix() {
assert_eq!(glob_literal_prefix("**/*.js"), ""); assert_eq!(glob_literal_prefix(Path::new("**/*.js")), Path::new(""));
assert_eq!(glob_literal_prefix("node_modules/**/*.js"), "node_modules"); assert_eq!(
assert_eq!(glob_literal_prefix("foo/{bar,baz}.js"), "foo"); glob_literal_prefix(Path::new("node_modules/**/*.js")),
assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js"); Path::new("node_modules")
);
assert_eq!(
glob_literal_prefix(Path::new("foo/{bar,baz}.js")),
Path::new("foo")
);
assert_eq!(
glob_literal_prefix(Path::new("foo/bar/baz.js")),
Path::new("foo/bar/baz.js")
);
#[cfg(target_os = "windows")]
{
assert_eq!(glob_literal_prefix(Path::new("**\\*.js")), Path::new(""));
assert_eq!(
glob_literal_prefix(Path::new("node_modules\\**/*.js")),
Path::new("node_modules")
);
assert_eq!(
glob_literal_prefix(Path::new("foo/{bar,baz}.js")),
Path::new("foo")
);
assert_eq!(
glob_literal_prefix(Path::new("foo\\bar\\baz.js")),
Path::new("foo/bar/baz.js")
);
}
} }

View file

@ -781,11 +781,19 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) { async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) {
fn add_root_for_windows(path: &str) -> String {
if cfg!(windows) {
format!("C:{}", path)
} else {
path.to_string()
}
}
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/the-root", add_root_for_windows("/the-root"),
json!({ json!({
".gitignore": "target\n", ".gitignore": "target\n",
"src": { "src": {
@ -813,7 +821,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
) )
.await; .await;
let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; let project = Project::test(fs.clone(), [add_root_for_windows("/the-root").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone()); let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang()); language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp( let mut fake_servers = language_registry.register_fake_lsp(
@ -829,7 +837,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
// Start the language server by opening a buffer with a compatible file extension. // Start the language server by opening a buffer with a compatible file extension.
let _ = project let _ = project
.update(cx, |project, cx| { .update(cx, |project, cx| {
project.open_local_buffer_with_lsp("/the-root/src/a.rs", cx) project.open_local_buffer_with_lsp(add_root_for_windows("/the-root/src/a.rs"), cx)
}) })
.await .await
.unwrap(); .unwrap();
@ -869,21 +877,21 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
lsp::DidChangeWatchedFilesRegistrationOptions { lsp::DidChangeWatchedFilesRegistrationOptions {
watchers: vec![ watchers: vec![
lsp::FileSystemWatcher { lsp::FileSystemWatcher {
glob_pattern: lsp::GlobPattern::String( glob_pattern: lsp::GlobPattern::String(add_root_for_windows(
"/the-root/Cargo.toml".to_string(), "/the-root/Cargo.toml",
), )),
kind: None, kind: None,
}, },
lsp::FileSystemWatcher { lsp::FileSystemWatcher {
glob_pattern: lsp::GlobPattern::String( glob_pattern: lsp::GlobPattern::String(add_root_for_windows(
"/the-root/src/*.{rs,c}".to_string(), "/the-root/src/*.{rs,c}",
), )),
kind: None, kind: None,
}, },
lsp::FileSystemWatcher { lsp::FileSystemWatcher {
glob_pattern: lsp::GlobPattern::String( glob_pattern: lsp::GlobPattern::String(add_root_for_windows(
"/the-root/target/y/**/*.rs".to_string(), "/the-root/target/y/**/*.rs",
), )),
kind: None, kind: None,
}, },
], ],
@ -936,21 +944,36 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
// Perform some file system mutations, two of which match the watched patterns, // Perform some file system mutations, two of which match the watched patterns,
// and one of which does not. // and one of which does not.
fs.create_file("/the-root/src/c.rs".as_ref(), Default::default()) fs.create_file(
.await add_root_for_windows("/the-root/src/c.rs").as_ref(),
.unwrap(); Default::default(),
fs.create_file("/the-root/src/d.txt".as_ref(), Default::default()) )
.await .await
.unwrap(); .unwrap();
fs.remove_file("/the-root/src/b.rs".as_ref(), Default::default()) fs.create_file(
.await add_root_for_windows("/the-root/src/d.txt").as_ref(),
.unwrap(); Default::default(),
fs.create_file("/the-root/target/x/out/x2.rs".as_ref(), Default::default()) )
.await .await
.unwrap(); .unwrap();
fs.create_file("/the-root/target/y/out/y2.rs".as_ref(), Default::default()) fs.remove_file(
.await add_root_for_windows("/the-root/src/b.rs").as_ref(),
.unwrap(); Default::default(),
)
.await
.unwrap();
fs.create_file(
add_root_for_windows("/the-root/target/x/out/x2.rs").as_ref(),
Default::default(),
)
.await
.unwrap();
fs.create_file(
add_root_for_windows("/the-root/target/y/out/y2.rs").as_ref(),
Default::default(),
)
.await
.unwrap();
// The language server receives events for the FS mutations that match its watch patterns. // The language server receives events for the FS mutations that match its watch patterns.
cx.executor().run_until_parked(); cx.executor().run_until_parked();
@ -958,15 +981,16 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
&*file_changes.lock(), &*file_changes.lock(),
&[ &[
lsp::FileEvent { lsp::FileEvent {
uri: lsp::Url::from_file_path("/the-root/src/b.rs").unwrap(), uri: lsp::Url::from_file_path(add_root_for_windows("/the-root/src/b.rs")).unwrap(),
typ: lsp::FileChangeType::DELETED, typ: lsp::FileChangeType::DELETED,
}, },
lsp::FileEvent { lsp::FileEvent {
uri: lsp::Url::from_file_path("/the-root/src/c.rs").unwrap(), uri: lsp::Url::from_file_path(add_root_for_windows("/the-root/src/c.rs")).unwrap(),
typ: lsp::FileChangeType::CREATED, typ: lsp::FileChangeType::CREATED,
}, },
lsp::FileEvent { lsp::FileEvent {
uri: lsp::Url::from_file_path("/the-root/target/y/out/y2.rs").unwrap(), uri: lsp::Url::from_file_path(add_root_for_windows("/the-root/target/y/out/y2.rs"))
.unwrap(),
typ: lsp::FileChangeType::CREATED, typ: lsp::FileChangeType::CREATED,
}, },
] ]