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())); } }