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 rooted = base_dir.join(pattern);
320 let (mut literal_dir, mut components) = if Path::new(pattern).has_root() {
321 (PathBuf::new(), rooted.components().collect_vec())
322 } else {
323 (
324 base_dir.to_path_buf(),
325 Path::new(pattern).components().collect_vec(),
326 )
327 };
328 let Some(file_part) = components.pop() else {
329 return vec![];
330 };
331
332 let mut dir_parts = components.into_iter().peekable();
334 while let Some(part) = dir_parts.next_if(|c| !is_glob_component(c)) {
335 literal_dir.push(part);
336 }
337 if !literal_dir.is_dir() {
338 return literal_dir
341 .ancestors()
342 .find(|p| p.is_dir())
343 .map(|p| vec![(p.to_path_buf(), RecursiveMode::NonRecursive)])
344 .unwrap_or_default();
345 }
346
347 let mut targets = vec![];
348 let mut current = vec![literal_dir];
349 for part in dir_parts {
350 let part = part.as_os_str().to_string_lossy();
351 if part == "**" {
353 targets.extend(current.into_iter().map(|d| (d, RecursiveMode::Recursive)));
354 return targets;
355 }
356 let Some(matcher) = component_matcher(&part) else {
357 match GlobBuilder::new(&normalize_path_for_glob(pattern)).build() {
358 Ok(_) => targets.extend(current.into_iter().map(|d| (d, RecursiveMode::Recursive))),
361 Err(e) => log::warn!("Invalid glob pattern '{pattern}': {e}"),
363 }
364 return targets;
365 };
366 targets.extend(
368 current
369 .iter()
370 .map(|d| (d.clone(), RecursiveMode::NonRecursive)),
371 );
372 current = current
373 .iter()
374 .flat_map(|d| matching_subdirs(d, &matcher))
375 .collect();
376 }
377
378 let mode = if file_part.as_os_str() == "**" {
379 RecursiveMode::Recursive
380 } else {
381 RecursiveMode::NonRecursive
382 };
383 targets.extend(current.into_iter().map(|d| (d, mode)));
384 targets
385}
386
387fn is_glob_component(component: &Component) -> bool {
388 component
389 .as_os_str()
390 .to_string_lossy()
391 .contains(['*', '?', '[', '{'])
392}
393
394fn component_matcher(component: &str) -> Option<GlobMatcher> {
397 GlobBuilder::new(component)
398 .case_insensitive(cfg!(target_os = "windows"))
399 .literal_separator(true)
400 .build()
401 .ok()
402 .map(|glob| glob.compile_matcher())
403}
404
405fn matching_subdirs(dir: &Path, matcher: &GlobMatcher) -> Vec<PathBuf> {
406 let Ok(entries) = std::fs::read_dir(dir) else {
407 return vec![];
408 };
409 entries
410 .flatten()
411 .filter(|e| matcher.is_match(e.file_name()))
412 .map(|e| e.path())
413 .filter(|p| p.is_dir())
414 .collect()
415}
416
417fn normalize_path_for_glob(path: &str) -> String {
428 if let Some(rest) = path.strip_prefix(r"\\?\UNC\") {
429 format!("//{}", rest.replace('\\', "/"))
430 } else {
431 path.strip_prefix(r"\\?\")
432 .unwrap_or(path)
433 .replace('\\', "/")
434 }
435}
436
437fn escape_glob_literal(text: &str) -> String {
440 let text = if cfg!(windows) {
441 text.to_string()
442 } else {
443 text.replace('\\', "\\\\")
444 };
445 globset::escape(&text)
446}
447
448pub fn path_matches_patterns(changed_path: &Path, patterns: &[String], base_dir: &Path) -> bool {
451 let changed_path_str = normalize_path_for_glob(&changed_path.to_string_lossy());
453
454 for pattern in patterns.iter().flat_map(|p| relative_alternatives(p)) {
456 let normalized_pattern = pattern.strip_prefix("./").unwrap_or(&pattern);
458
459 let full_pattern = if Path::new(normalized_pattern).is_absolute() {
461 normalize_path_for_glob(normalized_pattern)
462 } else if Path::new(normalized_pattern).has_root() {
463 normalize_path_for_glob(&base_dir.join(normalized_pattern).to_string_lossy())
465 } else {
466 let base = normalize_path_for_glob(&base_dir.to_string_lossy());
469 let base = escape_glob_literal(base.trim_end_matches('/'));
470 format!("{base}/{}", normalize_path_for_glob(normalized_pattern))
471 };
472
473 let glob = globset::GlobBuilder::new(&full_pattern)
475 .case_insensitive(cfg!(target_os = "windows"))
476 .literal_separator(true) .build();
478
479 if let Ok(glob) = glob {
480 let matcher = glob.compile_matcher();
481 if matcher.is_match(&changed_path_str) {
482 return true;
483 }
484 }
485 }
486 false
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492 use std::fs;
493 use tempfile::TempDir;
494
495 #[test]
496 fn test_normalize_watch_path_existing_directory() {
497 let temp_dir = TempDir::new().unwrap();
498 let dir_path = temp_dir.path().join("test_dir");
499 fs::create_dir(&dir_path).unwrap();
500
501 let normalized = normalize_watch_path(&dir_path);
503 assert!(normalized.is_absolute());
504 assert!(normalized.exists());
505 }
506
507 #[test]
508 fn test_normalize_watch_path_nonexistent_path() {
509 #[cfg(unix)]
513 let path = PathBuf::from("/nonexistent/path/to/dir");
514 #[cfg(windows)]
515 let path = PathBuf::from(r"C:\nonexistent\path\to\dir");
516
517 let normalized = normalize_watch_path(&path);
519 assert_eq!(normalized, path);
520 }
521
522 #[test]
523 fn test_normalize_watch_path_deduplication() {
524 let temp_dir = TempDir::new().unwrap();
525 let dir_path = temp_dir.path().join("test_dir");
526 fs::create_dir(&dir_path).unwrap();
527
528 let subdir = dir_path.join("subdir");
530 fs::create_dir(&subdir).unwrap();
531
532 let path1 = subdir.clone();
535 let path2 = subdir.join("..").join("subdir");
536
537 let normalized1 = normalize_watch_path(&path1);
538 let normalized2 = normalize_watch_path(&path2);
539
540 assert_eq!(normalized1, normalized2);
542 }
543
544 fn canon(path: &Path) -> PathBuf {
545 normalize_watch_path(path)
546 }
547
548 fn expand(patterns: &[&str], base_dir: &Path) -> HashMap<PathBuf, RecursiveMode> {
549 let patterns = patterns.iter().map(|p| p.to_string()).collect_vec();
550 expand_watch_patterns(&patterns, base_dir)
551 }
552
553 #[test]
554 fn test_expand_watch_patterns_specific_file() {
555 let temp_dir = TempDir::new().unwrap();
556 let base_dir = temp_dir.path();
557 fs::write(base_dir.join("package.json"), "{}").unwrap();
558 fs::create_dir_all(base_dir.join("node_modules/a/b")).unwrap();
560
561 let dirs = expand(&["package.json"], base_dir);
562
563 assert_eq!(
564 dirs,
565 HashMap::from([(canon(base_dir), RecursiveMode::NonRecursive)])
566 );
567 }
568
569 #[test]
570 fn test_expand_watch_patterns_recursive_glob() {
571 let temp_dir = TempDir::new().unwrap();
572 let base_dir = temp_dir.path();
573 let subdir = base_dir.join("src");
574 fs::create_dir_all(subdir.join("nested")).unwrap();
575 fs::write(subdir.join("file1.rs"), "").unwrap();
576 fs::write(subdir.join("nested/file2.rs"), "").unwrap();
577
578 let dirs = expand(&["src/**/*.rs"], base_dir);
579
580 assert_eq!(
581 dirs,
582 HashMap::from([(canon(&subdir), RecursiveMode::Recursive)])
583 );
584 }
585
586 #[test]
587 fn test_expand_watch_patterns_single_level_glob() {
588 let temp_dir = TempDir::new().unwrap();
589 let base_dir = temp_dir.path();
590 fs::create_dir_all(base_dir.join("config/nested")).unwrap();
591
592 let dirs = expand(&["config/*.toml"], base_dir);
593
594 assert_eq!(
595 dirs,
596 HashMap::from([(canon(&base_dir.join("config")), RecursiveMode::NonRecursive)])
597 );
598 }
599
600 #[test]
601 fn test_expand_watch_patterns_glob_directory_component() {
602 let temp_dir = TempDir::new().unwrap();
603 let base_dir = temp_dir.path();
604 fs::create_dir_all(base_dir.join("crates/a/deep")).unwrap();
605 fs::create_dir_all(base_dir.join("crates/b")).unwrap();
606 fs::write(base_dir.join("crates/file.toml"), "").unwrap();
607
608 let dirs = expand(&["crates/*/Cargo.toml"], base_dir);
609
610 assert_eq!(
612 dirs,
613 HashMap::from([
614 (canon(&base_dir.join("crates")), RecursiveMode::NonRecursive),
615 (
616 canon(&base_dir.join("crates/a")),
617 RecursiveMode::NonRecursive
618 ),
619 (
620 canon(&base_dir.join("crates/b")),
621 RecursiveMode::NonRecursive
622 ),
623 ])
624 );
625 }
626
627 #[test]
628 fn test_expand_watch_patterns_embedded_double_star() {
629 let temp_dir = TempDir::new().unwrap();
630 let base_dir = temp_dir.path();
631 fs::create_dir_all(base_dir.join("src/a")).unwrap();
632 fs::create_dir_all(base_dir.join("src/b")).unwrap();
633
634 let dirs = expand(&["src/foo**bar.rs", "sr**/x.rs"], base_dir);
636
637 assert_eq!(
638 dirs,
639 HashMap::from([
640 (canon(base_dir), RecursiveMode::NonRecursive),
641 (canon(&base_dir.join("src")), RecursiveMode::NonRecursive),
642 ])
643 );
644 }
645
646 #[test]
647 fn test_expand_watch_patterns_trailing_double_star() {
648 let temp_dir = TempDir::new().unwrap();
649 let base_dir = temp_dir.path();
650 fs::create_dir(base_dir.join("src")).unwrap();
651
652 let dirs = expand(&["src/**"], base_dir);
653
654 assert_eq!(
655 dirs,
656 HashMap::from([(canon(&base_dir.join("src")), RecursiveMode::Recursive)])
657 );
658 }
659
660 #[test]
661 fn test_expand_watch_patterns_alternatives_with_separator() {
662 let temp_dir = TempDir::new().unwrap();
663 let base_dir = temp_dir.path();
664 fs::create_dir_all(base_dir.join("src/api")).unwrap();
665 fs::create_dir(base_dir.join("lib")).unwrap();
666
667 let dirs = expand(&["{src/api,lib}/*.rs"], base_dir);
669 assert_eq!(
670 dirs,
671 HashMap::from([
672 (
673 canon(&base_dir.join("src/api")),
674 RecursiveMode::NonRecursive
675 ),
676 (canon(&base_dir.join("lib")), RecursiveMode::NonRecursive),
677 ])
678 );
679 assert!(path_matches_patterns(
680 &base_dir.join("src/api/main.rs"),
681 &["{src/api,lib}/*.rs".to_string()],
682 base_dir
683 ));
684
685 let dirs = expand(&["[a/b]/*.rs"], base_dir);
688 assert_eq!(
689 dirs,
690 HashMap::from([(canon(base_dir), RecursiveMode::Recursive)])
691 );
692
693 assert!(expand(&["[z-a]/*.rs"], base_dir).is_empty());
695 }
696
697 #[test]
698 fn test_expand_braces() {
699 let mut expanded = expand_braces("{src/{a,b},lib}/*.{rs,toml}");
700 expanded.sort();
701 assert_eq!(
702 expanded,
703 [
704 "lib/*.rs",
705 "lib/*.toml",
706 "src/a/*.rs",
707 "src/a/*.toml",
708 "src/b/*.rs",
709 "src/b/*.toml",
710 ]
711 );
712
713 assert_eq!(expand_braces("[{]x}/*.rs"), ["[{]x}/*.rs"]);
715 #[cfg(unix)]
716 assert_eq!(expand_braces(r"\{a,b}.rs"), [r"\{a,b}.rs"]);
717
718 let many = "{a,b,c,d,e}/{a,b,c,d,e}/{a,b,c}/*.rs";
720 assert_eq!(expand_braces(many), [many]);
721 }
722
723 #[test]
724 fn test_expand_watch_patterns_alternatives_stay_relative() {
725 let temp_dir = TempDir::new().unwrap();
726 let base_dir = temp_dir.path();
727 fs::create_dir_all(base_dir.join("src/x")).unwrap();
728
729 let dirs = expand(&["{src,}/**/*.rs", "{/src/x,lib}/*.rs"], base_dir);
731
732 assert_eq!(
733 dirs,
734 HashMap::from([
735 (canon(base_dir), RecursiveMode::Recursive),
736 (canon(&base_dir.join("src")), RecursiveMode::Recursive),
737 (canon(&base_dir.join("src/x")), RecursiveMode::NonRecursive),
738 ])
739 );
740 }
741
742 #[test]
743 fn test_relative_alternatives() {
744 assert_eq!(relative_alternatives("{src,}/*.rs"), ["*.rs", "src/*.rs"]);
745 assert_eq!(relative_alternatives("/{a,b}.rs"), ["/b.rs", "/a.rs"]);
747 #[cfg(windows)]
748 assert_eq!(relative_alternatives(r"\src\main.rs"), [r"\src\main.rs"]);
749 }
750
751 #[test]
752 fn test_path_matches_patterns_alternatives_stay_relative() {
753 let temp_dir = TempDir::new().unwrap();
754 let base_dir = temp_dir.path();
755 let patterns = ["{/src/x,lib}/*.rs".to_string()];
756
757 assert!(path_matches_patterns(
759 &base_dir.join("src/x/main.rs"),
760 &patterns,
761 base_dir
762 ));
763 assert!(path_matches_patterns(
764 &base_dir.join("lib/main.rs"),
765 &patterns,
766 base_dir
767 ));
768 assert!(!path_matches_patterns(
769 &base_dir.join("src/main.rs"),
770 &patterns,
771 base_dir
772 ));
773 }
774
775 #[test]
776 fn test_expand_watch_patterns_recursive_wins() {
777 let temp_dir = TempDir::new().unwrap();
778 let base_dir = temp_dir.path();
779 fs::create_dir(base_dir.join("src")).unwrap();
780
781 let dirs = expand(&["src/main.rs", "src/**/*.rs"], base_dir);
782
783 assert_eq!(
784 dirs,
785 HashMap::from([(canon(&base_dir.join("src")), RecursiveMode::Recursive)])
786 );
787 }
788
789 #[test]
790 fn test_expand_watch_patterns_nonexistent_file() {
791 let temp_dir = TempDir::new().unwrap();
792 let base_dir = temp_dir.path();
793
794 let dirs = expand(&["config.toml"], base_dir);
796
797 assert_eq!(
798 dirs,
799 HashMap::from([(canon(base_dir), RecursiveMode::NonRecursive)])
800 );
801 }
802
803 #[test]
804 fn test_expand_watch_patterns_nonexistent_directory() {
805 let temp_dir = TempDir::new().unwrap();
806 let base_dir = temp_dir.path();
807 fs::create_dir(base_dir.join("config")).unwrap();
808
809 let dirs = expand(&["config/app/*.toml", "lib/**/*.ts"], base_dir);
811
812 assert_eq!(
813 dirs,
814 HashMap::from([
815 (canon(base_dir), RecursiveMode::NonRecursive),
816 (canon(&base_dir.join("config")), RecursiveMode::NonRecursive),
817 ])
818 );
819 }
820
821 #[test]
822 fn test_watched_entries() {
823 let temp_dir = TempDir::new().unwrap();
824 let dir = temp_dir.path().join("new");
825 fs::create_dir_all(dir.join("nested")).unwrap();
826 fs::write(dir.join("a.toml"), "").unwrap();
827 fs::write(dir.join("nested/b.toml"), "").unwrap();
828
829 let mut paths = watched_entries(&dir, RecursiveMode::NonRecursive);
830 paths.sort();
831 assert_eq!(paths, vec![dir.join("a.toml"), dir.join("nested")]);
832
833 let mut paths = watched_entries(&dir, RecursiveMode::Recursive);
834 paths.sort();
835 assert_eq!(
836 paths,
837 vec![
838 dir.join("a.toml"),
839 dir.join("nested"),
840 dir.join("nested/b.toml"),
841 ]
842 );
843 }
844
845 #[test]
846 fn test_path_matches_patterns_simple() {
847 let temp_dir = TempDir::new().unwrap();
848 let base_dir = temp_dir.path();
849
850 let test_txt = base_dir.join("test.txt");
852 let test_rs = base_dir.join("test.rs");
853 fs::write(&test_txt, "").unwrap();
854 fs::write(&test_rs, "").unwrap();
855
856 assert!(path_matches_patterns(
858 &test_txt,
859 &["*.txt".to_string()],
860 base_dir
861 ));
862
863 assert!(!path_matches_patterns(
865 &test_rs,
866 &["*.txt".to_string()],
867 base_dir
868 ));
869 }
870
871 #[test]
872 fn test_path_matches_patterns_recursive_glob() {
873 let temp_dir = TempDir::new().unwrap();
874 let base_dir = temp_dir.path();
875 let src_dir = base_dir.join("src");
876 let deep_dir = src_dir.join("deep");
877 fs::create_dir_all(&deep_dir).unwrap();
878
879 let deep_file = deep_dir.join("file.rs");
881 let src_file = src_dir.join("file.rs");
882 fs::write(&deep_file, "").unwrap();
883 fs::write(&src_file, "").unwrap();
884
885 assert!(path_matches_patterns(
887 &deep_file,
888 &["src/**/*.rs".to_string()],
889 base_dir
890 ));
891
892 assert!(path_matches_patterns(
894 &src_file,
895 &["src/**/*.rs".to_string()],
896 base_dir
897 ));
898 }
899
900 #[test]
901 fn test_path_matches_patterns_multiple_patterns() {
902 let temp_dir = TempDir::new().unwrap();
903 let base_dir = temp_dir.path();
904
905 let cargo_toml = base_dir.join("Cargo.toml");
907 let main_rs = base_dir.join("main.rs");
908 let readme_md = base_dir.join("README.md");
909 fs::write(&cargo_toml, "").unwrap();
910 fs::write(&main_rs, "").unwrap();
911 fs::write(&readme_md, "").unwrap();
912
913 let patterns = vec!["*.rs".to_string(), "*.toml".to_string()];
915 assert!(path_matches_patterns(&cargo_toml, &patterns, base_dir));
916 assert!(path_matches_patterns(&main_rs, &patterns, base_dir));
917 assert!(!path_matches_patterns(&readme_md, &patterns, base_dir));
918 }
919
920 #[test]
921 fn test_path_matches_patterns_relative_prefix() {
922 let temp_dir = TempDir::new().unwrap();
923 let base_dir = temp_dir.path();
924
925 let test_file = base_dir.join("config.json");
927 fs::write(&test_file, "{}").unwrap();
928
929 assert!(path_matches_patterns(
931 &test_file,
932 &["./config.json".to_string()],
933 base_dir
934 ));
935
936 assert!(path_matches_patterns(
938 &test_file,
939 &["config.json".to_string()],
940 base_dir
941 ));
942 }
943
944 #[test]
945 fn test_expand_watch_patterns_relative_prefix() {
946 let temp_dir = TempDir::new().unwrap();
947 let base_dir = temp_dir.path();
948
949 let test_file = base_dir.join("config.json");
951 fs::write(&test_file, "{}").unwrap();
952
953 let dirs = expand(&["./config.json"], base_dir);
955
956 assert_eq!(
957 dirs,
958 HashMap::from([(canon(base_dir), RecursiveMode::NonRecursive)])
959 );
960 }
961
962 #[test]
965 fn test_glob_characters_in_base_dir_are_literal() {
966 let temp_dir = TempDir::new().unwrap();
967 for name in ["app[1]", "app{a,b}", "app*"] {
968 if cfg!(windows) && name.contains('*') {
969 continue;
970 }
971 let base_dir = temp_dir.path().join(name);
972 fs::create_dir_all(base_dir.join("src")).unwrap();
973 fs::create_dir_all(temp_dir.path().join("app1")).unwrap();
974 let index = base_dir.join("index.js");
975 fs::write(&index, "").unwrap();
976
977 assert_eq!(
978 expand(&["*.js"], &base_dir),
979 HashMap::from([(canon(&base_dir), RecursiveMode::NonRecursive)]),
980 "{name}"
981 );
982 assert_eq!(
983 expand(&["src/**/*.js"], &base_dir),
984 HashMap::from([(canon(&base_dir.join("src")), RecursiveMode::Recursive)]),
985 "{name}"
986 );
987 assert!(
988 path_matches_patterns(&index, &["*.js".to_string()], &base_dir),
989 "{name}"
990 );
991 assert!(
992 !path_matches_patterns(
993 &temp_dir.path().join("app1/index.js"),
994 &["*.js".to_string()],
995 &base_dir
996 ),
997 "{name}"
998 );
999 }
1000 }
1001}