1use crate::Result;
2use crate::pitchfork_toml::WatchMode;
3use globset::{GlobBuilder, GlobMatcher};
4use itertools::Itertools;
5use miette::IntoDiagnostic;
6use notify::event::ModifyKind;
7use notify::{Config, EventKind, PollWatcher, RecommendedWatcher, RecursiveMode};
8use notify_debouncer_full::{DebounceEventResult, Debouncer, NoCache, new_debouncer_opt};
9use std::collections::HashMap;
10use std::path::{Component, Path, PathBuf};
11use std::time::Duration;
12
13#[derive(Debug)]
15pub struct WatchEvents {
16 pub paths: Vec<PathBuf>,
18 pub created: Vec<PathBuf>,
21}
22
23pub struct WatchFiles {
24 pub rx: tokio::sync::mpsc::Receiver<WatchEvents>,
25 backend: WatchFilesBackend,
26}
27
28enum WatchFilesBackend {
34 Native(Debouncer<RecommendedWatcher, NoCache>),
35 Poll(Debouncer<PollWatcher, NoCache>),
36}
37
38impl WatchFiles {
39 pub fn new(duration: Duration, mode: WatchMode, poll_interval: Duration) -> Result<Self> {
40 let h = tokio::runtime::Handle::current();
41 let (tx, rx) = tokio::sync::mpsc::channel(256);
42 let make_callback = |tx: tokio::sync::mpsc::Sender<WatchEvents>,
43 h: tokio::runtime::Handle| {
44 move |res: DebounceEventResult| {
45 let Ok(ev) = res else { return };
46 let mut paths = vec![];
47 let mut created = vec![];
48 for e in ev.iter().filter(|e| {
49 matches!(
50 e.kind,
51 EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
52 )
53 }) {
54 paths.extend(e.paths.iter().cloned());
55 if matches!(
56 e.kind,
57 EventKind::Create(_) | EventKind::Modify(ModifyKind::Name(_))
58 ) {
59 created.extend(e.paths.iter().cloned());
60 }
61 }
62 if paths.is_empty() {
63 return;
64 }
65 let events = WatchEvents {
66 paths: paths.into_iter().unique().collect(),
67 created: created.into_iter().unique().collect(),
68 };
69 let tx = tx.clone();
70 h.spawn(async move {
71 let _ = tx.send(events).await;
73 });
74 }
75 };
76
77 let backend = match mode {
78 WatchMode::Native => WatchFilesBackend::Native(
79 new_debouncer_opt(
80 duration,
81 None,
82 make_callback(tx.clone(), h.clone()),
83 NoCache::new(),
84 Config::default(),
85 )
86 .into_diagnostic()?,
87 ),
88 WatchMode::Poll => WatchFilesBackend::Poll(
89 new_debouncer_opt(
90 duration,
91 None,
92 make_callback(tx.clone(), h.clone()),
93 NoCache::new(),
94 Config::default().with_poll_interval(poll_interval),
95 )
96 .into_diagnostic()?,
97 ),
98 WatchMode::Auto => {
99 return Err(miette::miette!(
100 "WatchMode::Auto must not be passed directly to WatchFiles::new; \
101 the caller must resolve auto to native or poll"
102 ));
103 }
104 };
105
106 Ok(Self { backend, rx })
107 }
108
109 pub fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> {
110 match &mut self.backend {
111 WatchFilesBackend::Native(debouncer) => {
112 debouncer.watch(path, recursive_mode).into_diagnostic()
113 }
114 WatchFilesBackend::Poll(debouncer) => {
115 debouncer.watch(path, recursive_mode).into_diagnostic()
116 }
117 }
118 }
119
120 pub fn unwatch(&mut self, path: &Path) -> Result<()> {
121 match &mut self.backend {
122 WatchFilesBackend::Native(debouncer) => debouncer.unwatch(path).into_diagnostic(),
123 WatchFilesBackend::Poll(debouncer) => debouncer.unwatch(path).into_diagnostic(),
124 }
125 }
126}
127
128pub fn watched_entries(dir: &Path, mode: RecursiveMode) -> Vec<PathBuf> {
131 let mut paths = vec![];
132 collect_entries(dir, mode, &mut paths);
133 paths
134}
135
136fn collect_entries(dir: &Path, mode: RecursiveMode, paths: &mut Vec<PathBuf>) {
137 let Ok(entries) = std::fs::read_dir(dir) else {
138 return;
139 };
140 for entry in entries.flatten() {
141 let path = entry.path();
142 if mode == RecursiveMode::Recursive && entry.file_type().is_ok_and(|t| t.is_dir()) {
143 collect_entries(&path, mode, paths);
144 }
145 paths.push(path);
146 }
147}
148
149fn normalize_watch_path(path: &Path) -> PathBuf {
160 match path.canonicalize() {
161 Ok(p) => {
162 #[cfg(windows)]
163 {
164 strip_verbatim_prefix(&p)
165 }
166 #[cfg(not(windows))]
167 {
168 p
169 }
170 }
171 Err(_) => {
172 if path.is_absolute() {
173 path.to_path_buf()
174 } else {
175 crate::env::CWD.join(path)
176 }
177 }
178 }
179}
180
181#[cfg(windows)]
184fn strip_verbatim_prefix(path: &Path) -> PathBuf {
185 let s = path.to_string_lossy();
186 if let Some(rest) = s.strip_prefix(r"\\?\") {
187 if let Some(unc) = rest.strip_prefix(r"UNC\") {
188 PathBuf::from(format!(r"\\{}", unc))
189 } else {
190 PathBuf::from(rest)
191 }
192 } else {
193 path.to_path_buf()
194 }
195}
196
197pub fn expand_watch_patterns(
207 patterns: &[String],
208 base_dir: &Path,
209) -> HashMap<PathBuf, RecursiveMode> {
210 let mut targets = HashMap::new();
211 for pattern in patterns {
212 for alt in relative_alternatives(pattern) {
213 for (dir, mode) in watch_targets_for_pattern(&alt, base_dir) {
214 insert_watch_target(&mut targets, normalize_watch_path(&dir), mode);
215 }
216 }
217 }
218 targets
219}
220
221fn relative_alternatives(pattern: &str) -> Vec<String> {
226 let path = Path::new(pattern);
227 if path.is_absolute() || path.has_root() {
228 return expand_braces(pattern);
229 }
230 expand_braces(pattern)
231 .into_iter()
232 .map(|alt| alt.trim_start_matches(['/', '\\']).to_string())
233 .filter(|alt| !Path::new(alt).is_absolute())
235 .collect()
236}
237
238fn expand_braces(pattern: &str) -> Vec<String> {
242 const MAX_ALTERNATIVES: usize = 64;
243 let mut expanded = vec![];
244 let mut pending = vec![pattern.to_string()];
245 while let Some(p) = pending.pop() {
246 match first_brace_group(&p) {
247 Some((start, end, alternatives)) => {
248 for alt in alternatives {
249 pending.push(format!("{}{alt}{}", &p[..start], &p[end + 1..]));
250 }
251 }
252 None => expanded.push(p),
253 }
254 if expanded.len() + pending.len() > MAX_ALTERNATIVES {
255 return vec![pattern.to_string()];
256 }
257 }
258 expanded
259}
260
261fn first_brace_group(pattern: &str) -> Option<(usize, usize, Vec<&str>)> {
264 let bytes = pattern.as_bytes();
265 let (mut start, mut alt_start, mut depth) = (0, 0, 0);
266 let mut in_class = false;
267 let mut alternatives = vec![];
268 let mut i = 0;
269 while i < bytes.len() {
270 match bytes[i] {
271 b'\\' if cfg!(not(windows)) => i += 1,
273 b']' if in_class => in_class = false,
274 _ if in_class => {}
275 b'[' => in_class = true,
276 b'{' => {
277 if depth == 0 {
278 start = i;
279 alt_start = i + 1;
280 }
281 depth += 1;
282 }
283 b',' if depth == 1 => {
284 alternatives.push(&pattern[alt_start..i]);
285 alt_start = i + 1;
286 }
287 b'}' if depth > 0 => {
288 depth -= 1;
289 if depth == 0 {
290 alternatives.push(&pattern[alt_start..i]);
291 return Some((start, i, alternatives));
292 }
293 }
294 _ => {}
295 }
296 i += 1;
297 }
298 None
299}
300
301pub fn insert_watch_target(
303 targets: &mut HashMap<PathBuf, RecursiveMode>,
304 dir: PathBuf,
305 mode: RecursiveMode,
306) {
307 let entry = targets.entry(dir).or_insert(mode);
308 if mode == RecursiveMode::Recursive {
309 *entry = RecursiveMode::Recursive;
310 }
311}
312
313fn watch_targets_for_pattern(pattern: &str, base_dir: &Path) -> Vec<(PathBuf, RecursiveMode)> {
314 let pattern = pattern.strip_prefix("./").unwrap_or(pattern);
316 let full_path = base_dir.join(pattern);
317 let mut components = full_path.components().collect_vec();
318 let Some(file_part) = components.pop() else {
319 return vec![];
320 };
321
322 let mut dir_parts = components.into_iter().peekable();
324 let mut literal_dir = PathBuf::new();
325 while let Some(part) = dir_parts.next_if(|c| !is_glob_component(c)) {
326 literal_dir.push(part);
327 }
328 if !literal_dir.is_dir() {
329 return literal_dir
332 .ancestors()
333 .find(|p| p.is_dir())
334 .map(|p| vec![(p.to_path_buf(), RecursiveMode::NonRecursive)])
335 .unwrap_or_default();
336 }
337
338 let mut targets = vec![];
339 let mut current = vec![literal_dir];
340 for part in dir_parts {
341 let part = part.as_os_str().to_string_lossy();
342 if part == "**" {
344 targets.extend(current.into_iter().map(|d| (d, RecursiveMode::Recursive)));
345 return targets;
346 }
347 let Some(matcher) = component_matcher(&part) else {
348 let full_pattern = normalize_path_for_glob(&full_path.to_string_lossy());
349 match GlobBuilder::new(&full_pattern).build() {
350 Ok(_) => targets.extend(current.into_iter().map(|d| (d, RecursiveMode::Recursive))),
353 Err(e) => log::warn!("Invalid glob pattern '{pattern}': {e}"),
355 }
356 return targets;
357 };
358 targets.extend(
360 current
361 .iter()
362 .map(|d| (d.clone(), RecursiveMode::NonRecursive)),
363 );
364 current = current
365 .iter()
366 .flat_map(|d| matching_subdirs(d, &matcher))
367 .collect();
368 }
369
370 let mode = if file_part.as_os_str() == "**" {
371 RecursiveMode::Recursive
372 } else {
373 RecursiveMode::NonRecursive
374 };
375 targets.extend(current.into_iter().map(|d| (d, mode)));
376 targets
377}
378
379fn is_glob_component(component: &Component) -> bool {
380 component
381 .as_os_str()
382 .to_string_lossy()
383 .contains(['*', '?', '[', '{'])
384}
385
386fn component_matcher(component: &str) -> Option<GlobMatcher> {
389 GlobBuilder::new(component)
390 .case_insensitive(cfg!(target_os = "windows"))
391 .literal_separator(true)
392 .build()
393 .ok()
394 .map(|glob| glob.compile_matcher())
395}
396
397fn matching_subdirs(dir: &Path, matcher: &GlobMatcher) -> Vec<PathBuf> {
398 let Ok(entries) = std::fs::read_dir(dir) else {
399 return vec![];
400 };
401 entries
402 .flatten()
403 .filter(|e| matcher.is_match(e.file_name()))
404 .map(|e| e.path())
405 .filter(|p| p.is_dir())
406 .collect()
407}
408
409fn normalize_path_for_glob(path: &str) -> String {
420 if let Some(rest) = path.strip_prefix(r"\\?\UNC\") {
421 format!("//{}", rest.replace('\\', "/"))
422 } else {
423 path.strip_prefix(r"\\?\")
424 .unwrap_or(path)
425 .replace('\\', "/")
426 }
427}
428
429pub fn path_matches_patterns(changed_path: &Path, patterns: &[String], base_dir: &Path) -> bool {
432 let changed_path_str = normalize_path_for_glob(&changed_path.to_string_lossy());
434
435 for pattern in patterns.iter().flat_map(|p| relative_alternatives(p)) {
437 let normalized_pattern = pattern.strip_prefix("./").unwrap_or(&pattern);
439
440 let full_pattern = if Path::new(normalized_pattern).is_absolute() {
442 normalize_path_for_glob(normalized_pattern)
443 } else {
444 normalize_path_for_glob(&base_dir.join(normalized_pattern).to_string_lossy())
445 };
446
447 let glob = globset::GlobBuilder::new(&full_pattern)
449 .case_insensitive(cfg!(target_os = "windows"))
450 .literal_separator(true) .build();
452
453 if let Ok(glob) = glob {
454 let matcher = glob.compile_matcher();
455 if matcher.is_match(&changed_path_str) {
456 return true;
457 }
458 }
459 }
460 false
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466 use std::fs;
467 use tempfile::TempDir;
468
469 #[test]
470 fn test_normalize_watch_path_existing_directory() {
471 let temp_dir = TempDir::new().unwrap();
472 let dir_path = temp_dir.path().join("test_dir");
473 fs::create_dir(&dir_path).unwrap();
474
475 let normalized = normalize_watch_path(&dir_path);
477 assert!(normalized.is_absolute());
478 assert!(normalized.exists());
479 }
480
481 #[test]
482 fn test_normalize_watch_path_nonexistent_path() {
483 #[cfg(unix)]
487 let path = PathBuf::from("/nonexistent/path/to/dir");
488 #[cfg(windows)]
489 let path = PathBuf::from(r"C:\nonexistent\path\to\dir");
490
491 let normalized = normalize_watch_path(&path);
493 assert_eq!(normalized, path);
494 }
495
496 #[test]
497 fn test_normalize_watch_path_deduplication() {
498 let temp_dir = TempDir::new().unwrap();
499 let dir_path = temp_dir.path().join("test_dir");
500 fs::create_dir(&dir_path).unwrap();
501
502 let subdir = dir_path.join("subdir");
504 fs::create_dir(&subdir).unwrap();
505
506 let path1 = subdir.clone();
509 let path2 = subdir.join("..").join("subdir");
510
511 let normalized1 = normalize_watch_path(&path1);
512 let normalized2 = normalize_watch_path(&path2);
513
514 assert_eq!(normalized1, normalized2);
516 }
517
518 fn canon(path: &Path) -> PathBuf {
519 normalize_watch_path(path)
520 }
521
522 fn expand(patterns: &[&str], base_dir: &Path) -> HashMap<PathBuf, RecursiveMode> {
523 let patterns = patterns.iter().map(|p| p.to_string()).collect_vec();
524 expand_watch_patterns(&patterns, base_dir)
525 }
526
527 #[test]
528 fn test_expand_watch_patterns_specific_file() {
529 let temp_dir = TempDir::new().unwrap();
530 let base_dir = temp_dir.path();
531 fs::write(base_dir.join("package.json"), "{}").unwrap();
532 fs::create_dir_all(base_dir.join("node_modules/a/b")).unwrap();
534
535 let dirs = expand(&["package.json"], base_dir);
536
537 assert_eq!(
538 dirs,
539 HashMap::from([(canon(base_dir), RecursiveMode::NonRecursive)])
540 );
541 }
542
543 #[test]
544 fn test_expand_watch_patterns_recursive_glob() {
545 let temp_dir = TempDir::new().unwrap();
546 let base_dir = temp_dir.path();
547 let subdir = base_dir.join("src");
548 fs::create_dir_all(subdir.join("nested")).unwrap();
549 fs::write(subdir.join("file1.rs"), "").unwrap();
550 fs::write(subdir.join("nested/file2.rs"), "").unwrap();
551
552 let dirs = expand(&["src/**/*.rs"], base_dir);
553
554 assert_eq!(
555 dirs,
556 HashMap::from([(canon(&subdir), RecursiveMode::Recursive)])
557 );
558 }
559
560 #[test]
561 fn test_expand_watch_patterns_single_level_glob() {
562 let temp_dir = TempDir::new().unwrap();
563 let base_dir = temp_dir.path();
564 fs::create_dir_all(base_dir.join("config/nested")).unwrap();
565
566 let dirs = expand(&["config/*.toml"], base_dir);
567
568 assert_eq!(
569 dirs,
570 HashMap::from([(canon(&base_dir.join("config")), RecursiveMode::NonRecursive)])
571 );
572 }
573
574 #[test]
575 fn test_expand_watch_patterns_glob_directory_component() {
576 let temp_dir = TempDir::new().unwrap();
577 let base_dir = temp_dir.path();
578 fs::create_dir_all(base_dir.join("crates/a/deep")).unwrap();
579 fs::create_dir_all(base_dir.join("crates/b")).unwrap();
580 fs::write(base_dir.join("crates/file.toml"), "").unwrap();
581
582 let dirs = expand(&["crates/*/Cargo.toml"], base_dir);
583
584 assert_eq!(
586 dirs,
587 HashMap::from([
588 (canon(&base_dir.join("crates")), RecursiveMode::NonRecursive),
589 (
590 canon(&base_dir.join("crates/a")),
591 RecursiveMode::NonRecursive
592 ),
593 (
594 canon(&base_dir.join("crates/b")),
595 RecursiveMode::NonRecursive
596 ),
597 ])
598 );
599 }
600
601 #[test]
602 fn test_expand_watch_patterns_embedded_double_star() {
603 let temp_dir = TempDir::new().unwrap();
604 let base_dir = temp_dir.path();
605 fs::create_dir_all(base_dir.join("src/a")).unwrap();
606 fs::create_dir_all(base_dir.join("src/b")).unwrap();
607
608 let dirs = expand(&["src/foo**bar.rs", "sr**/x.rs"], base_dir);
610
611 assert_eq!(
612 dirs,
613 HashMap::from([
614 (canon(base_dir), RecursiveMode::NonRecursive),
615 (canon(&base_dir.join("src")), RecursiveMode::NonRecursive),
616 ])
617 );
618 }
619
620 #[test]
621 fn test_expand_watch_patterns_trailing_double_star() {
622 let temp_dir = TempDir::new().unwrap();
623 let base_dir = temp_dir.path();
624 fs::create_dir(base_dir.join("src")).unwrap();
625
626 let dirs = expand(&["src/**"], base_dir);
627
628 assert_eq!(
629 dirs,
630 HashMap::from([(canon(&base_dir.join("src")), RecursiveMode::Recursive)])
631 );
632 }
633
634 #[test]
635 fn test_expand_watch_patterns_alternatives_with_separator() {
636 let temp_dir = TempDir::new().unwrap();
637 let base_dir = temp_dir.path();
638 fs::create_dir_all(base_dir.join("src/api")).unwrap();
639 fs::create_dir(base_dir.join("lib")).unwrap();
640
641 let dirs = expand(&["{src/api,lib}/*.rs"], base_dir);
643 assert_eq!(
644 dirs,
645 HashMap::from([
646 (
647 canon(&base_dir.join("src/api")),
648 RecursiveMode::NonRecursive
649 ),
650 (canon(&base_dir.join("lib")), RecursiveMode::NonRecursive),
651 ])
652 );
653 assert!(path_matches_patterns(
654 &base_dir.join("src/api/main.rs"),
655 &["{src/api,lib}/*.rs".to_string()],
656 base_dir
657 ));
658
659 let dirs = expand(&["[a/b]/*.rs"], base_dir);
662 assert_eq!(
663 dirs,
664 HashMap::from([(canon(base_dir), RecursiveMode::Recursive)])
665 );
666
667 assert!(expand(&["[z-a]/*.rs"], base_dir).is_empty());
669 }
670
671 #[test]
672 fn test_expand_braces() {
673 let mut expanded = expand_braces("{src/{a,b},lib}/*.{rs,toml}");
674 expanded.sort();
675 assert_eq!(
676 expanded,
677 [
678 "lib/*.rs",
679 "lib/*.toml",
680 "src/a/*.rs",
681 "src/a/*.toml",
682 "src/b/*.rs",
683 "src/b/*.toml",
684 ]
685 );
686
687 assert_eq!(expand_braces("[{]x}/*.rs"), ["[{]x}/*.rs"]);
689 #[cfg(unix)]
690 assert_eq!(expand_braces(r"\{a,b}.rs"), [r"\{a,b}.rs"]);
691
692 let many = "{a,b,c,d,e}/{a,b,c,d,e}/{a,b,c}/*.rs";
694 assert_eq!(expand_braces(many), [many]);
695 }
696
697 #[test]
698 fn test_expand_watch_patterns_alternatives_stay_relative() {
699 let temp_dir = TempDir::new().unwrap();
700 let base_dir = temp_dir.path();
701 fs::create_dir_all(base_dir.join("src/x")).unwrap();
702
703 let dirs = expand(&["{src,}/**/*.rs", "{/src/x,lib}/*.rs"], base_dir);
705
706 assert_eq!(
707 dirs,
708 HashMap::from([
709 (canon(base_dir), RecursiveMode::Recursive),
710 (canon(&base_dir.join("src")), RecursiveMode::Recursive),
711 (canon(&base_dir.join("src/x")), RecursiveMode::NonRecursive),
712 ])
713 );
714 }
715
716 #[test]
717 fn test_relative_alternatives() {
718 assert_eq!(relative_alternatives("{src,}/*.rs"), ["*.rs", "src/*.rs"]);
719 assert_eq!(relative_alternatives("/{a,b}.rs"), ["/b.rs", "/a.rs"]);
721 #[cfg(windows)]
722 assert_eq!(relative_alternatives(r"\src\main.rs"), [r"\src\main.rs"]);
723 }
724
725 #[test]
726 fn test_path_matches_patterns_alternatives_stay_relative() {
727 let temp_dir = TempDir::new().unwrap();
728 let base_dir = temp_dir.path();
729 let patterns = ["{/src/x,lib}/*.rs".to_string()];
730
731 assert!(path_matches_patterns(
733 &base_dir.join("src/x/main.rs"),
734 &patterns,
735 base_dir
736 ));
737 assert!(path_matches_patterns(
738 &base_dir.join("lib/main.rs"),
739 &patterns,
740 base_dir
741 ));
742 assert!(!path_matches_patterns(
743 &base_dir.join("src/main.rs"),
744 &patterns,
745 base_dir
746 ));
747 }
748
749 #[test]
750 fn test_expand_watch_patterns_recursive_wins() {
751 let temp_dir = TempDir::new().unwrap();
752 let base_dir = temp_dir.path();
753 fs::create_dir(base_dir.join("src")).unwrap();
754
755 let dirs = expand(&["src/main.rs", "src/**/*.rs"], base_dir);
756
757 assert_eq!(
758 dirs,
759 HashMap::from([(canon(&base_dir.join("src")), RecursiveMode::Recursive)])
760 );
761 }
762
763 #[test]
764 fn test_expand_watch_patterns_nonexistent_file() {
765 let temp_dir = TempDir::new().unwrap();
766 let base_dir = temp_dir.path();
767
768 let dirs = expand(&["config.toml"], base_dir);
770
771 assert_eq!(
772 dirs,
773 HashMap::from([(canon(base_dir), RecursiveMode::NonRecursive)])
774 );
775 }
776
777 #[test]
778 fn test_expand_watch_patterns_nonexistent_directory() {
779 let temp_dir = TempDir::new().unwrap();
780 let base_dir = temp_dir.path();
781 fs::create_dir(base_dir.join("config")).unwrap();
782
783 let dirs = expand(&["config/app/*.toml", "lib/**/*.ts"], base_dir);
785
786 assert_eq!(
787 dirs,
788 HashMap::from([
789 (canon(base_dir), RecursiveMode::NonRecursive),
790 (canon(&base_dir.join("config")), RecursiveMode::NonRecursive),
791 ])
792 );
793 }
794
795 #[test]
796 fn test_watched_entries() {
797 let temp_dir = TempDir::new().unwrap();
798 let dir = temp_dir.path().join("new");
799 fs::create_dir_all(dir.join("nested")).unwrap();
800 fs::write(dir.join("a.toml"), "").unwrap();
801 fs::write(dir.join("nested/b.toml"), "").unwrap();
802
803 let mut paths = watched_entries(&dir, RecursiveMode::NonRecursive);
804 paths.sort();
805 assert_eq!(paths, vec![dir.join("a.toml"), dir.join("nested")]);
806
807 let mut paths = watched_entries(&dir, RecursiveMode::Recursive);
808 paths.sort();
809 assert_eq!(
810 paths,
811 vec![
812 dir.join("a.toml"),
813 dir.join("nested"),
814 dir.join("nested/b.toml"),
815 ]
816 );
817 }
818
819 #[test]
820 fn test_path_matches_patterns_simple() {
821 let temp_dir = TempDir::new().unwrap();
822 let base_dir = temp_dir.path();
823
824 let test_txt = base_dir.join("test.txt");
826 let test_rs = base_dir.join("test.rs");
827 fs::write(&test_txt, "").unwrap();
828 fs::write(&test_rs, "").unwrap();
829
830 assert!(path_matches_patterns(
832 &test_txt,
833 &["*.txt".to_string()],
834 base_dir
835 ));
836
837 assert!(!path_matches_patterns(
839 &test_rs,
840 &["*.txt".to_string()],
841 base_dir
842 ));
843 }
844
845 #[test]
846 fn test_path_matches_patterns_recursive_glob() {
847 let temp_dir = TempDir::new().unwrap();
848 let base_dir = temp_dir.path();
849 let src_dir = base_dir.join("src");
850 let deep_dir = src_dir.join("deep");
851 fs::create_dir_all(&deep_dir).unwrap();
852
853 let deep_file = deep_dir.join("file.rs");
855 let src_file = src_dir.join("file.rs");
856 fs::write(&deep_file, "").unwrap();
857 fs::write(&src_file, "").unwrap();
858
859 assert!(path_matches_patterns(
861 &deep_file,
862 &["src/**/*.rs".to_string()],
863 base_dir
864 ));
865
866 assert!(path_matches_patterns(
868 &src_file,
869 &["src/**/*.rs".to_string()],
870 base_dir
871 ));
872 }
873
874 #[test]
875 fn test_path_matches_patterns_multiple_patterns() {
876 let temp_dir = TempDir::new().unwrap();
877 let base_dir = temp_dir.path();
878
879 let cargo_toml = base_dir.join("Cargo.toml");
881 let main_rs = base_dir.join("main.rs");
882 let readme_md = base_dir.join("README.md");
883 fs::write(&cargo_toml, "").unwrap();
884 fs::write(&main_rs, "").unwrap();
885 fs::write(&readme_md, "").unwrap();
886
887 let patterns = vec!["*.rs".to_string(), "*.toml".to_string()];
889 assert!(path_matches_patterns(&cargo_toml, &patterns, base_dir));
890 assert!(path_matches_patterns(&main_rs, &patterns, base_dir));
891 assert!(!path_matches_patterns(&readme_md, &patterns, base_dir));
892 }
893
894 #[test]
895 fn test_path_matches_patterns_relative_prefix() {
896 let temp_dir = TempDir::new().unwrap();
897 let base_dir = temp_dir.path();
898
899 let test_file = base_dir.join("config.json");
901 fs::write(&test_file, "{}").unwrap();
902
903 assert!(path_matches_patterns(
905 &test_file,
906 &["./config.json".to_string()],
907 base_dir
908 ));
909
910 assert!(path_matches_patterns(
912 &test_file,
913 &["config.json".to_string()],
914 base_dir
915 ));
916 }
917
918 #[test]
919 fn test_expand_watch_patterns_relative_prefix() {
920 let temp_dir = TempDir::new().unwrap();
921 let base_dir = temp_dir.path();
922
923 let test_file = base_dir.join("config.json");
925 fs::write(&test_file, "{}").unwrap();
926
927 let dirs = expand(&["./config.json"], base_dir);
929
930 assert_eq!(
931 dirs,
932 HashMap::from([(canon(base_dir), RecursiveMode::NonRecursive)])
933 );
934 }
935}