Skip to main content

lattice_listing/
dir.rs

1//! One directory read, shared by both listing majors.
2//!
3//! DL.7: oil and the file tree each carried their own `read_dir` +
4//! dirs-first-alpha sort (`read_dir_entries` / `read_dir_sorted`),
5//! differing only in whether they yielded a `String` name or a
6//! `PathBuf`. Same syscall, same ordering rule, same error handling,
7//! written twice — and the ordering rule is user-visible, so a change
8//! to one would have silently disagreed with the other.
9//!
10//! This is the *whole* of what those two modules genuinely shared.
11//! Their entry models and their rope renderers deliberately did not
12//! converge — see the module docs on each for why.
13
14use std::path::{Path, PathBuf};
15
16/// One filesystem entry, in the shape both listings need.
17///
18/// Carries the name and the full path because the two majors want
19/// different halves: oil's rope is bare names (its `:w` diff reads
20/// them as filenames), while the tree keys rows and icons off paths.
21/// Computing both once here is cheaper than either caller
22/// re-deriving the other from a `read_dir` result it has already
23/// dropped.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct DirEntry {
26    pub path: PathBuf,
27    pub name: String,
28    pub is_dir: bool,
29}
30
31/// Read `dir`, sorted **directories first, then alphabetically** —
32/// the ordering both listings present and the one a user navigates by
33/// muscle memory.
34///
35/// Ordering is by `name`, not by `path`. Within a single directory
36/// the two agree, so this is not a behaviour change; naming the
37/// weaker key would just invite a future caller to pass entries from
38/// more than one directory and get a surprising order.
39///
40/// I/O errors propagate: a listing that silently dropped unreadable
41/// entries would show a directory that does not match the disk, and
42/// oil would then diff `:w` against that wrong picture.
43pub fn read_dir_sorted(dir: &Path) -> std::io::Result<Vec<DirEntry>> {
44    let mut out: Vec<DirEntry> = Vec::new();
45    for entry in std::fs::read_dir(dir)? {
46        let entry = entry?;
47        let is_dir = entry.file_type()?.is_dir();
48        out.push(DirEntry {
49            path: entry.path(),
50            name: entry.file_name().to_string_lossy().into_owned(),
51            is_dir,
52        });
53    }
54    out.sort_by(|a, b| match (a.is_dir, b.is_dir) {
55        (true, false) => std::cmp::Ordering::Less,
56        (false, true) => std::cmp::Ordering::Greater,
57        _ => a.name.cmp(&b.name),
58    });
59    Ok(out)
60}
61
62#[cfg(test)]
63mod tests {
64    #![allow(clippy::unwrap_used)]
65    use super::*;
66
67    fn tempdir(tag: &str) -> PathBuf {
68        use std::sync::atomic::{AtomicU64, Ordering};
69        static COUNTER: AtomicU64 = AtomicU64::new(0);
70        let nanos = std::time::SystemTime::now()
71            .duration_since(std::time::UNIX_EPOCH)
72            .map(|d| d.as_nanos())
73            .unwrap_or(0);
74        let n = COUNTER.fetch_add(1, Ordering::Relaxed);
75        let d = std::env::temp_dir().join(format!("lattice-dir-{tag}-{nanos}-{n}"));
76        std::fs::create_dir_all(&d).unwrap();
77        d
78    }
79
80    #[test]
81    fn directories_sort_before_files_then_alphabetically() {
82        let dir = tempdir("sort");
83        std::fs::write(dir.join("b.txt"), "").unwrap();
84        std::fs::write(dir.join("a.txt"), "").unwrap();
85        std::fs::create_dir(dir.join("zeta")).unwrap();
86        std::fs::create_dir(dir.join("alpha")).unwrap();
87
88        let names: Vec<String> = read_dir_sorted(&dir)
89            .unwrap()
90            .into_iter()
91            .map(|e| e.name)
92            .collect();
93        assert_eq!(names, vec!["alpha", "zeta", "a.txt", "b.txt"]);
94
95        std::fs::remove_dir_all(dir).ok();
96    }
97
98    #[test]
99    fn each_entry_carries_both_halves_the_majors_need() {
100        let dir = tempdir("halves");
101        std::fs::create_dir(dir.join("sub")).unwrap();
102        std::fs::write(dir.join("f.rs"), "").unwrap();
103
104        let entries = read_dir_sorted(&dir).unwrap();
105        let sub = &entries[0];
106        assert_eq!(sub.name, "sub", "oil's rope reads the bare name");
107        assert_eq!(
108            sub.path,
109            dir.join("sub"),
110            "the tree keys rows and icons off the path"
111        );
112        assert!(sub.is_dir);
113        assert!(!entries[1].is_dir);
114
115        std::fs::remove_dir_all(dir).ok();
116    }
117
118    /// An unreadable directory is an error, not an empty listing.
119    /// Oil diffs `:w` against what this returns, so a silently empty
120    /// read would look like "the user deleted everything".
121    #[test]
122    fn a_missing_directory_is_an_error_not_an_empty_listing() {
123        let missing = std::env::temp_dir().join("lattice-dir-does-not-exist-please");
124        let _ = std::fs::remove_dir_all(&missing);
125        assert!(read_dir_sorted(&missing).is_err());
126    }
127}