Skip to main content

code_moniker_workspace/
walk.rs

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	walk_lang_files_cancellable(root, || false)
13}
14
15pub fn walk_lang_files_cancellable(root: &Path, cancelled: impl Fn() -> bool) -> Vec<WalkedFile> {
16	ignore::WalkBuilder::new(root)
17		.build()
18		.take_while(|_| !cancelled())
19		.filter_map(|entry| entry.ok())
20		.filter(|e| e.file_type().is_some_and(|t| t.is_file()))
21		.filter_map(|e| {
22			let p = e.into_path();
23			let lang = path_to_lang(&p).ok()?;
24			Some(WalkedFile { path: p, lang })
25		})
26		.collect()
27}
28
29pub fn explicit_lang_file(path: &Path) -> Option<WalkedFile> {
30	ignore::WalkBuilder::new(path)
31		.build()
32		.filter_map(|entry| entry.ok())
33		.filter(|e| e.file_type().is_some_and(|t| t.is_file()))
34		.find_map(|e| {
35			let p = e.into_path();
36			let lang = path_to_lang(&p).ok()?;
37			Some(WalkedFile { path: p, lang })
38		})
39}
40
41#[cfg(test)]
42mod tests {
43	use super::*;
44	use std::collections::HashSet;
45	use std::fs;
46
47	fn write(root: &Path, rel: &str, body: &str) {
48		let p = root.join(rel);
49		if let Some(parent) = p.parent() {
50			fs::create_dir_all(parent).unwrap();
51		}
52		fs::write(p, body).unwrap();
53	}
54
55	#[test]
56	fn walks_supported_extensions_only() {
57		let tmp = tempfile::tempdir().unwrap();
58		let root = tmp.path();
59		write(root, "a.ts", "");
60		write(root, "b.rs", "");
61		write(root, "c.txt", "ignored");
62		write(root, "nested/d.py", "");
63		let mut files: HashSet<(String, Lang)> = walk_lang_files(root)
64			.into_iter()
65			.map(|f| {
66				let rel = f.path.strip_prefix(root).unwrap().to_string_lossy().into();
67				(rel, f.lang)
68			})
69			.collect();
70		assert!(files.remove(&("a.ts".into(), Lang::Ts)));
71		assert!(files.remove(&("b.rs".into(), Lang::Rs)));
72		assert!(files.remove(&("nested/d.py".into(), Lang::Python)));
73		assert!(files.is_empty(), "unexpected files: {files:?}");
74	}
75
76	#[test]
77	fn respects_gitignore() {
78		let tmp = tempfile::tempdir().unwrap();
79		let root = tmp.path();
80		write(root, ".gitignore", "skip/\n");
81		write(root, "kept.ts", "");
82		write(root, "skip/dropped.ts", "");
83		fs::create_dir_all(root.join(".git")).unwrap();
84		let files: Vec<String> = walk_lang_files(root)
85			.into_iter()
86			.map(|f| f.path.strip_prefix(root).unwrap().to_string_lossy().into())
87			.collect();
88		assert_eq!(files, vec!["kept.ts".to_string()]);
89	}
90
91	#[test]
92	fn explicit_file_accepts_supported_file() {
93		let tmp = tempfile::tempdir().unwrap();
94		let root = tmp.path();
95		write(root, "kept.ts", "");
96
97		assert!(explicit_lang_file(&root.join("kept.ts")).is_some());
98		assert!(explicit_lang_file(&root.join("kept.txt")).is_none());
99	}
100}