fresh/view/file_tree/
tree.rs

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