1use 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, HashSet};
8use std::path::PathBuf;
9
10#[derive(Debug)]
12pub struct FileTreeView {
13 tree: FileTree,
15 selected_node: Option<NodeId>,
17 multi_selection: HashSet<NodeId>,
19 selection_anchor: Option<NodeId>,
21 scroll_offset: usize,
23 sort_mode: SortMode,
25 ignore_patterns: IgnorePatterns,
27 pub(crate) viewport_height: usize,
29 search: FileExplorerSearch,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum SortMode {
36 Name,
38 Type,
40 Modified,
42}
43
44impl FileTreeView {
45 pub fn new(tree: FileTree) -> Self {
47 let root_id = tree.root_id();
48 Self {
49 tree,
50 selected_node: Some(root_id),
51 multi_selection: HashSet::new(),
52 selection_anchor: None,
53 scroll_offset: 0,
54 sort_mode: SortMode::Type,
55 ignore_patterns: IgnorePatterns::new(),
56 viewport_height: 10, search: FileExplorerSearch::new(),
58 }
59 }
60
61 fn filtered_visible_nodes(&self) -> Vec<NodeId> {
66 let mut result = Vec::new();
67 self.collect_filtered_visible(self.tree.root_id(), &mut result);
68 result
69 }
70
71 fn collect_filtered_visible(&self, id: NodeId, result: &mut Vec<NodeId>) {
73 let is_root = id == self.tree.root_id();
74 if !is_root && !self.is_node_visible(id) {
75 return;
76 }
77
78 result.push(id);
79
80 if let Some(node) = self.tree.get_node(id) {
81 if node.is_expanded() {
82 for &child_id in &node.children {
83 self.collect_filtered_visible(child_id, result);
84 }
85 }
86 }
87 }
88
89 pub fn set_viewport_height(&mut self, height: usize) {
91 self.viewport_height = height;
92 }
93
94 pub fn tree(&self) -> &FileTree {
96 &self.tree
97 }
98
99 pub fn tree_mut(&mut self) -> &mut FileTree {
101 &mut self.tree
102 }
103
104 pub fn get_display_nodes(&self) -> Vec<(NodeId, usize)> {
108 let visible = self.filtered_visible_nodes();
109 visible
110 .into_iter()
111 .map(|id| {
112 let depth = self.tree.get_depth(id);
113 (id, depth)
114 })
115 .collect()
116 }
117
118 pub fn get_selected(&self) -> Option<NodeId> {
120 self.selected_node
121 }
122
123 pub fn set_selected(&mut self, node_id: Option<NodeId>) {
125 self.selected_node = node_id;
126 }
127
128 pub fn select_next(&mut self) {
130 self.clear_multi_selection();
131 let visible = self.filtered_visible_nodes();
132 if visible.is_empty() {
133 return;
134 }
135
136 if let Some(current) = self.selected_node {
137 if let Some(pos) = visible.iter().position(|&id| id == current) {
138 if pos + 1 < visible.len() {
139 self.selected_node = Some(visible[pos + 1]);
140 }
141 }
142 } else {
143 self.selected_node = Some(visible[0]);
144 }
145 }
146
147 pub fn select_prev(&mut self) {
149 self.clear_multi_selection();
150 let visible = self.filtered_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 if pos > 0 {
158 self.selected_node = Some(visible[pos - 1]);
159 }
160 }
161 } else {
162 self.selected_node = Some(visible[0]);
163 }
164 }
165
166 pub fn select_page_up(&mut self) {
168 if self.viewport_height == 0 {
169 return;
170 }
171
172 let visible = self.filtered_visible_nodes();
173 if visible.is_empty() {
174 return;
175 }
176
177 if let Some(current) = self.selected_node {
178 if let Some(pos) = visible.iter().position(|&id| id == current) {
179 let new_pos = pos.saturating_sub(self.viewport_height);
180 self.selected_node = Some(visible[new_pos]);
181 }
182 } else {
183 self.selected_node = Some(visible[0]);
184 }
185 }
186
187 pub fn select_page_down(&mut self) {
189 if self.viewport_height == 0 {
190 return;
191 }
192
193 let visible = self.filtered_visible_nodes();
194 if visible.is_empty() {
195 return;
196 }
197
198 if let Some(current) = self.selected_node {
199 if let Some(pos) = visible.iter().position(|&id| id == current) {
200 let new_pos = (pos + self.viewport_height).min(visible.len() - 1);
201 self.selected_node = Some(visible[new_pos]);
202 }
203 } else {
204 self.selected_node = Some(visible[0]);
205 }
206 }
207
208 pub fn update_scroll_for_selection(&mut self) {
216 if self.viewport_height == 0 {
217 return;
218 }
219 let visible = self.filtered_visible_nodes();
220 self.update_scroll_with_nodes(&visible);
221 }
222
223 fn update_scroll_with_nodes(&mut self, visible: &[NodeId]) {
224 if self.viewport_height == 0 {
225 return;
226 }
227 if let Some(selected) = self.selected_node {
228 if let Some(pos) = visible.iter().position(|&id| id == selected) {
229 if pos < self.scroll_offset {
230 self.scroll_offset = pos;
231 } else if pos >= self.scroll_offset + self.viewport_height {
232 self.scroll_offset = pos - self.viewport_height + 1;
233 }
234 }
235 }
236 }
237
238 pub fn select_first(&mut self) {
240 let visible = self.filtered_visible_nodes();
241 if !visible.is_empty() {
242 self.selected_node = Some(visible[0]);
243 }
244 }
245
246 pub fn select_last(&mut self) {
248 let visible = self.filtered_visible_nodes();
249 if !visible.is_empty() {
250 self.selected_node = Some(*visible.last().unwrap());
251 }
252 }
253
254 pub fn toggle_select(&mut self) {
256 if let Some(cursor) = self.selected_node {
257 if self.multi_selection.contains(&cursor) {
258 self.multi_selection.remove(&cursor);
259 } else {
260 self.multi_selection.insert(cursor);
261 }
262 self.selection_anchor = Some(cursor);
263 }
264 }
265
266 pub fn extend_selection_up(&mut self) {
268 let visible = self.filtered_visible_nodes();
269 if visible.is_empty() {
270 return;
271 }
272 let Some(current) = self.selected_node else {
273 return;
274 };
275 let Some(pos) = visible.iter().position(|&id| id == current) else {
276 return;
277 };
278 if self.multi_selection.is_empty() {
282 self.multi_selection.insert(current);
283 self.selection_anchor = Some(current);
284 }
285 if pos == 0 {
286 return;
287 }
288 let anchor = self.selection_anchor.unwrap_or(current);
289 let new_pos = pos - 1;
290 self.selected_node = Some(visible[new_pos]);
291 let anchor_pos = visible
292 .iter()
293 .position(|&id| id == anchor)
294 .unwrap_or(new_pos);
295 let (lo, hi) = (new_pos.min(anchor_pos), new_pos.max(anchor_pos));
296 self.multi_selection = visible[lo..=hi].iter().copied().collect();
297 self.update_scroll_with_nodes(&visible);
298 }
299
300 pub fn extend_selection_down(&mut self) {
302 let visible = self.filtered_visible_nodes();
303 if visible.is_empty() {
304 return;
305 }
306 let Some(current) = self.selected_node else {
307 return;
308 };
309 let Some(pos) = visible.iter().position(|&id| id == current) else {
310 return;
311 };
312 if self.multi_selection.is_empty() {
316 self.multi_selection.insert(current);
317 self.selection_anchor = Some(current);
318 }
319 if pos + 1 >= visible.len() {
320 return;
321 }
322 let anchor = self.selection_anchor.unwrap_or(current);
323 let new_pos = pos + 1;
324 self.selected_node = Some(visible[new_pos]);
325 let anchor_pos = visible
326 .iter()
327 .position(|&id| id == anchor)
328 .unwrap_or(new_pos);
329 let (lo, hi) = (new_pos.min(anchor_pos), new_pos.max(anchor_pos));
330 self.multi_selection = visible[lo..=hi].iter().copied().collect();
331 self.update_scroll_with_nodes(&visible);
332 }
333
334 pub fn select_all(&mut self) {
336 let visible = self.filtered_visible_nodes();
337 self.multi_selection = visible.iter().copied().collect();
338 self.selection_anchor = self.selected_node;
339 }
340
341 pub fn clear_multi_selection(&mut self) {
343 self.multi_selection.clear();
344 self.selection_anchor = None;
345 }
346
347 pub fn has_multi_selection(&self) -> bool {
352 !self.multi_selection.is_empty()
353 }
354
355 pub fn multi_selection(&self) -> &HashSet<NodeId> {
357 &self.multi_selection
358 }
359
360 pub fn effective_selection(&self) -> Vec<NodeId> {
367 if self.multi_selection.is_empty() {
368 return self.selected_node.into_iter().collect();
369 }
370 self.filtered_visible_nodes()
374 .into_iter()
375 .filter(|id| self.multi_selection.contains(id))
376 .collect()
377 }
378
379 pub fn select_parent(&mut self) {
381 if let Some(current) = self.selected_node {
382 if let Some(node) = self.tree.get_node(current) {
383 if let Some(parent_id) = node.parent {
384 self.selected_node = Some(parent_id);
385 }
386 }
387 }
388 }
389
390 pub fn get_scroll_offset(&self) -> usize {
392 self.scroll_offset
393 }
394
395 pub fn set_scroll_offset(&mut self, offset: usize) {
397 self.scroll_offset = offset;
398 }
399
400 pub fn ensure_visible(&mut self, viewport_height: usize) {
408 if viewport_height == 0 {
409 return;
410 }
411
412 if let Some(selected) = self.selected_node {
413 let visible = self.filtered_visible_nodes();
414 if let Some(pos) = visible.iter().position(|&id| id == selected) {
415 if pos < self.scroll_offset {
417 self.scroll_offset = pos;
418 }
419 else if pos >= self.scroll_offset + viewport_height {
421 self.scroll_offset = pos - viewport_height + 1;
422 }
423 }
424 }
425 }
426
427 pub fn get_sort_mode(&self) -> SortMode {
429 self.sort_mode
430 }
431
432 pub fn set_sort_mode(&mut self, mode: SortMode) {
434 self.sort_mode = mode;
435 }
437
438 pub fn get_selected_entry(&self) -> Option<&DirEntry> {
440 self.selected_node
441 .and_then(|id| self.tree.get_node(id))
442 .map(|node| &node.entry)
443 }
444
445 pub fn navigate_to_path(&mut self, path: &std::path::Path) {
447 if let Some(node) = self.tree.get_node_by_path(path) {
448 self.selected_node = Some(node.id);
449 self.update_scroll_for_selection();
450 }
451 }
452
453 pub fn get_selected_index(&self) -> Option<usize> {
455 if let Some(selected) = self.selected_node {
456 let visible = self.filtered_visible_nodes();
457 visible.iter().position(|&id| id == selected)
458 } else {
459 None
460 }
461 }
462
463 pub fn get_node_at_index(&self, index: usize) -> Option<NodeId> {
465 let visible = self.filtered_visible_nodes();
466 visible.get(index).copied()
467 }
468
469 pub fn visible_count(&self) -> usize {
471 self.filtered_visible_nodes().len()
472 }
473
474 pub fn ignore_patterns(&self) -> &IgnorePatterns {
476 &self.ignore_patterns
477 }
478
479 pub fn ignore_patterns_mut(&mut self) -> &mut IgnorePatterns {
481 &mut self.ignore_patterns
482 }
483
484 pub fn toggle_show_hidden(&mut self) {
486 self.ignore_patterns.toggle_show_hidden();
487 }
488
489 pub fn toggle_show_gitignored(&mut self) {
491 self.ignore_patterns.toggle_show_gitignored();
492 }
493
494 pub fn is_node_visible(&self, node_id: NodeId) -> bool {
496 if let Some(node) = self.tree.get_node(node_id) {
497 !self
498 .ignore_patterns
499 .is_ignored(&node.entry.path, node.is_dir())
500 } else {
501 false
502 }
503 }
504
505 pub fn load_gitignore_from_bytes(
508 &mut self,
509 dir_path: &std::path::Path,
510 contents: &[u8],
511 mtime: Option<std::time::SystemTime>,
512 ) {
513 self.ignore_patterns
514 .load_gitignore_from_bytes(dir_path, contents, mtime);
515 }
516
517 pub async fn expand_and_select_file(&mut self, path: &std::path::Path) -> bool {
535 if let Some(node_id) = self.tree.expand_to_path(path).await {
536 self.selected_node = Some(node_id);
537 true
538 } else {
539 false
540 }
541 }
542
543 pub fn collect_symlink_mappings(&self) -> HashMap<PathBuf, PathBuf> {
549 let mut mappings = HashMap::new();
550
551 for node_id in self.filtered_visible_nodes() {
552 if let Some(node) = self.tree.get_node(node_id) {
553 if node.entry.is_symlink() && node.is_dir() && node.is_expanded() {
555 if let Ok(canonical) = node.entry.path.canonicalize() {
557 if canonical != node.entry.path {
558 mappings.insert(node.entry.path.clone(), canonical);
559 }
560 }
561 }
562 }
563 }
564
565 mappings
566 }
567
568 pub fn search_query(&self) -> &str {
572 self.search.query()
573 }
574
575 pub fn is_search_active(&self) -> bool {
577 self.search.is_active()
578 }
579
580 pub fn search_push_char(&mut self, c: char) {
582 self.search.push_char(c);
583 self.jump_to_first_match();
584 }
585
586 pub fn search_pop_char(&mut self) {
588 self.search.pop_char();
589 if self.search.is_active() {
590 self.jump_to_first_match();
591 }
592 }
593
594 pub fn search_clear(&mut self) {
596 self.search.clear();
597 }
598
599 fn get_matching_nodes(&self) -> Vec<NodeId> {
601 if !self.search.is_active() {
602 return self.filtered_visible_nodes();
603 }
604
605 self.filtered_visible_nodes()
606 .into_iter()
607 .filter(|&id| {
608 if let Some(node) = self.tree.get_node(id) {
609 self.search.matches(&node.entry.name)
610 } else {
611 false
612 }
613 })
614 .collect()
615 }
616
617 fn jump_to_first_match(&mut self) {
619 let matching = self.get_matching_nodes();
620 if let Some(&first) = matching.first() {
621 self.selected_node = Some(first);
622 self.update_scroll_for_selection();
623 }
624 }
625
626 pub fn select_next_match(&mut self) {
628 if !self.search.is_active() {
629 self.select_next();
630 return;
631 }
632
633 let matching = self.get_matching_nodes();
634 if matching.is_empty() {
635 return;
636 }
637
638 if let Some(current) = self.selected_node {
639 if let Some(pos) = matching.iter().position(|&id| id == current) {
640 let next_pos = (pos + 1) % matching.len();
642 self.selected_node = Some(matching[next_pos]);
643 } else {
644 self.selected_node = Some(matching[0]);
646 }
647 } else {
648 self.selected_node = Some(matching[0]);
649 }
650 }
651
652 pub fn select_prev_match(&mut self) {
654 if !self.search.is_active() {
655 self.select_prev();
656 return;
657 }
658
659 let matching = self.get_matching_nodes();
660 if matching.is_empty() {
661 return;
662 }
663
664 if let Some(current) = self.selected_node {
665 if let Some(pos) = matching.iter().position(|&id| id == current) {
666 let prev_pos = if pos == 0 {
668 matching.len() - 1
669 } else {
670 pos - 1
671 };
672 self.selected_node = Some(matching[prev_pos]);
673 } else {
674 self.selected_node = Some(*matching.last().unwrap());
676 }
677 } else {
678 self.selected_node = Some(*matching.last().unwrap());
679 }
680 }
681
682 pub fn get_match_for_node(&self, node_id: NodeId) -> Option<FuzzyMatch> {
684 if !self.search.is_active() {
685 return None;
686 }
687
688 self.tree
689 .get_node(node_id)
690 .and_then(|node| self.search.match_name(&node.entry.name))
691 }
692
693 pub fn node_matches_search(&self, node_id: NodeId) -> bool {
695 if !self.search.is_active() {
696 return true;
697 }
698
699 self.tree
700 .get_node(node_id)
701 .map(|node| self.search.matches(&node.entry.name))
702 .unwrap_or(false)
703 }
704}
705
706#[cfg(test)]
707mod tests {
708 use super::*;
709 use crate::model::filesystem::StdFileSystem;
710 use crate::services::fs::FsManager;
711 use std::fs as std_fs;
712 use std::sync::Arc;
713 use tempfile::TempDir;
714
715 async fn create_test_view() -> (TempDir, FileTreeView) {
716 let temp_dir = TempDir::new().unwrap();
717 let temp_path = temp_dir.path();
718
719 std_fs::create_dir(temp_path.join("dir1")).unwrap();
721 std_fs::write(temp_path.join("dir1/file1.txt"), "content1").unwrap();
722 std_fs::write(temp_path.join("dir1/file2.txt"), "content2").unwrap();
723 std_fs::create_dir(temp_path.join("dir2")).unwrap();
724 std_fs::write(temp_path.join("file3.txt"), "content3").unwrap();
725
726 let backend = Arc::new(StdFileSystem);
727 let manager = Arc::new(FsManager::new(backend));
728 let tree = FileTree::new(temp_path.to_path_buf(), manager)
729 .await
730 .unwrap();
731 let view = FileTreeView::new(tree);
732
733 (temp_dir, view)
734 }
735
736 #[tokio::test]
737 async fn test_view_creation() {
738 let (_temp_dir, view) = create_test_view().await;
739
740 assert!(view.get_selected().is_some());
741 assert_eq!(view.get_scroll_offset(), 0);
742 assert_eq!(view.get_sort_mode(), SortMode::Type);
743 }
744
745 #[tokio::test]
746 async fn test_get_display_nodes() {
747 let (_temp_dir, mut view) = create_test_view().await;
748
749 let display = view.get_display_nodes();
751 assert_eq!(display.len(), 1);
752 assert_eq!(display[0].1, 0); let root_id = view.tree().root_id();
756 view.tree_mut().expand_node(root_id).await.unwrap();
757
758 let display = view.get_display_nodes();
759 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); }
767
768 #[tokio::test]
769 async fn test_navigation() {
770 let (_temp_dir, mut view) = create_test_view().await;
771
772 let root_id = view.tree().root_id();
773 view.tree_mut().expand_node(root_id).await.unwrap();
774
775 let root_id = view.tree().root_id();
776 assert_eq!(view.get_selected(), Some(root_id));
777
778 view.select_next();
780 assert_ne!(view.get_selected(), Some(root_id));
781
782 view.select_prev();
784 assert_eq!(view.get_selected(), Some(root_id));
785
786 view.select_last();
788 let visible = view.tree().get_visible_nodes();
789 assert_eq!(view.get_selected(), Some(*visible.last().unwrap()));
790
791 view.select_first();
793 assert_eq!(view.get_selected(), Some(root_id));
794 }
795
796 #[tokio::test]
797 async fn test_select_parent() {
798 let (_temp_dir, mut view) = create_test_view().await;
799
800 let root_id = view.tree().root_id();
801 view.tree_mut().expand_node(root_id).await.unwrap();
802
803 view.select_next();
805 let child_id = view.get_selected().unwrap();
806 assert_ne!(child_id, root_id);
807
808 view.select_parent();
810 assert_eq!(view.get_selected(), Some(root_id));
811 }
812
813 #[tokio::test]
814 async fn test_ensure_visible() {
815 let (_temp_dir, mut view) = create_test_view().await;
816
817 let root_id = view.tree().root_id();
818 view.tree_mut().expand_node(root_id).await.unwrap();
819
820 let viewport_height = 2;
821
822 view.select_last();
824 view.ensure_visible(viewport_height);
825
826 let selected_index = view.get_selected_index().unwrap();
828 assert!(selected_index >= view.get_scroll_offset());
829 assert!(selected_index < view.get_scroll_offset() + viewport_height);
830
831 view.select_first();
833 view.ensure_visible(viewport_height);
834
835 assert_eq!(view.get_scroll_offset(), 0);
837 }
838
839 #[tokio::test]
840 async fn test_get_selected_entry() {
841 let (_temp_dir, view) = create_test_view().await;
842
843 let entry = view.get_selected_entry();
844 assert!(entry.is_some());
845 assert!(entry.unwrap().is_dir());
846 }
847
848 #[tokio::test]
849 async fn test_navigate_to_path() {
850 let (_temp_dir, mut view) = create_test_view().await;
851
852 let root_id = view.tree().root_id();
853 view.tree_mut().expand_node(root_id).await.unwrap();
854
855 let dir1_path = view.tree().root_path().join("dir1");
856 view.navigate_to_path(&dir1_path);
857
858 let selected_entry = view.get_selected_entry().unwrap();
859 assert_eq!(selected_entry.name, "dir1");
860 }
861
862 #[tokio::test]
863 async fn test_get_selected_index() {
864 let (_temp_dir, mut view) = create_test_view().await;
865
866 let root_id = view.tree().root_id();
867 view.tree_mut().expand_node(root_id).await.unwrap();
868
869 assert_eq!(view.get_selected_index(), Some(0));
871
872 view.select_next();
874 assert_eq!(view.get_selected_index(), Some(1));
875
876 view.select_last();
878 let visible_count = view.visible_count();
879 assert_eq!(view.get_selected_index(), Some(visible_count - 1));
880 }
881
882 #[tokio::test]
883 async fn test_visible_count() {
884 let (_temp_dir, mut view) = create_test_view().await;
885
886 assert_eq!(view.visible_count(), 1);
888
889 let root_id = view.tree().root_id();
891 view.tree_mut().expand_node(root_id).await.unwrap();
892 assert_eq!(view.visible_count(), 4); }
894
895 #[tokio::test]
896 async fn test_sort_mode() {
897 let (_temp_dir, mut view) = create_test_view().await;
898
899 assert_eq!(view.get_sort_mode(), SortMode::Type);
900
901 view.set_sort_mode(SortMode::Name);
902 assert_eq!(view.get_sort_mode(), SortMode::Name);
903
904 view.set_sort_mode(SortMode::Modified);
905 assert_eq!(view.get_sort_mode(), SortMode::Modified);
906 }
907}