use super::ignore::IgnorePatterns;
use super::node::NodeId;
use super::search::FileExplorerSearch;
use super::tree::FileTree;
use crate::input::fuzzy::FuzzyMatch;
use crate::model::filesystem::DirEntry;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
#[derive(Debug)]
pub struct FileTreeView {
tree: FileTree,
selected_node: Option<NodeId>,
multi_selection: HashSet<NodeId>,
selection_anchor: Option<NodeId>,
scroll_offset: usize,
sort_mode: SortMode,
ignore_patterns: IgnorePatterns,
pub(crate) viewport_height: usize,
search: FileExplorerSearch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortMode {
Name,
Type,
Modified,
}
impl FileTreeView {
pub fn new(tree: FileTree) -> Self {
let root_id = tree.root_id();
Self {
tree,
selected_node: Some(root_id),
multi_selection: HashSet::new(),
selection_anchor: None,
scroll_offset: 0,
sort_mode: SortMode::Type,
ignore_patterns: IgnorePatterns::new(),
viewport_height: 10, search: FileExplorerSearch::new(),
}
}
fn filtered_visible_nodes(&self) -> Vec<NodeId> {
let mut result = Vec::new();
self.collect_filtered_visible(self.tree.root_id(), &mut result);
result
}
fn collect_filtered_visible(&self, id: NodeId, result: &mut Vec<NodeId>) {
let is_root = id == self.tree.root_id();
if !is_root && !self.is_node_visible(id) {
return;
}
result.push(id);
if let Some(node) = self.tree.get_node(id) {
if node.is_expanded() {
for &child_id in &node.children {
self.collect_filtered_visible(child_id, result);
}
}
}
}
pub fn set_viewport_height(&mut self, height: usize) {
self.viewport_height = height;
}
pub fn tree(&self) -> &FileTree {
&self.tree
}
pub fn tree_mut(&mut self) -> &mut FileTree {
&mut self.tree
}
pub fn get_display_nodes(&self) -> Vec<(NodeId, usize)> {
let visible = self.filtered_visible_nodes();
visible
.into_iter()
.map(|id| {
let depth = self.tree.get_depth(id);
(id, depth)
})
.collect()
}
pub fn get_selected(&self) -> Option<NodeId> {
self.selected_node
}
pub fn set_selected(&mut self, node_id: Option<NodeId>) {
self.selected_node = node_id;
}
pub fn select_next(&mut self) {
self.clear_multi_selection();
let visible = self.filtered_visible_nodes();
if visible.is_empty() {
return;
}
if let Some(current) = self.selected_node {
if let Some(pos) = visible.iter().position(|&id| id == current) {
if pos + 1 < visible.len() {
self.selected_node = Some(visible[pos + 1]);
}
}
} else {
self.selected_node = Some(visible[0]);
}
}
pub fn select_prev(&mut self) {
self.clear_multi_selection();
let visible = self.filtered_visible_nodes();
if visible.is_empty() {
return;
}
if let Some(current) = self.selected_node {
if let Some(pos) = visible.iter().position(|&id| id == current) {
if pos > 0 {
self.selected_node = Some(visible[pos - 1]);
}
}
} else {
self.selected_node = Some(visible[0]);
}
}
pub fn select_page_up(&mut self) {
if self.viewport_height == 0 {
return;
}
let visible = self.filtered_visible_nodes();
if visible.is_empty() {
return;
}
if let Some(current) = self.selected_node {
if let Some(pos) = visible.iter().position(|&id| id == current) {
let new_pos = pos.saturating_sub(self.viewport_height);
self.selected_node = Some(visible[new_pos]);
}
} else {
self.selected_node = Some(visible[0]);
}
}
pub fn select_page_down(&mut self) {
if self.viewport_height == 0 {
return;
}
let visible = self.filtered_visible_nodes();
if visible.is_empty() {
return;
}
if let Some(current) = self.selected_node {
if let Some(pos) = visible.iter().position(|&id| id == current) {
let new_pos = (pos + self.viewport_height).min(visible.len() - 1);
self.selected_node = Some(visible[new_pos]);
}
} else {
self.selected_node = Some(visible[0]);
}
}
pub fn update_scroll_for_selection(&mut self) {
if self.viewport_height == 0 {
return;
}
let visible = self.filtered_visible_nodes();
self.update_scroll_with_nodes(&visible);
}
fn update_scroll_with_nodes(&mut self, visible: &[NodeId]) {
if self.viewport_height == 0 {
return;
}
if let Some(selected) = self.selected_node {
if let Some(pos) = visible.iter().position(|&id| id == selected) {
if pos < self.scroll_offset {
self.scroll_offset = pos;
} else if pos >= self.scroll_offset + self.viewport_height {
self.scroll_offset = pos - self.viewport_height + 1;
}
}
}
}
pub fn select_first(&mut self) {
let visible = self.filtered_visible_nodes();
if !visible.is_empty() {
self.selected_node = Some(visible[0]);
}
}
pub fn select_last(&mut self) {
let visible = self.filtered_visible_nodes();
if !visible.is_empty() {
self.selected_node = Some(*visible.last().unwrap());
}
}
pub fn toggle_select(&mut self) {
if let Some(cursor) = self.selected_node {
if self.multi_selection.contains(&cursor) {
self.multi_selection.remove(&cursor);
} else {
self.multi_selection.insert(cursor);
}
self.selection_anchor = Some(cursor);
}
}
pub fn extend_selection_up(&mut self) {
let visible = self.filtered_visible_nodes();
if visible.is_empty() {
return;
}
let Some(current) = self.selected_node else {
return;
};
let Some(pos) = visible.iter().position(|&id| id == current) else {
return;
};
if self.multi_selection.is_empty() {
self.multi_selection.insert(current);
self.selection_anchor = Some(current);
}
if pos == 0 {
return;
}
let anchor = self.selection_anchor.unwrap_or(current);
let new_pos = pos - 1;
self.selected_node = Some(visible[new_pos]);
let anchor_pos = visible
.iter()
.position(|&id| id == anchor)
.unwrap_or(new_pos);
let (lo, hi) = (new_pos.min(anchor_pos), new_pos.max(anchor_pos));
self.multi_selection = visible[lo..=hi].iter().copied().collect();
self.update_scroll_with_nodes(&visible);
}
pub fn extend_selection_down(&mut self) {
let visible = self.filtered_visible_nodes();
if visible.is_empty() {
return;
}
let Some(current) = self.selected_node else {
return;
};
let Some(pos) = visible.iter().position(|&id| id == current) else {
return;
};
if self.multi_selection.is_empty() {
self.multi_selection.insert(current);
self.selection_anchor = Some(current);
}
if pos + 1 >= visible.len() {
return;
}
let anchor = self.selection_anchor.unwrap_or(current);
let new_pos = pos + 1;
self.selected_node = Some(visible[new_pos]);
let anchor_pos = visible
.iter()
.position(|&id| id == anchor)
.unwrap_or(new_pos);
let (lo, hi) = (new_pos.min(anchor_pos), new_pos.max(anchor_pos));
self.multi_selection = visible[lo..=hi].iter().copied().collect();
self.update_scroll_with_nodes(&visible);
}
pub fn select_all(&mut self) {
let visible = self.filtered_visible_nodes();
self.multi_selection = visible.iter().copied().collect();
self.selection_anchor = self.selected_node;
}
pub fn clear_multi_selection(&mut self) {
self.multi_selection.clear();
self.selection_anchor = None;
}
pub fn has_multi_selection(&self) -> bool {
!self.multi_selection.is_empty()
}
pub fn multi_selection(&self) -> &HashSet<NodeId> {
&self.multi_selection
}
pub fn effective_selection(&self) -> Vec<NodeId> {
if self.multi_selection.is_empty() {
return self.selected_node.into_iter().collect();
}
self.filtered_visible_nodes()
.into_iter()
.filter(|id| self.multi_selection.contains(id))
.collect()
}
pub fn select_parent(&mut self) {
if let Some(current) = self.selected_node {
if let Some(node) = self.tree.get_node(current) {
if let Some(parent_id) = node.parent {
self.selected_node = Some(parent_id);
}
}
}
}
pub fn get_scroll_offset(&self) -> usize {
self.scroll_offset
}
pub fn set_scroll_offset(&mut self, offset: usize) {
self.scroll_offset = offset;
}
pub fn ensure_visible(&mut self, viewport_height: usize) {
if viewport_height == 0 {
return;
}
if let Some(selected) = self.selected_node {
let visible = self.filtered_visible_nodes();
if let Some(pos) = visible.iter().position(|&id| id == selected) {
if pos < self.scroll_offset {
self.scroll_offset = pos;
}
else if pos >= self.scroll_offset + viewport_height {
self.scroll_offset = pos - viewport_height + 1;
}
}
}
}
pub fn get_sort_mode(&self) -> SortMode {
self.sort_mode
}
pub fn set_sort_mode(&mut self, mode: SortMode) {
self.sort_mode = mode;
}
pub fn get_selected_entry(&self) -> Option<&DirEntry> {
self.selected_node
.and_then(|id| self.tree.get_node(id))
.map(|node| &node.entry)
}
pub fn navigate_to_path(&mut self, path: &std::path::Path) {
if let Some(node) = self.tree.get_node_by_path(path) {
self.selected_node = Some(node.id);
self.update_scroll_for_selection();
}
}
pub fn get_selected_index(&self) -> Option<usize> {
if let Some(selected) = self.selected_node {
let visible = self.filtered_visible_nodes();
visible.iter().position(|&id| id == selected)
} else {
None
}
}
pub fn get_node_at_index(&self, index: usize) -> Option<NodeId> {
let visible = self.filtered_visible_nodes();
visible.get(index).copied()
}
pub fn visible_count(&self) -> usize {
self.filtered_visible_nodes().len()
}
pub fn ignore_patterns(&self) -> &IgnorePatterns {
&self.ignore_patterns
}
pub fn ignore_patterns_mut(&mut self) -> &mut IgnorePatterns {
&mut self.ignore_patterns
}
pub fn toggle_show_hidden(&mut self) {
self.ignore_patterns.toggle_show_hidden();
}
pub fn toggle_show_gitignored(&mut self) {
self.ignore_patterns.toggle_show_gitignored();
}
pub fn is_node_visible(&self, node_id: NodeId) -> bool {
if let Some(node) = self.tree.get_node(node_id) {
!self
.ignore_patterns
.is_ignored(&node.entry.path, node.is_dir())
} else {
false
}
}
pub fn load_gitignore_from_bytes(
&mut self,
dir_path: &std::path::Path,
contents: &[u8],
mtime: Option<std::time::SystemTime>,
) {
self.ignore_patterns
.load_gitignore_from_bytes(dir_path, contents, mtime);
}
pub async fn expand_and_select_file(&mut self, path: &std::path::Path) -> bool {
if let Some(node_id) = self.tree.expand_to_path(path).await {
self.selected_node = Some(node_id);
true
} else {
false
}
}
pub fn collect_symlink_mappings(&self) -> HashMap<PathBuf, PathBuf> {
let mut mappings = HashMap::new();
for node_id in self.filtered_visible_nodes() {
if let Some(node) = self.tree.get_node(node_id) {
if node.entry.is_symlink() && node.is_dir() && node.is_expanded() {
if let Ok(canonical) = node.entry.path.canonicalize() {
if canonical != node.entry.path {
mappings.insert(node.entry.path.clone(), canonical);
}
}
}
}
}
mappings
}
pub fn search_query(&self) -> &str {
self.search.query()
}
pub fn is_search_active(&self) -> bool {
self.search.is_active()
}
pub fn search_push_char(&mut self, c: char) {
self.search.push_char(c);
self.jump_to_first_match();
}
pub fn search_pop_char(&mut self) {
self.search.pop_char();
if self.search.is_active() {
self.jump_to_first_match();
}
}
pub fn search_clear(&mut self) {
self.search.clear();
}
fn get_matching_nodes(&self) -> Vec<NodeId> {
if !self.search.is_active() {
return self.filtered_visible_nodes();
}
self.filtered_visible_nodes()
.into_iter()
.filter(|&id| {
if let Some(node) = self.tree.get_node(id) {
self.search.matches(&node.entry.name)
} else {
false
}
})
.collect()
}
fn jump_to_first_match(&mut self) {
let matching = self.get_matching_nodes();
if let Some(&first) = matching.first() {
self.selected_node = Some(first);
self.update_scroll_for_selection();
}
}
pub fn select_next_match(&mut self) {
if !self.search.is_active() {
self.select_next();
return;
}
let matching = self.get_matching_nodes();
if matching.is_empty() {
return;
}
if let Some(current) = self.selected_node {
if let Some(pos) = matching.iter().position(|&id| id == current) {
let next_pos = (pos + 1) % matching.len();
self.selected_node = Some(matching[next_pos]);
} else {
self.selected_node = Some(matching[0]);
}
} else {
self.selected_node = Some(matching[0]);
}
}
pub fn select_prev_match(&mut self) {
if !self.search.is_active() {
self.select_prev();
return;
}
let matching = self.get_matching_nodes();
if matching.is_empty() {
return;
}
if let Some(current) = self.selected_node {
if let Some(pos) = matching.iter().position(|&id| id == current) {
let prev_pos = if pos == 0 {
matching.len() - 1
} else {
pos - 1
};
self.selected_node = Some(matching[prev_pos]);
} else {
self.selected_node = Some(*matching.last().unwrap());
}
} else {
self.selected_node = Some(*matching.last().unwrap());
}
}
pub fn get_match_for_node(&self, node_id: NodeId) -> Option<FuzzyMatch> {
if !self.search.is_active() {
return None;
}
self.tree
.get_node(node_id)
.and_then(|node| self.search.match_name(&node.entry.name))
}
pub fn node_matches_search(&self, node_id: NodeId) -> bool {
if !self.search.is_active() {
return true;
}
self.tree
.get_node(node_id)
.map(|node| self.search.matches(&node.entry.name))
.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::filesystem::StdFileSystem;
use crate::services::fs::FsManager;
use std::fs as std_fs;
use std::sync::Arc;
use tempfile::TempDir;
async fn create_test_view() -> (TempDir, FileTreeView) {
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path();
std_fs::create_dir(temp_path.join("dir1")).unwrap();
std_fs::write(temp_path.join("dir1/file1.txt"), "content1").unwrap();
std_fs::write(temp_path.join("dir1/file2.txt"), "content2").unwrap();
std_fs::create_dir(temp_path.join("dir2")).unwrap();
std_fs::write(temp_path.join("file3.txt"), "content3").unwrap();
let backend = Arc::new(StdFileSystem);
let manager = Arc::new(FsManager::new(backend));
let tree = FileTree::new(temp_path.to_path_buf(), manager)
.await
.unwrap();
let view = FileTreeView::new(tree);
(temp_dir, view)
}
#[tokio::test]
async fn test_view_creation() {
let (_temp_dir, view) = create_test_view().await;
assert!(view.get_selected().is_some());
assert_eq!(view.get_scroll_offset(), 0);
assert_eq!(view.get_sort_mode(), SortMode::Type);
}
#[tokio::test]
async fn test_get_display_nodes() {
let (_temp_dir, mut view) = create_test_view().await;
let display = view.get_display_nodes();
assert_eq!(display.len(), 1);
assert_eq!(display[0].1, 0);
let root_id = view.tree().root_id();
view.tree_mut().expand_node(root_id).await.unwrap();
let display = view.get_display_nodes();
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); }
#[tokio::test]
async fn test_navigation() {
let (_temp_dir, mut view) = create_test_view().await;
let root_id = view.tree().root_id();
view.tree_mut().expand_node(root_id).await.unwrap();
let root_id = view.tree().root_id();
assert_eq!(view.get_selected(), Some(root_id));
view.select_next();
assert_ne!(view.get_selected(), Some(root_id));
view.select_prev();
assert_eq!(view.get_selected(), Some(root_id));
view.select_last();
let visible = view.tree().get_visible_nodes();
assert_eq!(view.get_selected(), Some(*visible.last().unwrap()));
view.select_first();
assert_eq!(view.get_selected(), Some(root_id));
}
#[tokio::test]
async fn test_select_parent() {
let (_temp_dir, mut view) = create_test_view().await;
let root_id = view.tree().root_id();
view.tree_mut().expand_node(root_id).await.unwrap();
view.select_next();
let child_id = view.get_selected().unwrap();
assert_ne!(child_id, root_id);
view.select_parent();
assert_eq!(view.get_selected(), Some(root_id));
}
#[tokio::test]
async fn test_ensure_visible() {
let (_temp_dir, mut view) = create_test_view().await;
let root_id = view.tree().root_id();
view.tree_mut().expand_node(root_id).await.unwrap();
let viewport_height = 2;
view.select_last();
view.ensure_visible(viewport_height);
let selected_index = view.get_selected_index().unwrap();
assert!(selected_index >= view.get_scroll_offset());
assert!(selected_index < view.get_scroll_offset() + viewport_height);
view.select_first();
view.ensure_visible(viewport_height);
assert_eq!(view.get_scroll_offset(), 0);
}
#[tokio::test]
async fn test_get_selected_entry() {
let (_temp_dir, view) = create_test_view().await;
let entry = view.get_selected_entry();
assert!(entry.is_some());
assert!(entry.unwrap().is_dir());
}
#[tokio::test]
async fn test_navigate_to_path() {
let (_temp_dir, mut view) = create_test_view().await;
let root_id = view.tree().root_id();
view.tree_mut().expand_node(root_id).await.unwrap();
let dir1_path = view.tree().root_path().join("dir1");
view.navigate_to_path(&dir1_path);
let selected_entry = view.get_selected_entry().unwrap();
assert_eq!(selected_entry.name, "dir1");
}
#[tokio::test]
async fn test_get_selected_index() {
let (_temp_dir, mut view) = create_test_view().await;
let root_id = view.tree().root_id();
view.tree_mut().expand_node(root_id).await.unwrap();
assert_eq!(view.get_selected_index(), Some(0));
view.select_next();
assert_eq!(view.get_selected_index(), Some(1));
view.select_last();
let visible_count = view.visible_count();
assert_eq!(view.get_selected_index(), Some(visible_count - 1));
}
#[tokio::test]
async fn test_visible_count() {
let (_temp_dir, mut view) = create_test_view().await;
assert_eq!(view.visible_count(), 1);
let root_id = view.tree().root_id();
view.tree_mut().expand_node(root_id).await.unwrap();
assert_eq!(view.visible_count(), 4); }
#[tokio::test]
async fn test_sort_mode() {
let (_temp_dir, mut view) = create_test_view().await;
assert_eq!(view.get_sort_mode(), SortMode::Type);
view.set_sort_mode(SortMode::Name);
assert_eq!(view.get_sort_mode(), SortMode::Name);
view.set_sort_mode(SortMode::Modified);
assert_eq!(view.get_sort_mode(), SortMode::Modified);
}
}