Skip to main content

ite_cli/
tree.rs

1//! Source-neutral tree model at the center of the application. Filesystem and
2//! JSON adapters build its flat node arena; `app` navigates it and `ui` renders
3//! it through the `tui_treelistview::TreeModel` implementation.
4//!
5//! Consumers read nodes only through `Tree` accessors. The stored representation
6//! is private so source-specific payloads—including filesystem names and spans
7//! into retained JSON—can change without touching the app, renderer, or picker.
8//! Accessors return owned values because those values may be derived on demand.
9
10use std::ffi::OsString;
11use std::ops::Range;
12use std::path::{Path, PathBuf};
13use std::sync::Arc;
14
15use tui_treelistview::{TreeChildren, TreeModel, TreeRevision};
16
17pub type NodeId = usize;
18
19/// Values used when the focused node is accepted or passed to a shell binding.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct ActionValues {
22    /// Text written to stdout when the node is accepted.
23    pub output: OsString,
24    /// Text written to stdout by the alternate accept action.
25    pub alternate_output: OsString,
26    /// Value exported to shell bindings as `$path`.
27    pub path: OsString,
28    /// Value exported to shell bindings as `$relpath`.
29    pub relpath: OsString,
30}
31
32impl ActionValues {
33    pub fn new(
34        output: impl Into<OsString>,
35        path: impl Into<OsString>,
36        relpath: impl Into<OsString>,
37    ) -> Self {
38        let output = output.into();
39        Self {
40            alternate_output: output.clone(),
41            output,
42            path: path.into(),
43            relpath: relpath.into(),
44        }
45    }
46
47    pub fn with_alternate_output(mut self, output: impl Into<OsString>) -> Self {
48        self.alternate_output = output.into();
49        self
50    }
51}
52
53/// Display and action values stored verbatim.
54#[derive(Debug)]
55struct Explicit {
56    name: String,
57    detail: Option<String>,
58    action: ActionValues,
59}
60
61/// How a JSON node is addressed within its parent.
62#[derive(Clone, Debug)]
63pub(crate) enum JsonKey {
64    /// The synthetic `$` root of a single-rooted document.
65    Root,
66    /// The synthetic root of a JSONL document: the virtual array of records.
67    JsonlRoot,
68    /// An object member; the span covers the raw key token, quotes included.
69    Member { key_span: Range<u32> },
70    /// An array element.
71    Index(u32),
72}
73
74/// Per-node data that differs by source.
75#[derive(Debug)]
76enum Payload {
77    /// Stored verbatim (tests, transitional). Boxed so the variant does not
78    /// inflate every filesystem or JSON node.
79    Explicit(Box<Explicit>),
80    /// A filesystem entry's final path component. Kept as raw `OsString` so
81    /// non-UTF-8 names survive into the derived paths handed to shell
82    /// bindings; only the displayed name goes through `to_string_lossy`.
83    Fs { file_name: OsString },
84    /// A JSON value: its byte span in the retained input plus how it is
85    /// addressed within its parent. Everything else is derived.
86    /// `child_count` is the immediate-child count discovered when the parent
87    /// was scanned; `None` until known (unvalidated JSONL records).
88    Json {
89        span: Range<u32>,
90        key: JsonKey,
91        child_count: Option<u32>,
92    },
93    /// A span that failed to scan (a corrupt JSONL record): kept selectable,
94    /// rendered as an error leaf whose raw text is the alternate output.
95    JsonError { span: Range<u32>, key: JsonKey },
96}
97
98#[derive(Debug)]
99struct Node {
100    parent: Option<NodeId>,
101    children: Vec<NodeId>,
102    is_container: bool,
103    depth: usize,
104    /// False while the node's children (or, for a pending scalar record, its
105    /// validation) have not been computed yet.
106    children_loaded: bool,
107    payload: Payload,
108}
109
110#[derive(Debug, Default)]
111pub struct Tree {
112    nodes: Vec<Node>,
113    roots: Vec<NodeId>,
114    view_root: Option<NodeId>,
115    revision: TreeRevision,
116    /// The scanned directory (canonicalized) filesystem paths derive from.
117    fs_root: Option<PathBuf>,
118    /// Whether the scan ignores ignore-files; lazy walks must match.
119    fs_no_ignore: bool,
120    /// The raw JSON input document that `Payload::Json` spans index into.
121    /// Shared so materialization can read it while appending nodes.
122    json_source: Option<Arc<Vec<u8>>>,
123    /// Nodes whose children/validation are still pending.
124    unloaded: usize,
125    /// Arena-order sweep position for [`Tree::index_some`].
126    cursor: NodeId,
127    /// Messages for spans that failed to scan, for the banner and exit report.
128    errors: Vec<String>,
129}
130
131impl Tree {
132    pub fn new() -> Self {
133        Self::default()
134    }
135
136    /// A tree over a directory scan rooted at `root` (canonicalized);
137    /// filesystem nodes derive their paths from it and lazy directory walks
138    /// reuse the scan's ignore setting.
139    pub(crate) fn new_fs(root: PathBuf, no_ignore: bool) -> Self {
140        Self {
141            fs_root: Some(root),
142            fs_no_ignore: no_ignore,
143            ..Self::default()
144        }
145    }
146
147    pub(crate) fn fs_no_ignore(&self) -> bool {
148        self.fs_no_ignore
149    }
150
151    pub fn push(
152        &mut self,
153        parent: Option<NodeId>,
154        name: impl Into<String>,
155        is_container: bool,
156        action: ActionValues,
157    ) -> NodeId {
158        self.push_with_detail(parent, name, None, is_container, action)
159    }
160
161    pub fn push_with_detail(
162        &mut self,
163        parent: Option<NodeId>,
164        name: impl Into<String>,
165        detail: Option<String>,
166        is_container: bool,
167        action: ActionValues,
168    ) -> NodeId {
169        self.push_node(
170            parent,
171            is_container,
172            true,
173            Payload::Explicit(Box::new(Explicit {
174                name: name.into(),
175                detail,
176                action,
177            })),
178        )
179    }
180
181    /// Append a filesystem entry; its action values are derived on demand.
182    /// Directories start unloaded — their contents come from a lazy walk.
183    pub(crate) fn push_fs(
184        &mut self,
185        parent: Option<NodeId>,
186        file_name: impl Into<OsString>,
187        is_container: bool,
188    ) -> NodeId {
189        self.push_node(
190            parent,
191            is_container,
192            !is_container,
193            Payload::Fs {
194                file_name: file_name.into(),
195            },
196        )
197    }
198
199    /// Append a JSON value node; its display and action values are derived on
200    /// demand from the retained input bytes.
201    pub(crate) fn push_json(
202        &mut self,
203        parent: Option<NodeId>,
204        span: Range<u32>,
205        key: JsonKey,
206        is_container: bool,
207        child_count: Option<u32>,
208        children_loaded: bool,
209    ) -> NodeId {
210        self.push_node(
211            parent,
212            is_container,
213            children_loaded,
214            Payload::Json {
215                span,
216                key,
217                child_count,
218            },
219        )
220    }
221
222    /// Attach the input document the JSON spans index into.
223    pub(crate) fn set_json_source(&mut self, bytes: Arc<Vec<u8>>) {
224        self.json_source = Some(bytes);
225    }
226
227    /// Drop every node with id >= `len` (discarding a partially scanned JSONL
228    /// record); nodes below `len` and their order are untouched.
229    pub(crate) fn truncate(&mut self, len: usize) {
230        for node in &self.nodes[len.min(self.nodes.len())..] {
231            if !node.children_loaded {
232                self.unloaded -= 1;
233            }
234        }
235        self.nodes.truncate(len);
236        self.roots.retain(|&id| id < len);
237        for node in &mut self.nodes {
238            node.children.retain(|&id| id < len);
239        }
240        self.cursor = self.cursor.min(len);
241    }
242
243    fn json_bytes(&self) -> &[u8] {
244        self.json_source
245            .as_ref()
246            .map_or(&[], |bytes| bytes.as_slice())
247    }
248
249    /// The shared input document, for materialization to read while pushing.
250    pub(crate) fn json_source_arc(&self) -> Option<Arc<Vec<u8>>> {
251        self.json_source.clone()
252    }
253
254    /// The byte span of a JSON (or error) node.
255    pub(crate) fn json_span(&self, id: NodeId) -> Option<Range<u32>> {
256        match &self.node(id).payload {
257            Payload::Json { span, .. } | Payload::JsonError { span, .. } => Some(span.clone()),
258            _ => None,
259        }
260    }
261
262    fn push_node(
263        &mut self,
264        parent: Option<NodeId>,
265        is_container: bool,
266        children_loaded: bool,
267        payload: Payload,
268    ) -> NodeId {
269        let id = self.nodes.len();
270        let depth = parent.map_or(0, |id| self.nodes[id].depth + 1);
271        self.nodes.push(Node {
272            parent,
273            children: Vec::new(),
274            is_container,
275            depth,
276            children_loaded,
277            payload,
278        });
279        if !children_loaded {
280            self.unloaded += 1;
281        }
282        match parent {
283            Some(parent) => self.nodes[parent].children.push(id),
284            None => self.roots.push(id),
285        }
286        id
287    }
288
289    /// Materialize the node's pending children (or validate a pending scalar
290    /// record). Returns true when work was actually done.
291    pub(crate) fn ensure_children(&mut self, id: NodeId) -> bool {
292        if self.nodes[id].children_loaded {
293            return false;
294        }
295        match self.nodes[id].payload {
296            Payload::Json { .. } => crate::json_tree::materialize(self, id),
297            Payload::Fs { .. } => crate::fstree::materialize(self, id),
298            _ => self.mark_children_loaded(id),
299        }
300        self.revision.advance();
301        true
302    }
303
304    /// Flip a node to loaded, keeping the pending-work counter accurate.
305    pub(crate) fn mark_children_loaded(&mut self, id: NodeId) {
306        if !self.nodes[id].children_loaded {
307            self.nodes[id].children_loaded = true;
308            self.unloaded -= 1;
309        }
310    }
311
312    /// Convert a node whose span failed to scan into a selectable error leaf
313    /// and record the message for the banner and the exit report.
314    pub(crate) fn set_json_error(&mut self, id: NodeId, message: String) {
315        let (span, key) = match &self.nodes[id].payload {
316            Payload::Json { span, key, .. } => (span.clone(), key.clone()),
317            _ => return,
318        };
319        self.nodes[id].payload = Payload::JsonError { span, key };
320        self.nodes[id].is_container = false;
321        self.mark_children_loaded(id);
322        self.errors.push(message);
323        self.revision.advance();
324    }
325
326    /// Advance the arena-order sweep, loading up to `budget` pending nodes.
327    /// Returns true when any work was done. The sweep together with
328    /// on-demand loading eventually visits every node, so a fully swept tree
329    /// has validated every byte of the input.
330    pub(crate) fn index_some(&mut self, budget: usize) -> bool {
331        let mut done = 0;
332        while done < budget && self.cursor < self.nodes.len() {
333            if self.nodes[self.cursor].children_loaded {
334                self.cursor += 1;
335            } else {
336                self.ensure_children(self.cursor);
337                done += 1;
338            }
339        }
340        done > 0
341    }
342
343    /// Run the sweep to completion (startup `--expand`, and tests).
344    pub(crate) fn index_all(&mut self) {
345        while self.index_some(usize::MAX) {}
346    }
347
348    /// True once every node's children are loaded and every span validated;
349    /// the jump picker requires this.
350    pub fn fully_indexed(&self) -> bool {
351        self.unloaded == 0
352    }
353
354    /// Nodes whose children/validation are still pending (progress display).
355    pub fn pending(&self) -> usize {
356        self.unloaded
357    }
358
359    /// Messages for spans that failed to scan, in discovery order.
360    pub fn errors(&self) -> &[String] {
361        &self.errors
362    }
363
364    /// Record a non-fatal source error (an unreadable directory) for the
365    /// banner and the exit report.
366    pub(crate) fn record_error(&mut self, message: String) {
367        self.errors.push(message);
368        self.revision.advance();
369    }
370
371    fn node(&self, id: NodeId) -> &Node {
372        &self.nodes[id]
373    }
374
375    /// The node's display name.
376    pub fn name(&self, id: NodeId) -> String {
377        let node = self.node(id);
378        match &node.payload {
379            Payload::Explicit(explicit) => explicit.name.clone(),
380            Payload::Fs { file_name } => file_name.to_string_lossy().into_owned(),
381            Payload::Json {
382                span,
383                key,
384                child_count,
385            } => {
386                let bytes = self.json_bytes();
387                let prefix = match key {
388                    JsonKey::Root | JsonKey::JsonlRoot => "$".to_owned(),
389                    JsonKey::Member { key_span } => crate::json_tree::key_text(bytes, key_span),
390                    JsonKey::Index(index) => format!("[{index}]"),
391                };
392                if node.is_container {
393                    // Loaded containers count their children; unloaded ones
394                    // fall back to the count discovered when their parent was
395                    // scanned, and show `…` when even that is pending
396                    // (unvalidated JSONL records).
397                    let count = if node.children_loaded {
398                        Some(node.children.len() as u32)
399                    } else {
400                        *child_count
401                    };
402                    // The JSONL root is a virtual array whatever its first
403                    // record's first byte happens to be.
404                    let object =
405                        !matches!(key, JsonKey::JsonlRoot) && bytes[span.start as usize] == b'{';
406                    match (object, count) {
407                        (true, Some(0)) => format!("{prefix} {{}}"),
408                        (true, Some(count)) => format!("{prefix} {{{count}}}"),
409                        (true, None) => format!("{prefix} {{…}}"),
410                        (false, Some(0)) => format!("{prefix} []"),
411                        (false, Some(count)) => format!("{prefix} [{count}]"),
412                        (false, None) => format!("{prefix} […]"),
413                    }
414                } else {
415                    format!("{prefix}: {}", crate::json_tree::value_text(bytes, span))
416                }
417            }
418            Payload::JsonError { key, .. } => {
419                let prefix = match key {
420                    JsonKey::Root | JsonKey::JsonlRoot => "$".to_owned(),
421                    JsonKey::Member { key_span } => {
422                        crate::json_tree::key_text(self.json_bytes(), key_span)
423                    }
424                    JsonKey::Index(index) => format!("[{index}]"),
425                };
426                format!("{prefix} ⚠")
427            }
428        }
429    }
430
431    /// Optional secondary text rendered after the name.
432    pub fn detail(&self, id: NodeId) -> Option<String> {
433        let node = self.node(id);
434        match &node.payload {
435            Payload::Explicit(explicit) => explicit.detail.clone(),
436            Payload::Fs { .. } => None,
437            Payload::Json { span, .. } => {
438                let bytes = self.json_bytes();
439                if !node.is_container || bytes.get(span.start as usize) != Some(&b'{') {
440                    return None;
441                }
442                let members = node.children.iter().map(|&child| {
443                    let child = self.node(child);
444                    match &child.payload {
445                        Payload::Json {
446                            span,
447                            key: JsonKey::Member { key_span },
448                            ..
449                        } if !child.is_container => Some((key_span.clone(), span.clone())),
450                        _ => None,
451                    }
452                });
453                crate::json_tree::object_preview(bytes, members)
454            }
455            Payload::JsonError { span, .. } => {
456                let mut text = crate::json_tree::raw_text(self.json_bytes(), span);
457                if text.len() > 512 {
458                    let mut end = 512;
459                    while !text.is_char_boundary(end) {
460                        end -= 1;
461                    }
462                    text.truncate(end);
463                }
464                Some(text)
465            }
466        }
467    }
468
469    pub fn parent(&self, id: NodeId) -> Option<NodeId> {
470        self.node(id).parent
471    }
472
473    /// 0 for roots.
474    pub fn depth(&self, id: NodeId) -> usize {
475        self.node(id).depth
476    }
477
478    /// Whether the node represents a container, including an empty one.
479    pub fn is_container(&self, id: NodeId) -> bool {
480        self.node(id).is_container
481    }
482
483    /// The node's children, in display order.
484    pub fn children_of(&self, id: NodeId) -> &[NodeId] {
485        &self.node(id).children
486    }
487
488    /// Text written to stdout when the node is accepted.
489    pub fn output(&self, id: NodeId) -> OsString {
490        match &self.node(id).payload {
491            Payload::Explicit(explicit) => explicit.action.output.clone(),
492            Payload::Fs { .. } => self.fs_path(id),
493            Payload::Json { .. } | Payload::JsonError { .. } => {
494                self.json_pointer_from(id, false).into()
495            }
496        }
497    }
498
499    /// Text written to stdout by the alternate accept action.
500    pub fn alternate_output(&self, id: NodeId) -> OsString {
501        match &self.node(id).payload {
502            Payload::Explicit(explicit) => explicit.action.alternate_output.clone(),
503            Payload::Fs { file_name } => file_name.clone(),
504            Payload::Json { span, .. } => {
505                crate::json_tree::value_text(self.json_bytes(), span).into()
506            }
507            Payload::JsonError { span, .. } => {
508                crate::json_tree::raw_text(self.json_bytes(), span).into()
509            }
510        }
511    }
512
513    /// Value exported to shell bindings as `$path`.
514    pub fn path(&self, id: NodeId) -> OsString {
515        match &self.node(id).payload {
516            Payload::Explicit(explicit) => explicit.action.path.clone(),
517            Payload::Fs { .. } => self.fs_path(id),
518            Payload::Json { .. } | Payload::JsonError { .. } => {
519                self.json_pointer_from(id, false).into()
520            }
521        }
522    }
523
524    /// Value exported to shell bindings as `$relpath`: the address within the
525    /// source's natural unit — the scan root for directories, the document
526    /// for JSON, the containing record for JSONL (the record itself is "").
527    pub fn relpath(&self, id: NodeId) -> OsString {
528        match &self.node(id).payload {
529            Payload::Explicit(explicit) => explicit.action.relpath.clone(),
530            Payload::Fs { .. } => self.fs_relpath(id).into_os_string(),
531            Payload::Json { .. } | Payload::JsonError { .. } => {
532                self.json_pointer_from(id, true).into()
533            }
534        }
535    }
536
537    /// The text the jump picker matches and displays: the node's
538    /// document-global address. Coincides with `relpath` for directory scans
539    /// and JSON documents; JSONL diverges, since a record-relative `relpath`
540    /// repeats across records.
541    pub fn jump_key(&self, id: NodeId) -> String {
542        match &self.node(id).payload {
543            Payload::Json { .. } | Payload::JsonError { .. } => self.json_pointer_from(id, false),
544            _ => self.relpath(id).to_string_lossy().into_owned(),
545        }
546    }
547
548    /// Root-relative path of a filesystem node: ancestor file names joined.
549    fn fs_relpath(&self, id: NodeId) -> PathBuf {
550        let mut components = Vec::new();
551        let mut cursor = Some(id);
552        while let Some(current) = cursor {
553            let node = self.node(current);
554            if let Payload::Fs { file_name } = &node.payload {
555                components.push(file_name.as_os_str());
556            }
557            cursor = node.parent;
558        }
559        components.iter().rev().collect()
560    }
561
562    /// Absolute path of a filesystem node: the scan root plus [`Self::fs_relpath`].
563    fn fs_path(&self, id: NodeId) -> OsString {
564        let root = self.fs_root.as_deref().unwrap_or(Path::new(""));
565        root.join(self.fs_relpath(id)).into_os_string()
566    }
567
568    /// Canonical JSON Pointer of a JSON node, assembled from its ancestor
569    /// keys. Synthetic roots contribute no token, so a single-rooted
570    /// document's root is the empty (whole-document) pointer. With
571    /// `within_record` the walk stops at the containing JSONL record, whose
572    /// own token is excluded — the record-relative address.
573    fn json_pointer_from(&self, id: NodeId, within_record: bool) -> String {
574        let mut tokens = Vec::new();
575        let mut cursor = Some(id);
576        while let Some(current) = cursor {
577            let node = self.node(current);
578            let (Payload::Json { key, .. } | Payload::JsonError { key, .. }) = &node.payload else {
579                break;
580            };
581            if within_record && self.is_jsonl_record(current) {
582                break;
583            }
584            match key {
585                JsonKey::Root | JsonKey::JsonlRoot => {}
586                JsonKey::Member { key_span } => {
587                    tokens.push(crate::json_tree::key_text(self.json_bytes(), key_span));
588                }
589                JsonKey::Index(index) => tokens.push(index.to_string()),
590            }
591            cursor = node.parent;
592        }
593        let mut pointer = String::new();
594        for token in tokens.iter().rev() {
595            pointer = crate::json_tree::append_pointer(&pointer, token);
596        }
597        pointer
598    }
599
600    /// Whether the node is a JSONL record: a direct child of the virtual
601    /// array root.
602    fn is_jsonl_record(&self, id: NodeId) -> bool {
603        self.node(id).parent.is_some_and(|parent| {
604            matches!(
605                &self.node(parent).payload,
606                Payload::Json {
607                    key: JsonKey::JsonlRoot,
608                    ..
609                }
610            )
611        })
612    }
613
614    pub fn len(&self) -> usize {
615        self.nodes.len()
616    }
617
618    pub fn is_empty(&self) -> bool {
619        self.nodes.is_empty()
620    }
621
622    pub fn root_ids(&self) -> &[NodeId] {
623        &self.roots
624    }
625
626    /// The temporary root used by the tree view, if it has been narrowed.
627    pub(crate) fn view_root(&self) -> Option<NodeId> {
628        self.view_root
629    }
630
631    /// Narrow the tree model to one subtree, or restore the original forest.
632    pub(crate) fn set_view_root(&mut self, root: Option<NodeId>) {
633        if self.view_root != root {
634            self.view_root = root;
635            self.revision.advance();
636        }
637    }
638
639    /// The node's parent in the current view. A temporary root has no parent.
640    pub(crate) fn view_parent(&self, id: NodeId) -> Option<NodeId> {
641        if self.view_root == Some(id) {
642            None
643        } else {
644            self.nodes[id].parent
645        }
646    }
647
648    /// Whether a node belongs to the subtree exposed by the current view.
649    pub(crate) fn is_in_view(&self, id: NodeId) -> bool {
650        let Some(root) = self.view_root else {
651            return true;
652        };
653        let mut cursor = Some(id);
654        while let Some(current) = cursor {
655            if current == root {
656                return true;
657            }
658            cursor = self.nodes[current].parent;
659        }
660        false
661    }
662
663    /// True when the node cannot be expanded. Containers are never leaves:
664    /// an empty directory is still a directory and `{}`/`[]` are still
665    /// containers — they simply open to nothing.
666    pub fn is_leaf(&self, id: NodeId) -> bool {
667        !self.node(id).is_container
668    }
669
670    /// All expandable nodes as `(id, parent)` pairs, in tree order.
671    pub fn branches(&self) -> impl Iterator<Item = (NodeId, Option<NodeId>)> + '_ {
672        self.nodes
673            .iter()
674            .enumerate()
675            .filter(|(id, _)| !self.is_leaf(*id))
676            .map(|(id, node)| (id, node.parent))
677    }
678
679    /// Reorder every sibling list (roots included) to put containers first,
680    /// keeping the existing relative order within each group.
681    pub(crate) fn containers_first(&mut self) {
682        let containers_first = |nodes: &[Node], ids: &mut Vec<NodeId>| {
683            ids.sort_by_key(|&id| !nodes[id].is_container);
684        };
685        let mut roots = std::mem::take(&mut self.roots);
686        containers_first(&self.nodes, &mut roots);
687        self.roots = roots;
688        for id in 0..self.nodes.len() {
689            let mut children = std::mem::take(&mut self.nodes[id].children);
690            containers_first(&self.nodes, &mut children);
691            self.nodes[id].children = children;
692        }
693    }
694
695    /// Reorder one node's children to put containers first (lazy directory
696    /// walks sort each sibling list as it materializes).
697    pub(crate) fn containers_first_children(&mut self, id: NodeId) {
698        let mut children = std::mem::take(&mut self.nodes[id].children);
699        children.sort_by_key(|&child| !self.nodes[child].is_container);
700        self.nodes[id].children = children;
701    }
702}
703
704impl TreeModel for Tree {
705    type Id = NodeId;
706
707    fn roots(&self) -> impl Iterator<Item = NodeId> + '_ {
708        self.roots
709            .iter()
710            .copied()
711            .filter(|_| self.view_root.is_none())
712            .chain(self.view_root)
713    }
714
715    fn children(&self, id: NodeId) -> TreeChildren<'_, NodeId> {
716        let node = &self.nodes[id];
717        if node.is_container && node.children.is_empty() {
718            // `Unloaded` rather than an empty slice (which the widget
719            // normalizes to `Leaf`): a childless container — unwalked,
720            // unvalidated, or genuinely empty — must stay a branch.
721            TreeChildren::Unloaded
722        } else {
723            TreeChildren::loaded(&node.children)
724        }
725    }
726
727    fn revision(&self) -> TreeRevision {
728        self.revision
729    }
730
731    fn size_hint(&self) -> usize {
732        self.nodes.len()
733    }
734}
735
736#[cfg(test)]
737mod tests {
738    use super::*;
739    use std::ffi::OsStr;
740
741    fn sample() -> (Tree, NodeId, NodeId, NodeId) {
742        let mut tree = Tree::new();
743        let dir = tree.push(
744            None,
745            "dir",
746            true,
747            ActionValues::new("dir", "/abs/dir", "dir"),
748        );
749        let file = tree.push_with_detail(
750            Some(dir),
751            "file",
752            Some("hint".to_owned()),
753            false,
754            ActionValues::new("dir/file", "/abs/dir/file", "dir/file")
755                .with_alternate_output("file"),
756        );
757        let leaf = tree.push(
758            None,
759            "leaf",
760            false,
761            ActionValues::new("leaf", "/abs/leaf", "leaf"),
762        );
763        (tree, dir, file, leaf)
764    }
765
766    #[test]
767    fn accessors_expose_hierarchy_and_display_data() {
768        let (tree, dir, file, leaf) = sample();
769        assert_eq!(tree.name(dir), "dir");
770        assert_eq!(tree.detail(dir), None);
771        assert_eq!(tree.detail(file).as_deref(), Some("hint"));
772        assert_eq!(tree.parent(dir), None);
773        assert_eq!(tree.parent(file), Some(dir));
774        assert_eq!(tree.depth(dir), 0);
775        assert_eq!(tree.depth(file), 1);
776        assert!(tree.is_container(dir));
777        assert!(!tree.is_container(leaf));
778        assert_eq!(tree.children_of(dir), [file]);
779        assert!(tree.children_of(leaf).is_empty());
780    }
781
782    #[test]
783    fn accessors_expose_action_values() {
784        let (tree, _, file, _) = sample();
785        assert_eq!(tree.output(file), OsStr::new("dir/file"));
786        assert_eq!(tree.alternate_output(file), OsStr::new("file"));
787        assert_eq!(tree.path(file), OsStr::new("/abs/dir/file"));
788        assert_eq!(tree.relpath(file), OsStr::new("dir/file"));
789    }
790
791    #[test]
792    fn jump_key_is_the_relpath_text_today() {
793        let (tree, dir, file, _) = sample();
794        assert_eq!(tree.jump_key(dir), "dir");
795        assert_eq!(tree.jump_key(file), "dir/file");
796    }
797
798    #[test]
799    fn fs_nodes_derive_action_values_from_the_hierarchy() {
800        let mut tree = Tree::new_fs("/scan/root".into(), false);
801        let dir = tree.push_fs(None, "b-dir", true);
802        let file = tree.push_fs(Some(dir), "inner.txt", false);
803
804        assert_eq!(tree.name(file), "inner.txt");
805        assert_eq!(tree.detail(file), None);
806        assert_eq!(tree.path(dir), OsStr::new("/scan/root/b-dir"));
807        assert_eq!(tree.path(file), OsStr::new("/scan/root/b-dir/inner.txt"));
808        assert_eq!(tree.relpath(file), OsStr::new("b-dir/inner.txt"));
809        assert_eq!(tree.output(file), tree.path(file));
810        assert_eq!(tree.alternate_output(file), OsStr::new("inner.txt"));
811        assert_eq!(tree.jump_key(file), "b-dir/inner.txt");
812        assert!(tree.is_container(dir));
813        assert!(!tree.is_container(file));
814    }
815
816    #[test]
817    fn containers_first_reorders_every_sibling_list_stably() {
818        let mut tree = Tree::new();
819        let a = tree.push(None, "a-file", false, ActionValues::new("", "", ""));
820        let b = tree.push(None, "b-dir", true, ActionValues::new("", "", ""));
821        let c = tree.push(None, "c-file", false, ActionValues::new("", "", ""));
822        let d = tree.push(None, "d-dir", true, ActionValues::new("", "", ""));
823        let inner_file = tree.push(Some(b), "x-file", false, ActionValues::new("", "", ""));
824        let inner_dir = tree.push(Some(b), "y-dir", true, ActionValues::new("", "", ""));
825
826        tree.containers_first();
827
828        assert_eq!(tree.root_ids(), [b, d, a, c]);
829        assert_eq!(tree.children_of(b), [inner_dir, inner_file]);
830    }
831}