Skip to main content

whycodes_index/
store.rs

1//! In-memory entry store: the source of truth for tools and browse mode.
2//!
3//! Entries are root-relative `/`-separated paths. A `FxHashMap` index gives
4//! O(1) dedup and removal for watcher deltas; linear scans back `browse` and
5//! glob filtering (50k entries ≈ single-digit ms, cheaper than any syscall
6//! storm the tools used to pay per call).
7
8use rustc_hash::FxHashMap;
9
10/// One indexed entry.
11#[derive(Debug, Clone)]
12pub struct Entry {
13    /// Root-relative path with `/` separators, e.g. `crates/tui/src/app.rs`.
14    pub rel: Box<str>,
15    /// Which root this entry belongs to (index into `WorkspaceIndex::roots`).
16    pub root: u16,
17    /// True for directories.
18    pub is_dir: bool,
19    /// File size in bytes (0 for dirs and symlinks).
20    pub size: u64,
21}
22
23/// Per-root view of the entry store.
24#[derive(Debug, Default)]
25pub struct IndexStore {
26    entries: Vec<Entry>,
27    /// `(root, rel)` → position in `entries`.
28    by_key: FxHashMap<(u16, Box<str>), u32>,
29}
30
31impl IndexStore {
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    pub fn len(&self) -> usize {
37        self.entries.len()
38    }
39
40    pub fn is_empty(&self) -> bool {
41        self.entries.is_empty()
42    }
43
44    pub fn entries(&self) -> &[Entry] {
45        &self.entries
46    }
47
48    /// Insert or update an entry. Returns true when newly inserted.
49    pub fn insert(&mut self, root: u16, rel: Box<str>, is_dir: bool, size: u64) -> bool {
50        let key = (root, rel.clone());
51        if let Some(&idx) = self.by_key.get(&key) {
52            let e = &mut self.entries[idx as usize];
53            e.is_dir = is_dir;
54            e.size = size;
55            return false;
56        }
57        let idx = self.entries.len() as u32;
58        self.entries.push(Entry {
59            rel,
60            root,
61            is_dir,
62            size,
63        });
64        self.by_key.insert(key, idx);
65        true
66    }
67
68    /// Remove one exact entry. Returns true when it existed.
69    pub fn remove(&mut self, root: u16, rel: &str) -> bool {
70        // Lookup allocates an owned key; watcher deltas are rare, so the
71        // simple path beats raw-entry hashing here.
72        let key = (root, Box::<str>::from(rel));
73        let Some(idx) = self.by_key.remove(&key) else {
74            return false;
75        };
76        let idx = idx as usize;
77        self.entries.swap_remove(idx);
78        if idx < self.entries.len() {
79            let moved = &self.entries[idx];
80            let moved_key = (moved.root, moved.rel.clone());
81            self.by_key.insert(moved_key, idx as u32);
82        }
83        true
84    }
85
86    /// Remove an entry and every descendant (watcher dir-removal).
87    pub fn remove_tree(&mut self, root: u16, rel: &str) -> usize {
88        let prefix = format!("{rel}/");
89        let mut removed = 0usize;
90        // Collect first to keep swap_remove bookkeeping simple.
91        let victims: Vec<Box<str>> = self
92            .entries
93            .iter()
94            .filter(|e| e.root == root && (&*e.rel == rel || e.rel.starts_with(&prefix)))
95            .map(|e| e.rel.clone())
96            .collect();
97        for rel in victims {
98            if self.remove(root, &rel) {
99                removed += 1;
100            }
101        }
102        removed
103    }
104
105    pub fn clear(&mut self) {
106        self.entries.clear();
107        self.by_key.clear();
108    }
109
110    /// Depth-1 children of `rel_dir` within `root` (`""` = top level),
111    /// dirs first then case-insensitive name order.
112    pub fn browse(&self, root: u16, rel_dir: &str) -> Vec<&Entry> {
113        let prefix = if rel_dir.is_empty() {
114            String::new()
115        } else {
116            format!("{}/", rel_dir.trim_end_matches('/'))
117        };
118        let mut out: Vec<&Entry> = self
119            .entries
120            .iter()
121            .filter(|e| {
122                e.root == root && e.rel.starts_with(prefix.as_str()) && {
123                    let rest = &e.rel[prefix.len()..];
124                    !rest.is_empty() && !rest.contains('/')
125                }
126            })
127            .collect();
128        out.sort_by(|a, b| {
129            b.is_dir
130                .cmp(&a.is_dir)
131                .then_with(|| a.rel.to_ascii_lowercase().cmp(&b.rel.to_ascii_lowercase()))
132        });
133        out
134    }
135
136    /// Iterate entries of one root without cloning.
137    pub fn iter_root(&self, root: u16) -> impl Iterator<Item = &Entry> {
138        self.entries.iter().filter(move |e| e.root == root)
139    }
140}
141
142#[cfg(test)]
143#[path = "store_tests.rs"]
144mod tests;