1use std::collections::HashMap;
12use std::ffi::OsString;
13use std::path::{Path, PathBuf};
14
15use termesh_core::NodeId;
16
17use crate::service::{DirEntryInfo, EntryKind, FsError};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum ChildState {
22 Unloaded,
24 Loading,
26 Loaded(Vec<NodeId>),
27 Error(FsError),
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct Node {
33 pub id: NodeId,
34 pub parent: Option<NodeId>,
35 pub name: OsString,
36 pub path: PathBuf,
37 pub kind: EntryKind,
38 pub expanded: bool,
39 pub children: ChildState,
40 alive: bool,
43}
44
45impl Node {
46 pub fn is_expandable(&self) -> bool {
49 self.kind == EntryKind::Dir
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Row {
56 pub id: NodeId,
57 pub depth: usize,
59 pub name: String,
60 pub kind: EntryKind,
61 pub expanded: bool,
62 pub is_expandable: bool,
63 pub error: Option<String>,
65 pub loading: bool,
66}
67
68#[derive(Debug, Clone)]
70pub struct FileTree {
71 nodes: Vec<Node>,
72 root: NodeId,
73 selected: NodeId,
74}
75
76impl FileTree {
77 pub fn new(root_path: impl Into<PathBuf>, display_name: impl Into<OsString>) -> Self {
79 let root_path = root_path.into();
80 let root = NodeId::new(0);
81 let node = Node {
82 id: root,
83 parent: None,
84 name: display_name.into(),
85 path: root_path,
86 kind: EntryKind::Dir,
87 expanded: false,
88 children: ChildState::Unloaded,
89 alive: true,
90 };
91 Self { nodes: vec![node], root, selected: root }
92 }
93
94 pub fn root(&self) -> NodeId {
95 self.root
96 }
97
98 pub fn selected(&self) -> NodeId {
99 self.selected
100 }
101
102 pub fn node(&self, id: NodeId) -> Option<&Node> {
103 self.nodes.get(id.0 as usize).filter(|n| n.alive)
104 }
105
106 fn node_mut(&mut self, id: NodeId) -> Option<&mut Node> {
107 self.nodes.get_mut(id.0 as usize).filter(|n| n.alive)
108 }
109
110 pub fn path_of(&self, id: NodeId) -> Option<&Path> {
111 self.node(id).map(|n| n.path.as_path())
112 }
113
114 #[must_use]
118 pub fn begin_load(&mut self, id: NodeId) -> Option<PathBuf> {
119 let node = self.node_mut(id)?;
120 if node.kind != EntryKind::Dir {
121 return None;
122 }
123 match node.children {
124 ChildState::Unloaded | ChildState::Error(_) => {
125 node.children = ChildState::Loading;
126 Some(node.path.clone())
127 }
128 ChildState::Loading | ChildState::Loaded(_) => None,
129 }
130 }
131
132 #[must_use]
134 pub fn expand(&mut self, id: NodeId) -> Option<PathBuf> {
135 let node = self.node_mut(id)?;
136 if node.kind != EntryKind::Dir {
137 return None;
138 }
139 node.expanded = true;
140 self.begin_load(id)
141 }
142
143 pub fn collapse(&mut self, id: NodeId) {
144 if let Some(node) = self.node_mut(id) {
145 node.expanded = false;
146 }
147 }
148
149 #[must_use]
151 pub fn toggle(&mut self, id: NodeId) -> Option<PathBuf> {
152 match self.node(id) {
153 Some(n) if n.is_expandable() && n.expanded => {
154 self.collapse(id);
155 None
156 }
157 Some(n) if n.is_expandable() => self.expand(id),
158 _ => None,
159 }
160 }
161
162 pub fn set_error(&mut self, id: NodeId, error: FsError) {
164 if let Some(node) = self.node_mut(id) {
165 node.children = ChildState::Error(error);
166 }
167 }
168
169 pub fn set_children(&mut self, id: NodeId, entries: Vec<DirEntryInfo>) {
177 let Some(parent) = self.node(id) else { return };
178
179 let existing: HashMap<OsString, NodeId> = match &parent.children {
181 ChildState::Loaded(ids) => ids
182 .iter()
183 .filter_map(|&cid| self.node(cid).map(|n| (n.name.clone(), cid)))
184 .collect(),
185 _ => HashMap::new(),
186 };
187
188 let mut new_children = Vec::with_capacity(entries.len());
189 let mut kept = Vec::with_capacity(entries.len());
190
191 for entry in entries {
192 match existing.get(&entry.name) {
193 Some(&cid) if self.node(cid).map(|n| n.kind) == Some(entry.kind) => {
195 if let Some(n) = self.node_mut(cid) {
196 n.path = entry.path;
198 }
199 kept.push(cid);
200 new_children.push(cid);
201 }
202 _ => new_children.push(self.push_node(Some(id), entry)),
205 }
206 }
207
208 for (_, cid) in existing {
210 if !kept.contains(&cid) {
211 self.kill_subtree(cid);
212 }
213 }
214
215 if let Some(node) = self.node_mut(id) {
216 node.children = ChildState::Loaded(new_children);
217 }
218
219 if self.node(self.selected).is_none() {
221 self.selected = self.nearest_live_ancestor(id);
222 }
223 }
224
225 fn push_node(&mut self, parent: Option<NodeId>, entry: DirEntryInfo) -> NodeId {
226 let id = NodeId::new(self.nodes.len() as u64);
227 self.nodes.push(Node {
228 id,
229 parent,
230 name: entry.name,
231 path: entry.path,
232 kind: entry.kind,
233 expanded: false,
234 children: ChildState::Unloaded,
235 alive: true,
236 });
237 id
238 }
239
240 fn kill_subtree(&mut self, id: NodeId) {
242 let mut stack = vec![id];
243 while let Some(cur) = stack.pop() {
244 let Some(node) = self.nodes.get_mut(cur.0 as usize) else { continue };
245 node.alive = false;
246 if let ChildState::Loaded(kids) = &node.children {
247 stack.extend(kids.iter().copied());
248 }
249 }
250 }
251
252 fn nearest_live_ancestor(&self, from: NodeId) -> NodeId {
253 let mut cur = Some(from);
254 while let Some(id) = cur {
255 if self.node(id).is_some() {
256 return id;
257 }
258 cur = self.nodes.get(id.0 as usize).and_then(|n| n.parent);
259 }
260 self.root
261 }
262
263 pub fn visible_rows(&self) -> Vec<Row> {
265 let mut rows = Vec::new();
266 self.push_rows(self.root, 0, &mut rows);
267 rows
268 }
269
270 fn push_rows(&self, id: NodeId, depth: usize, out: &mut Vec<Row>) {
271 let Some(node) = self.node(id) else { return };
272 out.push(Row {
273 id,
274 depth,
275 name: node.name.to_string_lossy().into_owned(),
276 kind: node.kind,
277 expanded: node.expanded,
278 is_expandable: node.is_expandable(),
279 error: match &node.children {
280 ChildState::Error(e) => Some(e.to_string()),
281 _ => None,
282 },
283 loading: matches!(node.children, ChildState::Loading),
284 });
285
286 if !node.expanded {
287 return;
288 }
289 if let ChildState::Loaded(children) = &node.children {
290 for &child in children {
291 self.push_rows(child, depth + 1, out);
292 }
293 }
294 }
295
296 pub fn find_by_path(&self, path: &Path) -> Option<NodeId> {
299 self.nodes.iter().find(|n| n.alive && n.path == path).map(|n| n.id)
300 }
301
302 pub fn dirs_to_refresh(&self, changed: &[PathBuf]) -> Vec<NodeId> {
309 let mut out: Vec<NodeId> = Vec::new();
310 for path in changed {
311 let candidate = self
313 .find_by_path(path)
314 .filter(|&id| self.is_loaded_dir(id))
315 .or_else(|| path.parent().and_then(|p| self.find_by_path(p)))
316 .filter(|&id| self.is_loaded_dir(id));
317
318 if let Some(id) = candidate {
319 if !out.contains(&id) {
320 out.push(id);
321 }
322 }
323 }
324 out
325 }
326
327 fn is_loaded_dir(&self, id: NodeId) -> bool {
328 self.node(id).is_some_and(|n| matches!(n.children, ChildState::Loaded(_)))
329 }
330
331 pub fn loaded_directories(&self) -> Vec<(NodeId, PathBuf)> {
335 self.nodes
336 .iter()
337 .filter(|node| {
338 node.alive
339 && node.kind == EntryKind::Dir
340 && matches!(node.children, ChildState::Loaded(_))
341 })
342 .map(|node| (node.id, node.path.clone()))
343 .collect()
344 }
345
346 #[must_use]
349 pub fn refresh(&mut self, id: NodeId) -> Option<PathBuf> {
350 self.node(id).filter(|n| n.kind == EntryKind::Dir).map(|n| n.path.clone())
351 }
352
353 pub fn select(&mut self, id: NodeId) {
356 if self.node(id).is_some() {
357 self.selected = id;
358 }
359 }
360
361 pub fn selected_row(&self) -> usize {
363 self.visible_rows().iter().position(|r| r.id == self.selected).unwrap_or(0)
364 }
365
366 pub fn select_next(&mut self) {
367 self.move_selection(1);
368 }
369
370 pub fn select_prev(&mut self) {
371 self.move_selection(-1);
372 }
373
374 fn move_selection(&mut self, delta: isize) {
375 let rows = self.visible_rows();
376 if rows.is_empty() {
377 return;
378 }
379 let cur = rows.iter().position(|r| r.id == self.selected).unwrap_or(0) as isize;
380 let next = (cur + delta).clamp(0, rows.len() as isize - 1) as usize;
383 self.selected = rows[next].id;
384 }
385
386 pub fn collapse_or_parent(&mut self) {
389 let Some(node) = self.node(self.selected) else { return };
390 if node.is_expandable() && node.expanded {
391 let id = node.id;
392 self.collapse(id);
393 } else if let Some(parent) = node.parent {
394 self.selected = parent;
395 }
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 fn entry(name: &str, kind: EntryKind) -> DirEntryInfo {
404 DirEntryInfo { name: name.into(), path: PathBuf::from("/r").join(name), kind }
405 }
406
407 fn dir(name: &str) -> DirEntryInfo {
408 entry(name, EntryKind::Dir)
409 }
410 fn file(name: &str) -> DirEntryInfo {
411 entry(name, EntryKind::File)
412 }
413
414 fn tree() -> FileTree {
415 FileTree::new("/r", "r")
416 }
417
418 fn names(t: &FileTree) -> Vec<String> {
419 t.visible_rows().iter().map(|r| r.name.clone()).collect()
420 }
421
422 #[test]
423 fn a_new_tree_shows_only_its_root() {
424 let t = tree();
425 assert_eq!(names(&t), ["r"]);
426 assert_eq!(t.selected(), t.root());
427 }
428
429 #[test]
430 fn expanding_requests_a_read_then_shows_children() {
431 let mut t = tree();
432 assert_eq!(t.expand(t.root()), Some(PathBuf::from("/r")), "needs a read");
433 assert!(t.visible_rows()[0].loading, "renders as loading meanwhile");
434
435 t.set_children(t.root(), vec![dir("src"), file("Cargo.toml")]);
436 assert_eq!(names(&t), ["r", "src", "Cargo.toml"]);
437 }
438
439 #[test]
440 fn expanding_an_already_loaded_directory_does_not_re_read() {
441 let mut t = tree();
442 let _ = t.expand(t.root());
443 t.set_children(t.root(), vec![dir("src")]);
444 t.collapse(t.root());
445 assert_eq!(t.expand(t.root()), None, "contents are still held; no duplicate work");
446 }
447
448 #[test]
449 fn collapsed_subtrees_contribute_no_rows() {
450 let mut t = tree();
451 let _ = t.expand(t.root());
452 t.set_children(t.root(), vec![dir("src")]);
453 let src = t.visible_rows()[1].id;
454 let _ = t.expand(src);
455 t.set_children(src, vec![file("main.rs")]);
456 assert_eq!(names(&t), ["r", "src", "main.rs"]);
457
458 t.collapse(src);
459 assert_eq!(names(&t), ["r", "src"], "children of a collapsed dir are not rendered");
460 }
461
462 #[test]
463 fn files_never_expand() {
464 let mut t = tree();
465 let _ = t.expand(t.root());
466 t.set_children(t.root(), vec![file("README.md")]);
467 let readme = t.visible_rows()[1].id;
468 assert_eq!(t.expand(readme), None);
469 assert!(!t.visible_rows()[1].is_expandable);
470 }
471
472 #[test]
473 fn symlinks_are_not_expandable() {
474 let mut t = tree();
475 let _ = t.expand(t.root());
476 t.set_children(t.root(), vec![entry("link", EntryKind::Symlink)]);
477 let link = t.visible_rows()[1].id;
478 assert_eq!(t.expand(link), None, "symlinks are shown but not traversed");
479 }
480
481 #[test]
482 fn a_failed_read_is_recorded_on_the_node_not_fatal() {
483 let mut t = tree();
484 let _ = t.expand(t.root());
485 t.set_children(t.root(), vec![dir("secret"), file("ok.txt")]);
486 let secret = t.visible_rows()[1].id;
487 let _ = t.expand(secret);
488 t.set_error(secret, FsError::PermissionDenied(PathBuf::from("/r/secret")));
489
490 let rows = t.visible_rows();
491 assert!(rows[1].error.as_ref().unwrap().contains("permission denied"));
492 assert_eq!(rows[2].name, "ok.txt", "the sibling still renders");
493 }
494
495 #[test]
496 fn a_failed_read_can_be_retried() {
497 let mut t = tree();
498 let _ = t.expand(t.root());
499 t.set_error(t.root(), FsError::PermissionDenied(PathBuf::from("/r")));
500 assert_eq!(t.begin_load(t.root()), Some(PathBuf::from("/r")), "errors are retryable");
501 }
502
503 #[test]
506 fn re_reading_preserves_node_identity_and_expansion() {
507 let mut t = tree();
508 let _ = t.expand(t.root());
509 t.set_children(t.root(), vec![dir("src"), file("a.txt")]);
510 let src = t.visible_rows()[1].id;
511 let _ = t.expand(src);
512 t.set_children(src, vec![file("main.rs")]);
513
514 t.set_children(t.root(), vec![dir("src"), file("a.txt"), file("b.txt")]);
516
517 assert_eq!(t.visible_rows()[1].id, src, "src keeps its NodeId");
518 assert_eq!(
519 names(&t),
520 ["r", "src", "main.rs", "a.txt", "b.txt"],
521 "src stays expanded and keeps its loaded children"
522 );
523 }
524
525 #[test]
526 fn vanished_entries_disappear_from_the_tree() {
527 let mut t = tree();
528 let _ = t.expand(t.root());
529 t.set_children(t.root(), vec![file("gone.txt"), file("stays.txt")]);
530 t.set_children(t.root(), vec![file("stays.txt")]);
531 assert_eq!(names(&t), ["r", "stays.txt"]);
532 }
533
534 #[test]
535 fn ids_are_never_reused_after_a_delete() {
536 let mut t = tree();
537 let _ = t.expand(t.root());
538 t.set_children(t.root(), vec![file("gone.txt")]);
539 let gone = t.visible_rows()[1].id;
540
541 t.set_children(t.root(), vec![file("new.txt")]);
542 let new = t.visible_rows()[1].id;
543 assert_ne!(gone, new, "a fresh entry must not inherit a dead id");
544 assert!(t.node(gone).is_none(), "the dead node is unreachable");
545 }
546
547 #[test]
548 fn a_name_reused_for_a_different_kind_gets_a_fresh_identity() {
549 let mut t = tree();
550 let _ = t.expand(t.root());
551 t.set_children(t.root(), vec![file("thing")]);
552 let as_file = t.visible_rows()[1].id;
553
554 t.set_children(t.root(), vec![dir("thing")]);
555 let as_dir = t.visible_rows()[1].id;
556 assert_ne!(as_file, as_dir, "file replaced by a directory is a different thing");
557 assert!(t.visible_rows()[1].is_expandable);
558 }
559
560 #[test]
561 fn selection_survives_a_re_read_that_keeps_it() {
562 let mut t = tree();
563 let _ = t.expand(t.root());
564 t.set_children(t.root(), vec![file("a.txt"), file("b.txt")]);
565 let b = t.visible_rows()[2].id;
566 t.select(b);
567
568 t.set_children(t.root(), vec![file("a.txt"), file("b.txt"), file("c.txt")]);
569 assert_eq!(t.selected(), b, "selection is untouched by unrelated churn");
570 }
571
572 #[test]
573 fn selection_falls_back_when_the_selected_node_is_deleted() {
574 let mut t = tree();
575 let _ = t.expand(t.root());
576 t.set_children(t.root(), vec![file("doomed.txt")]);
577 let doomed = t.visible_rows()[1].id;
578 t.select(doomed);
579
580 t.set_children(t.root(), vec![file("other.txt")]);
581 assert_eq!(t.selected(), t.root(), "falls back to a live ancestor, never dangles");
582 assert!(t.node(t.selected()).is_some());
583 }
584
585 #[test]
588 fn navigation_walks_visible_rows_and_clamps_at_the_ends() {
589 let mut t = tree();
590 let _ = t.expand(t.root());
591 t.set_children(t.root(), vec![file("a"), file("b")]);
592
593 t.select_next();
594 assert_eq!(t.selected_row(), 1);
595 t.select_next();
596 assert_eq!(t.selected_row(), 2);
597 t.select_next();
598 assert_eq!(t.selected_row(), 2, "clamps at the bottom rather than wrapping");
599
600 t.select_prev();
601 t.select_prev();
602 t.select_prev();
603 assert_eq!(t.selected_row(), 0, "clamps at the top");
604 }
605
606 #[test]
607 fn navigation_skips_collapsed_children() {
608 let mut t = tree();
609 let _ = t.expand(t.root());
610 t.set_children(t.root(), vec![dir("src"), file("z.txt")]);
611 let src = t.visible_rows()[1].id;
612 let _ = t.expand(src);
613 t.set_children(src, vec![file("hidden_when_collapsed.rs")]);
614 t.collapse(src);
615
616 t.select(src);
617 t.select_next();
618 assert_eq!(t.visible_rows()[t.selected_row()].name, "z.txt");
619 }
620
621 #[test]
622 fn left_collapses_then_steps_to_the_parent() {
623 let mut t = tree();
624 let _ = t.expand(t.root());
625 t.set_children(t.root(), vec![dir("src")]);
626 let src = t.visible_rows()[1].id;
627 let _ = t.expand(src);
628 t.set_children(src, vec![file("main.rs")]);
629 t.select(src);
630
631 t.collapse_or_parent();
632 assert!(!t.visible_rows()[1].expanded, "first press collapses");
633 t.collapse_or_parent();
634 assert_eq!(t.selected(), t.root(), "second press moves to the parent");
635 }
636
637 fn loaded_tree() -> (FileTree, NodeId) {
640 let mut t = tree();
641 let _ = t.expand(t.root());
642 t.set_children(t.root(), vec![dir("src"), file("a.txt")]);
643 let src = t.visible_rows()[1].id;
644 let _ = t.expand(src);
645 t.set_children(src, vec![file("main.rs")]);
646 (t, src)
647 }
648
649 #[test]
650 fn a_changed_file_refreshes_its_containing_directory() {
651 let (t, src) = loaded_tree();
652 let changed = vec![PathBuf::from("/r/src/main.rs")];
653 assert_eq!(t.dirs_to_refresh(&changed), vec![src]);
654 }
655
656 #[test]
657 fn a_changed_directory_refreshes_itself() {
658 let (t, src) = loaded_tree();
659 assert_eq!(t.dirs_to_refresh(&[PathBuf::from("/r/src")]), vec![src]);
660 }
661
662 #[test]
663 fn a_burst_in_one_directory_coalesces_to_one_read() {
664 let (t, src) = loaded_tree();
665 let changed = vec![
666 PathBuf::from("/r/src/main.rs"),
667 PathBuf::from("/r/src/other.rs"),
668 PathBuf::from("/r/src/third.rs"),
669 ];
670 assert_eq!(t.dirs_to_refresh(&changed), vec![src], "deduplicated to one read");
671 }
672
673 #[test]
674 fn changes_under_unloaded_directories_are_ignored() {
675 let mut t = tree();
676 let _ = t.expand(t.root());
677 t.set_children(t.root(), vec![dir("never_opened")]);
678 let changed = vec![PathBuf::from("/r/never_opened/whatever.rs")];
681 assert!(t.dirs_to_refresh(&changed).is_empty());
682 }
683
684 #[test]
685 fn changes_outside_the_tree_are_ignored() {
686 let (t, _) = loaded_tree();
687 assert!(t.dirs_to_refresh(&[PathBuf::from("/somewhere/else/x.rs")]).is_empty());
688 }
689
690 #[test]
691 fn find_by_path_only_returns_live_nodes() {
692 let mut t = tree();
693 let _ = t.expand(t.root());
694 t.set_children(t.root(), vec![file("doomed.txt")]);
695 assert!(t.find_by_path(Path::new("/r/doomed.txt")).is_some());
696
697 t.set_children(t.root(), vec![]);
698 assert!(t.find_by_path(Path::new("/r/doomed.txt")).is_none(), "tombstones stay hidden");
699 }
700
701 #[test]
702 fn toggle_expands_then_collapses() {
703 let mut t = tree();
704 assert!(t.toggle(t.root()).is_some(), "first toggle asks for a read");
705 t.set_children(t.root(), vec![file("a")]);
706 assert_eq!(names(&t), ["r", "a"]);
707 assert!(t.toggle(t.root()).is_none());
708 assert_eq!(names(&t), ["r"]);
709 }
710}