fresh/view/file_tree/
view.rs1use super::ignore::IgnorePatterns;
2use super::node::NodeId;
3use super::search::FileExplorerSearch;
4use super::tree::FileTree;
5use crate::input::fuzzy::FuzzyMatch;
6use crate::model::filesystem::DirEntry;
7use std::collections::HashMap;
8use std::path::PathBuf;
9
10#[derive(Debug)]
12pub struct FileTreeView {
13 tree: FileTree,
15 selected_node: Option<NodeId>,
17 scroll_offset: usize,
19 sort_mode: SortMode,
21 ignore_patterns: IgnorePatterns,
23 pub(crate) viewport_height: usize,
25 search: FileExplorerSearch,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum SortMode {
32 Name,
34 Type,
36 Modified,
38}
39
40impl FileTreeView {
41 pub fn new(tree: FileTree) -> Self {
43 let root_id = tree.root_id();
44 Self {
45 tree,
46 selected_node: Some(root_id),
47 scroll_offset: 0,
48 sort_mode: SortMode::Type,
49 ignore_patterns: IgnorePatterns::new(),
50 viewport_height: 10, search: FileExplorerSearch::new(),
52 }
53 }
54
55 fn filtered_visible_nodes(&self) -> Vec<NodeId> {
60 let mut result = Vec::new();
61 self.collect_filtered_visible(self.tree.root_id(), &mut result);
62 result
63 }
64
65 fn collect_filtered_visible(&self, id: NodeId, result: &mut Vec<NodeId>) {
67 let is_root = id == self.tree.root_id();
68 if !is_root && !self.is_node_visible(id) {
69 return;
70 }
71
72 result.push(id);
73
74 if let Some(node) = self.tree.get_node(id) {
75 if node.is_expanded() {
76 for &child_id in &node.children {
77 self.collect_filtered_visible(child_id, result);
78 }
79 }
80 }
81 }
82
83 pub fn set_viewport_height(&mut self, height: usize) {
85 self.viewport_height = height;
86 }
87
88 pub fn tree(&self) -> &FileTree {
90 &self.tree
91 }
92
93 pub fn tree_mut(&mut self) -> &mut FileTree {
95 &mut self.tree
96 }
97
98 pub fn get_display_nodes(&self) -> Vec<(NodeId, usize)> {
102 let visible = self.filtered_visible_nodes();
103 visible
104 .into_iter()
105 .map(|id| {
106 let depth = self.tree.get_depth(id);
107 (id, depth)
108 })
109 .collect()
110 }
111
112 pub fn get_selected(&self) -> Option<NodeId> {
114 self.selected_node
115 }
116
117 pub fn set_selected(&mut self, node_id: Option<NodeId>) {
119 self.selected_node = node_id;
120 }
121
122 pub fn select_next(&mut self) {
124 let visible = self.filtered_visible_nodes();
125 if visible.is_empty() {
126 return;
127 }
128
129 if let Some(current) = self.selected_node {
130 if let Some(pos) = visible.iter().position(|&id| id == current) {
131 if pos + 1 < visible.len() {
132 self.selected_node = Some(visible[pos + 1]);
133 }
134 }
135 } else {
136 self.selected_node = Some(visible[0]);
137 }
138 }
139
140 pub fn select_prev(&mut self) {
142 let visible = self.filtered_visible_nodes();
143 if visible.is_empty() {
144 return;
145 }
146
147 if let Some(current) = self.selected_node {
148 if let Some(pos) = visible.iter().position(|&id| id == current) {
149 if pos > 0 {
150 self.selected_node = Some(visible[pos - 1]);
151 }
152 }
153 } else {
154 self.selected_node = Some(visible[0]);
155 }
156 }
157
158 pub fn select_page_up(&mut self) {
160 if self.viewport_height == 0 {
161 return;
162 }
163
164 let visible = self.filtered_visible_nodes();
165 if visible.is_empty() {
166 return;
167 }
168
169 if let Some(current) = self.selected_node {
170 if let Some(pos) = visible.iter().position(|&id| id == current) {
171 let new_pos = pos.saturating_sub(self.viewport_height);
172 self.selected_node = Some(visible[new_pos]);
173 }
174 } else {
175 self.selected_node = Some(visible[0]);
176 }
177 }
178
179 pub fn select_page_down(&mut self) {
181 if self.viewport_height == 0 {
182 return;
183 }
184
185 let visible = self.filtered_visible_nodes();
186 if visible.is_empty() {
187 return;
188 }
189
190 if let Some(current) = self.selected_node {
191 if let Some(pos) = visible.iter().position(|&id| id == current) {
192 let new_pos = (pos + self.viewport_height).min(visible.len() - 1);
193 self.selected_node = Some(visible[new_pos]);
194 }
195 } else {
196 self.selected_node = Some(visible[0]);
197 }
198 }
199
200 pub fn update_scroll_for_selection(&mut self) {
208 if self.viewport_height == 0 {
209 return;
210 }
211
212 if let Some(selected) = self.selected_node {
213 let visible = self.filtered_visible_nodes();
214 if let Some(pos) = visible.iter().position(|&id| id == selected) {
215 if pos < self.scroll_offset {
220 self.scroll_offset = pos;
221 }
222 else if pos >= self.scroll_offset + self.viewport_height {
224 self.scroll_offset = pos - self.viewport_height + 1;
225 }
226 }
228 }
229 }
230
231 pub fn select_first(&mut self) {
233 let visible = self.filtered_visible_nodes();
234 if !visible.is_empty() {
235 self.selected_node = Some(visible[0]);
236 }
237 }
238
239 pub fn select_last(&mut self) {
241 let visible = self.filtered_visible_nodes();
242 if !visible.is_empty() {
243 self.selected_node = Some(*visible.last().unwrap());
244 }
245 }
246
247 pub fn select_parent(&mut self) {
249 if let Some(current) = self.selected_node {
250 if let Some(node) = self.tree.get_node(current) {
251 if let Some(parent_id) = node.parent {
252 self.selected_node = Some(parent_id);
253 }
254 }
255 }
256 }
257
258 pub fn get_scroll_offset(&self) -> usize {
260 self.scroll_offset
261 }
262
263 pub fn set_scroll_offset(&mut self, offset: usize) {
265 self.scroll_offset = offset;
266 }
267
268 pub fn ensure_visible(&mut self, viewport_height: usize) {
276 if viewport_height == 0 {
277 return;
278 }
279
280 if let Some(selected) = self.selected_node {
281 let visible = self.filtered_visible_nodes();
282 if let Some(pos) = visible.iter().position(|&id| id == selected) {
283 if pos < self.scroll_offset {
285 self.scroll_offset = pos;
286 }
287 else if pos >= self.scroll_offset + viewport_height {
289 self.scroll_offset = pos - viewport_height + 1;
290 }
291 }
292 }
293 }
294
295 pub fn get_sort_mode(&self) -> SortMode {
297 self.sort_mode
298 }
299
300 pub fn set_sort_mode(&mut self, mode: SortMode) {
302 self.sort_mode = mode;
303 }
305
306 pub fn get_selected_entry(&self) -> Option<&DirEntry> {
308 self.selected_node
309 .and_then(|id| self.tree.get_node(id))
310 .map(|node| &node.entry)
311 }
312
313 pub fn navigate_to_path(&mut self, path: &std::path::Path) {
315 if let Some(node) = self.tree.get_node_by_path(path) {
316 self.selected_node = Some(node.id);
317 }
318 }
319
320 pub fn get_selected_index(&self) -> Option<usize> {
322 if let Some(selected) = self.selected_node {
323 let visible = self.filtered_visible_nodes();
324 visible.iter().position(|&id| id == selected)
325 } else {
326 None
327 }
328 }
329
330 pub fn get_node_at_index(&self, index: usize) -> Option<NodeId> {
332 let visible = self.filtered_visible_nodes();
333 visible.get(index).copied()
334 }
335
336 pub fn visible_count(&self) -> usize {
338 self.filtered_visible_nodes().len()
339 }
340
341 pub fn ignore_patterns(&self) -> &IgnorePatterns {
343 &self.ignore_patterns
344 }
345
346 pub fn ignore_patterns_mut(&mut self) -> &mut IgnorePatterns {
348 &mut self.ignore_patterns
349 }
350
351 pub fn toggle_show_hidden(&mut self) {
353 self.ignore_patterns.toggle_show_hidden();
354 }
355
356 pub fn toggle_show_gitignored(&mut self) {
358 self.ignore_patterns.toggle_show_gitignored();
359 }
360
361 pub fn is_node_visible(&self, node_id: NodeId) -> bool {
363 if let Some(node) = self.tree.get_node(node_id) {
364 !self
365 .ignore_patterns
366 .is_ignored(&node.entry.path, node.is_dir())
367 } else {
368 false
369 }
370 }
371
372 pub fn load_gitignore_for_dir(&mut self, dir_path: &std::path::Path) -> std::io::Result<()> {
376 self.ignore_patterns.load_gitignore(dir_path)
377 }
378
379 pub async fn expand_and_select_file(&mut self, path: &std::path::Path) -> bool {
397 if let Some(node_id) = self.tree.expand_to_path(path).await {
398 self.selected_node = Some(node_id);
399 true
400 } else {
401 false
402 }
403 }
404
405 pub fn collect_symlink_mappings(&self) -> HashMap<PathBuf, PathBuf> {
411 let mut mappings = HashMap::new();
412
413 for node_id in self.filtered_visible_nodes() {
414 if let Some(node) = self.tree.get_node(node_id) {
415 if node.entry.is_symlink() && node.is_dir() && node.is_expanded() {
417 if let Ok(canonical) = node.entry.path.canonicalize() {
419 if canonical != node.entry.path {
420 mappings.insert(node.entry.path.clone(), canonical);
421 }
422 }
423 }
424 }
425 }
426
427 mappings
428 }
429
430 pub fn search_query(&self) -> &str {
434 self.search.query()
435 }
436
437 pub fn is_search_active(&self) -> bool {
439 self.search.is_active()
440 }
441
442 pub fn search_push_char(&mut self, c: char) {
444 self.search.push_char(c);
445 self.jump_to_first_match();
446 }
447
448 pub fn search_pop_char(&mut self) {
450 self.search.pop_char();
451 if self.search.is_active() {
452 self.jump_to_first_match();
453 }
454 }
455
456 pub fn search_clear(&mut self) {
458 self.search.clear();
459 }
460
461 fn get_matching_nodes(&self) -> Vec<NodeId> {
463 if !self.search.is_active() {
464 return self.filtered_visible_nodes();
465 }
466
467 self.filtered_visible_nodes()
468 .into_iter()
469 .filter(|&id| {
470 if let Some(node) = self.tree.get_node(id) {
471 self.search.matches(&node.entry.name)
472 } else {
473 false
474 }
475 })
476 .collect()
477 }
478
479 fn jump_to_first_match(&mut self) {
481 let matching = self.get_matching_nodes();
482 if let Some(&first) = matching.first() {
483 self.selected_node = Some(first);
484 self.update_scroll_for_selection();
485 }
486 }
487
488 pub fn select_next_match(&mut self) {
490 if !self.search.is_active() {
491 self.select_next();
492 return;
493 }
494
495 let matching = self.get_matching_nodes();
496 if matching.is_empty() {
497 return;
498 }
499
500 if let Some(current) = self.selected_node {
501 if let Some(pos) = matching.iter().position(|&id| id == current) {
502 let next_pos = (pos + 1) % matching.len();
504 self.selected_node = Some(matching[next_pos]);
505 } else {
506 self.selected_node = Some(matching[0]);
508 }
509 } else {
510 self.selected_node = Some(matching[0]);
511 }
512 }
513
514 pub fn select_prev_match(&mut self) {
516 if !self.search.is_active() {
517 self.select_prev();
518 return;
519 }
520
521 let matching = self.get_matching_nodes();
522 if matching.is_empty() {
523 return;
524 }
525
526 if let Some(current) = self.selected_node {
527 if let Some(pos) = matching.iter().position(|&id| id == current) {
528 let prev_pos = if pos == 0 {
530 matching.len() - 1
531 } else {
532 pos - 1
533 };
534 self.selected_node = Some(matching[prev_pos]);
535 } else {
536 self.selected_node = Some(*matching.last().unwrap());
538 }
539 } else {
540 self.selected_node = Some(*matching.last().unwrap());
541 }
542 }
543
544 pub fn get_match_for_node(&self, node_id: NodeId) -> Option<FuzzyMatch> {
546 if !self.search.is_active() {
547 return None;
548 }
549
550 self.tree
551 .get_node(node_id)
552 .and_then(|node| self.search.match_name(&node.entry.name))
553 }
554
555 pub fn node_matches_search(&self, node_id: NodeId) -> bool {
557 if !self.search.is_active() {
558 return true;
559 }
560
561 self.tree
562 .get_node(node_id)
563 .map(|node| self.search.matches(&node.entry.name))
564 .unwrap_or(false)
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571 use crate::model::filesystem::StdFileSystem;
572 use crate::services::fs::FsManager;
573 use std::fs as std_fs;
574 use std::sync::Arc;
575 use tempfile::TempDir;
576
577 async fn create_test_view() -> (TempDir, FileTreeView) {
578 let temp_dir = TempDir::new().unwrap();
579 let temp_path = temp_dir.path();
580
581 std_fs::create_dir(temp_path.join("dir1")).unwrap();
583 std_fs::write(temp_path.join("dir1/file1.txt"), "content1").unwrap();
584 std_fs::write(temp_path.join("dir1/file2.txt"), "content2").unwrap();
585 std_fs::create_dir(temp_path.join("dir2")).unwrap();
586 std_fs::write(temp_path.join("file3.txt"), "content3").unwrap();
587
588 let backend = Arc::new(StdFileSystem);
589 let manager = Arc::new(FsManager::new(backend));
590 let tree = FileTree::new(temp_path.to_path_buf(), manager)
591 .await
592 .unwrap();
593 let view = FileTreeView::new(tree);
594
595 (temp_dir, view)
596 }
597
598 #[tokio::test]
599 async fn test_view_creation() {
600 let (_temp_dir, view) = create_test_view().await;
601
602 assert!(view.get_selected().is_some());
603 assert_eq!(view.get_scroll_offset(), 0);
604 assert_eq!(view.get_sort_mode(), SortMode::Type);
605 }
606
607 #[tokio::test]
608 async fn test_get_display_nodes() {
609 let (_temp_dir, mut view) = create_test_view().await;
610
611 let display = view.get_display_nodes();
613 assert_eq!(display.len(), 1);
614 assert_eq!(display[0].1, 0); let root_id = view.tree().root_id();
618 view.tree_mut().expand_node(root_id).await.unwrap();
619
620 let display = view.get_display_nodes();
621 assert_eq!(display.len(), 4); assert_eq!(display[0].1, 0); assert_eq!(display[1].1, 1); assert_eq!(display[2].1, 1); assert_eq!(display[3].1, 1); }
629
630 #[tokio::test]
631 async fn test_navigation() {
632 let (_temp_dir, mut view) = create_test_view().await;
633
634 let root_id = view.tree().root_id();
635 view.tree_mut().expand_node(root_id).await.unwrap();
636
637 let root_id = view.tree().root_id();
638 assert_eq!(view.get_selected(), Some(root_id));
639
640 view.select_next();
642 assert_ne!(view.get_selected(), Some(root_id));
643
644 view.select_prev();
646 assert_eq!(view.get_selected(), Some(root_id));
647
648 view.select_last();
650 let visible = view.tree().get_visible_nodes();
651 assert_eq!(view.get_selected(), Some(*visible.last().unwrap()));
652
653 view.select_first();
655 assert_eq!(view.get_selected(), Some(root_id));
656 }
657
658 #[tokio::test]
659 async fn test_select_parent() {
660 let (_temp_dir, mut view) = create_test_view().await;
661
662 let root_id = view.tree().root_id();
663 view.tree_mut().expand_node(root_id).await.unwrap();
664
665 view.select_next();
667 let child_id = view.get_selected().unwrap();
668 assert_ne!(child_id, root_id);
669
670 view.select_parent();
672 assert_eq!(view.get_selected(), Some(root_id));
673 }
674
675 #[tokio::test]
676 async fn test_ensure_visible() {
677 let (_temp_dir, mut view) = create_test_view().await;
678
679 let root_id = view.tree().root_id();
680 view.tree_mut().expand_node(root_id).await.unwrap();
681
682 let viewport_height = 2;
683
684 view.select_last();
686 view.ensure_visible(viewport_height);
687
688 let selected_index = view.get_selected_index().unwrap();
690 assert!(selected_index >= view.get_scroll_offset());
691 assert!(selected_index < view.get_scroll_offset() + viewport_height);
692
693 view.select_first();
695 view.ensure_visible(viewport_height);
696
697 assert_eq!(view.get_scroll_offset(), 0);
699 }
700
701 #[tokio::test]
702 async fn test_get_selected_entry() {
703 let (_temp_dir, view) = create_test_view().await;
704
705 let entry = view.get_selected_entry();
706 assert!(entry.is_some());
707 assert!(entry.unwrap().is_dir());
708 }
709
710 #[tokio::test]
711 async fn test_navigate_to_path() {
712 let (_temp_dir, mut view) = create_test_view().await;
713
714 let root_id = view.tree().root_id();
715 view.tree_mut().expand_node(root_id).await.unwrap();
716
717 let dir1_path = view.tree().root_path().join("dir1");
718 view.navigate_to_path(&dir1_path);
719
720 let selected_entry = view.get_selected_entry().unwrap();
721 assert_eq!(selected_entry.name, "dir1");
722 }
723
724 #[tokio::test]
725 async fn test_get_selected_index() {
726 let (_temp_dir, mut view) = create_test_view().await;
727
728 let root_id = view.tree().root_id();
729 view.tree_mut().expand_node(root_id).await.unwrap();
730
731 assert_eq!(view.get_selected_index(), Some(0));
733
734 view.select_next();
736 assert_eq!(view.get_selected_index(), Some(1));
737
738 view.select_last();
740 let visible_count = view.visible_count();
741 assert_eq!(view.get_selected_index(), Some(visible_count - 1));
742 }
743
744 #[tokio::test]
745 async fn test_visible_count() {
746 let (_temp_dir, mut view) = create_test_view().await;
747
748 assert_eq!(view.visible_count(), 1);
750
751 let root_id = view.tree().root_id();
753 view.tree_mut().expand_node(root_id).await.unwrap();
754 assert_eq!(view.visible_count(), 4); }
756
757 #[tokio::test]
758 async fn test_sort_mode() {
759 let (_temp_dir, mut view) = create_test_view().await;
760
761 assert_eq!(view.get_sort_mode(), SortMode::Type);
762
763 view.set_sort_mode(SortMode::Name);
764 assert_eq!(view.get_sort_mode(), SortMode::Name);
765
766 view.set_sort_mode(SortMode::Modified);
767 assert_eq!(view.get_sort_mode(), SortMode::Modified);
768 }
769}