fresh/view/file_tree/
view.rs1use super::ignore::IgnorePatterns;
2use super::node::NodeId;
3use super::tree::FileTree;
4use crate::model::filesystem::DirEntry;
5
6#[derive(Debug)]
8pub struct FileTreeView {
9 tree: FileTree,
11 selected_node: Option<NodeId>,
13 scroll_offset: usize,
15 sort_mode: SortMode,
17 ignore_patterns: IgnorePatterns,
19 pub(crate) viewport_height: usize,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum SortMode {
26 Name,
28 Type,
30 Modified,
32}
33
34impl FileTreeView {
35 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, }
46 }
47
48 pub fn set_viewport_height(&mut self, height: usize) {
50 self.viewport_height = height;
51 }
52
53 pub fn tree(&self) -> &FileTree {
55 &self.tree
56 }
57
58 pub fn tree_mut(&mut self) -> &mut FileTree {
60 &mut self.tree
61 }
62
63 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 pub fn get_selected(&self) -> Option<NodeId> {
79 self.selected_node
80 }
81
82 pub fn set_selected(&mut self, node_id: Option<NodeId>) {
84 self.selected_node = node_id;
85 }
86
87 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 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 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 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 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 if pos < self.scroll_offset {
185 self.scroll_offset = pos;
186 }
187 else if pos >= self.scroll_offset + self.viewport_height {
189 self.scroll_offset = pos - self.viewport_height + 1;
190 }
191 }
193 }
194 }
195
196 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 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 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 pub fn get_scroll_offset(&self) -> usize {
225 self.scroll_offset
226 }
227
228 pub fn set_scroll_offset(&mut self, offset: usize) {
230 self.scroll_offset = offset;
231 }
232
233 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 pos < self.scroll_offset {
250 self.scroll_offset = pos;
251 }
252 else if pos >= self.scroll_offset + viewport_height {
254 self.scroll_offset = pos - viewport_height + 1;
255 }
256 }
257 }
258 }
259
260 pub fn get_sort_mode(&self) -> SortMode {
262 self.sort_mode
263 }
264
265 pub fn set_sort_mode(&mut self, mode: SortMode) {
267 self.sort_mode = mode;
268 }
270
271 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 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 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 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 pub fn visible_count(&self) -> usize {
303 self.tree.get_visible_nodes().len()
304 }
305
306 pub fn ignore_patterns(&self) -> &IgnorePatterns {
308 &self.ignore_patterns
309 }
310
311 pub fn ignore_patterns_mut(&mut self) -> &mut IgnorePatterns {
313 &mut self.ignore_patterns
314 }
315
316 pub fn toggle_show_hidden(&mut self) {
318 self.ignore_patterns.toggle_show_hidden();
319 }
320
321 pub fn toggle_show_gitignored(&mut self) {
323 self.ignore_patterns.toggle_show_gitignored();
324 }
325
326 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 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 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 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 let display = view.get_display_nodes();
416 assert_eq!(display.len(), 1);
417 assert_eq!(display[0].1, 0); 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); assert_eq!(display[0].1, 0); assert_eq!(display[1].1, 1); assert_eq!(display[2].1, 1); assert_eq!(display[3].1, 1); }
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 view.select_next();
445 assert_ne!(view.get_selected(), Some(root_id));
446
447 view.select_prev();
449 assert_eq!(view.get_selected(), Some(root_id));
450
451 view.select_last();
453 let visible = view.tree().get_visible_nodes();
454 assert_eq!(view.get_selected(), Some(*visible.last().unwrap()));
455
456 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 view.select_next();
470 let child_id = view.get_selected().unwrap();
471 assert_ne!(child_id, root_id);
472
473 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 view.select_last();
489 view.ensure_visible(viewport_height);
490
491 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 view.select_first();
498 view.ensure_visible(viewport_height);
499
500 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 assert_eq!(view.get_selected_index(), Some(0));
536
537 view.select_next();
539 assert_eq!(view.get_selected_index(), Some(1));
540
541 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 assert_eq!(view.visible_count(), 1);
553
554 let root_id = view.tree().root_id();
556 view.tree_mut().expand_node(root_id).await.unwrap();
557 assert_eq!(view.visible_count(), 4); }
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}