Skip to main content

fancy_tree/sorting/
mod.rs

1//! Module for sorting paths.
2pub use direction::Direction;
3pub use directories::Directories;
4pub use method::Method;
5use mlua::{FromLua, Lua};
6use std::borrow::Cow;
7use std::cmp::Ordering;
8use std::ffi::OsStr;
9use std::path::Path;
10
11mod direction;
12mod directories;
13mod method;
14
15/// Sorting options for paths.
16///
17/// The sorting priorities are:
18///
19/// 1. directories
20/// 2. method
21#[derive(Debug)]
22#[non_exhaustive]
23pub struct Sorting {
24    /// How sorting should be done overall.
25    pub method: Method,
26    /// The direction to sort.
27    pub direction: Direction,
28    /// Where to place directories.
29    pub directories: Directories,
30    /// Whether to ignore case or not.
31    ///
32    /// Defaults to `false` on Windows, `true` otherwise.
33    pub ignore_case: bool,
34    /// Ignore the leading dot in dotfiles.
35    ///
36    /// Assuming case is ignored, here is the difference:
37    ///
38    /// # Keeping dot
39    ///
40    /// 1. `.dockerignore`
41    /// 2. `.editorconfig`
42    /// 3. `Dockerfile`
43    ///
44    /// # Ignoring dot
45    ///
46    /// 1. `.dockerignore`
47    /// 2. `Dockerfile`
48    /// 3. `.editorconfig`
49    pub ignore_dot: bool,
50}
51
52impl Sorting {
53    /// Default value for whether or not to ignore case.
54    const DEFAULT_IGNORE_CASE: bool = cfg!(windows);
55
56    /// Default value for if to ignore the dot in a dotfile.
57    const DEFAULT_IGNORE_DOT: bool = false;
58
59    /// Cleans the dot if necessary.
60    fn clean_dot<'a>(&self, os_str: &'a OsStr) -> &'a OsStr {
61        if self.ignore_dot {
62            let bytes = os_str.as_encoded_bytes();
63            let bytes = bytes.strip_prefix(b".").unwrap_or(bytes);
64            // SAFETY:
65            // - Bytes are all from a valid OsStr and should be safe for conversion.
66            unsafe { OsStr::from_encoded_bytes_unchecked(bytes) }
67        } else {
68            os_str
69        }
70    }
71
72    /// Cleans up casing for case-insensitive ordering if necessary.
73    fn clean_casing<'a>(&self, os_str: &'a OsStr) -> Cow<'a, OsStr> {
74        if self.ignore_case {
75            Cow::from(os_str.to_ascii_lowercase())
76        } else {
77            Cow::from(os_str)
78        }
79    }
80
81    /// Cleans the filename for the path.
82    fn clean_path<'a>(&self, path: &'a Path) -> Cow<'a, OsStr> {
83        let file_name = path
84            .file_name()
85            .expect("Path should always terminate in a named component");
86        let file_name = self.clean_dot(file_name);
87        self.clean_casing(file_name)
88    }
89
90    /// Compares two paths.
91    pub fn cmp<L, R>(&self, left: L, right: R) -> Ordering
92    where
93        L: AsRef<Path>,
94        R: AsRef<Path>,
95    {
96        let ordering = self.directories.cmp(&left, &right).then_with(|| {
97            let left = self.clean_path(left.as_ref());
98            let right = self.clean_path(right.as_ref());
99            self.method.cmp(left, right)
100        });
101        match self.direction {
102            Direction::Asc => ordering,
103            Direction::Desc => ordering.reverse(),
104        }
105    }
106}
107
108impl Default for Sorting {
109    fn default() -> Self {
110        Self {
111            method: Default::default(),
112            direction: Default::default(),
113            directories: Default::default(),
114            ignore_case: Self::DEFAULT_IGNORE_CASE,
115            ignore_dot: Self::DEFAULT_IGNORE_DOT,
116        }
117    }
118}
119
120impl FromLua for Sorting {
121    fn from_lua(value: mlua::Value, lua: &Lua) -> mlua::Result<Self> {
122        let table = mlua::Table::from_lua(value, lua)?;
123        let method = table.get::<Option<Method>>("method")?.unwrap_or_default();
124        let direction = table
125            .get::<Option<Direction>>("direction")?
126            .unwrap_or_default();
127        let directories = table
128            .get::<Option<Directories>>("directories")?
129            .unwrap_or_default();
130        let ignore_case = table
131            .get::<Option<bool>>("ignore_case")?
132            .unwrap_or(Self::DEFAULT_IGNORE_CASE);
133        let ignore_dot = table
134            .get::<Option<bool>>("ignore_dot")?
135            .unwrap_or(Self::DEFAULT_IGNORE_DOT);
136
137        let sorting = Self {
138            method,
139            direction,
140            directories,
141            ignore_case,
142            ignore_dot,
143        };
144        Ok(sorting)
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use rstest::rstest;
152
153    #[rstest]
154    #[case(false, ".env", ".env")]
155    #[case(true, ".env", "env")]
156    #[case(true, "foo.txt", "foo.txt")]
157    fn test_clean_dot(#[case] ignore_dot: bool, #[case] s: &str, #[case] expected: &str) {
158        let sorting = Sorting {
159            ignore_dot,
160            ..Default::default()
161        };
162
163        assert_eq!(OsStr::new(expected), sorting.clean_dot(OsStr::new(s)))
164    }
165
166    #[rstest]
167    #[case(false, "Dockerfile", "Dockerfile")]
168    #[case(true, "Dockerfile", "dockerfile")]
169    fn test_clean_casing(#[case] ignore_case: bool, #[case] s: &str, #[case] expected: &str) {
170        let sorting = Sorting {
171            ignore_case,
172            ..Default::default()
173        };
174
175        assert_eq!(OsStr::new(expected), sorting.clean_casing(OsStr::new(s)))
176    }
177}