1use std::path::{Path, PathBuf};
2
3use crate::lang::path_to_lang;
4use code_moniker_core::lang::Lang;
5
6pub struct WalkedFile {
7 pub path: PathBuf,
8 pub lang: Lang,
9}
10
11pub fn walk_lang_files(root: &Path) -> Vec<WalkedFile> {
12 ignore::WalkBuilder::new(root)
13 .build()
14 .filter_map(|entry| entry.ok())
15 .filter(|e| e.file_type().is_some_and(|t| t.is_file()))
16 .filter_map(|e| {
17 let p = e.into_path();
18 let lang = path_to_lang(&p).ok()?;
19 Some(WalkedFile { path: p, lang })
20 })
21 .collect()
22}
23
24#[cfg(test)]
25mod tests {
26 use super::*;
27 use std::collections::HashSet;
28 use std::fs;
29
30 fn write(root: &Path, rel: &str, body: &str) {
31 let p = root.join(rel);
32 if let Some(parent) = p.parent() {
33 fs::create_dir_all(parent).unwrap();
34 }
35 fs::write(p, body).unwrap();
36 }
37
38 #[test]
39 fn walks_supported_extensions_only() {
40 let tmp = tempfile::tempdir().unwrap();
41 let root = tmp.path();
42 write(root, "a.ts", "");
43 write(root, "b.rs", "");
44 write(root, "c.txt", "ignored");
45 write(root, "nested/d.py", "");
46 let mut files: HashSet<(String, Lang)> = walk_lang_files(root)
47 .into_iter()
48 .map(|f| {
49 let rel = f.path.strip_prefix(root).unwrap().to_string_lossy().into();
50 (rel, f.lang)
51 })
52 .collect();
53 assert!(files.remove(&("a.ts".into(), Lang::Ts)));
54 assert!(files.remove(&("b.rs".into(), Lang::Rs)));
55 assert!(files.remove(&("nested/d.py".into(), Lang::Python)));
56 assert!(files.is_empty(), "unexpected files: {files:?}");
57 }
58
59 #[test]
60 fn respects_gitignore() {
61 let tmp = tempfile::tempdir().unwrap();
62 let root = tmp.path();
63 write(root, ".gitignore", "skip/\n");
64 write(root, "kept.ts", "");
65 write(root, "skip/dropped.ts", "");
66 fs::create_dir_all(root.join(".git")).unwrap();
67 let files: Vec<String> = walk_lang_files(root)
68 .into_iter()
69 .map(|f| f.path.strip_prefix(root).unwrap().to_string_lossy().into())
70 .collect();
71 assert_eq!(files, vec!["kept.ts".to_string()]);
72 }
73}