use std::fs;
use std::path::Path;
use miette::{IntoDiagnostic, Result, WrapErr};
use crate::commands::init::model::{HiddenKind, Node};
use crate::commands::init::tui::TuiState;
use crate::commands::init::tui::gitignore::{GitignoreMatcher, classify_hidden, host_join};
pub(super) fn load_children_for_focused(state: &mut TuiState) -> Result<()> {
let focused_path = state.tree.focused().path.clone();
let host_path = host_join(&state.cwd_host, &focused_path);
let children = read_dir_children(
&host_path,
&focused_path,
&state.gitignore,
state.hidden_visible,
)?;
state.tree.populate_focused_children(children);
Ok(())
}
pub(super) fn reload_tree_for_visibility(state: &mut TuiState) -> Result<()> {
let focused_workspace_path = state.tree.focused().path.clone();
let cwd_host = state.cwd_host.clone();
let show_hidden = state.hidden_visible;
reload_loaded_subtree(
state.tree.root_mut(),
&cwd_host,
&state.gitignore,
show_hidden,
)?;
let new_path =
locate_visible_path(state.tree.root(), &focused_workspace_path).unwrap_or_default();
state.tree.set_focus_path(new_path);
Ok(())
}
fn reload_loaded_subtree(
node: &mut Node,
cwd_host: &Path,
gitignore: &GitignoreMatcher,
show_hidden: bool,
) -> Result<()> {
if !node.children_loaded {
return Ok(());
}
let host_path = host_join(cwd_host, &node.path);
let mut new_children = read_dir_children(&host_path, &node.path, gitignore, show_hidden)?;
let mut old_by_name: std::collections::HashMap<std::ffi::OsString, Node> =
std::collections::HashMap::new();
for child in std::mem::take(&mut node.children) {
old_by_name.insert(child.name.clone(), child);
}
for new_child in &mut new_children {
if let Some(old) = old_by_name.remove(&new_child.name) {
new_child.state = old.state;
new_child.expanded = old.expanded;
new_child.children_loaded = old.children_loaded;
new_child.children = old.children;
reload_loaded_subtree(new_child, cwd_host, gitignore, show_hidden)?;
}
}
node.children = new_children;
Ok(())
}
fn locate_visible_path(root: &Node, target: &Path) -> Option<Vec<usize>> {
let mut path: Vec<usize> = Vec::new();
if locate_visible_path_rec(root, target, &mut path) {
Some(path)
} else {
None
}
}
fn locate_visible_path_rec(node: &Node, target: &Path, path: &mut Vec<usize>) -> bool {
if node.path == target {
return true;
}
if !node.expanded {
return false;
}
for (i, child) in node.children.iter().enumerate() {
path.push(i);
if locate_visible_path_rec(child, target, path) {
return true;
}
path.pop();
}
false
}
pub(super) fn read_dir_children(
host_dir: &Path,
workspace_dir: &Path,
gitignore: &GitignoreMatcher,
show_hidden: bool,
) -> Result<Vec<Node>> {
let mut entries: Vec<Node> = Vec::new();
let read = fs::read_dir(host_dir)
.into_diagnostic()
.wrap_err_with(|| format!("failed to read directory `{}`", host_dir.display()))?;
for entry in read {
let entry = entry
.into_diagnostic()
.wrap_err_with(|| format!("failed to read entry under `{}`", host_dir.display()))?;
let file_type = entry
.file_type()
.into_diagnostic()
.wrap_err_with(|| format!("failed to stat `{}`", entry.path().display()))?;
if !file_type.is_dir() {
continue;
}
let name = entry.file_name();
let kind = classify_hidden(&name, &entry.path(), gitignore);
if kind != HiddenKind::Visible && !show_hidden {
continue;
}
let mut child_workspace = workspace_dir.to_path_buf();
child_workspace.push(&name);
let mut node = Node::new(name, child_workspace);
node.hidden_kind = kind;
entries.push(node);
}
entries.sort_by(|a, b| a.name.cmp(&b.name));
Ok(entries)
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::Path;
use tempfile::TempDir;
use crate::commands::init::model::{HiddenKind, Node};
use crate::commands::init::tui::gitignore::GitignoreMatcher;
use super::read_dir_children;
fn names_of(nodes: &[Node]) -> Vec<String> {
nodes
.iter()
.map(|n| n.name.to_string_lossy().into_owned())
.collect()
}
fn kind_of(nodes: &[Node], name: &str) -> Option<HiddenKind> {
nodes
.iter()
.find(|n| n.name.as_os_str() == std::ffi::OsStr::new(name))
.map(|n| n.hidden_kind)
}
fn write_gitignore(dir: &Path, body: &str) {
fs::write(dir.join(".gitignore"), body).unwrap();
}
#[test]
fn read_dir_children_skips_dotfile_and_gitignored_when_show_hidden_false() {
let tmp = TempDir::new().unwrap();
fs::create_dir(tmp.path().join("src")).unwrap();
fs::create_dir(tmp.path().join(".git")).unwrap();
fs::create_dir(tmp.path().join("node_modules")).unwrap();
write_gitignore(tmp.path(), "node_modules/\n");
let matcher = GitignoreMatcher::new(tmp.path());
let children = read_dir_children(tmp.path(), Path::new("/"), &matcher, false).unwrap();
assert_eq!(names_of(&children), vec!["src".to_owned()]);
}
#[test]
fn read_dir_children_includes_hidden_with_kinds_when_show_hidden_true() {
let tmp = TempDir::new().unwrap();
fs::create_dir(tmp.path().join("src")).unwrap();
fs::create_dir(tmp.path().join(".git")).unwrap();
fs::create_dir(tmp.path().join("node_modules")).unwrap();
write_gitignore(tmp.path(), "node_modules/\n");
let matcher = GitignoreMatcher::new(tmp.path());
let children = read_dir_children(tmp.path(), Path::new("/"), &matcher, true).unwrap();
assert_eq!(
names_of(&children),
vec![
".git".to_owned(),
"node_modules".to_owned(),
"src".to_owned()
],
);
assert_eq!(kind_of(&children, ".git"), Some(HiddenKind::Dotfile));
assert_eq!(
kind_of(&children, "node_modules"),
Some(HiddenKind::Gitignored),
);
assert_eq!(kind_of(&children, "src"), Some(HiddenKind::Visible));
}
#[test]
fn read_dir_children_classifies_dotfile_dir_that_is_also_gitignored_as_gitignored() {
let tmp = TempDir::new().unwrap();
fs::create_dir(tmp.path().join(".cache")).unwrap();
write_gitignore(tmp.path(), ".cache/\n");
let matcher = GitignoreMatcher::new(tmp.path());
let children = read_dir_children(tmp.path(), Path::new("/"), &matcher, true).unwrap();
assert_eq!(kind_of(&children, ".cache"), Some(HiddenKind::Gitignored));
}
#[test]
fn read_dir_children_with_no_gitignore_file_still_skips_dotfiles() {
let tmp = TempDir::new().unwrap();
fs::create_dir(tmp.path().join("lib")).unwrap();
fs::create_dir(tmp.path().join(".hidden")).unwrap();
let matcher = GitignoreMatcher::new(tmp.path());
let children = read_dir_children(tmp.path(), Path::new("/"), &matcher, false).unwrap();
assert_eq!(names_of(&children), vec!["lib".to_owned()]);
}
#[test]
fn read_dir_children_ignores_regular_files() {
let tmp = TempDir::new().unwrap();
fs::create_dir(tmp.path().join("lib")).unwrap();
fs::write(tmp.path().join("README.md"), "").unwrap();
let matcher = GitignoreMatcher::new(tmp.path());
let children = read_dir_children(tmp.path(), Path::new("/"), &matcher, true).unwrap();
assert_eq!(names_of(&children), vec!["lib".to_owned()]);
}
}