ts_path/
relative.rs

1//! Find the path to navigate from a source path to a target path.
2
3use core::iter::repeat_n;
4use std::{
5    env::current_dir,
6    fs,
7    path::{Component, Path, PathBuf},
8};
9
10use crate::NormalizePath;
11
12/// Extension trait to get the relative path.
13pub trait RelativePath {
14    /// Returns the path to navigate from a source path to self.
15    fn relative_to(&self, source: &Path) -> PathBuf;
16
17    /// Returns the path to navigate from the current directory to self.
18    fn relative_to_cwd(&self) -> PathBuf {
19        let current_dur = current_dir().unwrap_or_else(|_| PathBuf::from("./"));
20        self.relative_to(&current_dur)
21    }
22}
23
24impl<P: AsRef<Path>> RelativePath for P {
25    fn relative_to(&self, source: &Path) -> PathBuf {
26        relative_path(source, self.as_ref())
27    }
28}
29
30/// Returns the path to navigate from a source path to a target path.
31pub fn relative_path(source: &Path, target: &Path) -> PathBuf {
32    let source = fs::canonicalize(source)
33        .unwrap_or_else(|_| source.to_path_buf())
34        .normalized();
35    let source: Vec<_> = source.components().collect();
36
37    let target = fs::canonicalize(target)
38        .unwrap_or_else(|_| target.to_path_buf())
39        .normalized();
40    let target: Vec<_> = target.components().collect();
41
42    let diverge_index = {
43        let mut index = 0;
44
45        for source_component in source.iter() {
46            let Some(target_component) = target.get(index) else {
47                break;
48            };
49
50            if source_component != target_component {
51                break;
52            }
53
54            index += 1;
55        }
56
57        index
58    };
59
60    let output_components: Vec<_> = repeat_n(&Component::ParentDir, source.len() - diverge_index)
61        .chain(target.get(diverge_index..).unwrap_or_default())
62        .collect();
63
64    if output_components.is_empty() {
65        PathBuf::from_iter(&[Component::CurDir])
66    } else {
67        PathBuf::from_iter(output_components)
68    }
69}
70
71#[cfg(test)]
72mod test {
73    use std::path::{Path, PathBuf};
74
75    use crate::relative::RelativePath;
76
77    #[test]
78    fn handles_relative() {
79        let source = Path::new("/root/dir-a/dir-b");
80        let target = Path::new("/root/dir-c/dir-d");
81        assert_eq!(
82            PathBuf::from("../../dir-c/dir-d"),
83            target.relative_to(source)
84        );
85
86        let source = Path::new("dir-a/dir-b");
87        let target = Path::new("dir-a/dir-b");
88        assert_eq!(PathBuf::from("."), target.relative_to(source));
89
90        let source = Path::new("../dir-a/dir-b");
91        let target = Path::new("./dir-a/./dir-b");
92        assert_eq!(
93            PathBuf::from("../../../dir-a/dir-b"),
94            target.relative_to(source)
95        );
96    }
97
98    #[test]
99    fn handles_current_dir() {
100        let target = Path::new("../ts-ansi/src/lib.rs")
101            .canonicalize()
102            .expect("canonicalize to succeed");
103        assert_eq!(Path::new("../ts-ansi/src/lib.rs"), target.relative_to_cwd());
104    }
105}