use super::*; #[test] fn virtual_catalog_compacts_and_retains_file_directory_collisions() { let mut picker = FilePicker::from_paths( PathBuf::new(), ["x", "x/y", "src/deep/deleted.rs", "src/deep/deleted.rs"] .map(PathBuf::from) .into_iter() .enumerate(), ); assert_eq!( picker .entries() .map(|row| row.label.to_owned()) .collect::>(), ["src/deep", "x", "x"] ); picker.reveal_path(Path::new("x")); assert!(!picker.entries().nth(picker.selected).unwrap().is_dir); assert_eq!(picker.accept(), Some(PathBuf::from("x"))); picker.reveal_path(Path::new("x/y")); assert_eq!(picker.accept(), Some(PathBuf::from("x/y"))); picker.reveal_path(Path::new("src/deep/deleted.rs")); assert_eq!(picker.accept(), Some(PathBuf::from("src/deep/deleted.rs"))); assert_eq!(picker.entries().filter(|row| !row.is_dir).count(), 3); let expected_index = |row: &FileRow<'_>| { if row.is_dir { None } else { Some(match row.path.to_str().unwrap() { "x" => 0, "x/y" => 1, "src/deep/deleted.rs" => 2, path => panic!("unexpected path {path}"), }) } }; for view in [FileView::List, FileView::Tree] { picker.set_view(view); for query in ["", "x", "deleted"] { picker.query = query.into(); picker.refilter(); let rows: Vec<_> = picker.entries().collect(); assert!(!rows.is_empty()); for row in rows { assert_eq!(row.catalog_index, expected_index(&row)); assert!(row.status.is_none() && row.old_path.is_none()); } } } } use std::sync::atomic::{AtomicU64, Ordering}; static TEMP_ID: AtomicU64 = AtomicU64::new(0); struct Directory(PathBuf); impl Directory { fn new() -> Self { let id = TEMP_ID.fetch_add(1, Ordering::Relaxed); let path = std::env::temp_dir().join(format!("vamp-files-test-{}-{id}", std::process::id())); fs::create_dir(&path).unwrap(); Self(path) } fn put(&self, name: &str, text: &str) { let path = self.0.join(name); fs::create_dir_all(path.parent().unwrap()).unwrap(); fs::write(path, text).unwrap(); } fn scan(&self) -> FilePicker { FilePicker::scan(self.0.clone(), FilePresentation::Floating, FileView::List).unwrap() } } impl Drop for Directory { fn drop(&mut self) { let _ = fs::remove_dir_all(&self.0); } } fn paths(picker: &FilePicker) -> Vec { picker .entries() .map(|entry| entry.path.to_owned()) .collect() } #[test] fn fuzzy_path_tokens_preserve_tree_context_selection_and_open_paths() { let directory = Directory::new(); let names = [ "src/fuzzy_parser.rs", "src/über_parser.rs", "src/other.rs", "tests/fuzzy_parser.rs", ]; for name in names { directory.put(name, ""); } for mut picker in [ directory.scan(), FilePicker::from_paths( directory.0.clone(), names.map(PathBuf::from).into_iter().enumerate(), ), ] { for view in [FileView::Tree, FileView::List] { picker.set_view(view); for (query, expected) in [ ("sr/fp RS", "src/fuzzy_parser.rs"), ("ÜPR SRC", "src/über_parser.rs"), ] { picker.query = query.into(); picker.refilter(); assert_eq!( picker .entries() .filter(|row| !row.is_dir) .map(|row| row.path) .collect::>(), [Path::new(expected)] ); assert_eq!(selected_path(&picker), Path::new(expected)); assert_eq!(picker.accept(), Some(directory.0.join(expected))); if view == FileView::Tree { assert_eq!(paths(&picker), [Path::new("src"), Path::new(expected)]); } } picker.query = "rs/ü".into(); picker.refilter(); assert!(picker.entries().next().is_none()); picker.clear_filter(); assert_eq!(selected_path(&picker), Path::new("src/über_parser.rs")); } } } #[test] fn scanned_files_and_directories_have_no_catalog_index() { let directory = Directory::new(); directory.put("nested/file.txt", ""); let mut picker = directory.scan(); for view in [FileView::List, FileView::Tree] { picker.set_view(view); picker.reveal_path(Path::new("nested/file.txt")); assert!(picker.entries().all(|row| row.catalog_index.is_none())); assert!( picker .entries() .all(|row| row.status.is_none() && row.old_path.is_none()) ); } } fn repository_change(path: &str, status: FileStatus, old_path: Option<&str>) -> WorkingFileChange { WorkingFileChange { path: path.into(), status, old_path: old_path.map(PathBuf::from), } } #[test] fn repository_metadata_keeps_clean_files_selection_and_filtered_views() { let directory = Directory::new(); for name in ["src/clean.rs", "src/modified.rs", "added.rs", "renamed.rs"] { directory.put(name, ""); } let mut picker = directory.scan(); picker.presentation = FilePresentation::Docked; picker.set_view(FileView::Tree); picker.reveal_path(Path::new("src/clean.rs")); picker.query = "src".into(); picker.refilter(); picker.focused = false; let changes = vec![ repository_change("src/modified.rs", FileStatus::Modified, None), repository_change("added.rs", FileStatus::Added, None), repository_change("renamed.rs", FileStatus::Renamed, Some("old.rs")), repository_change("vanished/deep/deleted.rs", FileStatus::Removed, None), ]; assert!(picker.set_repository_changes(changes.clone())); assert!(!picker.set_repository_changes(changes)); assert_eq!(selected_path(&picker), Path::new("src/clean.rs")); assert_eq!(picker.query, "src"); assert!(!picker.focused); assert_eq!(picker.presentation, FilePresentation::Docked); for view in [FileView::List, FileView::Tree] { picker.set_view(view); for query in ["", "src", "modified.rs", "deleted.rs"] { picker.query = query.into(); picker.refilter(); let rows: Vec<_> = picker.entries().collect(); assert!(!rows.is_empty()); for row in rows { assert!(row.catalog_index.is_none()); if row.is_dir || row.path == Path::new("src/clean.rs") { assert!(row.status.is_none() && row.old_path.is_none()); } else { let status = match row.path.to_str().unwrap() { "src/modified.rs" => FileStatus::Modified, "added.rs" => FileStatus::Added, "renamed.rs" => FileStatus::Renamed, "vanished/deep/deleted.rs" => FileStatus::Removed, other => panic!("unexpected {other}"), }; assert_eq!(row.status, Some(status)); assert_eq!( row.old_path, (status == FileStatus::Renamed).then(|| Path::new("old.rs")) ); } } if query.is_empty() || query == "src" { assert!( picker .entries() .any(|row| row.path == Path::new("src/clean.rs")) ); } } } picker.set_view(FileView::List); picker.clear_filter(); picker.reveal_path(Path::new("src/clean.rs")); assert!(picker.set_repository_changes(Vec::new())); assert_eq!(selected_path(&picker), Path::new("src/clean.rs")); assert_eq!(picker.entries().len(), 4); assert!(picker.nodes.iter().all(|node| !node.virtual_entry)); assert!( picker .entries() .all(|row| row.status.is_none() && row.old_path.is_none()) ); assert!(!picker.set_repository_changes(Vec::new())); } #[test] fn deleted_catalog_leaves_survive_refresh_and_restored_files_survive_clear() { let directory = Directory::new(); directory.put("keep.txt", ""); let mut picker = directory.scan(); let deleted = Path::new("lost/deep/deleted.txt"); picker.set_repository_changes(vec![repository_change( "lost/deep/deleted.txt", FileStatus::Removed, None, )]); picker.set_view(FileView::Tree); assert_eq!( picker.entries().find(|row| row.is_dir).unwrap().label, "lost/deep" ); picker.reveal_path(deleted); picker.query = "deleted".into(); picker.refilter(); picker.start_search(); picker.focused = false; picker.refresh(); assert_eq!(selected_path(&picker), deleted); assert!(picker.searching && !picker.focused); assert_eq!(picker.query, "deleted"); assert_eq!(picker.accept(), Some(directory.0.join(deleted))); assert!(!directory.0.join(deleted).exists()); assert_eq!( picker.entries().find(|row| !row.is_dir).unwrap().status, Some(FileStatus::Removed) ); directory.put("lost/deep/deleted.txt", "restored"); assert!(picker.set_repository_changes(vec![repository_change( "lost/deep/deleted.txt", FileStatus::Removed, None, )])); picker.refresh(); assert_eq!(selected_path(&picker), deleted); assert!(picker.nodes.iter().all(|node| !node.virtual_entry)); assert!(picker.set_repository_changes(Vec::new())); assert_eq!(selected_path(&picker), deleted); assert_eq!(picker.accept(), Some(directory.0.join(deleted))); assert_eq!( fs::read_to_string(directory.0.join(deleted)).unwrap(), "restored" ); assert!(picker.entries().all(|row| row.status.is_none())); fs::remove_dir_all(directory.0.join("lost")).unwrap(); picker.refresh(); picker.set_repository_changes(vec![repository_change( "lost/deep/deleted.txt", FileStatus::Removed, None, )]); picker.reveal_path(deleted); picker.set_repository_changes(Vec::new()); assert!(picker.entries().len() == 0); picker.clear_filter(); assert_eq!(paths(&picker), [PathBuf::from("keep.txt")]); assert!(picker.nodes.iter().all(|node| !node.virtual_entry)); } #[test] fn repository_metadata_cannot_introduce_unsafe_or_existing_excluded_paths() { let directory = Directory::new(); directory.put(".gitignore", "ignored/\n"); directory.put("ignored/hidden.txt", ""); directory.put("directory/keep.txt", ""); let mut picker = directory.scan(); let mut changes: Vec<_> = [ "", ".", "../outside.txt", "directory/../../outside.txt", ".git/secret", "nested/.jj/secret", "/outside.txt", "directory", "ignored/hidden.txt", ] .into_iter() .map(|path| repository_change(path, FileStatus::Removed, None)) .collect(); #[cfg(unix)] { std::os::unix::fs::symlink(&directory.0, directory.0.join("link")).unwrap(); changes.push(repository_change( "link/missing.txt", FileStatus::Removed, None, )); } changes.push(repository_change("missing.txt", FileStatus::Removed, None)); picker.set_repository_changes(changes); assert_eq!( paths(&picker), [".gitignore", "directory/keep.txt", "missing.txt"].map(PathBuf::from) ); assert_eq!( picker.entries().filter(|row| row.status.is_some()).count(), 1 ); assert_eq!( picker.entries().last().unwrap().status, Some(FileStatus::Removed) ); assert!(!directory.0.join("missing.txt").exists()); } #[test] fn nested_ignore_negation_hidden_files_and_vcs_metadata() { let directory = Directory::new(); directory.put(".gitignore", "*.log\nignored/\n"); directory.put("nested/.gitignore", "!keep.log\nlocal.txt\n"); for name in [ "gone.log", "nested/gone.log", "nested/keep.log", "nested/local.txt", "nested/ok.txt", "ignored/hidden.txt", ".hidden", ".git/objects/secret", "nested/.git/secret", ".jj/secret", "nested/.jj/secret", ] { directory.put(name, "text"); } let picker = directory.scan(); assert_eq!( paths(&picker), [ ".gitignore", ".hidden", "nested/.gitignore", "nested/keep.log", "nested/ok.txt" ] .map(PathBuf::from) ); assert!(picker.error.is_none()); } #[test] fn literal_unicode_tokens_grapheme_deletion_and_navigation() { let directory = Directory::new(); for name in ["a space 界[e\u{301}].txt", "b.txt", "c.txt"] { directory.put(name, "text"); } let mut picker = directory.scan(); picker.navigate(false); assert_eq!(picker.selected, 2); picker.navigate(true); assert_eq!(picker.selected, 0); picker.query = "界 SPACE [E\u{301}]".into(); picker.refilter(); assert_eq!(paths(&picker), [PathBuf::from("a space 界[e\u{301}].txt")]); picker.query = "e\u{301}👩‍💻".into(); assert!(picker.handle_filter_input(&crate::editor::Input::Backspace)); assert_eq!(picker.query, "e\u{301}"); assert!(picker.handle_filter_input(&crate::editor::Input::Backspace)); assert_eq!(picker.query, ""); assert!(picker.handle_filter_input(&crate::editor::Input::Backspace)); assert_eq!(picker.entries().len(), 3); picker.query = "no match".into(); picker.refilter(); for forward in [true, false] { picker.navigate(forward); assert_eq!(picker.selected, 0); assert_eq!(picker.entries().len(), 0); } } #[test] fn word_and_line_deletion_refilter_only_editable_queries() { use crate::editor::Input; for view in [FileView::List, FileView::Tree] { let mut picker = FilePicker::from_paths( PathBuf::new(), ["alpha one.txt", "alpha two.txt", "beta.txt"] .map(PathBuf::from) .into_iter() .enumerate(), ); picker.set_view(view); picker.query = "alpha missing".into(); picker.refilter(); assert_eq!(picker.entries().len(), 0); if view == FileView::Tree { for input in [Input::Backspace, Input::AltBackspace, Input::SuperBackspace] { assert!(!picker.handle_filter_input(&input)); assert_eq!(picker.query, "alpha missing"); } picker.start_search(); } assert!(picker.handle_filter_input(&Input::AltBackspace)); assert_eq!(picker.query, "alpha "); assert_eq!(picker.entries().len(), 2); assert!(picker.handle_filter_input(&Input::SuperBackspace)); assert_eq!(picker.query, ""); assert_eq!(picker.entries().len(), 3); for input in [Input::Backspace, Input::AltBackspace, Input::SuperBackspace] { assert!(picker.handle_filter_input(&input)); assert_eq!(picker.entries().len(), 3); } } } #[test] fn empty_scan_refresh_and_failure_preserve_reachable_state() { let directory = Directory::new(); let mut picker = directory.scan(); picker.navigate(false); assert!(picker.handle_filter_input(&crate::editor::Input::Backspace)); assert_eq!(picker.entries().len(), 0); directory.put("new.txt", "text"); picker.query = "new".into(); picker.refresh(); assert_eq!(picker.query, "new"); assert_eq!(paths(&picker), [PathBuf::from("new.txt")]); fs::remove_dir_all(&directory.0).unwrap(); picker.refresh(); assert_eq!(paths(&picker), [PathBuf::from("new.txt")]); assert!(picker.error.as_ref().unwrap().contains("Refresh failed")); assert!( FilePicker::scan( directory.0.clone(), FilePresentation::Docked, FileView::Tree ) .is_err() ); } #[test] fn malformed_ignore_warning_survives_query_and_navigation() { let directory = Directory::new(); directory.put(".gitignore", "[z-a]\n"); directory.put("ok.txt", "text"); let mut picker = directory.scan(); assert!(picker.error.as_ref().unwrap().contains("Incomplete scan")); picker.query = "ok".into(); picker.refilter(); assert_eq!(paths(&picker), [PathBuf::from("ok.txt")]); picker.navigate(true); assert!(picker.error.as_ref().unwrap().contains("Incomplete scan")); directory.put(".gitignore", ""); picker.refresh(); assert!(picker.error.is_none()); } #[test] fn only_project_ignore_rules_apply_and_non_directory_roots_fail() { let directory = Directory::new(); directory.put(".gitignore", "*.txt\n"); directory.put("project/visible.txt", "text"); let root = directory.0.join("project"); let picker = FilePicker::scan(root, FilePresentation::Floating, FileView::List).unwrap(); assert_eq!(paths(&picker), [PathBuf::from("visible.txt")]); assert!( FilePicker::scan( directory.0.join("project/visible.txt"), FilePresentation::Docked, FileView::Tree, ) .is_err() ); } #[cfg(unix)] #[test] fn symlink_loops_and_file_links_are_not_followed_and_raw_paths_survive() { use std::os::unix::fs::symlink; let directory = Directory::new(); directory.put("real.txt", "text"); symlink(&directory.0, directory.0.join("loop")).unwrap(); symlink(directory.0.join("real.txt"), directory.0.join("alias.txt")).unwrap(); let raw = PathBuf::from("raw界\n.txt"); fs::write(directory.0.join(&raw), "text").unwrap(); let picker = directory.scan(); assert_eq!(picker.entries().len(), 2); let entry = picker .entries() .find(|entry| entry.path == raw.as_path()) .unwrap(); assert!(!entry.label.chars().any(char::is_control)); assert_eq!( fs::read_to_string(directory.0.join(entry.path)).unwrap(), "text" ); } fn tree(directory: &Directory) -> FilePicker { FilePicker::scan( directory.0.clone(), FilePresentation::Docked, FileView::Tree, ) .unwrap() } fn select(picker: &mut FilePicker, path: &str) { let selected = picker .entries() .position(|row| row.path == Path::new(path)) .unwrap(); picker.selected = selected; } fn selected_path(picker: &FilePicker) -> &Path { picker.entries().nth(picker.selected).unwrap().path } #[test] fn tree_orders_folders_first_keeps_empty_dirs_and_compacts_only_directory_chains() { let directory = Directory::new(); directory.put("a.txt", ""); directory.put("folder/only.txt", ""); directory.put("chain/one/two/a.txt", ""); directory.put("chain/one/two/z.txt", ""); fs::create_dir(directory.0.join("empty")).unwrap(); let mut picker = tree(&directory); assert_eq!(picker.view, FileView::Tree); assert!(!picker.searching); assert_eq!( picker .entries() .map(|row| (row.label, row.is_dir, row.depth)) .collect::>(), [ ("chain/one/two", true, 0), ("empty", true, 0), ("folder", true, 0), ("a.txt", false, 0) ] ); assert_eq!(selected_path(&picker), Path::new("chain/one/two")); picker.right(); assert_eq!( picker .entries() .map(|row| (row.label, row.depth)) .collect::>(), [ ("chain/one/two", 0), ("a.txt", 1), ("z.txt", 1), ("empty", 0), ("folder", 0), ("a.txt", 0) ] ); picker.right(); assert_eq!(selected_path(&picker), Path::new("chain/one/two/a.txt")); let rows = paths(&picker); picker.right(); assert_eq!(paths(&picker), rows); picker.left(); assert_eq!(selected_path(&picker), Path::new("chain/one/two")); picker.left(); assert!(!picker.entries().next().unwrap().expanded); picker.left(); assert_eq!(picker.selected, 0); select(&mut picker, "folder"); assert!(picker.accept().is_none()); assert_eq!( picker.entries().nth(picker.selected + 1).unwrap().label, "only.txt" ); picker.right(); assert_eq!(picker.accept(), Some(directory.0.join("folder/only.txt"))); select(&mut picker, "empty"); picker.right(); assert!(picker.entries().nth(picker.selected).unwrap().expanded); picker.right(); assert_eq!(selected_path(&picker), Path::new("empty")); } #[test] fn tree_search_reveals_ancestors_without_mutating_saved_expansion_or_compaction() { let directory = Directory::new(); directory.put("src/module/deep/needle.txt", ""); directory.put("src/module/sibling.txt", ""); directory.put("src/root.txt", ""); directory.put("other/keep.txt", ""); let mut picker = tree(&directory); select(&mut picker, "other"); picker.right(); select(&mut picker, "src"); let expanded = picker.expanded.clone(); picker.start_search(); picker.query = "NEEDLE".into(); picker.refilter(); assert_eq!( picker .entries() .map(|row| (row.label, row.depth)) .collect::>(), [("src", 0), ("module", 1), ("deep", 2), ("needle.txt", 3)] ); assert_eq!( selected_path(&picker), Path::new("src/module/deep/needle.txt") ); assert_eq!(picker.expanded, expanded); picker.left(); let filtered = paths(&picker); picker.finish_search(); assert_eq!(picker.query, "NEEDLE"); assert_eq!(paths(&picker), filtered); assert_eq!(selected_path(&picker), Path::new("src/module/deep")); assert_eq!(picker.expanded, expanded); picker.start_search(); picker.query = "deep".into(); picker.refilter(); select(&mut picker, "src/module/deep"); assert!(picker.accept().is_none()); assert!(!picker.searching); assert_eq!(picker.query, "deep"); assert_eq!(selected_path(&picker), Path::new("src/module/deep")); assert!(!picker.entries().nth(picker.selected).unwrap().expanded); assert_eq!(picker.expanded, expanded); picker.right(); assert!(picker.entries().nth(picker.selected).unwrap().expanded); assert_eq!(picker.query, "deep"); let chain = Directory::new(); chain.put("one/two/three/a.txt", ""); chain.put("one/two/three/b.txt", ""); let mut picker = tree(&chain); picker.start_search(); picker.query = "a.txt".into(); picker.refilter(); assert_eq!( picker.entries().map(|row| row.label).collect::>(), ["one/two/three", "a.txt"] ); } #[test] fn view_switches_restore_directory_choices_and_reveal_explicit_file_choices() { let directory = Directory::new(); directory.put("src/a.txt", ""); directory.put("src/b.txt", ""); directory.put("z.txt", ""); let mut picker = tree(&directory); picker.right(); let open = paths(&picker); picker.set_view(FileView::List); assert_eq!( paths(&picker), ["src/a.txt", "src/b.txt", "z.txt"].map(PathBuf::from) ); picker.set_view(FileView::Tree); assert_eq!(selected_path(&picker), Path::new("src")); assert_eq!(paths(&picker), open); picker.left(); picker.toggle_view(); picker.navigate(true); assert_eq!(selected_path(&picker), Path::new("src/b.txt")); picker.toggle_view(); assert_eq!(selected_path(&picker), Path::new("src/b.txt")); assert!(picker.entries().next().unwrap().expanded); picker.left(); picker.toggle_view(); picker.toggle_view(); assert_eq!(selected_path(&picker), Path::new("src")); } #[test] fn filtering_and_refresh_preserve_raw_selection_and_surviving_expansion() { let directory = Directory::new(); directory.put("src/a.txt", ""); directory.put("src/b.txt", ""); directory.put("z.txt", ""); let mut picker = directory.scan(); select(&mut picker, "src/b.txt"); picker.query = "absent".into(); picker.refilter(); assert_eq!(picker.entries().len(), 0); picker.query.clear(); picker.refilter(); assert_eq!(selected_path(&picker), Path::new("src/b.txt")); picker.set_view(FileView::Tree); assert_eq!(selected_path(&picker), Path::new("src/b.txt")); fs::remove_file(directory.0.join("src/b.txt")).unwrap(); directory.put("src/c.txt", ""); picker.refresh(); assert_eq!(selected_path(&picker), Path::new("src")); assert!(picker.entries().next().unwrap().expanded); assert!(paths(&picker).contains(&PathBuf::from("src/c.txt"))); fs::remove_dir_all(directory.0.join("src")).unwrap(); picker.refresh(); assert_eq!(selected_path(&picker), Path::new("z.txt")); assert!(picker.expanded.is_empty()); picker.toggle_view(); picker.toggle_view(); assert_eq!(selected_path(&picker), Path::new("z.txt")); } #[test] fn tree_search_subfocus_and_expansion_survive_view_toggles_and_refresh() { let directory = Directory::new(); directory.put("src/deep/a.txt", ""); directory.put("src/other.txt", ""); let mut picker = tree(&directory); picker.start_search(); assert!(picker.searching); assert_eq!(picker.entries().len(), 4); assert!(picker.expanded.is_empty()); picker.toggle_view(); assert_eq!(picker.view, FileView::List); picker.toggle_view(); assert!(picker.searching); assert!(picker.expanded.is_empty()); picker.query = "a.txt".into(); picker.refilter(); assert_eq!(selected_path(&picker), Path::new("src/deep/a.txt")); picker.refresh(); assert!(picker.searching); assert_eq!(selected_path(&picker), Path::new("src/deep/a.txt")); let filtered = paths(&picker); picker.finish_search(); assert!(!picker.searching); assert_eq!(picker.query, "a.txt"); assert_eq!(paths(&picker), filtered); assert_eq!(selected_path(&picker), Path::new("src/deep/a.txt")); assert!(picker.expanded.is_empty()); picker.toggle_view(); picker.refresh(); picker.toggle_view(); assert!(!picker.searching); assert_eq!(picker.query, "a.txt"); assert_eq!(selected_path(&picker), Path::new("src/deep/a.txt")); picker.start_search(); assert!(picker.searching); assert_eq!(picker.query, "a.txt"); assert_eq!(selected_path(&picker), Path::new("src/deep/a.txt")); } #[test] fn clearing_filter_preserves_mode_focus_current_path_and_empty_result_fallback() { let directory = Directory::new(); directory.put("src/deep/needle.txt", ""); directory.put("src/other.txt", ""); directory.put("other/keep.txt", ""); for (view, searching) in [ (FileView::Tree, false), (FileView::Tree, true), (FileView::List, false), ] { let mut picker = tree(&directory); select(&mut picker, "other"); picker.right(); let expanded = picker.expanded.clone(); picker.set_view(view); if view == FileView::Tree { picker.start_search(); } picker.query = "needle".into(); picker.refilter(); if !searching && view == FileView::Tree { picker.finish_search(); } let expected = Path::new("src/deep/needle.txt"); assert_eq!(selected_path(&picker), expected); picker.clear_filter(); assert!(picker.query.is_empty()); assert_eq!(picker.searching, searching); assert!(picker.focused); assert_eq!(selected_path(&picker), expected); assert!(picker.expanded.is_superset(&expanded)); assert!(paths(&picker).contains(&PathBuf::from("other/keep.txt"))); let rows = paths(&picker); let expanded = picker.expanded.clone(); picker.clear_filter(); assert_eq!(paths(&picker), rows); assert_eq!(picker.expanded, expanded); assert_eq!(selected_path(&picker), expected); assert_eq!(picker.searching, searching); picker.query = "absent".into(); picker.refilter(); picker.navigate(true); picker.navigate(false); assert!(picker.rows.is_empty()); picker.refresh(); assert!(picker.rows.is_empty()); picker.clear_filter(); assert_eq!(selected_path(&picker), expected); assert_eq!(picker.searching, searching); assert!(picker.focused); } let empty = Directory::new(); let mut picker = tree(&empty); picker.start_search(); picker.query = "absent".into(); picker.refilter(); picker.finish_search(); picker.clear_filter(); assert!(picker.rows.is_empty()); assert_eq!(picker.selected, 0); assert!(!picker.searching); assert!(picker.focused); } #[test] fn filtered_navigation_toggles_context_folders_and_query_edits_reveal_matches() { let directory = Directory::new(); directory.put("src/deep/needle.txt", ""); directory.put("src/other.txt", ""); let mut picker = tree(&directory); picker.start_search(); picker.query = "needle".into(); picker.refilter(); let expanded = picker.expanded.clone(); picker.left(); assert_eq!(selected_path(&picker), Path::new("src/deep")); picker.right(); assert_eq!(selected_path(&picker), Path::new("src/deep/needle.txt")); assert_eq!(picker.expanded, expanded); picker.finish_search(); picker.left(); picker.left(); assert_eq!(selected_path(&picker), Path::new("src/deep")); assert!(!picker.entries().nth(picker.selected).unwrap().expanded); assert_eq!(picker.query, "needle"); assert!(!paths(&picker).contains(&PathBuf::from("src/deep/needle.txt"))); picker.start_search(); picker.finish_search(); assert_eq!(selected_path(&picker), Path::new("src/deep")); assert!(!picker.entries().nth(picker.selected).unwrap().expanded); picker.right(); assert!(picker.entries().nth(picker.selected).unwrap().expanded); picker.right(); assert_eq!(selected_path(&picker), Path::new("src/deep/needle.txt")); picker.left(); assert!(picker.accept().is_none()); assert!(!picker.entries().nth(picker.selected).unwrap().expanded); assert!(picker.accept().is_none()); assert!(picker.entries().nth(picker.selected).unwrap().expanded); picker.start_search(); assert!(picker.accept().is_none()); assert!(!picker.searching); assert_eq!(picker.query, "needle"); assert_eq!(selected_path(&picker), Path::new("src/deep")); assert!(!picker.entries().nth(picker.selected).unwrap().expanded); picker.start_search(); picker.query.push_str(".txt"); picker.refilter(); assert!(picker.filter_collapsed.is_empty()); assert_eq!(selected_path(&picker), Path::new("src/deep/needle.txt")); picker.query.clear(); picker.refilter(); picker.finish_search(); assert_eq!(selected_path(&picker), Path::new("src/deep/needle.txt")); } #[test] fn filtered_collapse_survives_refresh_compaction_and_view_switching_by_raw_path() { let directory = Directory::new(); directory.put("one/a.txt", ""); directory.put("one/two/needle.txt", ""); let mut picker = tree(&directory); picker.start_search(); picker.query = "needle".into(); picker.refilter(); picker.finish_search(); picker.left(); picker.left(); picker.left(); picker.left(); assert_eq!(selected_path(&picker), Path::new("one")); assert_eq!(paths(&picker), [PathBuf::from("one")]); picker.toggle_view(); assert_eq!(selected_path(&picker), Path::new("one/two/needle.txt")); picker.toggle_view(); assert_eq!(paths(&picker), [PathBuf::from("one")]); assert!(!picker.searching); fs::remove_file(directory.0.join("one/a.txt")).unwrap(); picker.focused = false; picker.refresh(); assert_eq!(selected_path(&picker), Path::new("one/two")); assert_eq!(paths(&picker), [PathBuf::from("one/two")]); assert!(!picker.searching && !picker.focused); assert_eq!(picker.query, "needle"); directory.put("one/a.txt", ""); picker.refresh(); assert_eq!(selected_path(&picker), Path::new("one")); assert_eq!(paths(&picker), [PathBuf::from("one")]); picker.right(); picker.right(); assert_eq!(selected_path(&picker), Path::new("one/two")); assert!(!picker.entries().nth(picker.selected).unwrap().expanded); picker.right(); assert!(picker.entries().nth(picker.selected).unwrap().expanded); picker.right(); assert_eq!(selected_path(&picker), Path::new("one/two/needle.txt")); fs::remove_file(directory.0.join("one/two/needle.txt")).unwrap(); picker.refresh(); assert!(picker.entries().len() == 0); picker.clear_filter(); assert_eq!(selected_path(&picker), Path::new("one/two")); assert!(!picker.searching && !picker.focused); } #[test] fn refresh_preserves_expansion_when_directory_chains_merge_and_split() { let directory = Directory::new(); directory.put("one/a.txt", ""); directory.put("one/two/b.txt", ""); let mut picker = tree(&directory); picker.right(); assert_eq!( paths(&picker), ["one", "one/two", "one/a.txt"].map(PathBuf::from) ); fs::remove_file(directory.0.join("one/a.txt")).unwrap(); picker.refresh(); assert_eq!(selected_path(&picker), Path::new("one/two")); assert_eq!( paths(&picker), ["one/two", "one/two/b.txt"].map(PathBuf::from) ); assert!(picker.entries().next().unwrap().expanded); directory.put("one/a.txt", ""); picker.refresh(); assert_eq!(selected_path(&picker), Path::new("one/two")); assert_eq!( paths(&picker), ["one", "one/two", "one/a.txt"].map(PathBuf::from) ); assert!(picker.entries().next().unwrap().expanded); assert!(!picker.entries().nth(1).unwrap().expanded); fs::remove_file(directory.0.join("one/a.txt")).unwrap(); picker.refresh(); picker.left(); assert_eq!(paths(&picker), [PathBuf::from("one/two")]); directory.put("one/a.txt", ""); picker.refresh(); assert_eq!(paths(&picker), [PathBuf::from("one")]); assert!(!picker.entries().next().unwrap().expanded); }