Skip to main content

rs_hack/commands/
neighbors.rs

1//! `neighbors` command: pure filesystem discovery of related files.
2//! No AST parsing — finds siblings, twin dirs, and test files for any .rs file.
3
4use std::path::{Path, PathBuf};
5
6use anyhow::Result;
7
8#[derive(Debug)]
9pub struct NeighborsReport {
10    pub target: PathBuf,
11    pub siblings: Vec<PathBuf>,
12    pub twin_files: Vec<PathBuf>,
13    pub test_files: Vec<PathBuf>,
14}
15
16pub fn run(path: &Path) -> Result<NeighborsReport> {
17    let path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
18
19    let parent = path.parent().unwrap_or_else(|| Path::new("."));
20    let stem = path
21        .file_stem()
22        .and_then(|s| s.to_str())
23        .unwrap_or("")
24        .to_string();
25
26    // --- Siblings: other .rs files in same directory ---
27    let mut siblings = Vec::new();
28    if let Ok(entries) = std::fs::read_dir(parent) {
29        for entry in entries.flatten() {
30            let ep = entry.path();
31            if ep == path {
32                continue;
33            }
34            if ep.extension().and_then(|s| s.to_str()) == Some("rs") {
35                siblings.push(ep);
36            }
37        }
38    }
39    siblings.sort();
40
41    // --- Twin dirs: walk up 2 ancestor levels, find sibling dirs whose name
42    //     is the input parent's name + digit suffix, collect matching filenames ---
43    let mut twin_files = Vec::new();
44
45    let parent_name = parent
46        .file_name()
47        .and_then(|s| s.to_str())
48        .unwrap_or("")
49        .to_string();
50
51    // grandparent (level 1 up from parent) and great-grandparent (level 2)
52    let mut search_roots: Vec<PathBuf> = Vec::new();
53    if let Some(grandparent) = parent.parent() {
54        search_roots.push(grandparent.to_path_buf());
55        if let Some(great) = grandparent.parent() {
56            search_roots.push(great.to_path_buf());
57        }
58    }
59
60    for root in &search_roots {
61        if let Ok(entries) = std::fs::read_dir(root) {
62            for entry in entries.flatten() {
63                let ep = entry.path();
64                if !ep.is_dir() {
65                    continue;
66                }
67                let dir_name = ep
68                    .file_name()
69                    .and_then(|s| s.to_str())
70                    .unwrap_or("")
71                    .to_string();
72                // Twin: parent_name + at least one digit suffix
73                if dir_name.starts_with(&parent_name)
74                    && dir_name.len() > parent_name.len()
75                    && dir_name[parent_name.len()..]
76                        .chars()
77                        .all(|c| c.is_ascii_digit())
78                {
79                    // Walk into this twin dir looking for files whose stem matches
80                    collect_matching_files(&ep, &stem, &mut twin_files);
81                }
82            }
83        }
84    }
85    twin_files.sort();
86
87    // --- Tests: nearest `tests/` dir at or above parent's grandparent ---
88    let mut test_files = Vec::new();
89
90    let mut search = Some(parent);
91    while let Some(dir) = search {
92        let tests_dir = dir.join("tests");
93        if tests_dir.is_dir() {
94            collect_matching_files(&tests_dir, &stem, &mut test_files);
95            break;
96        }
97        search = dir.parent();
98    }
99    test_files.sort();
100
101    Ok(NeighborsReport {
102        target: path,
103        siblings,
104        twin_files,
105        test_files,
106    })
107}
108
109/// Walk `dir` recursively and collect all .rs files whose stem contains `stem`.
110fn collect_matching_files(dir: &Path, stem: &str, out: &mut Vec<PathBuf>) {
111    if let Ok(entries) = std::fs::read_dir(dir) {
112        for entry in entries.flatten() {
113            let ep = entry.path();
114            if ep.is_dir() {
115                collect_matching_files(&ep, stem, out);
116            } else if ep.extension().and_then(|s| s.to_str()) == Some("rs") {
117                let file_stem = ep.file_stem().and_then(|s| s.to_str()).unwrap_or("");
118                if file_stem.contains(stem) {
119                    out.push(ep);
120                }
121            }
122        }
123    }
124}