1use crate::commands::watch::{CommentType, WatchOptions};
17use crate::{commands::watch as watch_cmd, config};
18use anyhow::Result;
19use clap::{Args, ValueEnum};
20use std::path::PathBuf;
21
22#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq)]
24pub enum WatchCommentType {
25 Todo,
27 Fixme,
29 Hack,
31 Xxx,
33 All,
35}
36
37impl From<WatchCommentType> for CommentType {
38 fn from(value: WatchCommentType) -> Self {
39 match value {
40 WatchCommentType::Todo => CommentType::Todo,
41 WatchCommentType::Fixme => CommentType::Fixme,
42 WatchCommentType::Hack => CommentType::Hack,
43 WatchCommentType::Xxx => CommentType::Xxx,
44 WatchCommentType::All => CommentType::All,
45 }
46 }
47}
48
49pub fn handle_watch(args: WatchArgs, force: bool) -> Result<()> {
50 let resolved = config::resolve_from_cwd()?;
51
52 let comment_types: Vec<CommentType> = if args.comments.is_empty() {
53 vec![CommentType::All]
54 } else {
55 args.comments.iter().map(|&c| c.into()).collect()
56 };
57
58 let patterns: Vec<String> = if args.patterns.is_empty() {
59 vec![
60 "*.rs".to_string(),
61 "*.ts".to_string(),
62 "*.js".to_string(),
63 "*.py".to_string(),
64 "*.go".to_string(),
65 "*.java".to_string(),
66 "*.md".to_string(),
67 "*.toml".to_string(),
68 "*.json".to_string(),
69 ]
70 } else {
71 args.patterns.clone()
72 };
73
74 let paths: Vec<PathBuf> = if args.paths.is_empty() {
75 vec![std::env::current_dir()?]
76 } else {
77 args.paths.clone()
78 };
79
80 watch_cmd::run_watch(
81 &resolved,
82 WatchOptions {
83 patterns,
84 debounce_ms: args.debounce_ms,
85 auto_queue: args.auto_queue,
86 notify: args.notify,
87 ignore_patterns: args.ignore_patterns,
88 comment_types,
89 paths,
90 force,
91 close_removed: args.close_removed,
92 },
93 )
94}
95
96#[derive(Args)]
97#[command(
98 about = "Watch files for changes and auto-detect tasks from TODO/FIXME/HACK/XXX comments",
99 after_long_help = "Examples:
100 ralph watch
101 ralph watch src/
102 ralph watch --patterns \"*.rs,*.toml\"
103 ralph watch --auto-queue
104 ralph watch --notify
105 ralph watch --comments todo,fixme
106 ralph watch --debounce-ms 1000
107 ralph watch --ignore-patterns \"vendor/,target/,node_modules/\"
108 ralph watch --auto-queue --close-removed"
109)]
110pub struct WatchArgs {
111 #[arg(value_name = "PATH")]
113 pub paths: Vec<PathBuf>,
114
115 #[arg(long, value_delimiter = ',')]
117 pub patterns: Vec<String>,
118
119 #[arg(long, default_value_t = 500)]
121 pub debounce_ms: u64,
122
123 #[arg(long)]
125 pub auto_queue: bool,
126
127 #[arg(long)]
129 pub notify: bool,
130
131 #[arg(long, value_delimiter = ',')]
133 pub ignore_patterns: Vec<String>,
134
135 #[arg(long, value_enum, value_delimiter = ',')]
137 pub comments: Vec<WatchCommentType>,
138
139 #[arg(long)]
141 pub close_removed: bool,
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use crate::cli::Cli;
148 use clap::{CommandFactory, Parser};
149
150 #[test]
151 fn watch_help_examples_exist() {
152 let mut cmd = Cli::command();
153 let watch = cmd.find_subcommand_mut("watch").expect("watch subcommand");
154 let help = watch.render_long_help().to_string();
155
156 assert!(help.contains("ralph watch"), "missing basic watch example");
157 assert!(
158 help.contains("--auto-queue"),
159 "missing --auto-queue example"
160 );
161 assert!(help.contains("--notify"), "missing --notify example");
162 assert!(help.contains("--comments"), "missing --comments example");
163 }
164
165 #[test]
166 fn watch_parses_default_args() {
167 let cli = Cli::try_parse_from(["ralph", "watch"]).expect("parse");
168
169 match cli.command {
170 crate::cli::Command::Watch(args) => {
171 assert!(args.paths.is_empty());
172 assert!(args.patterns.is_empty());
173 assert_eq!(args.debounce_ms, 500);
174 assert!(!args.auto_queue);
175 assert!(!args.notify);
176 assert!(args.ignore_patterns.is_empty());
177 assert!(args.comments.is_empty());
178 }
179 _ => panic!("expected watch command"),
180 }
181 }
182
183 #[test]
184 fn watch_parses_paths() {
185 let cli = Cli::try_parse_from(["ralph", "watch", "src/", "tests/"]).expect("parse");
186
187 match cli.command {
188 crate::cli::Command::Watch(args) => {
189 assert_eq!(args.paths.len(), 2);
190 assert_eq!(args.paths[0], PathBuf::from("src/"));
191 assert_eq!(args.paths[1], PathBuf::from("tests/"));
192 }
193 _ => panic!("expected watch command"),
194 }
195 }
196
197 #[test]
198 fn watch_parses_patterns() {
199 let cli =
200 Cli::try_parse_from(["ralph", "watch", "--patterns", "*.rs,*.toml"]).expect("parse");
201
202 match cli.command {
203 crate::cli::Command::Watch(args) => {
204 assert_eq!(args.patterns, vec!["*.rs", "*.toml"]);
205 }
206 _ => panic!("expected watch command"),
207 }
208 }
209
210 #[test]
211 fn watch_parses_debounce() {
212 let cli = Cli::try_parse_from(["ralph", "watch", "--debounce-ms", "1000"]).expect("parse");
213
214 match cli.command {
215 crate::cli::Command::Watch(args) => {
216 assert_eq!(args.debounce_ms, 1000);
217 }
218 _ => panic!("expected watch command"),
219 }
220 }
221
222 #[test]
223 fn watch_parses_auto_queue() {
224 let cli = Cli::try_parse_from(["ralph", "watch", "--auto-queue"]).expect("parse");
225
226 match cli.command {
227 crate::cli::Command::Watch(args) => {
228 assert!(args.auto_queue);
229 }
230 _ => panic!("expected watch command"),
231 }
232 }
233
234 #[test]
235 fn watch_parses_notify() {
236 let cli = Cli::try_parse_from(["ralph", "watch", "--notify"]).expect("parse");
237
238 match cli.command {
239 crate::cli::Command::Watch(args) => {
240 assert!(args.notify);
241 }
242 _ => panic!("expected watch command"),
243 }
244 }
245
246 #[test]
247 fn watch_parses_comments() {
248 let cli =
249 Cli::try_parse_from(["ralph", "watch", "--comments", "todo,fixme"]).expect("parse");
250
251 match cli.command {
252 crate::cli::Command::Watch(args) => {
253 assert_eq!(args.comments.len(), 2);
254 assert_eq!(args.comments[0], WatchCommentType::Todo);
255 assert_eq!(args.comments[1], WatchCommentType::Fixme);
256 }
257 _ => panic!("expected watch command"),
258 }
259 }
260
261 #[test]
262 fn watch_parses_ignore_patterns() {
263 let cli = Cli::try_parse_from(["ralph", "watch", "--ignore-patterns", "vendor/,target/"])
264 .expect("parse");
265
266 match cli.command {
267 crate::cli::Command::Watch(args) => {
268 assert_eq!(args.ignore_patterns, vec!["vendor/", "target/"]);
269 }
270 _ => panic!("expected watch command"),
271 }
272 }
273
274 #[test]
275 fn watch_parses_close_removed() {
276 let cli = Cli::try_parse_from(["ralph", "watch", "--close-removed"]).expect("parse");
277
278 match cli.command {
279 crate::cli::Command::Watch(args) => {
280 assert!(args.close_removed);
281 }
282 _ => panic!("expected watch command"),
283 }
284 }
285}