use std::collections::BTreeMap;
use super::ExplorerState;
#[derive(Debug, Clone)]
pub struct ExplorerNode {
pub name: String,
pub is_dir: bool,
pub depth: usize,
pub rel_path: String,
}
impl ExplorerState {
pub fn selection(&self) -> Option<&ExplorerNode> {
self.visible
.get(self.selected)
.and_then(|&i| self.nodes.get(i))
}
pub fn toggle_selected(&mut self) -> bool {
let Some(&node_idx) = self.visible.get(self.selected) else {
return false;
};
let n = &self.nodes[node_idx];
if !n.is_dir {
return false;
}
if self.expanded.contains(&n.rel_path) {
self.expanded.remove(&n.rel_path);
} else {
self.expanded.insert(n.rel_path.clone());
}
true
}
pub fn move_down(&mut self) {
if !self.visible.is_empty() {
self.selected = (self.selected + 1).min(self.visible.len() - 1);
}
}
pub fn move_up(&mut self) {
self.selected = self.selected.saturating_sub(1);
}
pub fn collapse_or_parent(&mut self) {
let Some(&node_idx) = self.visible.get(self.selected) else {
return;
};
let n = &self.nodes[node_idx];
if n.is_dir && self.expanded.contains(&n.rel_path) {
self.expanded.remove(&n.rel_path);
self.refilter();
return;
}
if n.depth == 0 {
return;
}
for i in (0..self.selected).rev() {
let node = &self.nodes[self.visible[i]];
if node.depth < n.depth {
self.selected = i;
return;
}
}
}
pub fn expand_or_descend(&mut self) {
let Some(&node_idx) = self.visible.get(self.selected) else {
return;
};
let (is_dir, rel_path, depth) = {
let n = &self.nodes[node_idx];
(n.is_dir, n.rel_path.clone(), n.depth)
};
if is_dir && !self.expanded.contains(&rel_path) {
self.expanded.insert(rel_path);
self.refilter();
if let Some(&next) = self.visible.get(self.selected + 1)
&& self.nodes[next].depth > depth
{
self.selected += 1;
}
return;
}
self.move_down();
}
pub fn select_by_path(&mut self, rel: &str) {
let mut prefix = rel;
while let Some(slash) = prefix.rfind('/') {
prefix = &prefix[..slash];
self.expanded.insert(prefix.to_string());
}
self.refilter();
if let Some(pos) = self
.visible
.iter()
.position(|&i| self.nodes[i].rel_path == rel)
{
self.selected = pos;
}
}
}
pub(super) fn build_nodes(files: &[String], dirs: &[String]) -> Vec<ExplorerNode> {
let mut children_by_parent: BTreeMap<String, BTreeMap<String, bool>> = BTreeMap::new();
for d in dirs {
let parts: Vec<&str> = d.split('/').collect();
let mut acc = String::new();
for part in &parts {
children_by_parent
.entry(acc.clone())
.or_default()
.insert((*part).to_string(), false);
if !acc.is_empty() {
acc.push('/');
}
acc.push_str(part);
}
}
for f in files {
let parts: Vec<&str> = f.split('/').collect();
let mut acc = String::new();
for (i, part) in parts.iter().enumerate() {
let is_last = i == parts.len() - 1;
children_by_parent
.entry(acc.clone())
.or_default()
.entry((*part).to_string())
.and_modify(|v| {
if !is_last {
*v = false; }
})
.or_insert(is_last); if !acc.is_empty() {
acc.push('/');
}
acc.push_str(part);
}
}
let mut out = Vec::new();
let mut stack: Vec<(String, usize)> = Vec::new();
push_children(&mut out, &children_by_parent, "", 0, &mut stack);
out
}
fn push_children(
out: &mut Vec<ExplorerNode>,
map: &BTreeMap<String, BTreeMap<String, bool>>,
parent: &str,
depth: usize,
_stack: &mut Vec<(String, usize)>,
) {
let Some(children) = map.get(parent) else {
return;
};
let mut entries: Vec<(&String, &bool)> = children.iter().collect();
entries.sort_by(|a, b| match (a.1, b.1) {
(false, true) => std::cmp::Ordering::Less,
(true, false) => std::cmp::Ordering::Greater,
_ => a.0.cmp(b.0),
});
for (name, is_file) in entries {
let is_dir = !*is_file;
let rel_path = if parent.is_empty() {
name.clone()
} else {
format!("{}/{}", parent, name)
};
out.push(ExplorerNode {
name: name.clone(),
is_dir,
depth,
rel_path: rel_path.clone(),
});
if is_dir {
push_children(out, map, &rel_path, depth + 1, _stack);
}
}
}
#[cfg(test)]
mod tests {
use super::super::tests::paths;
use super::*;
#[test]
fn build_nodes_dirs_before_files() {
let n = build_nodes(&paths(), &[]);
let rels: Vec<&str> = n.iter().map(|x| x.rel_path.as_str()).collect();
assert_eq!(
rels,
vec![
"src",
"src/finder",
"src/finder/fuzzy.rs",
"src/finder/mod.rs",
"src/ui",
"src/ui/fuzzy",
"src/ui/fuzzy/list.rs",
"Cargo.toml",
]
);
let depths: Vec<usize> = n.iter().map(|x| x.depth).collect();
assert_eq!(depths, vec![0, 1, 2, 2, 1, 2, 3, 0]);
}
}