1use std::collections::HashSet;
2use std::path::Path;
3use std::sync::mpsc;
4use std::time::Duration;
5
6use anyhow::Result;
7use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
8
9use crate::formatters;
10use crate::scanner::Scanner;
11
12const DEBOUNCE_MS: u64 = 200;
13
14pub fn watch(scanner: &Scanner) -> Result<()> {
15 let dirs = resolve_watch_dirs(&scanner.files);
16 if dirs.is_empty() {
17 eprintln!("Nothing to watch. Specify a directory or file pattern.");
18 return Ok(());
19 }
20
21 let (tx, rx) = mpsc::channel::<Vec<String>>();
22
23 let mut watcher = RecommendedWatcher::new(
24 move |res: Result<Event, notify::Error>| {
25 if let Ok(event) = res {
26 let paths: Vec<String> = event
27 .paths
28 .iter()
29 .filter_map(|p| {
30 let ext = p.extension().and_then(|e| e.to_str())?;
31 if matches!(ext, "js" | "jsx" | "ts" | "tsx") {
32 Some(p.to_string_lossy().to_string())
33 } else {
34 None
35 }
36 })
37 .collect();
38 if !paths.is_empty() {
39 let _ = tx.send(paths);
40 }
41 }
42 },
43 Config::default(),
44 )?;
45
46 for dir in &dirs {
47 watcher.watch(dir, RecursiveMode::Recursive)?;
48 }
49
50 eprintln!(
51 "Watching {} director(ies) for changes. Ctrl+C to stop.",
52 dirs.len()
53 );
54
55 run_scan_loop(scanner, &rx)
56}
57
58fn run_scan_loop(scanner: &Scanner, rx: &mpsc::Receiver<Vec<String>>) -> Result<()> {
59 loop {
60 let mut changed = HashSet::new();
61
62 match rx.recv() {
63 Ok(paths) => {
64 for p in paths {
65 changed.insert(p);
66 }
67 }
68 Err(_) => break,
69 }
70
71 loop {
72 match rx.recv_timeout(Duration::from_millis(DEBOUNCE_MS)) {
73 Ok(paths) => {
74 for p in paths {
75 changed.insert(p);
76 }
77 }
78 Err(mpsc::RecvTimeoutError::Timeout) => break,
79 Err(mpsc::RecvTimeoutError::Disconnected) => return Ok(()),
80 }
81 }
82
83 let changed: Vec<String> = changed.into_iter().collect();
84
85 if changed.is_empty() {
86 continue;
87 }
88
89 eprint!("\r[{} file(s) changed] Scanning...", changed.len());
90
91 match scanner.scan_paths(&changed) {
92 Ok(results) => {
93 let formatter = formatters::get_formatter("stylish");
94 let output = formatter.format(&results, false);
95 print!("{output}");
96 }
97 Err(e) => {
98 eprintln!("\rScan error: {e}");
99 }
100 }
101 }
102
103 Ok(())
104}
105
106fn resolve_watch_dirs(patterns: &[String]) -> Vec<Box<Path>> {
107 let mut dirs = Vec::new();
108 for pattern in patterns {
109 let path = Path::new(pattern);
110 if path.is_dir() {
111 dirs.push(path.into());
112 } else if path.is_file() {
113 if let Some(parent) = path.parent() {
114 dirs.push(parent.into());
115 }
116 } else {
117 if let Some(parent) = path.parent() {
118 if parent.to_string_lossy().is_empty() || parent == Path::new(".") {
119 if let Ok(cwd) = std::env::current_dir() {
120 dirs.push(cwd.into());
121 }
122 } else if parent.is_dir() {
123 dirs.push(parent.into());
124 }
125 }
126 }
127 }
128 dirs.sort();
129 dirs.dedup();
130 dirs
131}