1use 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"
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 if let Some(path_str) = relative_path.to_str() {
41 if !path_str.is_empty() {
42 if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
44 files.push(format!("{path_str}/"));
45 } else {
46 files.push(path_str.to_string());
47 }
48 }
49 }
50 }
51 }
52
53 let mut filtered_files = if let Some(query) = query {
55 if query.is_empty() {
56 files
57 } else {
58 let matcher = SkimMatcherV2::default();
59 let mut scored_files: Vec<(i64, String)> = files
60 .into_iter()
61 .filter_map(|file| matcher.fuzzy_match(&file, query).map(|score| (score, file)))
62 .collect();
63
64 scored_files.sort_by(|a, b| b.0.cmp(&a.0));
66
67 scored_files.into_iter().map(|(_, file)| file).collect()
68 }
69 } else {
70 files
71 };
72
73 if let Some(max) = max_results {
75 if max > 0 && filtered_files.len() > max {
76 filtered_files.truncate(max);
77 }
78 }
79
80 Ok(filtered_files)
81 }
82}
83
84pub struct GitStatusUtils;
86
87impl GitStatusUtils {
88 pub fn get_git_status(repo_path: &Path) -> Result<String, std::io::Error> {
90 let mut result = String::new();
91
92 let repo = gix::discover(repo_path)
93 .map_err(|e| std::io::Error::other(format!("Failed to open git repository: {e}")))?;
94
95 match repo.head_name() {
97 Ok(Some(name)) => {
98 let branch = name.as_bstr().to_string();
99 let branch = branch.strip_prefix("refs/heads/").unwrap_or(&branch);
101 result.push_str(&format!("Current branch: {branch}\n\n"));
102 }
103 Ok(None) => {
104 result.push_str("Current branch: HEAD (detached)\n\n");
105 }
106 Err(e) => {
107 if e.to_string().contains("does not exist") {
109 result.push_str("Current branch: <unborn>\n\n");
110 } else {
111 return Err(std::io::Error::other(format!("Failed to get HEAD: {e}")));
112 }
113 }
114 }
115
116 let iter = repo
118 .status(gix::progress::Discard)
119 .map_err(|e| std::io::Error::other(format!("Failed to get git status: {e}")))?
120 .into_index_worktree_iter(Vec::new())
121 .map_err(|e| std::io::Error::other(format!("Failed to get git status: {e}")))?;
122 result.push_str("Status:\n");
123 use gix::bstr::ByteSlice;
124 use gix::status::index_worktree::iter::Summary;
125 let mut has_changes = false;
126 for item_res in iter {
127 let item = item_res
128 .map_err(|e| std::io::Error::other(format!("Failed to get git status: {e}")))?;
129 if let Some(summary) = item.summary() {
130 has_changes = true;
131 let path = item.rela_path().to_str_lossy();
132 let (status_char, wt_char) = match summary {
133 Summary::Added => (" ", "?"),
134 Summary::Removed => ("D", " "),
135 Summary::Modified => ("M", " "),
136 Summary::TypeChange => ("T", " "),
137 Summary::Renamed => ("R", " "),
138 Summary::Copied => ("C", " "),
139 Summary::IntentToAdd => ("A", " "),
140 Summary::Conflict => ("U", "U"),
141 };
142 result.push_str(&format!("{status_char}{wt_char} {path}\n"));
143 }
144 }
145 if !has_changes {
146 result.push_str("Working tree clean\n");
147 }
148
149 result.push_str("\nRecent commits:\n");
151 match repo.head_id() {
152 Ok(head_id) => {
153 let oid = head_id.detach();
154 let mut count = 0;
155 if let Ok(object) = repo.find_object(oid) {
156 if let Ok(commit) = object.try_into_commit() {
157 let summary_bytes = commit.message_raw_sloppy();
159 let summary = summary_bytes
160 .lines()
161 .next()
162 .and_then(|line| std::str::from_utf8(line).ok())
163 .unwrap_or("<no summary>");
164 let short_id = oid.to_hex().to_string();
165 let short_id = &short_id[..7.min(short_id.len())];
166 result.push_str(&format!("{short_id} {summary}\n"));
167 count = 1;
168 }
169 }
170 if count == 0 {
171 result.push_str("<no commits>\n");
172 }
173 }
174 Err(_) => {
175 result.push_str("<no commits>\n");
176 }
177 }
178
179 Ok(result)
180 }
181}
182
183pub struct DirectoryStructureUtils;
185
186impl DirectoryStructureUtils {
187 pub fn get_directory_structure(
190 root_path: &Path,
191 max_depth: usize,
192 max_items: Option<usize>,
193 ) -> Result<String, std::io::Error> {
194 let mut structure = vec![root_path.display().to_string()];
195
196 let (paths, truncated) = Self::collect_directory_paths(root_path, max_depth, max_items)?;
198 structure.extend(paths);
199
200 structure.sort();
201 let mut result = structure.join("\n");
202
203 if truncated > 0 {
204 result.push_str(&format!("\n... and {truncated} more items"));
205 }
206
207 Ok(result)
208 }
209
210 fn collect_directory_paths(
213 root_path: &Path,
214 max_depth: usize,
215 max_items: Option<usize>,
216 ) -> Result<(Vec<String>, usize), std::io::Error> {
217 let mut paths = Vec::new();
218 let mut item_count = 0;
219 let mut truncated = 0;
220 let limit = max_items.unwrap_or(usize::MAX);
221 let mut walker_seen_dirs = std::collections::HashSet::new();
222
223 let walker = WalkBuilder::new(root_path)
226 .max_depth(Some(max_depth))
227 .hidden(true) .build();
229
230 for entry in walker {
231 let entry = match entry {
232 Ok(e) => e,
233 Err(_) => continue,
234 };
235
236 if entry.path() == root_path {
238 continue;
239 }
240
241 if let Ok(relative_path) = entry.path().strip_prefix(root_path) {
242 if let Some(path_str) = relative_path.to_str() {
243 if !path_str.is_empty() {
244 if entry.depth() == 1
246 && entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false)
247 {
248 if let Some(dir_name) = relative_path.file_name() {
249 walker_seen_dirs.insert(dir_name.to_string_lossy().to_string());
250 }
251 }
252
253 if item_count >= limit {
254 truncated += 1;
255 continue;
256 }
257
258 if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
259 paths.push(format!("{path_str}/"));
260 } else {
261 paths.push(path_str.to_string());
262 }
263 item_count += 1;
264 }
265 }
266 }
267 }
268
269 if max_depth > 0 {
272 let entries = std::fs::read_dir(root_path)?;
273 for entry in entries {
274 let entry = match entry {
275 Ok(e) => e,
276 Err(_) => continue,
277 };
278
279 let path = entry.path();
280 if !path.is_dir() {
281 continue;
282 }
283
284 let file_name = match path.file_name() {
285 Some(name) => name.to_string_lossy().to_string(),
286 None => continue,
287 };
288
289 if walker_seen_dirs.contains(&file_name) {
291 continue;
292 }
293
294 if item_count >= limit {
296 truncated += 1;
297 continue;
298 }
299
300 let dir_item_count = Self::count_items_in_dir(&path);
302 if dir_item_count > 0 {
303 paths.push(format!("{file_name}/ ({dir_item_count} items)"));
304 } else {
305 paths.push(format!("{file_name}/ (empty)"));
306 }
307 item_count += 1;
308 }
309 }
310
311 Ok((paths, truncated))
312 }
313
314 fn count_items_in_dir(dir: &Path) -> usize {
316 std::fs::read_dir(dir)
317 .map(|entries| entries.count())
318 .unwrap_or(0)
319 }
320}
321
322pub struct EnvironmentUtils;
324
325impl EnvironmentUtils {
326 pub fn get_platform() -> &'static str {
328 if cfg!(target_os = "windows") {
329 "windows"
330 } else if cfg!(target_os = "macos") {
331 "macos"
332 } else if cfg!(target_os = "linux") {
333 "linux"
334 } else {
335 "unknown"
336 }
337 }
338
339 pub fn get_current_date() -> String {
341 use chrono::Local;
342 Local::now().format("%Y-%m-%d").to_string()
343 }
344
345 pub fn is_git_repo(path: &Path) -> bool {
347 gix::discover(path).is_ok()
348 }
349
350 pub fn read_readme(path: &Path) -> Option<String> {
352 let readme_path = path.join("README.md");
353 std::fs::read_to_string(readme_path).ok()
354 }
355
356 pub fn read_claude_md(path: &Path) -> Option<String> {
358 let claude_path = path.join("CLAUDE.md");
359 std::fs::read_to_string(claude_path).ok()
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366 use tempfile::tempdir;
367
368 #[test]
369 fn test_list_files_empty_dir() {
370 let temp_dir = tempdir().unwrap();
371 let files = FileListingUtils::list_files(temp_dir.path(), None, None).unwrap();
372 assert!(files.is_empty());
373 }
374
375 #[test]
376 fn test_list_files_with_content() {
377 let temp_dir = tempdir().unwrap();
378
379 std::fs::write(temp_dir.path().join("test.rs"), "test").unwrap();
381 std::fs::write(temp_dir.path().join("main.rs"), "main").unwrap();
382 std::fs::create_dir(temp_dir.path().join("src")).unwrap();
383 std::fs::write(temp_dir.path().join("src/lib.rs"), "lib").unwrap();
384
385 let files = FileListingUtils::list_files(temp_dir.path(), None, None).unwrap();
386 assert_eq!(files.len(), 4); assert!(files.contains(&"test.rs".to_string()));
388 assert!(files.contains(&"main.rs".to_string()));
389 assert!(files.contains(&"src/".to_string()));
390 assert!(files.contains(&"src/lib.rs".to_string()));
391 }
392
393 #[test]
394 fn test_list_files_with_query() {
395 let temp_dir = tempdir().unwrap();
396 std::fs::write(temp_dir.path().join("test.rs"), "test").unwrap();
397 std::fs::write(temp_dir.path().join("main.rs"), "main").unwrap();
398
399 let files = FileListingUtils::list_files(temp_dir.path(), Some("test"), None).unwrap();
400 assert_eq!(files.len(), 1);
401 assert_eq!(files[0], "test.rs");
402 }
403
404 #[test]
405 fn test_platform_detection() {
406 let platform = EnvironmentUtils::get_platform();
407 assert!(["windows", "macos", "linux", "unknown"].contains(&platform));
408 }
409
410 #[test]
411 fn test_date_format() {
412 let date = EnvironmentUtils::get_current_date();
413 assert_eq!(date.len(), 10);
415 assert_eq!(date.chars().nth(4), Some('-'));
416 assert_eq!(date.chars().nth(7), Some('-'));
417 }
418
419 #[test]
420 fn test_git_repo_detection() {
421 let temp_dir = tempdir().unwrap();
422 assert!(!EnvironmentUtils::is_git_repo(temp_dir.path()));
423
424 gix::init(temp_dir.path()).unwrap();
426 assert!(EnvironmentUtils::is_git_repo(temp_dir.path()));
427 }
428
429 #[test]
430 #[cfg(unix)]
431 fn test_list_files_skips_inaccessible() {
432 use std::os::unix::fs::PermissionsExt;
433
434 let temp_dir = tempdir().unwrap();
435
436 std::fs::write(temp_dir.path().join("readable.txt"), "test").unwrap();
438
439 let restricted_dir = temp_dir.path().join("restricted");
441 std::fs::create_dir(&restricted_dir).unwrap();
442 std::fs::write(restricted_dir.join("hidden.txt"), "secret").unwrap();
443
444 let mut perms = std::fs::metadata(&restricted_dir).unwrap().permissions();
446 perms.set_mode(0o000);
447 std::fs::set_permissions(&restricted_dir, perms).unwrap();
448
449 let files = FileListingUtils::list_files(temp_dir.path(), None, None).unwrap();
451
452 assert!(files.contains(&"readable.txt".to_string()));
454
455 let mut perms = std::fs::metadata(&restricted_dir).unwrap().permissions();
460 perms.set_mode(0o755);
461 std::fs::set_permissions(&restricted_dir, perms).unwrap();
462 }
463
464 #[test]
465 #[cfg(unix)]
466 fn test_directory_structure_skips_inaccessible() {
467 use std::os::unix::fs::PermissionsExt;
468
469 let temp_dir = tempdir().unwrap();
470
471 let accessible_dir = temp_dir.path().join("accessible");
473 std::fs::create_dir(&accessible_dir).unwrap();
474 std::fs::write(accessible_dir.join("file.txt"), "test").unwrap();
475
476 let restricted_dir = temp_dir.path().join("restricted");
478 std::fs::create_dir(&restricted_dir).unwrap();
479 std::fs::write(restricted_dir.join("hidden.txt"), "secret").unwrap();
480
481 let mut perms = std::fs::metadata(&restricted_dir).unwrap().permissions();
483 perms.set_mode(0o000);
484 std::fs::set_permissions(&restricted_dir, perms).unwrap();
485
486 let result =
488 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, None).unwrap();
489
490 assert!(result.contains("accessible/"));
492
493 let mut perms = std::fs::metadata(&restricted_dir).unwrap().permissions();
497 perms.set_mode(0o755);
498 std::fs::set_permissions(&restricted_dir, perms).unwrap();
499 }
500
501 #[test]
502 fn test_directory_structure_empty_dir() {
503 let temp_dir = tempdir().unwrap();
504 let expected = temp_dir.path().display().to_string();
505 let result =
506 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, None).unwrap();
507 assert_eq!(result, expected);
508 }
509
510 #[test]
511 fn test_directory_structure_with_gitignored_dirs() {
512 let temp_dir = tempdir().unwrap();
513
514 std::fs::write(
516 temp_dir.path().join(".gitignore"),
517 "target/\nnode_modules/\n*.log",
518 )
519 .unwrap();
520
521 std::fs::create_dir(temp_dir.path().join("src")).unwrap();
523 std::fs::write(temp_dir.path().join("src/main.rs"), "main").unwrap();
524 std::fs::write(temp_dir.path().join("Cargo.toml"), "cargo").unwrap();
525
526 std::fs::create_dir(temp_dir.path().join("target")).unwrap();
528 std::fs::create_dir(temp_dir.path().join("target/debug")).unwrap();
529 std::fs::write(temp_dir.path().join("target/debug/app"), "binary").unwrap();
530
531 std::fs::create_dir(temp_dir.path().join("node_modules")).unwrap();
532 std::fs::create_dir(temp_dir.path().join("node_modules/pkg1")).unwrap();
533 std::fs::create_dir(temp_dir.path().join("node_modules/pkg2")).unwrap();
534 std::fs::write(temp_dir.path().join("node_modules/pkg1/index.js"), "js").unwrap();
535
536 std::fs::create_dir(temp_dir.path().join(".git")).unwrap();
538 std::fs::write(temp_dir.path().join(".git/config"), "config").unwrap();
539 std::fs::write(temp_dir.path().join(".git/HEAD"), "HEAD").unwrap();
540
541 std::fs::write(temp_dir.path().join("debug.log"), "log").unwrap();
543
544 let mut expected_lines = [
548 temp_dir.path().display().to_string(),
549 ".git/ (2 items)".to_string(), "Cargo.toml".to_string(),
551 "node_modules/ (2 items)".to_string(), "src/".to_string(),
553 "src/main.rs".to_string(),
554 "target/ (1 items)".to_string(), ];
556 expected_lines.sort();
557 let expected = expected_lines.join("\n");
558
559 let result =
560 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, None).unwrap();
561 assert_eq!(result, expected);
562 }
563
564 #[test]
565 fn test_directory_structure_with_files() {
566 let temp_dir = tempdir().unwrap();
567
568 std::fs::write(temp_dir.path().join("file1.txt"), "content1").unwrap();
570 std::fs::write(temp_dir.path().join("file2.rs"), "content2").unwrap();
571
572 let mut expected_lines = [
573 temp_dir.path().display().to_string(),
574 "file1.txt".to_string(),
575 "file2.rs".to_string(),
576 ];
577 expected_lines.sort();
578 let expected = expected_lines.join("\n");
579
580 let result =
581 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, None).unwrap();
582 assert_eq!(result, expected);
583 }
584
585 #[test]
586 fn test_directory_structure_with_subdirs() {
587 let temp_dir = tempdir().unwrap();
588
589 std::fs::create_dir(temp_dir.path().join("src")).unwrap();
591 std::fs::create_dir(temp_dir.path().join("tests")).unwrap();
592 std::fs::write(temp_dir.path().join("src/main.rs"), "main").unwrap();
593 std::fs::write(temp_dir.path().join("tests/test.rs"), "test").unwrap();
594
595 let mut expected_lines = [
596 temp_dir.path().display().to_string(),
597 "src/".to_string(),
598 "src/main.rs".to_string(),
599 "tests/".to_string(),
600 "tests/test.rs".to_string(),
601 ];
602 expected_lines.sort();
603 let expected = expected_lines.join("\n");
604
605 let result =
606 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, None).unwrap();
607 assert_eq!(result, expected);
608 }
609
610 #[test]
611 fn test_directory_structure_max_depth_zero() {
612 let temp_dir = tempdir().unwrap();
613
614 std::fs::create_dir(temp_dir.path().join("src")).unwrap();
616 std::fs::write(temp_dir.path().join("src/lib.rs"), "lib").unwrap();
617
618 let expected = temp_dir.path().display().to_string();
619 let result =
620 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 0, None).unwrap();
621 assert_eq!(result, expected);
622 }
623
624 #[test]
625 fn test_directory_structure_max_depth_one() {
626 let temp_dir = tempdir().unwrap();
627
628 std::fs::create_dir(temp_dir.path().join("src")).unwrap();
630 std::fs::create_dir(temp_dir.path().join("src/nested")).unwrap();
631 std::fs::write(temp_dir.path().join("file.txt"), "root file").unwrap();
632 std::fs::write(temp_dir.path().join("src/lib.rs"), "lib").unwrap();
633 std::fs::write(temp_dir.path().join("src/nested/deep.rs"), "deep").unwrap();
634
635 let mut expected_lines = [
637 temp_dir.path().display().to_string(),
638 "file.txt".to_string(),
639 "src/".to_string(),
640 ];
641 expected_lines.sort();
642 let expected = expected_lines.join("\n");
643
644 let result =
645 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 1, None).unwrap();
646 assert_eq!(result, expected);
647 }
648
649 #[test]
650 fn test_directory_structure_deeply_nested() {
651 let temp_dir = tempdir().unwrap();
652
653 std::fs::create_dir(temp_dir.path().join("a")).unwrap();
655 std::fs::create_dir(temp_dir.path().join("a/b")).unwrap();
656 std::fs::create_dir(temp_dir.path().join("a/b/c")).unwrap();
657 std::fs::write(temp_dir.path().join("a/file1.txt"), "1").unwrap();
658 std::fs::write(temp_dir.path().join("a/b/file2.txt"), "2").unwrap();
659 std::fs::write(temp_dir.path().join("a/b/c/file3.txt"), "3").unwrap();
660
661 let mut expected_lines = [
664 temp_dir.path().display().to_string(),
665 "a/".to_string(),
666 "a/b/".to_string(),
667 "a/file1.txt".to_string(),
668 ];
669 expected_lines.sort();
670 let expected = expected_lines.join("\n");
671
672 let result =
673 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 2, None).unwrap();
674 assert_eq!(result, expected);
675 }
676
677 #[test]
678 fn test_directory_structure_mixed_content() {
679 let temp_dir = tempdir().unwrap();
680
681 std::fs::write(temp_dir.path().join("README.md"), "readme").unwrap();
683 std::fs::write(temp_dir.path().join("Cargo.toml"), "cargo").unwrap();
684 std::fs::create_dir(temp_dir.path().join("src")).unwrap();
685 std::fs::create_dir(temp_dir.path().join("tests")).unwrap();
686 std::fs::create_dir(temp_dir.path().join(".git")).unwrap();
687 std::fs::write(temp_dir.path().join("src/lib.rs"), "lib").unwrap();
688 std::fs::write(temp_dir.path().join("src/main.rs"), "main").unwrap();
689 std::fs::write(temp_dir.path().join("tests/integration.rs"), "test").unwrap();
690 std::fs::write(temp_dir.path().join(".git/config"), "config").unwrap();
691
692 let mut expected_lines = vec![
694 temp_dir.path().display().to_string(),
695 ".git/ (1 items)".to_string(), "Cargo.toml".to_string(),
697 "README.md".to_string(),
698 "src/".to_string(),
699 "src/lib.rs".to_string(),
700 "src/main.rs".to_string(),
701 "tests/".to_string(),
702 "tests/integration.rs".to_string(),
703 ];
704 expected_lines.sort();
705 let expected = expected_lines.join("\n");
706
707 let result =
708 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, None).unwrap();
709 assert_eq!(result, expected);
710 }
711
712 #[test]
713 fn test_directory_structure_with_hidden_files() {
714 let temp_dir = tempdir().unwrap();
715
716 std::fs::write(temp_dir.path().join("README.md"), "readme").unwrap();
718 std::fs::write(temp_dir.path().join(".env"), "secrets").unwrap(); std::fs::write(temp_dir.path().join(".gitignore"), "*.log").unwrap(); std::fs::create_dir(temp_dir.path().join("src")).unwrap();
722 std::fs::write(temp_dir.path().join("src/main.rs"), "main").unwrap();
723
724 std::fs::create_dir(temp_dir.path().join(".cache")).unwrap(); std::fs::write(temp_dir.path().join(".cache/data"), "cached").unwrap();
726
727 std::fs::create_dir(temp_dir.path().join(".hidden")).unwrap(); std::fs::create_dir(temp_dir.path().join(".hidden/nested")).unwrap();
729 std::fs::write(temp_dir.path().join(".hidden/file.txt"), "hidden").unwrap();
730
731 let mut expected_lines = [
734 temp_dir.path().display().to_string(),
735 ".cache/ (1 items)".to_string(), ".hidden/ (2 items)".to_string(), "README.md".to_string(),
739 "src/".to_string(),
740 "src/main.rs".to_string(),
741 ];
742 expected_lines.sort();
743 let expected = expected_lines.join("\n");
744
745 let result =
746 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, None).unwrap();
747 assert_eq!(result, expected);
748 }
749
750 #[test]
751 fn test_directory_structure_special_chars() {
752 let temp_dir = tempdir().unwrap();
753
754 std::fs::write(temp_dir.path().join("file with spaces.txt"), "content").unwrap();
756 std::fs::write(temp_dir.path().join("file-with-dashes.rs"), "content").unwrap();
757 std::fs::write(temp_dir.path().join("file_with_underscores.md"), "content").unwrap();
758 std::fs::create_dir(temp_dir.path().join("dir with spaces")).unwrap();
759
760 let mut expected_lines = [
761 temp_dir.path().display().to_string(),
762 "dir with spaces/".to_string(),
763 "file with spaces.txt".to_string(),
764 "file-with-dashes.rs".to_string(),
765 "file_with_underscores.md".to_string(),
766 ];
767 expected_lines.sort();
768 let expected = expected_lines.join("\n");
769
770 let result =
771 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, None).unwrap();
772 assert_eq!(result, expected);
773 }
774
775 #[test]
776 fn test_directory_structure_with_max_items_limit() {
777 let temp_dir = tempdir().unwrap();
778
779 for i in 0..20 {
781 std::fs::write(temp_dir.path().join(format!("file{i:02}.txt")), "content").unwrap();
782 }
783
784 let result =
786 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, Some(5)).unwrap();
787
788 let lines: Vec<&str> = result.lines().collect();
789
790 assert_eq!(lines[0], temp_dir.path().display().to_string());
792 assert_eq!(lines.len(), 7); assert_eq!(lines[6], "... and 15 more items");
794
795 for line in lines.iter().take(6).skip(1) {
797 assert!(line.ends_with(".txt"));
798 }
799 }
800
801 #[test]
802 fn test_directory_structure_with_dirs_and_max_items() {
803 let temp_dir = tempdir().unwrap();
804
805 std::fs::create_dir(temp_dir.path().join("dir1")).unwrap();
807 std::fs::create_dir(temp_dir.path().join("dir2")).unwrap();
808 std::fs::write(temp_dir.path().join("file1.txt"), "content").unwrap();
809 std::fs::write(temp_dir.path().join("file2.txt"), "content").unwrap();
810 std::fs::create_dir(temp_dir.path().join("dir3")).unwrap();
811
812 let result =
814 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, Some(3)).unwrap();
815
816 let expected = format!(
817 "{}\ndir2/\nfile1.txt\nfile2.txt\n... and 2 more items",
818 temp_dir.path().display()
819 );
820
821 assert_eq!(result, expected);
822 }
823
824 #[test]
825 fn test_directory_structure_no_truncation_when_under_limit() {
826 let temp_dir = tempdir().unwrap();
827
828 std::fs::write(temp_dir.path().join("file1.txt"), "content").unwrap();
830 std::fs::write(temp_dir.path().join("file2.txt"), "content").unwrap();
831 std::fs::create_dir(temp_dir.path().join("subdir")).unwrap();
832
833 let result =
835 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, Some(100))
836 .unwrap();
837
838 assert!(!result.contains("... and"));
840 assert!(!result.contains("more items"));
841
842 let lines: Vec<&str> = result.lines().collect();
843 assert_eq!(lines.len(), 4); }
845
846 #[test]
847 fn test_directory_structure_with_hidden_dirs_and_limit() {
848 let temp_dir = tempdir().unwrap();
849
850 for i in 0..5 {
852 std::fs::write(temp_dir.path().join(format!("file{i}.txt")), "content").unwrap();
853 }
854
855 std::fs::create_dir(temp_dir.path().join(".hidden1")).unwrap();
857 std::fs::write(temp_dir.path().join(".hidden1/file.txt"), "hidden").unwrap();
858
859 std::fs::create_dir(temp_dir.path().join(".hidden2")).unwrap();
860 std::fs::write(temp_dir.path().join(".hidden2/file.txt"), "hidden").unwrap();
861
862 let result =
864 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, Some(4)).unwrap();
865
866 let lines: Vec<&str> = result.lines().collect();
867
868 assert_eq!(lines[0], temp_dir.path().display().to_string());
870 assert_eq!(lines.len(), 6); assert_eq!(lines[5], "... and 3 more items");
872
873 for line in lines.iter().take(5).skip(1) {
876 assert!(line.ends_with(".txt"));
877 }
878 }
879}