1use std::collections::HashMap;
11use std::fs;
12use std::io;
13use std::path::{Path, PathBuf};
14use std::time::SystemTime;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct EntryInfo {
19 pub name: String,
21 pub path: PathBuf,
23 pub is_dir: bool,
25 pub is_symlink: bool,
27 pub size: u64,
29 pub modified: Option<SystemTime>,
31}
32
33pub trait DirSource {
37 fn list_dir(&self, path: &Path) -> io::Result<Vec<EntryInfo>>;
38}
39
40#[derive(Debug, Default, Clone, Copy)]
42pub struct StdDirSource;
43
44impl DirSource for StdDirSource {
45 fn list_dir(&self, path: &Path) -> io::Result<Vec<EntryInfo>> {
46 let mut entries = Vec::new();
47 for entry in fs::read_dir(path)? {
48 let entry = entry?;
49 let file_type = entry.file_type()?;
50 let is_symlink = file_type.is_symlink();
51 let is_dir = if file_type.is_dir() {
54 true
55 } else if is_symlink {
56 fs::metadata(entry.path()).map(|m| m.is_dir()).unwrap_or(false)
57 } else {
58 false
59 };
60 let meta = entry.metadata().ok();
61 let size = if is_dir {
62 0
63 } else {
64 meta.as_ref().map(|m| m.len()).unwrap_or(0)
65 };
66 let modified = meta.as_ref().and_then(|m| m.modified().ok());
67 entries.push(EntryInfo {
68 name: entry.file_name().to_string_lossy().into_owned(),
69 path: entry.path(),
70 is_dir,
71 is_symlink,
72 size,
73 modified,
74 });
75 }
76 Ok(entries)
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum SortKey {
83 Name,
85 Size,
87 Modified,
89 Type,
91}
92
93impl SortKey {
94 pub fn as_str(&self) -> &'static str {
96 match self {
97 SortKey::Name => "name",
98 SortKey::Size => "size",
99 SortKey::Modified => "modified",
100 SortKey::Type => "type",
101 }
102 }
103
104 pub fn parse(raw: &str) -> Option<Self> {
106 match raw.to_ascii_lowercase().as_str() {
107 "name" => Some(SortKey::Name),
108 "size" => Some(SortKey::Size),
109 "modified" | "mtime" | "date" => Some(SortKey::Modified),
110 "type" | "kind" | "ext" | "extension" => Some(SortKey::Type),
111 _ => None,
112 }
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub struct SortOptions {
119 pub dirs_first: bool,
122 pub key: SortKey,
124 pub ascending: bool,
126}
127
128impl Default for SortOptions {
129 fn default() -> Self {
130 Self {
131 dirs_first: true,
132 key: SortKey::Name,
133 ascending: true,
134 }
135 }
136}
137
138fn type_key(name: &str) -> String {
141 match name.rsplit_once('.') {
142 Some((stem, ext)) if !stem.is_empty() && !ext.is_empty() => ext.to_lowercase(),
143 _ => String::new(),
144 }
145}
146
147fn compare_entries(a: &EntryInfo, b: &EntryInfo, opts: SortOptions) -> std::cmp::Ordering {
152 use std::cmp::Ordering;
153
154 if opts.dirs_first {
155 match (a.is_dir, b.is_dir) {
156 (true, false) => return Ordering::Less,
157 (false, true) => return Ordering::Greater,
158 _ => {}
159 }
160 }
161
162 let primary = match opts.key {
165 SortKey::Name => a
166 .name
167 .to_lowercase()
168 .cmp(&b.name.to_lowercase())
169 .then_with(|| a.name.cmp(&b.name)),
170 SortKey::Size => a.size.cmp(&b.size),
171 SortKey::Modified => a.modified.cmp(&b.modified),
172 SortKey::Type => type_key(&a.name)
173 .cmp(&type_key(&b.name))
174 .then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase())),
175 };
176
177 if primary == Ordering::Equal && opts.key != SortKey::Name {
178 let tie = a
179 .name
180 .to_lowercase()
181 .cmp(&b.name.to_lowercase())
182 .then_with(|| a.name.cmp(&b.name));
183 return if opts.ascending { tie } else { tie.reverse() };
184 }
185
186 if opts.ascending {
187 primary
188 } else {
189 primary.reverse()
190 }
191}
192
193fn sort_dir_entries(entries: &mut [EntryInfo], opts: SortOptions) {
195 entries.sort_by(|a, b| compare_entries(a, b, opts));
196}
197
198#[derive(Debug, Default)]
200pub struct DirNode {
201 pub loaded: bool,
203 pub expanded: bool,
205 pub entries: Vec<EntryInfo>,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq)]
212pub enum Change {
213 Created { path: PathBuf },
214 Removed { path: PathBuf },
215 Renamed { from: PathBuf, to: PathBuf },
216 Modified { path: PathBuf },
217 Rescan,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct VisibleRow {
225 pub path: PathBuf,
226 pub name: String,
227 pub depth: usize,
228 pub is_dir: bool,
229 pub is_symlink: bool,
230 pub expanded: bool,
231 pub has_children: bool,
232 pub matches: bool,
234}
235
236pub struct TreeModel {
239 root: PathBuf,
240 sort: SortOptions,
241 show_hidden: bool,
242 filter: String,
243 dirs: HashMap<PathBuf, DirNode>,
246}
247
248impl TreeModel {
249 pub fn new(root: PathBuf, sort: SortOptions, show_hidden: bool) -> Self {
253 let root_node = DirNode {
254 loaded: false,
255 expanded: true,
256 entries: Vec::new(),
257 };
258 let mut dirs = HashMap::new();
259 dirs.insert(root.clone(), root_node);
260 Self {
261 root,
262 sort,
263 show_hidden,
264 filter: String::new(),
265 dirs,
266 }
267 }
268
269 pub fn root(&self) -> &Path {
271 &self.root
272 }
273
274 pub fn filter(&self) -> &str {
276 &self.filter
277 }
278
279 pub fn set_filter(&mut self, filter: &str) {
281 self.filter = filter.trim().to_lowercase();
282 }
283
284 pub fn sort(&self) -> SortOptions {
286 self.sort
287 }
288
289 pub fn set_sort(&mut self, sort: SortOptions) {
293 self.sort = sort;
294 }
295
296 pub fn show_hidden(&self) -> bool {
298 self.show_hidden
299 }
300
301 pub fn set_show_hidden(&mut self, show_hidden: bool) {
304 self.show_hidden = show_hidden;
305 }
306
307 pub fn reload_all(&mut self, src: &dyn DirSource) {
311 let paths: Vec<PathBuf> = self
312 .dirs
313 .iter()
314 .filter(|(_, node)| node.loaded)
315 .map(|(path, _)| path.clone())
316 .collect();
317 for path in paths {
318 let _ = self.reload(&path, src);
319 }
320 }
321
322 pub fn under_root(&self, path: &Path) -> bool {
324 path.starts_with(&self.root)
325 }
326
327 pub fn is_expanded(&self, path: &Path) -> bool {
329 self.dirs.get(path).is_some_and(|n| n.expanded)
330 }
331
332 pub fn node(&self, path: &Path) -> Option<&DirNode> {
334 self.dirs.get(path)
335 }
336
337 pub fn expanded_loaded_dirs(&self) -> Vec<PathBuf> {
342 self.dirs
343 .iter()
344 .filter(|(_, node)| node.loaded && node.expanded)
345 .map(|(path, _)| path.clone())
346 .collect()
347 }
348
349 pub fn expand(&mut self, path: &Path, src: &dyn DirSource) -> Result<(), io::Error> {
352 if let Some(node) = self.dirs.get_mut(path)
353 && node.loaded
354 && node.expanded
355 {
356 return Ok(());
357 }
358 let result = self.reload(path, src);
359 if let Some(node) = self.dirs.get_mut(path) {
362 node.expanded = true;
363 }
364 result
365 }
366
367 pub fn collapse(&mut self, path: &Path) {
369 if let Some(node) = self.dirs.get_mut(path) {
370 node.expanded = false;
371 node.entries.clear();
372 node.loaded = false;
373 self.remove_subtree_suffixes(path);
374 }
375 }
376
377 pub fn toggle(&mut self, path: &Path, src: &dyn DirSource) -> Result<(), io::Error> {
379 let expanded = self.is_expanded(path);
380 if expanded {
381 self.collapse(path);
382 Ok(())
383 } else {
384 self.expand(path, src)
385 }
386 }
387
388 fn reload(&mut self, path: &Path, src: &dyn DirSource) -> Result<(), io::Error> {
390 let expanded = self.dirs.get(path).is_some_and(|n| n.expanded);
392 let mut node = DirNode {
393 loaded: true,
396 expanded,
397 entries: Vec::new(),
398 };
399 let result = src.list_dir(path);
400 match result {
401 Ok(list) => {
402 let mut entries = list;
403 if !self.show_hidden {
404 entries.retain(|e| !is_hidden(&e.name));
405 }
406 sort_dir_entries(&mut entries, self.sort);
407 node.entries = entries;
408 }
409 Err(err) => {
410 self.dirs.insert(path.to_path_buf(), node);
411 return Err(err);
412 }
413 }
414 self.dirs.insert(path.to_path_buf(), node);
415 Ok(())
416 }
417
418 pub fn apply(&mut self, change: &Change, src: &dyn DirSource) {
421 match change {
422 Change::Created { path } => self.refresh_parent(path, src),
423 Change::Removed { path } => {
424 self.remove_subtree(path);
425 self.refresh_parent(path, src);
426 }
427 Change::Renamed { from, to } => {
428 self.remove_subtree(from);
429 self.refresh_parent(from, src);
430 self.refresh_parent(to, src);
431 }
432 Change::Modified { path } => {
433 if self.dirs.get(path).is_some_and(|n| n.loaded) {
438 let _ = self.reload(path, src);
439 }
440 }
441 Change::Rescan => {
442 const MAX_RESCAN_DIRS: usize = 512;
454 let mut reload: Vec<PathBuf> = self
455 .dirs
456 .iter()
457 .filter(|(_, node)| node.loaded && node.expanded)
458 .map(|(path, _)| path.clone())
459 .collect();
460 reload.sort();
461 reload.truncate(MAX_RESCAN_DIRS);
462 for node in self.dirs.values_mut() {
463 node.loaded = false;
464 node.entries.clear();
465 }
466 for path in reload {
467 let _ = self.reload(&path, src);
468 }
469 }
470 }
471 }
472
473 fn refresh_parent(&mut self, path: &Path, src: &dyn DirSource) {
475 let Some(parent) = path.parent() else {
476 return; };
478 if self.dirs.get(parent).is_some_and(|n| n.loaded) {
479 let _ = self.reload(parent, src);
480 }
481 }
482
483 fn remove_subtree(&mut self, path: &Path) {
485 let keys: Vec<PathBuf> = self
486 .dirs
487 .keys()
488 .filter(|k| k.starts_with(path))
489 .cloned()
490 .collect();
491 for key in keys {
492 self.dirs.remove(&key);
493 }
494 }
495
496 fn remove_subtree_suffixes(&mut self, path: &Path) {
499 let keys: Vec<PathBuf> = self
500 .dirs
501 .keys()
502 .filter(|k| k.starts_with(path) && *k != path)
503 .cloned()
504 .collect();
505 for key in keys {
506 self.dirs.remove(&key);
507 }
508 }
509
510 pub fn visible_rows(&self) -> Vec<VisibleRow> {
524 let mut rows = Vec::new();
525 self.visible_rows_into(&mut rows);
526 rows
527 }
528
529 pub fn visible_rows_into(&self, out: &mut Vec<VisibleRow>) {
533 out.clear();
534 self.collect_children(&self.root, 0, out);
535 }
536
537 fn collect_children(&self, path: &Path, depth: usize, out: &mut Vec<VisibleRow>) {
542 let Some(node) = self.dirs.get(path) else {
543 return;
544 };
545 if !node.expanded || !node.loaded {
546 return;
547 }
548 let filtering = !self.filter.is_empty();
549 for entry in &node.entries {
550 let name_matches =
551 filtering && entry.name.to_lowercase().contains(&self.filter);
552 let kept = if filtering {
555 if entry.is_dir {
556 name_matches || self.subtree_has_match(&entry.path)
557 } else {
558 name_matches
559 }
560 } else {
561 true
562 };
563 if !kept {
564 continue;
565 }
566 let is_dir = entry.is_dir;
567 let child_node = self.dirs.get(&entry.path);
568 let expanded = child_node.is_some_and(|n| n.expanded);
569 let has_children = child_node.is_some_and(|n| n.loaded && !n.entries.is_empty());
570 out.push(VisibleRow {
571 path: entry.path.clone(),
572 name: entry.name.clone(),
573 depth,
574 is_dir,
575 is_symlink: entry.is_symlink,
576 expanded,
577 has_children,
578 matches: filtering && kept,
579 });
580 if is_dir {
581 self.collect_children(&entry.path, depth + 1, out);
582 }
583 }
584 }
585
586 fn subtree_has_match(&self, dir: &Path) -> bool {
590 let Some(node) = self.dirs.get(dir) else {
591 return false;
592 };
593 if !node.loaded || !node.expanded {
594 return false;
595 }
596 node.entries.iter().any(|e| {
597 e.name.to_lowercase().contains(&self.filter)
598 || (e.is_dir && self.subtree_has_match(&e.path))
599 })
600 }
601}
602
603pub(crate) fn is_hidden(name: &str) -> bool {
606 name.starts_with('.') && name != "." && name != ".."
607}
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612
613 #[derive(Default)]
615 struct MemFs {
616 dirs: HashMap<PathBuf, Vec<EntryInfo>>,
617 }
618
619 impl MemFs {
620 fn dir(&mut self, path: impl AsRef<Path>, children: Vec<(&str, bool, bool)>) {
621 let path = path.as_ref();
622 let entries = children
623 .into_iter()
624 .map(|(name, is_dir, is_symlink)| EntryInfo {
625 name: name.to_owned(),
626 path: path.join(name),
627 is_dir,
628 is_symlink,
629 size: 0,
630 modified: None,
631 })
632 .collect();
633 self.dirs.insert(path.to_path_buf(), entries);
634 }
635
636 fn dir_sized(&mut self, path: impl AsRef<Path>, children: Vec<(&str, bool, bool, u64, u64)>) {
639 let path = path.as_ref();
640 let entries = children
641 .into_iter()
642 .map(|(name, is_dir, is_symlink, size, secs)| EntryInfo {
643 name: name.to_owned(),
644 path: path.join(name),
645 is_dir,
646 is_symlink,
647 size,
648 modified: Some(std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs)),
649 })
650 .collect();
651 self.dirs.insert(path.to_path_buf(), entries);
652 }
653 }
654
655 impl DirSource for MemFs {
656 fn list_dir(&self, path: &Path) -> io::Result<Vec<EntryInfo>> {
657 self.dirs.get(path).cloned().ok_or_else(|| {
658 io::Error::new(io::ErrorKind::NotFound, format!("no such dir: {}", path.display()))
659 })
660 }
661 }
662
663 fn row_names(rows: &[VisibleRow]) -> Vec<String> {
664 rows.iter().map(|r| r.name.clone()).collect()
665 }
666
667 #[test]
668 fn visible_rows_into_reuses_and_clears_buffer() {
669 let mut fs = MemFs::default();
670 fs.dir("/r", vec![("a", true, false), ("b.txt", false, false)]);
671 let mut model = TreeModel::new("/r".into(), SortOptions::default(), false);
672 model.expand(Path::new("/r"), &fs).unwrap();
673
674 let mut buf = vec![VisibleRow {
677 path: "/stale".into(),
678 name: "stale".into(),
679 depth: 9,
680 is_dir: false,
681 is_symlink: false,
682 expanded: false,
683 has_children: false,
684 matches: false,
685 }];
686 model.visible_rows_into(&mut buf);
687 assert_eq!(row_names(&buf), vec!["a", "b.txt"]);
688
689 model.collapse(Path::new("/r"));
691 model.visible_rows_into(&mut buf);
692 assert!(buf.is_empty());
693 }
694
695 #[test]
696 fn starts_with_just_the_root() {
697 let model = TreeModel::new("/r".into(), SortOptions::default(), false);
700 let rows = model.visible_rows();
701 assert_eq!(rows.len(), 0);
702 }
703
704 #[test]
705 fn expand_is_lazy_and_shows_sorted_children() {
706 let mut fs = MemFs::default();
707 fs.dir("/r", vec![("z.txt", false, false), ("a", true, false), ("b.txt", false, false)]);
708 let mut model = TreeModel::new("/r".into(), SortOptions::default(), false);
709
710 model.expand(Path::new("/r"), &fs).unwrap();
711 let rows = model.visible_rows();
712 assert_eq!(row_names(&rows), vec!["a", "b.txt", "z.txt"]); assert_eq!(rows[0].depth, 0);
715 assert!(rows[0].is_dir);
716 }
717
718 #[test]
719 fn nested_expansion_and_depth() {
720 let mut fs = MemFs::default();
721 fs.dir("/r", vec![("a", true, false)]);
722 fs.dir("/r/a", vec![("b", true, false)]);
723 fs.dir("/r/a/b", vec![("file.txt", false, false)]);
724
725 let mut model = TreeModel::new("/r".into(), SortOptions::default(), false);
726 model.expand(Path::new("/r"), &fs).unwrap();
727 model.expand(Path::new("/r/a"), &fs).unwrap();
728 model.expand(Path::new("/r/a/b"), &fs).unwrap();
729
730 let rows = model.visible_rows();
731 let depths: Vec<usize> = rows.iter().map(|r| r.depth).collect();
733 assert_eq!(depths, vec![0, 1, 2]);
734 assert_eq!(row_names(&rows), vec!["a", "b", "file.txt"]);
735 }
736
737 #[test]
738 fn collapse_hides_children() {
739 let mut fs = MemFs::default();
740 fs.dir("/r", vec![("a", true, false)]);
741 fs.dir("/r/a", vec![("x", false, false)]);
742
743 let mut model = TreeModel::new("/r".into(), SortOptions::default(), false);
744 model.expand(Path::new("/r"), &fs).unwrap();
745 model.expand(Path::new("/r/a"), &fs).unwrap();
746 assert_eq!(row_names(&model.visible_rows()), vec!["a", "x"]);
748
749 model.collapse(Path::new("/r/a"));
750 assert_eq!(row_names(&model.visible_rows()), vec!["a"]);
751 assert!(!model.is_expanded(Path::new("/r/a")));
752 }
753
754 #[test]
755 fn sorting_respects_dirs_first_and_case() {
756 let mut fs = MemFs::default();
757 fs.dir(
758 "/r",
759 vec![
760 ("beta", false, false),
761 ("Alpha", false, false),
762 ("GAMMA", true, false),
763 ("alpha", false, false),
764 ],
765 );
766 let mut model = TreeModel::new("/r".into(), SortOptions::default(), false);
767 model.expand(Path::new("/r"), &fs).unwrap();
768 assert_eq!(row_names(&model.visible_rows()), vec!["GAMMA", "Alpha", "alpha", "beta"]);
770 }
771
772 #[test]
773 fn dirs_first_disabled_interleaves() {
774 let mut fs = MemFs::default();
775 fs.dir("/r", vec![("b", true, false), ("a", false, false)]);
776 let mut model = TreeModel::new(
777 "/r".into(),
778 SortOptions { dirs_first: false, ..SortOptions::default() },
779 false,
780 );
781 model.expand(Path::new("/r"), &fs).unwrap();
782 assert_eq!(row_names(&model.visible_rows()), vec!["a", "b"]);
784 }
785
786 #[test]
787 fn sort_by_size_orders_within_groups_and_descends() {
788 let mut fs = MemFs::default();
789 fs.dir_sized(
790 "/r",
791 vec![
792 ("small.txt", false, false, 10, 0),
793 ("big.txt", false, false, 900, 0),
794 ("mid.txt", false, false, 100, 0),
795 ],
796 );
797 let asc = SortOptions { key: SortKey::Size, ..SortOptions::default() };
798 let mut model = TreeModel::new("/r".into(), asc, false);
799 model.expand(Path::new("/r"), &fs).unwrap();
800 assert_eq!(row_names(&model.visible_rows()), vec!["small.txt", "mid.txt", "big.txt"]);
801
802 let desc = SortOptions {
803 key: SortKey::Size,
804 ascending: false,
805 ..SortOptions::default()
806 };
807 let mut model = TreeModel::new("/r".into(), desc, false);
808 model.expand(Path::new("/r"), &fs).unwrap();
809 assert_eq!(row_names(&model.visible_rows()), vec!["big.txt", "mid.txt", "small.txt"]);
810 }
811
812 #[test]
813 fn sort_by_modified_orders_by_time() {
814 let mut fs = MemFs::default();
815 fs.dir_sized(
816 "/r",
817 vec![
818 ("old.txt", false, false, 1, 100),
819 ("new.txt", false, false, 1, 300),
820 ("mid.txt", false, false, 1, 200),
821 ],
822 );
823 let opts = SortOptions { key: SortKey::Modified, ..SortOptions::default() };
824 let mut model = TreeModel::new("/r".into(), opts, false);
825 model.expand(Path::new("/r"), &fs).unwrap();
826 assert_eq!(row_names(&model.visible_rows()), vec!["old.txt", "mid.txt", "new.txt"]);
827 }
828
829 #[test]
830 fn sort_by_type_groups_by_extension() {
831 let mut fs = MemFs::default();
832 fs.dir(
833 "/r",
834 vec![
835 ("a.txt", false, false),
836 ("b.rs", false, false),
837 ("c.txt", false, false),
838 ("d.md", false, false),
839 ],
840 );
841 let opts = SortOptions { key: SortKey::Type, ..SortOptions::default() };
842 let mut model = TreeModel::new("/r".into(), opts, false);
843 model.expand(Path::new("/r"), &fs).unwrap();
844 assert_eq!(row_names(&model.visible_rows()), vec!["d.md", "b.rs", "a.txt", "c.txt"]);
846 }
847
848 #[test]
849 fn reload_all_applies_hidden_toggle() {
850 let mut fs = MemFs::default();
851 fs.dir("/r", vec![(".hidden", false, false), ("shown", false, false)]);
852 let mut model = TreeModel::new("/r".into(), SortOptions::default(), false);
853 model.expand(Path::new("/r"), &fs).unwrap();
854 assert_eq!(row_names(&model.visible_rows()), vec!["shown"]);
855
856 model.set_show_hidden(true);
857 model.reload_all(&fs);
858 assert_eq!(row_names(&model.visible_rows()), vec![".hidden", "shown"]);
859 }
860
861 #[test]
862 fn hidden_entries_are_skipped_unless_enabled() {
863 let mut fs = MemFs::default();
864 fs.dir("/r", vec![(".git", true, false), (".env", false, false), ("src", true, false)]);
865
866 let mut hidden = TreeModel::new("/r".into(), SortOptions::default(), false);
867 hidden.expand(Path::new("/r"), &fs).unwrap();
868 assert_eq!(row_names(&hidden.visible_rows()), vec!["src"]);
870
871 let mut visible = TreeModel::new("/r".into(), SortOptions::default(), true);
872 visible.expand(Path::new("/r"), &fs).unwrap();
873 assert_eq!(row_names(&visible.visible_rows()), vec![".git", "src", ".env"]);
875 }
876
877 #[test]
878 fn filter_narrows_visible_rows() {
879 let mut fs = MemFs::default();
880 fs.dir(
881 "/r",
882 vec![
883 ("main.rs", false, false),
884 ("README.md", false, false),
885 ("src", true, false),
886 ],
887 );
888 fs.dir("/r/src", vec![("main.rs", false, false), ("test.rs", false, false)]);
889
890 let mut model = TreeModel::new("/r".into(), SortOptions::default(), false);
891 model.expand(Path::new("/r"), &fs).unwrap();
892 model.expand(Path::new("/r/src"), &fs).unwrap();
893 model.set_filter("MAIN");
894 let rows = model.visible_rows();
895 assert_eq!(row_names(&rows), vec!["src", "main.rs", "main.rs"]);
898 assert!(rows.iter().all(|r| r.matches));
899 model.set_filter("");
900 assert_eq!(row_names(&model.visible_rows()).len(), 5);
902 }
903
904 #[test]
905 fn filter_does_not_peek_into_collapsed_dirs() {
906 let mut fs = MemFs::default();
907 fs.dir("/r", vec![("src", true, false)]);
908 fs.dir("/r/src", vec![("needle.txt", false, false)]);
909
910 let mut model = TreeModel::new("/r".into(), SortOptions::default(), false);
911 model.expand(Path::new("/r"), &fs).unwrap();
912 model.set_filter("needle");
913 assert_eq!(row_names(&model.visible_rows()), Vec::<String>::new()); }
916
917 #[test]
918 fn created_event_refreshes_loaded_parent() {
919 let mut fs = MemFs::default();
920 fs.dir("/r", Vec::new());
921 let mut model = TreeModel::new("/r".into(), SortOptions::default(), false);
922 model.expand(Path::new("/r"), &fs).unwrap();
923 assert!(model.visible_rows().iter().all(|r| r.name != "new.txt"));
924
925 fs.dir("/r", vec![("new.txt", false, false)]);
926 model.apply(&Change::Created { path: "/r/new.txt".into() }, &fs);
927 assert!(row_names(&model.visible_rows()).contains(&"new.txt".to_owned()));
928 }
929
930 #[test]
931 fn created_event_inside_no_loaded_parent_is_ignored_cheaply() {
932 let mut fs = MemFs::default();
933 fs.dir("/r", vec![("deep", true, false)]);
934 fs.dir("/r/deep", vec![("a.txt", false, false)]);
935 let mut model = TreeModel::new("/r".into(), SortOptions::default(), false);
936 model.expand(Path::new("/r"), &fs).unwrap();
937 fs.dir("/r/deep", vec![("a.txt", false, false), ("b.txt", false, false)]);
939 model.apply(&Change::Created { path: "/r/deep/b.txt".into() }, &fs);
940 let names = row_names(&model.visible_rows());
941 assert!(!names.contains(&"b.txt".to_owned()));
942 }
943
944 #[test]
945 fn removed_event_drops_subtree_cache() {
946 let mut fs = MemFs::default();
947 fs.dir("/r", vec![("a", true, false), ("keep.txt", false, false)]);
948 fs.dir("/r/a", vec![("x", false, false)]);
949 let mut model = TreeModel::new("/r".into(), SortOptions::default(), false);
950 model.expand(Path::new("/r"), &fs).unwrap();
951 model.expand(Path::new("/r/a"), &fs).unwrap();
952 assert_eq!(row_names(&model.visible_rows()), vec!["a", "x", "keep.txt"]);
954
955 fs.dir("/r", vec![("keep.txt", false, false)]);
956 model.apply(&Change::Removed { path: "/r/a".into() }, &fs);
957 assert_eq!(row_names(&model.visible_rows()), vec!["keep.txt"]);
958 assert!(!model.dirs.contains_key(Path::new("/r/a")), "subtree cache must be dropped");
959 }
960
961 #[test]
962 fn renamed_event_moves_entries() {
963 let mut fs = MemFs::default();
964 fs.dir("/r", vec![("old.txt", false, false), ("other", true, false)]);
965 let mut model = TreeModel::new("/r".into(), SortOptions::default(), false);
966 model.expand(Path::new("/r"), &fs).unwrap();
967
968 fs.dir("/r", vec![("other", true, false), ("new.txt", false, false)]);
969 model.apply(
970 &Change::Renamed { from: "/r/old.txt".into(), to: "/r/new.txt".into() },
971 &fs,
972 );
973 let names = row_names(&model.visible_rows());
974 assert!(!names.contains(&"old.txt".to_owned()));
975 assert!(names.contains(&"new.txt".to_owned()));
976 }
977
978 #[test]
979 fn rescan_reloads_even_after_repeated_cache() {
980 let mut fs = MemFs::default();
981 fs.dir("/r", vec![("a", true, false)]);
982 let mut model = TreeModel::new("/r".into(), SortOptions::default(), false);
983 model.expand(Path::new("/r"), &fs).unwrap();
984
985 fs.dir("/r", vec![("a", true, false), ("new", false, false)]);
987 model.apply(&Change::Rescan, &fs);
988 assert!(row_names(&model.visible_rows()).contains(&"new".to_owned()));
989 }
990
991 #[test]
992 fn expand_error_does_not_panic_and_drops_cache_state() {
993 let fs = MemFs::default(); let mut model = TreeModel::new("/r".into(), SortOptions::default(), false);
995 assert!(model.expand(Path::new("/r"), &fs).is_err());
996 assert_eq!(model.visible_rows().len(), 0);
998 }
999
1000 #[test]
1001 fn unreadable_expanded_dir_reports_error() {
1002 let mut fs = MemFs::default();
1003 fs.dir("/r", vec![("locked", true, false)]);
1004 let mut model = TreeModel::new("/r".into(), SortOptions::default(), false);
1005 model.expand(Path::new("/r"), &fs).unwrap();
1006 assert!(model.expand(Path::new("/r/locked"), &fs).is_err());
1007 }
1008
1009 #[test]
1010 fn is_hidden_rules() {
1011 assert!(is_hidden(".gitignore"));
1012 assert!(!is_hidden("."));
1013 assert!(!is_hidden(".."));
1014 assert!(!is_hidden("visible"));
1015 }
1016}