@@ -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",
@@ -0,0 +1,121 @@
+use anyhow::{anyhow, Result};
+use std::path::Path;
+
+#[derive(Default)]
+pub struct LspGlobSet {
+ patterns: Vec<glob::Pattern>,
+}
+
+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()));
+ }
+}
@@ -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<Language>,
adapter: Arc<CachedLspAdapter>,
server: Arc<LanguageServer>,
+ watched_paths: LspGlobSet,
simulate_disk_based_diagnostics_completion: Option<Task<()>>,
},
}
@@ -2046,8 +2049,26 @@ impl Project {
})
.detach();
language_server
- .on_request::<lsp::request::RegisterCapability, _, _>(|_, _| async {
- Ok(())
+ .on_request::<lsp::request::RegisterCapability, _, _>({
+ 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<Self>,
+ ) {
+ 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<Self>,
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::notification::DidChangeWatchedFiles>(
- 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::<lsp::notification::DidChangeWatchedFiles>(params)
+ .log_err();
+ }
}
}
}
@@ -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::request::RegisterCapability>(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::<lsp::notification::DidChangeWatchedFiles, _>({
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(),