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 filesystem_path(&self, id: NodeId) -> Option<OsString> {
528 match &self.node(id).payload {
529 Payload::Explicit(explicit) => Some(explicit.action.path.clone()),
530 Payload::Fs { .. } => Some(self.fs_path(id)),
531 Payload::Json { .. } | Payload::JsonError { .. } => None,
532 }
533 }
534
535 pub fn relpath(&self, id: NodeId) -> OsString {
539 match &self.node(id).payload {
540 Payload::Explicit(explicit) => explicit.action.relpath.clone(),
541 Payload::Fs { .. } => self.fs_relpath(id).into_os_string(),
542 Payload::Json { .. } | Payload::JsonError { .. } => {
543 self.json_pointer_from(id, true).into()
544 }
545 }
546 }
547
548 pub fn jump_key(&self, id: NodeId) -> String {
553 match &self.node(id).payload {
554 Payload::Json { .. } | Payload::JsonError { .. } => self.json_pointer_from(id, false),
555 _ => self.relpath(id).to_string_lossy().into_owned(),
556 }
557 }
558
559 fn fs_relpath(&self, id: NodeId) -> PathBuf {
561 let mut components = Vec::new();
562 let mut cursor = Some(id);
563 while let Some(current) = cursor {
564 let node = self.node(current);
565 if let Payload::Fs { file_name } = &node.payload {
566 components.push(file_name.as_os_str());
567 }
568 cursor = node.parent;
569 }
570 components.iter().rev().collect()
571 }
572
573 fn fs_path(&self, id: NodeId) -> OsString {
575 let root = self.fs_root.as_deref().unwrap_or(Path::new(""));
576 root.join(self.fs_relpath(id)).into_os_string()
577 }
578
579 fn json_pointer_from(&self, id: NodeId, within_record: bool) -> String {
585 let mut tokens = Vec::new();
586 let mut cursor = Some(id);
587 while let Some(current) = cursor {
588 let node = self.node(current);
589 let (Payload::Json { key, .. } | Payload::JsonError { key, .. }) = &node.payload else {
590 break;
591 };
592 if within_record && self.is_jsonl_record(current) {
593 break;
594 }
595 match key {
596 JsonKey::Root | JsonKey::JsonlRoot => {}
597 JsonKey::Member { key_span } => {
598 tokens.push(crate::json_tree::key_text(self.json_bytes(), key_span));
599 }
600 JsonKey::Index(index) => tokens.push(index.to_string()),
601 }
602 cursor = node.parent;
603 }
604 let mut pointer = String::new();
605 for token in tokens.iter().rev() {
606 pointer = crate::json_tree::append_pointer(&pointer, token);
607 }
608 pointer
609 }
610
611 fn is_jsonl_record(&self, id: NodeId) -> bool {
614 self.node(id).parent.is_some_and(|parent| {
615 matches!(
616 &self.node(parent).payload,
617 Payload::Json {
618 key: JsonKey::JsonlRoot,
619 ..
620 }
621 )
622 })
623 }
624
625 pub fn len(&self) -> usize {
626 self.nodes.len()
627 }
628
629 pub fn is_empty(&self) -> bool {
630 self.nodes.is_empty()
631 }
632
633 pub fn root_ids(&self) -> &[NodeId] {
634 &self.roots
635 }
636
637 pub(crate) fn view_root(&self) -> Option<NodeId> {
639 self.view_root
640 }
641
642 pub(crate) fn set_view_root(&mut self, root: Option<NodeId>) {
644 if self.view_root != root {
645 self.view_root = root;
646 self.revision.advance();
647 }
648 }
649
650 pub(crate) fn view_parent(&self, id: NodeId) -> Option<NodeId> {
652 if self.view_root == Some(id) {
653 None
654 } else {
655 self.nodes[id].parent
656 }
657 }
658
659 pub(crate) fn is_in_view(&self, id: NodeId) -> bool {
661 let Some(root) = self.view_root else {
662 return true;
663 };
664 let mut cursor = Some(id);
665 while let Some(current) = cursor {
666 if current == root {
667 return true;
668 }
669 cursor = self.nodes[current].parent;
670 }
671 false
672 }
673
674 pub fn is_leaf(&self, id: NodeId) -> bool {
678 !self.node(id).is_container
679 }
680
681 pub fn branches(&self) -> impl Iterator<Item = (NodeId, Option<NodeId>)> + '_ {
683 self.nodes
684 .iter()
685 .enumerate()
686 .filter(|(id, _)| !self.is_leaf(*id))
687 .map(|(id, node)| (id, node.parent))
688 }
689
690 pub(crate) fn containers_first(&mut self) {
693 let containers_first = |nodes: &[Node], ids: &mut Vec<NodeId>| {
694 ids.sort_by_key(|&id| !nodes[id].is_container);
695 };
696 let mut roots = std::mem::take(&mut self.roots);
697 containers_first(&self.nodes, &mut roots);
698 self.roots = roots;
699 for id in 0..self.nodes.len() {
700 let mut children = std::mem::take(&mut self.nodes[id].children);
701 containers_first(&self.nodes, &mut children);
702 self.nodes[id].children = children;
703 }
704 }
705
706 pub(crate) fn containers_first_children(&mut self, id: NodeId) {
709 let mut children = std::mem::take(&mut self.nodes[id].children);
710 children.sort_by_key(|&child| !self.nodes[child].is_container);
711 self.nodes[id].children = children;
712 }
713}
714
715impl TreeModel for Tree {
716 type Id = NodeId;
717
718 fn roots(&self) -> impl Iterator<Item = NodeId> + '_ {
719 self.roots
720 .iter()
721 .copied()
722 .filter(|_| self.view_root.is_none())
723 .chain(self.view_root)
724 }
725
726 fn children(&self, id: NodeId) -> TreeChildren<'_, NodeId> {
727 let node = &self.nodes[id];
728 if node.is_container && node.children.is_empty() {
729 TreeChildren::Unloaded
733 } else {
734 TreeChildren::loaded(&node.children)
735 }
736 }
737
738 fn revision(&self) -> TreeRevision {
739 self.revision
740 }
741
742 fn size_hint(&self) -> usize {
743 self.nodes.len()
744 }
745}
746
747#[cfg(test)]
748mod tests {
749 use super::*;
750 use std::ffi::OsStr;
751
752 fn sample() -> (Tree, NodeId, NodeId, NodeId) {
753 let mut tree = Tree::new();
754 let dir = tree.push(
755 None,
756 "dir",
757 true,
758 ActionValues::new("dir", "/abs/dir", "dir"),
759 );
760 let file = tree.push_with_detail(
761 Some(dir),
762 "file",
763 Some("hint".to_owned()),
764 false,
765 ActionValues::new("dir/file", "/abs/dir/file", "dir/file")
766 .with_alternate_output("file"),
767 );
768 let leaf = tree.push(
769 None,
770 "leaf",
771 false,
772 ActionValues::new("leaf", "/abs/leaf", "leaf"),
773 );
774 (tree, dir, file, leaf)
775 }
776
777 #[test]
778 fn accessors_expose_hierarchy_and_display_data() {
779 let (tree, dir, file, leaf) = sample();
780 assert_eq!(tree.name(dir), "dir");
781 assert_eq!(tree.detail(dir), None);
782 assert_eq!(tree.detail(file).as_deref(), Some("hint"));
783 assert_eq!(tree.parent(dir), None);
784 assert_eq!(tree.parent(file), Some(dir));
785 assert_eq!(tree.depth(dir), 0);
786 assert_eq!(tree.depth(file), 1);
787 assert!(tree.is_container(dir));
788 assert!(!tree.is_container(leaf));
789 assert_eq!(tree.children_of(dir), [file]);
790 assert!(tree.children_of(leaf).is_empty());
791 }
792
793 #[test]
794 fn accessors_expose_action_values() {
795 let (tree, _, file, _) = sample();
796 assert_eq!(tree.output(file), OsStr::new("dir/file"));
797 assert_eq!(tree.alternate_output(file), OsStr::new("file"));
798 assert_eq!(tree.path(file), OsStr::new("/abs/dir/file"));
799 assert_eq!(tree.relpath(file), OsStr::new("dir/file"));
800 }
801
802 #[test]
803 fn jump_key_is_the_relpath_text_today() {
804 let (tree, dir, file, _) = sample();
805 assert_eq!(tree.jump_key(dir), "dir");
806 assert_eq!(tree.jump_key(file), "dir/file");
807 }
808
809 #[test]
810 fn fs_nodes_derive_action_values_from_the_hierarchy() {
811 let mut tree = Tree::new_fs("/scan/root".into(), false);
812 let dir = tree.push_fs(None, "b-dir", true);
813 let file = tree.push_fs(Some(dir), "inner.txt", false);
814
815 assert_eq!(tree.name(file), "inner.txt");
816 assert_eq!(tree.detail(file), None);
817 assert_eq!(tree.path(dir), OsStr::new("/scan/root/b-dir"));
818 assert_eq!(tree.path(file), OsStr::new("/scan/root/b-dir/inner.txt"));
819 assert_eq!(tree.relpath(file), OsStr::new("b-dir/inner.txt"));
820 assert_eq!(tree.output(file), tree.path(file));
821 assert_eq!(tree.alternate_output(file), OsStr::new("inner.txt"));
822 assert_eq!(tree.jump_key(file), "b-dir/inner.txt");
823 assert!(tree.is_container(dir));
824 assert!(!tree.is_container(file));
825 }
826
827 #[test]
828 fn only_filesystem_backed_nodes_have_a_filesystem_path() {
829 let mut tree = Tree::new_fs("/scan/root".into(), false);
830 let dir = tree.push_fs(None, "b-dir", true);
831 let file = tree.push_fs(Some(dir), "inner.txt", false);
832 assert_eq!(
833 tree.filesystem_path(file),
834 Some(OsString::from("/scan/root/b-dir/inner.txt"))
835 );
836
837 let json = crate::json_tree::from_reader(r#"{"a": 1}"#.as_bytes()).unwrap();
838 let member = json.root_ids()[0];
839 assert!(!json.path(member).is_empty());
841 assert_eq!(json.filesystem_path(member), None);
842 }
843
844 #[test]
845 fn containers_first_reorders_every_sibling_list_stably() {
846 let mut tree = Tree::new();
847 let a = tree.push(None, "a-file", false, ActionValues::new("", "", ""));
848 let b = tree.push(None, "b-dir", true, ActionValues::new("", "", ""));
849 let c = tree.push(None, "c-file", false, ActionValues::new("", "", ""));
850 let d = tree.push(None, "d-dir", true, ActionValues::new("", "", ""));
851 let inner_file = tree.push(Some(b), "x-file", false, ActionValues::new("", "", ""));
852 let inner_dir = tree.push(Some(b), "y-dir", true, ActionValues::new("", "", ""));
853
854 tree.containers_first();
855
856 assert_eq!(tree.root_ids(), [b, d, a, c]);
857 assert_eq!(tree.children_of(b), [inner_dir, inner_file]);
858 }
859}