Skip to main content

fresh/view/file_tree/
view.rs

1use super::ignore::IgnorePatterns;
2use super::node::NodeId;
3use super::tree::FileTree;
4use crate::model::filesystem::DirEntry;
5
6/// View state for file tree navigation and filtering
7#[derive(Debug)]
8pub struct FileTreeView {
9    /// The underlying tree model
10    tree: FileTree,
11    /// Currently selected node
12    selected_node: Option<NodeId>,
13    /// Scroll offset (index into visible nodes)
14    scroll_offset: usize,
15    /// Sort mode for entries
16    sort_mode: SortMode,
17    /// Ignore patterns for filtering
18    ignore_patterns: IgnorePatterns,
19    /// Last known viewport height (for scrolling calculations)
20    pub(crate) viewport_height: usize,
21}
22
23/// Sort mode for file tree entries
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum SortMode {
26    /// Sort by name alphabetically
27    Name,
28    /// Sort by type (directories first, then files)
29    Type,
30    /// Sort by modification time (newest first)
31    Modified,
32}
33
34impl FileTreeView {
35    /// Create a new file tree view
36    pub fn new(tree: FileTree) -> Self {
37        let root_id = tree.root_id();
38        Self {
39            tree,
40            selected_node: Some(root_id),
41            scroll_offset: 0,
42            sort_mode: SortMode::Type,
43            ignore_patterns: IgnorePatterns::new(),
44            viewport_height: 10, // Default, will be updated during rendering
45        }
46    }
47
48    /// Set the viewport height (should be called during rendering)
49    pub fn set_viewport_height(&mut self, height: usize) {
50        self.viewport_height = height;
51    }
52
53    /// Get the underlying tree
54    pub fn tree(&self) -> &FileTree {
55        &self.tree
56    }
57
58    /// Get mutable reference to the underlying tree
59    pub fn tree_mut(&mut self) -> &mut FileTree {
60        &mut self.tree
61    }
62
63    /// Get currently visible nodes with their indent levels
64    ///
65    /// Returns a list of (NodeId, indent_level) tuples for rendering.
66    pub fn get_display_nodes(&self) -> Vec<(NodeId, usize)> {
67        let visible = self.tree.get_visible_nodes();
68        visible
69            .into_iter()
70            .map(|id| {
71                let depth = self.tree.get_depth(id);
72                (id, depth)
73            })
74            .collect()
75    }
76
77    /// Get the currently selected node ID
78    pub fn get_selected(&self) -> Option<NodeId> {
79        self.selected_node
80    }
81
82    /// Set the selected node
83    pub fn set_selected(&mut self, node_id: Option<NodeId>) {
84        self.selected_node = node_id;
85    }
86
87    /// Select the next visible node
88    pub fn select_next(&mut self) {
89        let visible = self.tree.get_visible_nodes();
90        if visible.is_empty() {
91            return;
92        }
93
94        if let Some(current) = self.selected_node {
95            if let Some(pos) = visible.iter().position(|&id| id == current) {
96                if pos + 1 < visible.len() {
97                    self.selected_node = Some(visible[pos + 1]);
98                }
99            }
100        } else {
101            self.selected_node = Some(visible[0]);
102        }
103    }
104
105    /// Select the previous visible node
106    pub fn select_prev(&mut self) {
107        let visible = self.tree.get_visible_nodes();
108        if visible.is_empty() {
109            return;
110        }
111
112        if let Some(current) = self.selected_node {
113            if let Some(pos) = visible.iter().position(|&id| id == current) {
114                if pos > 0 {
115                    self.selected_node = Some(visible[pos - 1]);
116                }
117            }
118        } else {
119            self.selected_node = Some(visible[0]);
120        }
121    }
122
123    /// Move selection up by a page (viewport height)
124    pub fn select_page_up(&mut self) {
125        if self.viewport_height == 0 {
126            return;
127        }
128
129        let visible = self.tree.get_visible_nodes();
130        if visible.is_empty() {
131            return;
132        }
133
134        if let Some(current) = self.selected_node {
135            if let Some(pos) = visible.iter().position(|&id| id == current) {
136                let new_pos = pos.saturating_sub(self.viewport_height);
137                self.selected_node = Some(visible[new_pos]);
138            }
139        } else {
140            self.selected_node = Some(visible[0]);
141        }
142    }
143
144    /// Move selection down by a page (viewport height)
145    pub fn select_page_down(&mut self) {
146        if self.viewport_height == 0 {
147            return;
148        }
149
150        let visible = self.tree.get_visible_nodes();
151        if visible.is_empty() {
152            return;
153        }
154
155        if let Some(current) = self.selected_node {
156            if let Some(pos) = visible.iter().position(|&id| id == current) {
157                let new_pos = (pos + self.viewport_height).min(visible.len() - 1);
158                self.selected_node = Some(visible[new_pos]);
159            }
160        } else {
161            self.selected_node = Some(visible[0]);
162        }
163    }
164
165    /// Update scroll offset to ensure symmetric scrolling behavior
166    ///
167    /// This should be called after navigation to implement symmetric scrolling:
168    /// - When moving down, cursor moves to bottom of viewport before scrolling
169    /// - When moving up, cursor moves to top of viewport before scrolling
170    ///
171    /// Uses the stored viewport_height which is updated during rendering.
172    pub fn update_scroll_for_selection(&mut self) {
173        if self.viewport_height == 0 {
174            return;
175        }
176
177        if let Some(selected) = self.selected_node {
178            let visible = self.tree.get_visible_nodes();
179            if let Some(pos) = visible.iter().position(|&id| id == selected) {
180                // Only scroll if cursor goes PAST the viewport edges
181                // This implements symmetric scrolling behavior
182
183                // If selection is above the visible area, scroll up
184                if pos < self.scroll_offset {
185                    self.scroll_offset = pos;
186                }
187                // If selection is below the visible area, scroll down
188                else if pos >= self.scroll_offset + self.viewport_height {
189                    self.scroll_offset = pos - self.viewport_height + 1;
190                }
191                // Otherwise, cursor is within viewport - don't scroll
192            }
193        }
194    }
195
196    /// Select the first visible node
197    pub fn select_first(&mut self) {
198        let visible = self.tree.get_visible_nodes();
199        if !visible.is_empty() {
200            self.selected_node = Some(visible[0]);
201        }
202    }
203
204    /// Select the last visible node
205    pub fn select_last(&mut self) {
206        let visible = self.tree.get_visible_nodes();
207        if !visible.is_empty() {
208            self.selected_node = Some(*visible.last().unwrap());
209        }
210    }
211
212    /// Select the parent of the currently selected node
213    pub fn select_parent(&mut self) {
214        if let Some(current) = self.selected_node {
215            if let Some(node) = self.tree.get_node(current) {
216                if let Some(parent_id) = node.parent {
217                    self.selected_node = Some(parent_id);
218                }
219            }
220        }
221    }
222
223    /// Get the scroll offset
224    pub fn get_scroll_offset(&self) -> usize {
225        self.scroll_offset
226    }
227
228    /// Set the scroll offset
229    pub fn set_scroll_offset(&mut self, offset: usize) {
230        self.scroll_offset = offset;
231    }
232
233    /// Ensure the selected node is visible within the viewport
234    ///
235    /// Adjusts scroll offset if necessary to keep the selected node visible.
236    ///
237    /// # Arguments
238    ///
239    /// * `viewport_height` - Number of visible lines in the viewport
240    pub fn ensure_visible(&mut self, viewport_height: usize) {
241        if viewport_height == 0 {
242            return;
243        }
244
245        if let Some(selected) = self.selected_node {
246            let visible = self.tree.get_visible_nodes();
247            if let Some(pos) = visible.iter().position(|&id| id == selected) {
248                // If selection is above viewport, scroll up
249                if pos < self.scroll_offset {
250                    self.scroll_offset = pos;
251                }
252                // If selection is below viewport, scroll down
253                else if pos >= self.scroll_offset + viewport_height {
254                    self.scroll_offset = pos - viewport_height + 1;
255                }
256            }
257        }
258    }
259
260    /// Get the sort mode
261    pub fn get_sort_mode(&self) -> SortMode {
262        self.sort_mode
263    }
264
265    /// Set the sort mode
266    pub fn set_sort_mode(&mut self, mode: SortMode) {
267        self.sort_mode = mode;
268        // TODO: Re-sort children when sort mode changes
269    }
270
271    /// Get selected node entry (convenience method)
272    pub fn get_selected_entry(&self) -> Option<&DirEntry> {
273        self.selected_node
274            .and_then(|id| self.tree.get_node(id))
275            .map(|node| &node.entry)
276    }
277
278    /// Navigate to a specific path if it exists in the tree
279    pub fn navigate_to_path(&mut self, path: &std::path::Path) {
280        if let Some(node) = self.tree.get_node_by_path(path) {
281            self.selected_node = Some(node.id);
282        }
283    }
284
285    /// Get the index of the selected node in the visible list
286    pub fn get_selected_index(&self) -> Option<usize> {
287        if let Some(selected) = self.selected_node {
288            let visible = self.tree.get_visible_nodes();
289            visible.iter().position(|&id| id == selected)
290        } else {
291            None
292        }
293    }
294
295    /// Get visible node at index (accounting for scroll offset)
296    pub fn get_node_at_index(&self, index: usize) -> Option<NodeId> {
297        let visible = self.tree.get_visible_nodes();
298        visible.get(index).copied()
299    }
300
301    /// Get the number of visible nodes
302    pub fn visible_count(&self) -> usize {
303        self.tree.get_visible_nodes().len()
304    }
305
306    /// Get reference to ignore patterns
307    pub fn ignore_patterns(&self) -> &IgnorePatterns {
308        &self.ignore_patterns
309    }
310
311    /// Get mutable reference to ignore patterns
312    pub fn ignore_patterns_mut(&mut self) -> &mut IgnorePatterns {
313        &mut self.ignore_patterns
314    }
315
316    /// Toggle showing hidden files
317    pub fn toggle_show_hidden(&mut self) {
318        self.ignore_patterns.toggle_show_hidden();
319    }
320
321    /// Toggle showing gitignored files
322    pub fn toggle_show_gitignored(&mut self) {
323        self.ignore_patterns.toggle_show_gitignored();
324    }
325
326    /// Check if a node should be visible (not filtered by ignore patterns)
327    pub fn is_node_visible(&self, node_id: NodeId) -> bool {
328        if let Some(node) = self.tree.get_node(node_id) {
329            !self
330                .ignore_patterns
331                .is_ignored(&node.entry.path, node.is_dir())
332        } else {
333            false
334        }
335    }
336
337    /// Load .gitignore for a directory
338    ///
339    /// This should be called when expanding a directory to load its .gitignore
340    pub fn load_gitignore_for_dir(&mut self, dir_path: &std::path::Path) -> std::io::Result<()> {
341        self.ignore_patterns.load_gitignore(dir_path)
342    }
343
344    /// Expand all parent directories and select the given file path
345    ///
346    /// This is useful for revealing a specific file in the tree when switching
347    /// focus to the file explorer. All parent directories will be expanded as needed,
348    /// and the file will be selected.
349    ///
350    /// # Arguments
351    ///
352    /// * `path` - The full path to the file to reveal and select
353    ///
354    /// # Returns
355    ///
356    /// Returns true if the file was successfully expanded and selected, false otherwise.
357    /// This will return false if:
358    /// - The path is not under the root directory
359    /// - The path doesn't exist
360    /// - There was an error expanding intermediate directories
361    pub async fn expand_and_select_file(&mut self, path: &std::path::Path) -> bool {
362        if let Some(node_id) = self.tree.expand_to_path(path).await {
363            self.selected_node = Some(node_id);
364            true
365        } else {
366            false
367        }
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374    use crate::model::filesystem::StdFileSystem;
375    use crate::services::fs::FsManager;
376    use std::fs as std_fs;
377    use std::sync::Arc;
378    use tempfile::TempDir;
379
380    async fn create_test_view() -> (TempDir, FileTreeView) {
381        let temp_dir = TempDir::new().unwrap();
382        let temp_path = temp_dir.path();
383
384        // Create test structure
385        std_fs::create_dir(temp_path.join("dir1")).unwrap();
386        std_fs::write(temp_path.join("dir1/file1.txt"), "content1").unwrap();
387        std_fs::write(temp_path.join("dir1/file2.txt"), "content2").unwrap();
388        std_fs::create_dir(temp_path.join("dir2")).unwrap();
389        std_fs::write(temp_path.join("file3.txt"), "content3").unwrap();
390
391        let backend = Arc::new(StdFileSystem);
392        let manager = Arc::new(FsManager::new(backend));
393        let tree = FileTree::new(temp_path.to_path_buf(), manager)
394            .await
395            .unwrap();
396        let view = FileTreeView::new(tree);
397
398        (temp_dir, view)
399    }
400
401    #[tokio::test]
402    async fn test_view_creation() {
403        let (_temp_dir, view) = create_test_view().await;
404
405        assert!(view.get_selected().is_some());
406        assert_eq!(view.get_scroll_offset(), 0);
407        assert_eq!(view.get_sort_mode(), SortMode::Type);
408    }
409
410    #[tokio::test]
411    async fn test_get_display_nodes() {
412        let (_temp_dir, mut view) = create_test_view().await;
413
414        // Initially only root
415        let display = view.get_display_nodes();
416        assert_eq!(display.len(), 1);
417        assert_eq!(display[0].1, 0); // Root has depth 0
418
419        // Expand root
420        let root_id = view.tree().root_id();
421        view.tree_mut().expand_node(root_id).await.unwrap();
422
423        let display = view.get_display_nodes();
424        assert_eq!(display.len(), 4); // root + 3 children
425
426        // Check depths
427        assert_eq!(display[0].1, 0); // root
428        assert_eq!(display[1].1, 1); // child
429        assert_eq!(display[2].1, 1); // child
430        assert_eq!(display[3].1, 1); // child
431    }
432
433    #[tokio::test]
434    async fn test_navigation() {
435        let (_temp_dir, mut view) = create_test_view().await;
436
437        let root_id = view.tree().root_id();
438        view.tree_mut().expand_node(root_id).await.unwrap();
439
440        let root_id = view.tree().root_id();
441        assert_eq!(view.get_selected(), Some(root_id));
442
443        // Select next
444        view.select_next();
445        assert_ne!(view.get_selected(), Some(root_id));
446
447        // Select prev
448        view.select_prev();
449        assert_eq!(view.get_selected(), Some(root_id));
450
451        // Select last
452        view.select_last();
453        let visible = view.tree().get_visible_nodes();
454        assert_eq!(view.get_selected(), Some(*visible.last().unwrap()));
455
456        // Select first
457        view.select_first();
458        assert_eq!(view.get_selected(), Some(root_id));
459    }
460
461    #[tokio::test]
462    async fn test_select_parent() {
463        let (_temp_dir, mut view) = create_test_view().await;
464
465        let root_id = view.tree().root_id();
466        view.tree_mut().expand_node(root_id).await.unwrap();
467
468        // Select first child
469        view.select_next();
470        let child_id = view.get_selected().unwrap();
471        assert_ne!(child_id, root_id);
472
473        // Select parent
474        view.select_parent();
475        assert_eq!(view.get_selected(), Some(root_id));
476    }
477
478    #[tokio::test]
479    async fn test_ensure_visible() {
480        let (_temp_dir, mut view) = create_test_view().await;
481
482        let root_id = view.tree().root_id();
483        view.tree_mut().expand_node(root_id).await.unwrap();
484
485        let viewport_height = 2;
486
487        // Select last item
488        view.select_last();
489        view.ensure_visible(viewport_height);
490
491        // Scroll offset should be adjusted
492        let selected_index = view.get_selected_index().unwrap();
493        assert!(selected_index >= view.get_scroll_offset());
494        assert!(selected_index < view.get_scroll_offset() + viewport_height);
495
496        // Select first item
497        view.select_first();
498        view.ensure_visible(viewport_height);
499
500        // Scroll offset should be 0
501        assert_eq!(view.get_scroll_offset(), 0);
502    }
503
504    #[tokio::test]
505    async fn test_get_selected_entry() {
506        let (_temp_dir, view) = create_test_view().await;
507
508        let entry = view.get_selected_entry();
509        assert!(entry.is_some());
510        assert!(entry.unwrap().is_dir());
511    }
512
513    #[tokio::test]
514    async fn test_navigate_to_path() {
515        let (_temp_dir, mut view) = create_test_view().await;
516
517        let root_id = view.tree().root_id();
518        view.tree_mut().expand_node(root_id).await.unwrap();
519
520        let dir1_path = view.tree().root_path().join("dir1");
521        view.navigate_to_path(&dir1_path);
522
523        let selected_entry = view.get_selected_entry().unwrap();
524        assert_eq!(selected_entry.name, "dir1");
525    }
526
527    #[tokio::test]
528    async fn test_get_selected_index() {
529        let (_temp_dir, mut view) = create_test_view().await;
530
531        let root_id = view.tree().root_id();
532        view.tree_mut().expand_node(root_id).await.unwrap();
533
534        // Root is at index 0
535        assert_eq!(view.get_selected_index(), Some(0));
536
537        // Move to next
538        view.select_next();
539        assert_eq!(view.get_selected_index(), Some(1));
540
541        // Move to last
542        view.select_last();
543        let visible_count = view.visible_count();
544        assert_eq!(view.get_selected_index(), Some(visible_count - 1));
545    }
546
547    #[tokio::test]
548    async fn test_visible_count() {
549        let (_temp_dir, mut view) = create_test_view().await;
550
551        // Initially only root
552        assert_eq!(view.visible_count(), 1);
553
554        // After expanding root
555        let root_id = view.tree().root_id();
556        view.tree_mut().expand_node(root_id).await.unwrap();
557        assert_eq!(view.visible_count(), 4); // root + 3 children
558    }
559
560    #[tokio::test]
561    async fn test_sort_mode() {
562        let (_temp_dir, mut view) = create_test_view().await;
563
564        assert_eq!(view.get_sort_mode(), SortMode::Type);
565
566        view.set_sort_mode(SortMode::Name);
567        assert_eq!(view.get_sort_mode(), SortMode::Name);
568
569        view.set_sort_mode(SortMode::Modified);
570        assert_eq!(view.get_sort_mode(), SortMode::Modified);
571    }
572}