Skip to main content

fresh/view/file_tree/
tree.rs

1use super::node::{NodeId, NodeState, TreeNode};
2use crate::model::filesystem::DirEntry;
3use crate::services::fs::FsManager;
4use std::collections::HashMap;
5use std::io;
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8
9/// File tree with lazy loading support
10///
11/// The tree starts with just the root node. Directories are only read
12/// when explicitly expanded via `expand_node()`. This makes the tree
13/// efficient even for very large directory structures.
14#[derive(Debug)]
15pub struct FileTree {
16    /// Root directory path
17    root_path: PathBuf,
18    /// All nodes indexed by ID
19    nodes: HashMap<NodeId, TreeNode>,
20    /// Path to node ID mapping for quick lookups
21    path_to_node: HashMap<PathBuf, NodeId>,
22    /// Root node ID
23    root_id: NodeId,
24    /// Next node ID to assign
25    next_id: usize,
26    /// Filesystem manager for async operations
27    fs_manager: Arc<FsManager>,
28}
29
30impl FileTree {
31    /// Create a new file tree rooted at the given path
32    ///
33    /// # Errors
34    ///
35    /// Returns an error if the root path doesn't exist or isn't a directory.
36    pub async fn new(root_path: PathBuf, fs_manager: Arc<FsManager>) -> io::Result<Self> {
37        // Verify root path exists and is a directory
38        if !fs_manager.exists(&root_path).await {
39            return Err(io::Error::new(
40                io::ErrorKind::NotFound,
41                format!("Path does not exist: {:?}", root_path),
42            ));
43        }
44
45        if !fs_manager.is_dir(&root_path).await? {
46            return Err(io::Error::new(
47                io::ErrorKind::InvalidInput,
48                format!("Path is not a directory: {:?}", root_path),
49            ));
50        }
51
52        // Get root entry
53        let root_entry = fs_manager.get_entry(&root_path).await?;
54
55        // Create root node
56        let root_id = NodeId(0);
57        let root_node = TreeNode::new(root_id, root_entry.clone(), None);
58
59        let mut nodes = HashMap::new();
60        nodes.insert(root_id, root_node);
61
62        let mut path_to_node = HashMap::new();
63        path_to_node.insert(root_path.clone(), root_id);
64
65        Ok(Self {
66            root_path,
67            nodes,
68            path_to_node,
69            root_id,
70            next_id: 1,
71            fs_manager,
72        })
73    }
74
75    /// Get the root node ID
76    pub fn root_id(&self) -> NodeId {
77        self.root_id
78    }
79
80    /// Get the root path
81    pub fn root_path(&self) -> &Path {
82        &self.root_path
83    }
84
85    /// Get a node by ID
86    pub fn get_node(&self, id: NodeId) -> Option<&TreeNode> {
87        self.nodes.get(&id)
88    }
89
90    /// Get a mutable reference to a node by ID
91    fn get_node_mut(&mut self, id: NodeId) -> Option<&mut TreeNode> {
92        self.nodes.get_mut(&id)
93    }
94
95    /// Get a node by path
96    pub fn get_node_by_path(&self, path: &Path) -> Option<&TreeNode> {
97        self.path_to_node
98            .get(path)
99            .and_then(|id| self.get_node(*id))
100    }
101
102    /// Get all nodes
103    pub fn all_nodes(&self) -> impl Iterator<Item = &TreeNode> {
104        self.nodes.values()
105    }
106
107    /// Expand a directory node (load its children)
108    ///
109    /// This is an async operation that reads the directory contents and creates
110    /// child nodes. If the directory is already expanded, this does nothing.
111    ///
112    /// # Errors
113    ///
114    /// Returns an error if the directory cannot be read.
115    pub async fn expand_node(&mut self, id: NodeId) -> io::Result<()> {
116        // Check if node exists and is a directory
117        let node = self
118            .get_node(id)
119            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Node not found"))?;
120
121        if !node.is_dir() {
122            return Err(io::Error::new(
123                io::ErrorKind::InvalidInput,
124                "Cannot expand a file node",
125            ));
126        }
127
128        // If already expanded, do nothing
129        if node.is_expanded() {
130            return Ok(());
131        }
132
133        // Set state to loading
134        if let Some(node) = self.get_node_mut(id) {
135            node.state = NodeState::Loading;
136        }
137
138        // Read directory contents with metadata (for file sizes)
139        let path = self.get_node(id).unwrap().entry.path.clone();
140        let result = self.fs_manager.list_dir_with_metadata(path.clone()).await;
141
142        match result {
143            Ok(entries) => {
144                // Sort entries: directories first, then by name
145                let mut sorted_entries = entries;
146                sorted_entries.sort_by(|a, b| match (a.is_dir(), b.is_dir()) {
147                    (true, false) => std::cmp::Ordering::Less,
148                    (false, true) => std::cmp::Ordering::Greater,
149                    _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
150                });
151
152                // Create child nodes
153                let mut child_ids = Vec::new();
154                for entry in sorted_entries {
155                    let child_id = self.add_node(entry, Some(id));
156                    child_ids.push(child_id);
157                }
158
159                // Update parent node
160                if let Some(node) = self.get_node_mut(id) {
161                    node.children = child_ids;
162                    node.state = NodeState::Expanded;
163                }
164
165                Ok(())
166            }
167            Err(e) => {
168                // Set error state
169                if let Some(node) = self.get_node_mut(id) {
170                    node.state = NodeState::Error(e.to_string());
171                }
172                Err(e)
173            }
174        }
175    }
176
177    /// Collapse a directory node
178    ///
179    /// This removes all child nodes from memory to save space.
180    /// They will be reloaded if the directory is expanded again.
181    pub fn collapse_node(&mut self, id: NodeId) {
182        if let Some(node) = self.get_node(id) {
183            if !node.is_dir() {
184                return;
185            }
186
187            // Collect child IDs to remove
188            let children_to_remove: Vec<NodeId> = node.children.clone();
189
190            // Remove all descendants recursively
191            for child_id in children_to_remove {
192                self.remove_node_recursive(child_id);
193            }
194        }
195
196        // Update parent node state
197        if let Some(node) = self.get_node_mut(id) {
198            node.children.clear();
199            node.state = NodeState::Collapsed;
200        }
201    }
202
203    /// Toggle node expansion (expand if collapsed, collapse if expanded)
204    pub async fn toggle_node(&mut self, id: NodeId) -> io::Result<()> {
205        let node = self
206            .get_node(id)
207            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Node not found"))?;
208
209        if !node.is_dir() {
210            return Ok(());
211        }
212
213        if node.is_expanded() {
214            self.collapse_node(id);
215            Ok(())
216        } else {
217            self.expand_node(id).await
218        }
219    }
220
221    /// Refresh a node (re-read directory contents)
222    ///
223    /// This is useful when filesystem contents have changed.
224    pub async fn refresh_node(&mut self, id: NodeId) -> io::Result<()> {
225        // Collapse and re-expand
226        self.collapse_node(id);
227        self.expand_node(id).await
228    }
229
230    /// Get all visible nodes in tree order
231    ///
232    /// Returns a flat list of nodes that should be visible, respecting
233    /// the expansion state of parent directories.
234    pub fn get_visible_nodes(&self) -> Vec<NodeId> {
235        let mut visible = Vec::new();
236        self.collect_visible_recursive(self.root_id, &mut visible);
237        visible
238    }
239
240    /// Recursively collect visible nodes
241    fn collect_visible_recursive(&self, id: NodeId, visible: &mut Vec<NodeId>) {
242        visible.push(id);
243
244        if let Some(node) = self.get_node(id) {
245            if node.is_expanded() {
246                for &child_id in &node.children {
247                    self.collect_visible_recursive(child_id, visible);
248                }
249            }
250        }
251    }
252
253    /// Get the parent chain for a node (from root to node)
254    pub fn get_ancestors(&self, id: NodeId) -> Vec<NodeId> {
255        let mut ancestors = Vec::new();
256        let mut current = Some(id);
257
258        while let Some(node_id) = current {
259            ancestors.push(node_id);
260            current = self.get_node(node_id).and_then(|n| n.parent);
261        }
262
263        ancestors.reverse();
264        ancestors
265    }
266
267    /// Get the depth of a node (root is 0)
268    pub fn get_depth(&self, id: NodeId) -> usize {
269        self.get_ancestors(id).len() - 1
270    }
271
272    /// Find node by relative path from root
273    pub fn find_by_relative_path(&self, relative_path: &Path) -> Option<NodeId> {
274        let full_path = self.root_path.join(relative_path);
275        self.path_to_node.get(&full_path).copied()
276    }
277
278    /// Add a new node to the tree
279    fn add_node(&mut self, entry: DirEntry, parent: Option<NodeId>) -> NodeId {
280        let id = NodeId(self.next_id);
281        self.next_id += 1;
282
283        let node = TreeNode::new(id, entry.clone(), parent);
284        self.path_to_node.insert(entry.path.clone(), id);
285        self.nodes.insert(id, node);
286
287        id
288    }
289
290    /// Remove a node and all its descendants
291    fn remove_node_recursive(&mut self, id: NodeId) {
292        if let Some(node) = self.get_node(id) {
293            let children = node.children.clone();
294            let path = node.entry.path.clone();
295
296            // Remove all children first
297            for child_id in children {
298                self.remove_node_recursive(child_id);
299            }
300
301            // Remove from path mapping
302            self.path_to_node.remove(&path);
303
304            // Remove node itself
305            self.nodes.remove(&id);
306        }
307    }
308
309    /// Get number of nodes currently in memory
310    pub fn node_count(&self) -> usize {
311        self.nodes.len()
312    }
313
314    /// Expand all directories along a path and return the final node ID
315    ///
316    /// This is useful for revealing a specific file in the tree, even if its
317    /// parent directories are collapsed. All parent directories will be expanded
318    /// as needed.
319    ///
320    /// # Arguments
321    ///
322    /// * `path` - The full path to the target file or directory
323    ///
324    /// # Returns
325    ///
326    /// Returns the NodeId of the target if found, or None if:
327    /// - The path is not under the root directory
328    /// - The path doesn't exist
329    /// - There was an error expanding intermediate directories
330    ///
331    /// # Example
332    ///
333    /// ```ignore
334    /// // Expand to src/components/App.js
335    /// if let Some(node_id) = tree.expand_to_path(&project_root.join("src/components/App.js")).await {
336    ///     // All parent directories (src, src/components) are now expanded
337    ///     // node_id points to App.js
338    /// }
339    /// ```
340    pub async fn expand_to_path(&mut self, path: &Path) -> Option<NodeId> {
341        // Check if path is under root
342        let relative_path = path.strip_prefix(&self.root_path).ok()?;
343
344        // Start from root
345        let mut current_id = self.root_id;
346
347        // Walk through each component of the path
348        for component in relative_path.components() {
349            let component_str = component.as_os_str().to_str()?;
350
351            // Expand current directory if it's not already expanded
352            let node = self.get_node(current_id)?;
353            if node.is_dir() && !node.is_expanded() {
354                // Expand this directory
355                if let Err(e) = self.expand_node(current_id).await {
356                    tracing::warn!("Failed to expand node during path traversal: {}", e);
357                    return None;
358                }
359            }
360
361            // Find the child with the matching name
362            let node = self.get_node(current_id)?;
363            let child_id = node
364                .children
365                .iter()
366                .find(|&&child_id| {
367                    if let Some(child_node) = self.get_node(child_id) {
368                        child_node.entry.name == component_str
369                    } else {
370                        false
371                    }
372                })
373                .copied();
374
375            match child_id {
376                Some(id) => current_id = id,
377                None => {
378                    // Child not found - path doesn't exist
379                    tracing::warn!("Component '{}' not found in tree", component_str);
380                    return None;
381                }
382            }
383        }
384
385        Some(current_id)
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392    use crate::model::filesystem::StdFileSystem;
393    use std::fs as std_fs;
394    use tempfile::TempDir;
395
396    async fn create_test_tree() -> (TempDir, FileTree) {
397        let temp_dir = TempDir::new().unwrap();
398        let temp_path = temp_dir.path();
399
400        // Create test structure:
401        // /
402        // ├── dir1/
403        // │   ├── file1.txt
404        // │   └── file2.txt
405        // ├── dir2/
406        // │   └── subdir/
407        // │       └── file3.txt
408        // └── file4.txt
409
410        std_fs::create_dir(temp_path.join("dir1")).unwrap();
411        std_fs::write(temp_path.join("dir1/file1.txt"), "content1").unwrap();
412        std_fs::write(temp_path.join("dir1/file2.txt"), "content2").unwrap();
413
414        std_fs::create_dir(temp_path.join("dir2")).unwrap();
415        std_fs::create_dir(temp_path.join("dir2/subdir")).unwrap();
416        std_fs::write(temp_path.join("dir2/subdir/file3.txt"), "content3").unwrap();
417
418        std_fs::write(temp_path.join("file4.txt"), "content4").unwrap();
419
420        let backend = Arc::new(StdFileSystem);
421        let manager = Arc::new(FsManager::new(backend));
422        let tree = FileTree::new(temp_path.to_path_buf(), manager)
423            .await
424            .unwrap();
425
426        (temp_dir, tree)
427    }
428
429    #[tokio::test]
430    async fn test_tree_creation() {
431        let (_temp_dir, tree) = create_test_tree().await;
432
433        assert_eq!(tree.node_count(), 1); // Only root initially
434        let root = tree.get_node(tree.root_id()).unwrap();
435        assert!(root.is_collapsed());
436        assert_eq!(root.children.len(), 0);
437    }
438
439    #[tokio::test]
440    async fn test_expand_root() {
441        let (_temp_dir, mut tree) = create_test_tree().await;
442
443        tree.expand_node(tree.root_id()).await.unwrap();
444
445        let root = tree.get_node(tree.root_id()).unwrap();
446        assert!(root.is_expanded());
447        assert_eq!(root.children.len(), 3); // dir1, dir2, file4.txt
448
449        // Should be 4 nodes: root + 3 children
450        assert_eq!(tree.node_count(), 4);
451    }
452
453    #[tokio::test]
454    async fn test_expand_nested() {
455        let (_temp_dir, mut tree) = create_test_tree().await;
456
457        // Expand root
458        tree.expand_node(tree.root_id()).await.unwrap();
459
460        // Find dir1 and expand it
461        let root = tree.get_node(tree.root_id()).unwrap();
462        let dir1_id = root.children[0]; // dir1 (directories come first)
463
464        tree.expand_node(dir1_id).await.unwrap();
465
466        let dir1 = tree.get_node(dir1_id).unwrap();
467        assert!(dir1.is_expanded());
468        assert_eq!(dir1.children.len(), 2); // file1.txt, file2.txt
469
470        // Total nodes: root + 3 children + 2 grandchildren = 6
471        assert_eq!(tree.node_count(), 6);
472    }
473
474    #[tokio::test]
475    async fn test_collapse_node() {
476        let (_temp_dir, mut tree) = create_test_tree().await;
477
478        // Expand root and dir1
479        tree.expand_node(tree.root_id()).await.unwrap();
480        let root = tree.get_node(tree.root_id()).unwrap();
481        let dir1_id = root.children[0];
482        tree.expand_node(dir1_id).await.unwrap();
483
484        assert_eq!(tree.node_count(), 6);
485
486        // Collapse dir1
487        tree.collapse_node(dir1_id);
488
489        let dir1 = tree.get_node(dir1_id).unwrap();
490        assert!(dir1.is_collapsed());
491        assert_eq!(dir1.children.len(), 0);
492
493        // Should remove the 2 child nodes
494        assert_eq!(tree.node_count(), 4);
495    }
496
497    #[tokio::test]
498    async fn test_toggle_node() {
499        let (_temp_dir, mut tree) = create_test_tree().await;
500
501        tree.expand_node(tree.root_id()).await.unwrap();
502        let root = tree.get_node(tree.root_id()).unwrap();
503        let dir1_id = root.children[0];
504
505        // Toggle to expand
506        tree.toggle_node(dir1_id).await.unwrap();
507        assert!(tree.get_node(dir1_id).unwrap().is_expanded());
508
509        // Toggle to collapse
510        tree.toggle_node(dir1_id).await.unwrap();
511        assert!(tree.get_node(dir1_id).unwrap().is_collapsed());
512    }
513
514    #[tokio::test]
515    async fn test_get_visible_nodes() {
516        let (_temp_dir, mut tree) = create_test_tree().await;
517
518        // Initially only root is visible
519        let visible = tree.get_visible_nodes();
520        assert_eq!(visible.len(), 1);
521
522        // Expand root
523        tree.expand_node(tree.root_id()).await.unwrap();
524        let visible = tree.get_visible_nodes();
525        assert_eq!(visible.len(), 4); // root + 3 children
526
527        // Expand dir1
528        let root = tree.get_node(tree.root_id()).unwrap();
529        let dir1_id = root.children[0];
530        tree.expand_node(dir1_id).await.unwrap();
531
532        let visible = tree.get_visible_nodes();
533        assert_eq!(visible.len(), 6); // root + 3 children + 2 grandchildren
534    }
535
536    #[tokio::test]
537    async fn test_get_ancestors() {
538        let (_temp_dir, mut tree) = create_test_tree().await;
539
540        tree.expand_node(tree.root_id()).await.unwrap();
541        let root = tree.get_node(tree.root_id()).unwrap();
542        let dir1_id = root.children[0];
543        tree.expand_node(dir1_id).await.unwrap();
544
545        let dir1 = tree.get_node(dir1_id).unwrap();
546        let file1_id = dir1.children[0];
547
548        let ancestors = tree.get_ancestors(file1_id);
549        assert_eq!(ancestors.len(), 3); // root -> dir1 -> file1
550        assert_eq!(ancestors[0], tree.root_id());
551        assert_eq!(ancestors[1], dir1_id);
552        assert_eq!(ancestors[2], file1_id);
553    }
554
555    #[tokio::test]
556    async fn test_get_depth() {
557        let (_temp_dir, mut tree) = create_test_tree().await;
558
559        tree.expand_node(tree.root_id()).await.unwrap();
560        let root = tree.get_node(tree.root_id()).unwrap();
561        let dir1_id = root.children[0];
562        tree.expand_node(dir1_id).await.unwrap();
563
564        assert_eq!(tree.get_depth(tree.root_id()), 0);
565        assert_eq!(tree.get_depth(dir1_id), 1);
566
567        let dir1 = tree.get_node(dir1_id).unwrap();
568        let file1_id = dir1.children[0];
569        assert_eq!(tree.get_depth(file1_id), 2);
570    }
571
572    #[tokio::test]
573    async fn test_sorted_entries() {
574        let (_temp_dir, mut tree) = create_test_tree().await;
575
576        tree.expand_node(tree.root_id()).await.unwrap();
577
578        let root = tree.get_node(tree.root_id()).unwrap();
579        let children: Vec<_> = root
580            .children
581            .iter()
582            .map(|&id| tree.get_node(id).unwrap())
583            .collect();
584
585        // Directories should come first
586        assert!(children[0].is_dir());
587        assert!(children[1].is_dir());
588        assert!(children[2].is_file());
589
590        // Directories should be sorted by name
591        assert_eq!(children[0].entry.name, "dir1");
592        assert_eq!(children[1].entry.name, "dir2");
593    }
594
595    #[tokio::test]
596    async fn test_refresh_node() {
597        let temp_dir = TempDir::new().unwrap();
598        let temp_path = temp_dir.path();
599
600        std_fs::create_dir(temp_path.join("dir1")).unwrap();
601        std_fs::write(temp_path.join("dir1/file1.txt"), "content").unwrap();
602
603        let backend = Arc::new(StdFileSystem);
604        let manager = Arc::new(FsManager::new(backend));
605        let mut tree = FileTree::new(temp_path.to_path_buf(), manager)
606            .await
607            .unwrap();
608
609        // Expand root and dir1
610        tree.expand_node(tree.root_id()).await.unwrap();
611        let root = tree.get_node(tree.root_id()).unwrap();
612        let dir1_id = root.children[0];
613        tree.expand_node(dir1_id).await.unwrap();
614
615        // Initially 1 file in dir1
616        let dir1 = tree.get_node(dir1_id).unwrap();
617        assert_eq!(dir1.children.len(), 1);
618
619        // Add another file
620        std_fs::write(temp_path.join("dir1/file2.txt"), "content2").unwrap();
621
622        // Refresh dir1
623        tree.refresh_node(dir1_id).await.unwrap();
624
625        // Should now have 2 files
626        let dir1 = tree.get_node(dir1_id).unwrap();
627        assert_eq!(dir1.children.len(), 2);
628    }
629
630    #[tokio::test]
631    async fn test_find_by_relative_path() {
632        let (_temp_dir, mut tree) = create_test_tree().await;
633
634        tree.expand_node(tree.root_id()).await.unwrap();
635        let root = tree.get_node(tree.root_id()).unwrap();
636        let dir1_id = root.children[0];
637
638        let found_id = tree.find_by_relative_path(Path::new("dir1"));
639        assert_eq!(found_id, Some(dir1_id));
640
641        let not_found = tree.find_by_relative_path(Path::new("nonexistent"));
642        assert_eq!(not_found, None);
643    }
644
645    #[tokio::test]
646    async fn test_expand_to_path() {
647        let (_temp_dir, mut tree) = create_test_tree().await;
648        let root_path = tree.root_path().to_path_buf();
649
650        // Initially tree is collapsed
651        assert_eq!(tree.node_count(), 1);
652
653        // Expand to a deeply nested file
654        let target_path = root_path.join("dir2/subdir/file3.txt");
655        let node_id = tree.expand_to_path(&target_path).await;
656
657        assert!(node_id.is_some(), "Should find the nested file");
658
659        // All parent directories should now be expanded
660        let root = tree.get_node(tree.root_id()).unwrap();
661        assert!(root.is_expanded(), "Root should be expanded");
662
663        // Find dir2
664        let dir2_node = root
665            .children
666            .iter()
667            .find_map(|&id| {
668                let node = tree.get_node(id)?;
669                if node.entry.name == "dir2" {
670                    Some(node)
671                } else {
672                    None
673                }
674            })
675            .expect("dir2 should exist");
676
677        assert!(dir2_node.is_expanded(), "dir2 should be expanded");
678
679        // Find subdir
680        let subdir_node = dir2_node
681            .children
682            .iter()
683            .find_map(|&id| {
684                let node = tree.get_node(id)?;
685                if node.entry.name == "subdir" {
686                    Some(node)
687                } else {
688                    None
689                }
690            })
691            .expect("subdir should exist");
692
693        assert!(subdir_node.is_expanded(), "subdir should be expanded");
694
695        // Verify the target file is found
696        let target_node = tree.get_node(node_id.unwrap()).unwrap();
697        assert_eq!(target_node.entry.name, "file3.txt");
698        assert!(target_node.is_file());
699    }
700
701    #[tokio::test]
702    async fn test_expand_to_path_not_under_root() {
703        let (_temp_dir, mut tree) = create_test_tree().await;
704
705        // Try to expand to a path not under root
706        let outside_path = PathBuf::from("/tmp/somefile.txt");
707        let result = tree.expand_to_path(&outside_path).await;
708
709        assert!(
710            result.is_none(),
711            "Should return None for paths outside root"
712        );
713    }
714
715    #[tokio::test]
716    async fn test_expand_to_path_nonexistent() {
717        let (_temp_dir, mut tree) = create_test_tree().await;
718        let root_path = tree.root_path().to_path_buf();
719
720        // Try to expand to a nonexistent file
721        let nonexistent_path = root_path.join("dir1/nonexistent.txt");
722        let result = tree.expand_to_path(&nonexistent_path).await;
723
724        assert!(result.is_none(), "Should return None for nonexistent paths");
725    }
726}