Skip to main content

fff_search/
path_utils.rs

1use std::path::{Path, PathBuf};
2
3#[cfg(windows)]
4pub fn canonicalize(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
5    dunce::canonicalize(path)
6}
7
8#[cfg(not(windows))]
9pub fn canonicalize(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
10    std::fs::canonicalize(path)
11}
12
13#[cfg(windows)]
14pub fn expand_tilde(path: &str) -> PathBuf {
15    return PathBuf::from(path);
16}
17
18#[cfg(not(windows))]
19pub fn expand_tilde(path: &str) -> PathBuf {
20    if let Some(stripped) = path.strip_prefix("~/")
21        && let Some(home_dir) = dirs::home_dir()
22    {
23        return home_dir.join(stripped);
24    }
25
26    PathBuf::from(path)
27}
28
29/// Calculate distance penalty based on directory proximity.
30/// Returns a negative penalty score based on how far the candidate is from the current file.
31///
32/// `candidate_dir` is the directory portion of the candidate path (e.g. `"src/components/"`).
33/// It may have a trailing `/` which is stripped internally.
34///
35/// Zero-allocation: walks both directory part iterators in lockstep.
36pub fn calculate_distance_penalty(current_file: Option<&str>, candidate_dir: &str) -> i32 {
37    let Some(current_path) = current_file else {
38        return 0;
39    };
40
41    let current_dir = Path::new(current_path).parent().unwrap_or(Path::new(""));
42    let candidate = Path::new(candidate_dir);
43
44    if current_dir == candidate {
45        return 0;
46    }
47
48    let mut current_parts = current_dir.components();
49    let mut candidate_parts = candidate.components();
50
51    let mut common_len = 0usize;
52    let mut current_total = 0usize;
53
54    loop {
55        match (current_parts.next(), candidate_parts.next()) {
56            (Some(a), Some(b)) => {
57                current_total += 1;
58                if a == b {
59                    common_len += 1;
60                } else {
61                    current_total += current_parts.count();
62                    break;
63                }
64            }
65            (Some(_), None) => {
66                current_total += 1 + current_parts.count();
67                break;
68            }
69            (None, _) => {
70                break;
71            }
72        }
73    }
74
75    let depth_from_common = current_total - common_len;
76    if depth_from_common == 0 {
77        return 0;
78    }
79
80    (-(depth_from_common as i32)).max(-20)
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    #[cfg(not(target_family = "windows"))]
89    fn test_calculate_distance_penalty() {
90        // candidate_dir is now just the directory portion (with or without trailing /)
91        assert_eq!(calculate_distance_penalty(None, "examples/user/test/"), 0);
92        // Same directory
93        assert_eq!(
94            calculate_distance_penalty(Some("examples/user/test/main.rs"), "examples/user/test/"),
95            0
96        );
97        //
98        // One level apart
99        assert_eq!(
100            calculate_distance_penalty(
101                Some("examples/user/test/subdir/file.rs"),
102                "examples/user/test/"
103            ),
104            -1
105        );
106        //
107        // Different subdirectories (same parent)
108        assert_eq!(
109            calculate_distance_penalty(
110                Some("examples/user/test/dir1/file.rs"),
111                "examples/user/test/dir2/"
112            ),
113            -1
114        );
115
116        assert_eq!(
117            calculate_distance_penalty(
118                Some("examples/audio-announce/src/lib/audio-announce.rs"),
119                "examples/audio-announce/src/"
120            ),
121            -1
122        );
123
124        assert_eq!(
125            calculate_distance_penalty(
126                Some("examples/audio-announce/src/audio-announce.rs"),
127                "examples/pixel/src/"
128            ),
129            -2
130        );
131
132        // Root level files (empty dir)
133        assert_eq!(calculate_distance_penalty(Some("main.rs"), ""), 0);
134    }
135
136    #[test]
137    #[cfg(target_family = "windows")]
138    fn distance_penalty_works_on_windows() {
139        assert_eq!(
140            calculate_distance_penalty(None, "examples\\user\\test\\"),
141            0
142        );
143        // Same directory
144        assert_eq!(
145            calculate_distance_penalty(
146                Some("examples\\user\\test\\main.rs"),
147                "examples\\user\\test\\"
148            ),
149            0
150        );
151        //
152        // One level apart
153        assert_eq!(
154            calculate_distance_penalty(
155                Some("examples\\user\\test\\subdir\\file.rs"),
156                "examples\\user\\test\\"
157            ),
158            -1
159        );
160    }
161}