1use 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#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct ActionValues {
22 pub output: OsString,
24 pub alternate_output: OsString,
26 pub path: OsString,
28 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#[derive(Debug)]
55struct Explicit {
56 name: String,
57 detail: Option<String>,
58 action: ActionValues,
59}
60
61#[derive(Clone, Debug)]
63pub(crate) enum JsonKey {
64 Root,
66 JsonlRoot,
68 Member { key_span: Range<u32> },
70 Index(u32),
72}
73
74#[derive(Debug)]
76enum Payload {
77 Explicit(Box<Explicit>),
80 Fs { file_name: OsString },
84 Json {
89 span: Range<u32>,
90 key: JsonKey,
91 child_count: Option<u32>,
92 },
93 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 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 fs_root: Option<PathBuf>,
118 fs_no_ignore: bool,
120 json_source: Option<Arc<Vec<u8>>>,
123 unloaded: usize,
125 cursor: NodeId,
127 errors: Vec<String>,
129}
130
131impl Tree {
132 pub fn new() -> Self {
133 Self::default()
134 }
135
136 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 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 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 pub(crate) fn set_json_source(&mut self, bytes: Arc<Vec<u8>>) {
224 self.json_source = Some(bytes);
225 }
226
227 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 pub(crate) fn json_source_arc(&self) -> Option<Arc<Vec<u8>>> {
251 self.json_source.clone()
252 }
253
254 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 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 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 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 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 pub(crate) fn index_all(&mut self) {
345 while self.index_some(usize::MAX) {}
346 }
347
348 pub fn fully_indexed(&self) -> bool {
351 self.unloaded == 0
352 }
353
354 pub fn pending(&self) -> usize {
356 self.unloaded
357 }
358
359 pub fn errors(&self) -> &[String] {
361 &self.errors
362 }
363
364 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 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 let count = if node.children_loaded {
398 Some(node.children.len() as u32)
399 } else {
400 *child_count
401 };
402 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 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 pub fn depth(&self, id: NodeId) -> usize {
475 self.node(id).depth
476 }
477
478 pub fn is_container(&self, id: NodeId) -> bool {
480 self.node(id).is_container
481 }
482
483 pub fn children_of(&self, id: NodeId) -> &[NodeId] {
485 &self.node(id).children
486 }
487
488 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 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 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 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 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 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 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 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 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 pub(crate) fn view_root(&self) -> Option<NodeId> {
628 self.view_root
629 }
630
631 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 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 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 pub fn is_leaf(&self, id: NodeId) -> bool {
667 !self.node(id).is_container
668 }
669
670 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 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 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 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}