From 3ff5aee4a1038763a29ff873baa7a13124570339 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 23 Mar 2023 15:27:43 -0700 Subject: [PATCH] Respect LSP servers watch glob patterns --- Cargo.lock | 5 +- crates/project/Cargo.toml | 1 + crates/project/src/lsp_glob_set.rs | 121 ++++++++++++++++++++++++++++ crates/project/src/project.rs | 86 ++++++++++++++++---- crates/project/src/project_tests.rs | 26 +++++- 5 files changed, 218 insertions(+), 21 deletions(-) create mode 100644 crates/project/src/lsp_glob_set.rs diff --git a/Cargo.lock b/Cargo.lock index ed799521d6fdd93e0a10e45233c72f610d7ba399..108dd2c2e7e6b477668ae914c4a85a4ec85bd882 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2591,9 +2591,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" @@ -4633,6 +4633,7 @@ dependencies = [ "futures 0.3.25", "fuzzy", "git", + "glob", "gpui", "ignore", "language", diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 2f73daa338656d0606b278472797f1b6d427d69c..b42a6fc674d230adeed763a4de8f10310d0b19e8 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -27,6 +27,7 @@ fs = { path = "../fs" } fsevent = { path = "../fsevent" } fuzzy = { path = "../fuzzy" } git = { path = "../git" } +glob = { version = "0.3.1" } gpui = { path = "../gpui" } language = { path = "../language" } lsp = { path = "../lsp" } diff --git a/crates/project/src/lsp_glob_set.rs b/crates/project/src/lsp_glob_set.rs new file mode 100644 index 0000000000000000000000000000000000000000..daac344a0a8fb4396da802ddc6f7325ffd47ea9f --- /dev/null +++ b/crates/project/src/lsp_glob_set.rs @@ -0,0 +1,121 @@ +use anyhow::{anyhow, Result}; +use std::path::Path; + +#[derive(Default)] +pub struct LspGlobSet { + patterns: Vec, +} + +impl LspGlobSet { + pub fn clear(&mut self) { + self.patterns.clear(); + } + + /// Add a pattern to the glob set. + /// + /// LSP's glob syntax supports bash-style brace expansion. For example, + /// the pattern '*.{js,ts}' would match all JavaScript or TypeScript files. + /// This is not a part of the standard libc glob syntax, and isn't supported + /// by the `glob` crate. So we pre-process the glob patterns, producing a + /// separate glob `Pattern` object for each part of a brace expansion. + pub fn add_pattern(&mut self, pattern: &str) -> Result<()> { + // Find all of the ranges of `pattern` that contain matched curly braces. + let mut expansion_ranges = Vec::new(); + let mut expansion_start_ix = None; + for (ix, c) in pattern.match_indices(|c| ['{', '}'].contains(&c)) { + match c { + "{" => { + if expansion_start_ix.is_some() { + return Err(anyhow!("nested braces in glob patterns aren't supported")); + } + expansion_start_ix = Some(ix); + } + "}" => { + if let Some(start_ix) = expansion_start_ix { + expansion_ranges.push(start_ix..ix + 1); + } + expansion_start_ix = None; + } + _ => {} + } + } + + // Starting with a single pattern, process each brace expansion by cloning + // the pattern once per element of the expansion. + let mut unexpanded_patterns = vec![]; + let mut expanded_patterns = vec![pattern.to_string()]; + + for outer_range in expansion_ranges.into_iter().rev() { + let inner_range = (outer_range.start + 1)..(outer_range.end - 1); + std::mem::swap(&mut unexpanded_patterns, &mut expanded_patterns); + for unexpanded_pattern in unexpanded_patterns.drain(..) { + for part in unexpanded_pattern[inner_range.clone()].split(',') { + let mut expanded_pattern = unexpanded_pattern.clone(); + expanded_pattern.replace_range(outer_range.clone(), part); + expanded_patterns.push(expanded_pattern); + } + } + } + + // Parse the final glob patterns and add them to the set. + for pattern in expanded_patterns { + let pattern = glob::Pattern::new(&pattern)?; + self.patterns.push(pattern); + } + + Ok(()) + } + + pub fn matches(&self, path: &Path) -> bool { + self.patterns + .iter() + .any(|pattern| pattern.matches_path(path)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_glob_set() { + let mut watch = LspGlobSet::default(); + watch.add_pattern("/a/**/*.rs").unwrap(); + watch.add_pattern("/a/**/Cargo.toml").unwrap(); + + assert!(watch.matches("/a/b.rs".as_ref())); + assert!(watch.matches("/a/b/c.rs".as_ref())); + + assert!(!watch.matches("/b/c.rs".as_ref())); + assert!(!watch.matches("/a/b.ts".as_ref())); + } + + #[test] + fn test_brace_expansion() { + let mut watch = LspGlobSet::default(); + watch.add_pattern("/a/*.{ts,js,tsx}").unwrap(); + + assert!(watch.matches("/a/one.js".as_ref())); + assert!(watch.matches("/a/two.ts".as_ref())); + assert!(watch.matches("/a/three.tsx".as_ref())); + + assert!(!watch.matches("/a/one.j".as_ref())); + assert!(!watch.matches("/a/two.s".as_ref())); + assert!(!watch.matches("/a/three.t".as_ref())); + assert!(!watch.matches("/a/four.t".as_ref())); + assert!(!watch.matches("/a/five.xt".as_ref())); + } + + #[test] + fn test_multiple_brace_expansion() { + let mut watch = LspGlobSet::default(); + watch.add_pattern("/a/{one,two,three}.{b*c,d*e}").unwrap(); + + assert!(watch.matches("/a/one.bic".as_ref())); + assert!(watch.matches("/a/two.dole".as_ref())); + assert!(watch.matches("/a/three.deeee".as_ref())); + + assert!(!watch.matches("/a/four.bic".as_ref())); + assert!(!watch.matches("/a/one.be".as_ref())); + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0c992486fa369e19f9752419e181e77fe8fcb02a..fedfa0c863f299ccc00be3a55ca664c7f8342877 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,5 +1,6 @@ mod ignore; mod lsp_command; +mod lsp_glob_set; pub mod search; pub mod terminals; pub mod worktree; @@ -33,10 +34,11 @@ use language::{ Transaction, Unclipped, }; use lsp::{ - DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer, LanguageString, - MarkedString, + DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, + DocumentHighlightKind, LanguageServer, LanguageString, MarkedString, }; use lsp_command::*; +use lsp_glob_set::LspGlobSet; use postage::watch; use rand::prelude::*; use search::SearchQuery; @@ -188,6 +190,7 @@ pub enum LanguageServerState { language: Arc, adapter: Arc, server: Arc, + watched_paths: LspGlobSet, simulate_disk_based_diagnostics_completion: Option>, }, } @@ -2046,8 +2049,26 @@ impl Project { }) .detach(); language_server - .on_request::(|_, _| async { - Ok(()) + .on_request::({ + let this = this.downgrade(); + move |params, mut cx| async move { + let this = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("project dropped"))?; + for reg in params.registrations { + if reg.method == "workspace/didChangeWatchedFiles" { + if let Some(options) = reg.register_options { + let options = serde_json::from_value(options)?; + this.update(&mut cx, |this, cx| { + this.on_lsp_did_change_watched_files( + server_id, options, cx, + ); + }); + } + } + } + Ok(()) + } }) .detach(); @@ -2117,6 +2138,7 @@ impl Project { LanguageServerState::Running { adapter: adapter.clone(), language, + watched_paths: Default::default(), server: language_server.clone(), simulate_disk_based_diagnostics_completion: None, }, @@ -2509,6 +2531,23 @@ impl Project { } } + fn on_lsp_did_change_watched_files( + &mut self, + language_server_id: usize, + params: DidChangeWatchedFilesRegistrationOptions, + cx: &mut ModelContext, + ) { + if let Some(LanguageServerState::Running { watched_paths, .. }) = + self.language_servers.get_mut(&language_server_id) + { + watched_paths.clear(); + for watcher in params.watchers { + watched_paths.add_pattern(&watcher.glob_pattern).log_err(); + } + cx.notify(); + } + } + async fn on_lsp_workspace_edit( this: WeakModelHandle, params: lsp::ApplyWorkspaceEditParams, @@ -4592,15 +4631,20 @@ impl Project { for ((server_worktree_id, _), server_id) in &self.language_server_ids { if *server_worktree_id == worktree_id { if let Some(server) = self.language_servers.get(server_id) { - if let LanguageServerState::Running { server, .. } = server { - server - .notify::( - lsp::DidChangeWatchedFilesParams { - changes: changes - .iter() - .map(|(path, change)| lsp::FileEvent { - uri: lsp::Url::from_file_path(abs_path.join(path)) - .unwrap(), + if let LanguageServerState::Running { + server, + watched_paths, + .. + } = server + { + let params = lsp::DidChangeWatchedFilesParams { + changes: changes + .iter() + .filter_map(|(path, change)| { + let path = abs_path.join(path); + if watched_paths.matches(&path) { + Some(lsp::FileEvent { + uri: lsp::Url::from_file_path(path).unwrap(), typ: match change { PathChange::Added => lsp::FileChangeType::CREATED, PathChange::Removed => lsp::FileChangeType::DELETED, @@ -4610,10 +4654,18 @@ impl Project { } }, }) - .collect(), - }, - ) - .log_err(); + } else { + None + } + }) + .collect(), + }; + + if !params.changes.is_empty() { + server + .notify::(params) + .log_err(); + } } } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 360e4d0c896d52f7b97c5466b97717ce28f9f087..023ce3c5bc4af7ec83e527311c337f52f3efef67 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -493,6 +493,24 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon // Keep track of the FS events reported to the language server. let fake_server = fake_servers.next().await.unwrap(); let file_changes = Arc::new(Mutex::new(Vec::new())); + fake_server + .request::(lsp::RegistrationParams { + registrations: vec![lsp::Registration { + id: Default::default(), + method: "workspace/didChangeWatchedFiles".to_string(), + register_options: serde_json::to_value( + lsp::DidChangeWatchedFilesRegistrationOptions { + watchers: vec![lsp::FileSystemWatcher { + glob_pattern: "*.{rs,c}".to_string(), + kind: None, + }], + }, + ) + .ok(), + }], + }) + .await + .unwrap(); fake_server.handle_notification::({ let file_changes = file_changes.clone(); move |params, _| { @@ -505,15 +523,19 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon cx.foreground().run_until_parked(); assert_eq!(file_changes.lock().len(), 0); - // Perform some file system mutations. + // Perform some file system mutations, two of which match the watched patterns, + // and one of which does not. fs.create_file("/the-root/c.rs".as_ref(), Default::default()) .await .unwrap(); + fs.create_file("/the-root/d.txt".as_ref(), Default::default()) + .await + .unwrap(); fs.remove_file("/the-root/b.rs".as_ref(), Default::default()) .await .unwrap(); - // The language server receives events for both FS mutations. + // The language server receives events for the FS mutations that match its watch patterns. cx.foreground().run_until_parked(); assert_eq!( &*file_changes.lock(),