steer_workspace/utils/
file_listing.rs1use fuzzy_matcher::FuzzyMatcher;
2use fuzzy_matcher::skim::SkimMatcherV2;
3use ignore::WalkBuilder;
4use std::path::Path;
5
6pub struct FileListingUtils;
8
9impl FileListingUtils {
10 pub fn list_files(
12 root_path: &Path,
13 query: Option<&str>,
14 max_results: Option<usize>,
15 ) -> Result<Vec<String>, std::io::Error> {
16 let mut files = Vec::new();
17
18 let walker = WalkBuilder::new(root_path)
20 .hidden(false) .filter_entry(|entry| {
22 entry.file_name() != ".git" && entry.file_name() != ".jj"
24 })
25 .build();
26
27 for entry in walker {
28 let entry = match entry {
29 Ok(e) => e,
30 Err(_) => continue, };
32
33 if entry.path() == root_path {
35 continue;
36 }
37
38 if let Ok(relative_path) = entry.path().strip_prefix(root_path)
40 && let Some(path_str) = relative_path.to_str()
41 && !path_str.is_empty()
42 {
43 if entry.file_type().is_some_and(|ft| ft.is_dir()) {
45 files.push(format!("{path_str}/"));
46 } else {
47 files.push(path_str.to_string());
48 }
49 }
50 }
51
52 let mut filtered_files = if let Some(query) = query {
54 if query.is_empty() {
55 files
56 } else {
57 let matcher = SkimMatcherV2::default();
58 let mut scored_files: Vec<(i64, String)> = files
59 .into_iter()
60 .filter_map(|file| matcher.fuzzy_match(&file, query).map(|score| (score, file)))
61 .collect();
62
63 scored_files.sort_by(|a, b| b.0.cmp(&a.0));
65
66 scored_files.into_iter().map(|(_, file)| file).collect()
67 }
68 } else {
69 files
70 };
71
72 if let Some(max) = max_results
74 && max > 0
75 && filtered_files.len() > max
76 {
77 filtered_files.truncate(max);
78 }
79
80 Ok(filtered_files)
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use tempfile::tempdir;
88
89 #[test]
90 fn test_list_files_empty_dir() {
91 let temp_dir = tempdir().unwrap();
92 let files = FileListingUtils::list_files(temp_dir.path(), None, None).unwrap();
93 assert!(files.is_empty());
94 }
95
96 #[test]
97 fn test_list_files_with_content() {
98 let temp_dir = tempdir().unwrap();
99
100 std::fs::write(temp_dir.path().join("test.rs"), "test").unwrap();
102 std::fs::write(temp_dir.path().join("main.rs"), "main").unwrap();
103 std::fs::create_dir(temp_dir.path().join("src")).unwrap();
104 std::fs::write(temp_dir.path().join("src/lib.rs"), "lib").unwrap();
105
106 let files = FileListingUtils::list_files(temp_dir.path(), None, None).unwrap();
107 assert_eq!(files.len(), 4); assert!(files.contains(&"test.rs".to_string()));
109 assert!(files.contains(&"main.rs".to_string()));
110 assert!(files.contains(&"src/".to_string()));
111 assert!(files.contains(&"src/lib.rs".to_string()));
112 }
113
114 #[test]
115 fn test_list_files_with_query() {
116 let temp_dir = tempdir().unwrap();
117 std::fs::write(temp_dir.path().join("test.rs"), "test").unwrap();
118 std::fs::write(temp_dir.path().join("main.rs"), "main").unwrap();
119
120 let files = FileListingUtils::list_files(temp_dir.path(), Some("test"), None).unwrap();
121 assert_eq!(files.len(), 1);
122 assert_eq!(files[0], "test.rs");
123 }
124
125 #[test]
126 #[cfg(unix)]
127 fn test_list_files_skips_inaccessible() {
128 use std::os::unix::fs::PermissionsExt;
129
130 let temp_dir = tempdir().unwrap();
131
132 std::fs::write(temp_dir.path().join("readable.txt"), "test").unwrap();
134
135 let restricted_dir = temp_dir.path().join("restricted");
137 std::fs::create_dir(&restricted_dir).unwrap();
138 std::fs::write(restricted_dir.join("hidden.txt"), "secret").unwrap();
139
140 let mut perms = std::fs::metadata(&restricted_dir).unwrap().permissions();
142 perms.set_mode(0o000);
143 std::fs::set_permissions(&restricted_dir, perms).unwrap();
144
145 let files = FileListingUtils::list_files(temp_dir.path(), None, None).unwrap();
147
148 assert!(files.contains(&"readable.txt".to_string()));
150
151 let mut perms = std::fs::metadata(&restricted_dir).unwrap().permissions();
153 perms.set_mode(0o755);
154 std::fs::set_permissions(&restricted_dir, perms).unwrap();
155 }
156}