Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/view/file_tree/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,38 @@ impl FileTree {
self.nodes.len()
}

/// Re-sort children of all expanded directories using the provided comparator
pub fn sort_all_nodes<F>(&mut self, cmp: F)
where
F: Fn(&FsEntry, &FsEntry) -> std::cmp::Ordering,
{
let expanded_dirs: Vec<NodeId> = self
.nodes
.values()
.filter(|n| n.is_expanded())
.map(|n| n.id)
.collect();

for dir_id in expanded_dirs {
let children_with_entries: Vec<(NodeId, FsEntry)> =
if let Some(node) = self.nodes.get(&dir_id) {
node.children
.iter()
.filter_map(|&id| self.nodes.get(&id).map(|n| (id, n.entry.clone())))
.collect()
} else {
continue;
};

let mut sorted = children_with_entries;
sorted.sort_by(|(_, a), (_, b)| cmp(a, b));

if let Some(node) = self.nodes.get_mut(&dir_id) {
node.children = sorted.into_iter().map(|(id, _)| id).collect();
}
}
}

/// Expand all directories along a path and return the final node ID
///
/// This is useful for revealing a specific file in the tree, even if its
Expand Down
124 changes: 122 additions & 2 deletions src/view/file_tree/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,30 @@ pub enum SortMode {
Modified,
}

impl SortMode {
/// Get a comparison function for this sort mode
pub fn comparator(self) -> fn(&FsEntry, &FsEntry) -> std::cmp::Ordering {
match self {
SortMode::Name => |a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()),
SortMode::Type => |a, b| match (a.is_dir(), b.is_dir()) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
},
SortMode::Modified => |a, b| {
let a_time = a.metadata.as_ref().and_then(|m| m.modified);
let b_time = b.metadata.as_ref().and_then(|m| m.modified);
match (a_time, b_time) {
(Some(at), Some(bt)) => bt.cmp(&at), // Newest first
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
}
},
}
}
}

impl FileTreeView {
/// Create a new file tree view
pub fn new(tree: FileTree) -> Self {
Expand Down Expand Up @@ -262,10 +286,13 @@ impl FileTreeView {
self.sort_mode
}

/// Set the sort mode
/// Set the sort mode and re-sort all expanded directories
pub fn set_sort_mode(&mut self, mode: SortMode) {
if self.sort_mode == mode {
return;
}
self.sort_mode = mode;
// TODO: Re-sort children when sort mode changes
self.tree.sort_all_nodes(mode.comparator());
}

/// Get selected node entry (convenience method)
Expand Down Expand Up @@ -568,4 +595,97 @@ mod tests {
view.set_sort_mode(SortMode::Modified);
assert_eq!(view.get_sort_mode(), SortMode::Modified);
}

#[tokio::test]
async fn test_sort_mode_changes_order() {
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();

// Initial order (Type sort)
let initial_names: Vec<_> = view
.get_display_nodes()
.iter()
.skip(1)
.filter_map(|(id, _)| view.tree().get_node(*id))
.map(|n| n.entry.name.clone())
.collect();

assert_eq!(initial_names, vec!["dir1", "dir2", "file3.txt"]);

// Change to Name sort
view.set_sort_mode(SortMode::Name);

let name_sorted: Vec<_> = view
.get_display_nodes()
.iter()
.skip(1)
.filter_map(|(id, _)| view.tree().get_node(*id))
.map(|n| n.entry.name.clone())
.collect();

assert_eq!(name_sorted, vec!["dir1", "dir2", "file3.txt"]);

// Setting same mode is a no-op
view.set_sort_mode(SortMode::Name);
assert_eq!(view.get_sort_mode(), SortMode::Name);
}

#[tokio::test]
async fn test_sort_mode_type_dirs_first() {
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path();

// Create structure where Name and Type sorts differ
std_fs::write(temp_path.join("aaa_file.txt"), "content").unwrap();
std_fs::create_dir(temp_path.join("bbb_dir")).unwrap();
std_fs::write(temp_path.join("ccc_file.txt"), "content").unwrap();

let backend = Arc::new(LocalFsBackend::new());
let manager = Arc::new(FsManager::new(backend));
let tree = FileTree::new(temp_path.to_path_buf(), manager)
.await
.unwrap();
let mut view = FileTreeView::new(tree);

let root_id = view.tree().root_id();
view.tree_mut().expand_node(root_id).await.unwrap();

// Type sort: directories first
let type_sorted: Vec<_> = view
.get_display_nodes()
.iter()
.skip(1)
.filter_map(|(id, _)| view.tree().get_node(*id))
.map(|n| n.entry.name.clone())
.collect();

assert_eq!(type_sorted, vec!["bbb_dir", "aaa_file.txt", "ccc_file.txt"]);

// Name sort: alphabetical
view.set_sort_mode(SortMode::Name);

let name_sorted: Vec<_> = view
.get_display_nodes()
.iter()
.skip(1)
.filter_map(|(id, _)| view.tree().get_node(*id))
.map(|n| n.entry.name.clone())
.collect();

assert_eq!(name_sorted, vec!["aaa_file.txt", "bbb_dir", "ccc_file.txt"]);

// Back to Type sort
view.set_sort_mode(SortMode::Type);

let back_to_type: Vec<_> = view
.get_display_nodes()
.iter()
.skip(1)
.filter_map(|(id, _)| view.tree().get_node(*id))
.map(|n| n.entry.name.clone())
.collect();

assert_eq!(back_to_type, vec!["bbb_dir", "aaa_file.txt", "ccc_file.txt"]);
}
}