use super::node::{NodeId, NodeState, TreeNode};
use crate::model::filesystem::DirEntry;
use crate::services::fs::FsManager;
use std::collections::HashMap;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Debug)]
pub struct FileTree {
root_path: PathBuf,
nodes: HashMap<NodeId, TreeNode>,
path_to_node: HashMap<PathBuf, NodeId>,
root_id: NodeId,
next_id: usize,
fs_manager: Arc<FsManager>,
}
impl FileTree {
pub async fn new(root_path: PathBuf, fs_manager: Arc<FsManager>) -> io::Result<Self> {
if !fs_manager.exists(&root_path).await {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("Path does not exist: {:?}", root_path),
));
}
if !fs_manager.is_dir(&root_path).await? {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Path is not a directory: {:?}", root_path),
));
}
let root_entry = fs_manager.get_entry(&root_path).await?;
let root_id = NodeId(0);
let root_node = TreeNode::new(root_id, root_entry.clone(), None);
let mut nodes = HashMap::new();
nodes.insert(root_id, root_node);
let mut path_to_node = HashMap::new();
path_to_node.insert(root_path.clone(), root_id);
Ok(Self {
root_path,
nodes,
path_to_node,
root_id,
next_id: 1,
fs_manager,
})
}
pub fn root_id(&self) -> NodeId {
self.root_id
}
pub fn root_path(&self) -> &Path {
&self.root_path
}
pub fn get_node(&self, id: NodeId) -> Option<&TreeNode> {
self.nodes.get(&id)
}
fn get_node_mut(&mut self, id: NodeId) -> Option<&mut TreeNode> {
self.nodes.get_mut(&id)
}
pub fn get_node_by_path(&self, path: &Path) -> Option<&TreeNode> {
self.path_to_node
.get(path)
.and_then(|id| self.get_node(*id))
}
pub fn all_nodes(&self) -> impl Iterator<Item = &TreeNode> {
self.nodes.values()
}
pub async fn expand_node(&mut self, id: NodeId) -> io::Result<()> {
let node = self
.get_node(id)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Node not found"))?;
if !node.is_dir() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Cannot expand a file node",
));
}
if node.is_expanded() {
return Ok(());
}
if let Some(node) = self.get_node_mut(id) {
node.state = NodeState::Loading;
}
let path = self.get_node(id).unwrap().entry.path.clone();
let result = self.fs_manager.list_dir_with_metadata(path.clone()).await;
match result {
Ok(entries) => {
let mut sorted_entries = entries;
sorted_entries.sort_by(|a, b| match (a.is_dir(), b.is_dir()) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
});
let mut child_ids = Vec::new();
for entry in sorted_entries {
let child_id = self.add_node(entry, Some(id));
child_ids.push(child_id);
}
if let Some(node) = self.get_node_mut(id) {
node.children = child_ids;
node.state = NodeState::Expanded;
}
Ok(())
}
Err(e) => {
if let Some(node) = self.get_node_mut(id) {
node.state = NodeState::Error(e.to_string());
}
Err(e)
}
}
}
pub fn collapse_node(&mut self, id: NodeId) {
if let Some(node) = self.get_node(id) {
if !node.is_dir() {
return;
}
let children_to_remove: Vec<NodeId> = node.children.clone();
for child_id in children_to_remove {
self.remove_node_recursive(child_id);
}
}
if let Some(node) = self.get_node_mut(id) {
node.children.clear();
node.state = NodeState::Collapsed;
}
}
pub async fn toggle_node(&mut self, id: NodeId) -> io::Result<()> {
let node = self
.get_node(id)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Node not found"))?;
if !node.is_dir() {
return Ok(());
}
if node.is_expanded() {
self.collapse_node(id);
Ok(())
} else {
self.expand_node(id).await
}
}
pub async fn refresh_node(&mut self, id: NodeId) -> io::Result<()> {
self.collapse_node(id);
self.expand_node(id).await
}
pub fn get_visible_nodes(&self) -> Vec<NodeId> {
let mut visible = Vec::new();
self.collect_visible_recursive(self.root_id, &mut visible);
visible
}
fn collect_visible_recursive(&self, id: NodeId, visible: &mut Vec<NodeId>) {
visible.push(id);
if let Some(node) = self.get_node(id) {
if node.is_expanded() {
for &child_id in &node.children {
self.collect_visible_recursive(child_id, visible);
}
}
}
}
pub fn get_ancestors(&self, id: NodeId) -> Vec<NodeId> {
let mut ancestors = Vec::new();
let mut current = Some(id);
while let Some(node_id) = current {
ancestors.push(node_id);
current = self.get_node(node_id).and_then(|n| n.parent);
}
ancestors.reverse();
ancestors
}
pub fn get_depth(&self, id: NodeId) -> usize {
self.get_ancestors(id).len() - 1
}
pub fn find_by_relative_path(&self, relative_path: &Path) -> Option<NodeId> {
let full_path = self.root_path.join(relative_path);
self.path_to_node.get(&full_path).copied()
}
fn add_node(&mut self, entry: DirEntry, parent: Option<NodeId>) -> NodeId {
let id = NodeId(self.next_id);
self.next_id += 1;
let node = TreeNode::new(id, entry.clone(), parent);
self.path_to_node.insert(entry.path.clone(), id);
self.nodes.insert(id, node);
id
}
fn remove_node_recursive(&mut self, id: NodeId) {
if let Some(node) = self.get_node(id) {
let children = node.children.clone();
let path = node.entry.path.clone();
for child_id in children {
self.remove_node_recursive(child_id);
}
self.path_to_node.remove(&path);
self.nodes.remove(&id);
}
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub async fn expand_to_path(&mut self, path: &Path) -> Option<NodeId> {
let relative_path = path.strip_prefix(&self.root_path).ok()?;
let mut current_id = self.root_id;
for component in relative_path.components() {
let component_str = component.as_os_str().to_str()?;
let node = self.get_node(current_id)?;
if node.is_dir() && !node.is_expanded() {
if let Err(e) = self.expand_node(current_id).await {
tracing::warn!("Failed to expand node during path traversal: {}", e);
return None;
}
}
let node = self.get_node(current_id)?;
let child_id = node
.children
.iter()
.find(|&&child_id| {
if let Some(child_node) = self.get_node(child_id) {
child_node.entry.name == component_str
} else {
false
}
})
.copied();
match child_id {
Some(id) => current_id = id,
None => {
tracing::warn!("Component '{}' not found in tree", component_str);
return None;
}
}
}
Some(current_id)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::filesystem::StdFileSystem;
use std::fs as std_fs;
use tempfile::TempDir;
async fn create_test_tree() -> (TempDir, FileTree) {
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path();
std_fs::create_dir(temp_path.join("dir1")).unwrap();
std_fs::write(temp_path.join("dir1/file1.txt"), "content1").unwrap();
std_fs::write(temp_path.join("dir1/file2.txt"), "content2").unwrap();
std_fs::create_dir(temp_path.join("dir2")).unwrap();
std_fs::create_dir(temp_path.join("dir2/subdir")).unwrap();
std_fs::write(temp_path.join("dir2/subdir/file3.txt"), "content3").unwrap();
std_fs::write(temp_path.join("file4.txt"), "content4").unwrap();
let backend = Arc::new(StdFileSystem);
let manager = Arc::new(FsManager::new(backend));
let tree = FileTree::new(temp_path.to_path_buf(), manager)
.await
.unwrap();
(temp_dir, tree)
}
#[tokio::test]
async fn test_tree_creation() {
let (_temp_dir, tree) = create_test_tree().await;
assert_eq!(tree.node_count(), 1); let root = tree.get_node(tree.root_id()).unwrap();
assert!(root.is_collapsed());
assert_eq!(root.children.len(), 0);
}
#[tokio::test]
async fn test_expand_root() {
let (_temp_dir, mut tree) = create_test_tree().await;
tree.expand_node(tree.root_id()).await.unwrap();
let root = tree.get_node(tree.root_id()).unwrap();
assert!(root.is_expanded());
assert_eq!(root.children.len(), 3);
assert_eq!(tree.node_count(), 4);
}
#[tokio::test]
async fn test_expand_nested() {
let (_temp_dir, mut tree) = create_test_tree().await;
tree.expand_node(tree.root_id()).await.unwrap();
let root = tree.get_node(tree.root_id()).unwrap();
let dir1_id = root.children[0];
tree.expand_node(dir1_id).await.unwrap();
let dir1 = tree.get_node(dir1_id).unwrap();
assert!(dir1.is_expanded());
assert_eq!(dir1.children.len(), 2);
assert_eq!(tree.node_count(), 6);
}
#[tokio::test]
async fn test_collapse_node() {
let (_temp_dir, mut tree) = create_test_tree().await;
tree.expand_node(tree.root_id()).await.unwrap();
let root = tree.get_node(tree.root_id()).unwrap();
let dir1_id = root.children[0];
tree.expand_node(dir1_id).await.unwrap();
assert_eq!(tree.node_count(), 6);
tree.collapse_node(dir1_id);
let dir1 = tree.get_node(dir1_id).unwrap();
assert!(dir1.is_collapsed());
assert_eq!(dir1.children.len(), 0);
assert_eq!(tree.node_count(), 4);
}
#[tokio::test]
async fn test_toggle_node() {
let (_temp_dir, mut tree) = create_test_tree().await;
tree.expand_node(tree.root_id()).await.unwrap();
let root = tree.get_node(tree.root_id()).unwrap();
let dir1_id = root.children[0];
tree.toggle_node(dir1_id).await.unwrap();
assert!(tree.get_node(dir1_id).unwrap().is_expanded());
tree.toggle_node(dir1_id).await.unwrap();
assert!(tree.get_node(dir1_id).unwrap().is_collapsed());
}
#[tokio::test]
async fn test_get_visible_nodes() {
let (_temp_dir, mut tree) = create_test_tree().await;
let visible = tree.get_visible_nodes();
assert_eq!(visible.len(), 1);
tree.expand_node(tree.root_id()).await.unwrap();
let visible = tree.get_visible_nodes();
assert_eq!(visible.len(), 4);
let root = tree.get_node(tree.root_id()).unwrap();
let dir1_id = root.children[0];
tree.expand_node(dir1_id).await.unwrap();
let visible = tree.get_visible_nodes();
assert_eq!(visible.len(), 6); }
#[tokio::test]
async fn test_get_ancestors() {
let (_temp_dir, mut tree) = create_test_tree().await;
tree.expand_node(tree.root_id()).await.unwrap();
let root = tree.get_node(tree.root_id()).unwrap();
let dir1_id = root.children[0];
tree.expand_node(dir1_id).await.unwrap();
let dir1 = tree.get_node(dir1_id).unwrap();
let file1_id = dir1.children[0];
let ancestors = tree.get_ancestors(file1_id);
assert_eq!(ancestors.len(), 3); assert_eq!(ancestors[0], tree.root_id());
assert_eq!(ancestors[1], dir1_id);
assert_eq!(ancestors[2], file1_id);
}
#[tokio::test]
async fn test_get_depth() {
let (_temp_dir, mut tree) = create_test_tree().await;
tree.expand_node(tree.root_id()).await.unwrap();
let root = tree.get_node(tree.root_id()).unwrap();
let dir1_id = root.children[0];
tree.expand_node(dir1_id).await.unwrap();
assert_eq!(tree.get_depth(tree.root_id()), 0);
assert_eq!(tree.get_depth(dir1_id), 1);
let dir1 = tree.get_node(dir1_id).unwrap();
let file1_id = dir1.children[0];
assert_eq!(tree.get_depth(file1_id), 2);
}
#[tokio::test]
async fn test_sorted_entries() {
let (_temp_dir, mut tree) = create_test_tree().await;
tree.expand_node(tree.root_id()).await.unwrap();
let root = tree.get_node(tree.root_id()).unwrap();
let children: Vec<_> = root
.children
.iter()
.map(|&id| tree.get_node(id).unwrap())
.collect();
assert!(children[0].is_dir());
assert!(children[1].is_dir());
assert!(children[2].is_file());
assert_eq!(children[0].entry.name, "dir1");
assert_eq!(children[1].entry.name, "dir2");
}
#[tokio::test]
async fn test_refresh_node() {
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path();
std_fs::create_dir(temp_path.join("dir1")).unwrap();
std_fs::write(temp_path.join("dir1/file1.txt"), "content").unwrap();
let backend = Arc::new(StdFileSystem);
let manager = Arc::new(FsManager::new(backend));
let mut tree = FileTree::new(temp_path.to_path_buf(), manager)
.await
.unwrap();
tree.expand_node(tree.root_id()).await.unwrap();
let root = tree.get_node(tree.root_id()).unwrap();
let dir1_id = root.children[0];
tree.expand_node(dir1_id).await.unwrap();
let dir1 = tree.get_node(dir1_id).unwrap();
assert_eq!(dir1.children.len(), 1);
std_fs::write(temp_path.join("dir1/file2.txt"), "content2").unwrap();
tree.refresh_node(dir1_id).await.unwrap();
let dir1 = tree.get_node(dir1_id).unwrap();
assert_eq!(dir1.children.len(), 2);
}
#[tokio::test]
async fn test_find_by_relative_path() {
let (_temp_dir, mut tree) = create_test_tree().await;
tree.expand_node(tree.root_id()).await.unwrap();
let root = tree.get_node(tree.root_id()).unwrap();
let dir1_id = root.children[0];
let found_id = tree.find_by_relative_path(Path::new("dir1"));
assert_eq!(found_id, Some(dir1_id));
let not_found = tree.find_by_relative_path(Path::new("nonexistent"));
assert_eq!(not_found, None);
}
#[tokio::test]
async fn test_expand_to_path() {
let (_temp_dir, mut tree) = create_test_tree().await;
let root_path = tree.root_path().to_path_buf();
assert_eq!(tree.node_count(), 1);
let target_path = root_path.join("dir2/subdir/file3.txt");
let node_id = tree.expand_to_path(&target_path).await;
assert!(node_id.is_some(), "Should find the nested file");
let root = tree.get_node(tree.root_id()).unwrap();
assert!(root.is_expanded(), "Root should be expanded");
let dir2_node = root
.children
.iter()
.find_map(|&id| {
let node = tree.get_node(id)?;
if node.entry.name == "dir2" {
Some(node)
} else {
None
}
})
.expect("dir2 should exist");
assert!(dir2_node.is_expanded(), "dir2 should be expanded");
let subdir_node = dir2_node
.children
.iter()
.find_map(|&id| {
let node = tree.get_node(id)?;
if node.entry.name == "subdir" {
Some(node)
} else {
None
}
})
.expect("subdir should exist");
assert!(subdir_node.is_expanded(), "subdir should be expanded");
let target_node = tree.get_node(node_id.unwrap()).unwrap();
assert_eq!(target_node.entry.name, "file3.txt");
assert!(target_node.is_file());
}
#[tokio::test]
async fn test_expand_to_path_not_under_root() {
let (_temp_dir, mut tree) = create_test_tree().await;
let outside_path = PathBuf::from("/tmp/somefile.txt");
let result = tree.expand_to_path(&outside_path).await;
assert!(
result.is_none(),
"Should return None for paths outside root"
);
}
#[tokio::test]
async fn test_expand_to_path_nonexistent() {
let (_temp_dir, mut tree) = create_test_tree().await;
let root_path = tree.root_path().to_path_buf();
let nonexistent_path = root_path.join("dir1/nonexistent.txt");
let result = tree.expand_to_path(&nonexistent_path).await;
assert!(result.is_none(), "Should return None for nonexistent paths");
}
}