1use std::collections::VecDeque;
2
3use anyhow::Result;
4use colored::Colorize;
5use log::debug;
6
7use super::{DirTree, NodeType, TreePrintConfig};
8
9impl DirTree<'_> {
10    pub fn print_tree(&self, config: &TreePrintConfig) -> Result<String> {
11        debug!("Start to print tree:\n{:?}", self.map);
12        let mut tree_builder = String::new(); let mut node_stack = VecDeque::<(usize, usize)>::new();
14        node_stack.push_back((self.root, 0));
15        let mut level_stack = VecDeque::<bool>::new();
16        while let Some((parent_idx, vec_idx)) = node_stack.pop_back() {
17            level_stack.pop_back();
18            if vec_idx >= self.map[parent_idx].children.len() {
19                continue;
20            }
21            let child_idx = self.map[parent_idx].children[vec_idx];
22            let child = &self.map[child_idx];
23
24            let is_local_last = vec_idx + 1 == self.map[parent_idx].children.len();
25            level_stack.push_back(is_local_last);
26            if !child.visible {
27                node_stack.push_back((parent_idx, vec_idx + 1));
28                continue;
29            }
30            let is_local_last = vec_idx + 1 == self.map[parent_idx].children.len();
31            if level_stack.len() > 1 {
32                for level_status in level_stack.iter().take(level_stack.len() - 1) {
33                    if *level_status {
34                        tree_builder.push_str("    ");
35                    } else {
36                        tree_builder.push_str("│   ");
37                    }
38                }
39            }
40
41            if is_local_last {
42                tree_builder.push_str("└── ");
43            } else {
44                tree_builder.push_str("├── ");
45            }
46
47            node_stack.push_back((parent_idx, vec_idx + 1));
48
49            match child.node_type {
50                NodeType::Symlink => {
51                    let mut line = if let Some(color) = config.symbol_color {
52                        format!(
53                            "{} -> {}",
54                            child.name.color(color),
55                            child.symlink_target.as_ref().unwrap()
56                        )
57                    } else {
58                        format!("{} -> {}", child.name, child.symlink_target.as_ref().unwrap())
59                    };
60                    if child.is_recursive {
61                        line = format!("{} [recursive, not followed]\n", line);
62                    } else {
63                        line.push('\n');
64                    }
65                    tree_builder.push_str(line.as_str());
66                    node_stack.push_back((child_idx, 0));
67                    level_stack.push_back(false);
68                }
69                NodeType::Dir => {
70                    let line = if let Some(color) = config.dir_color {
71                        format!("{}\n", child.name.color(color))
72                    } else {
73                        format!("{}\n", child.name)
74                    };
75                    tree_builder.push_str(line.as_str());
76                    node_stack.push_back((child_idx, 0));
77                    level_stack.push_back(false);
78                }
79                NodeType::File => {
80                    let line = if let Some(color) = config.file_color {
81                        format!("{}\n", child.name.color(color))
82                    } else {
83                        format!("{}\n", child.name)
84                    };
85                    tree_builder.push_str(line.as_str());
86                }
87                NodeType::Other => {
88                    let line = if let Some(color) = config.tree_color {
89                        format!("{}\n", child.name.color(color))
90                    } else {
91                        format!("{}\n", child.name)
92                    };
93                    tree_builder.push_str(line.as_str());
94                }
95                NodeType::Invalid => {
96                    panic!("Should not have any invalid type");
97                }
98            }
99        }
100        tree_builder.pop();
101        Ok(tree_builder)
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use bumpalo::Bump;
108    use pretty_assertions::assert_eq;
109    use regex::Regex;
110
111    use super::*;
112    use crate::util::defer::cleanup;
113    use crate::util::fs_util::create_symlink;
114    use crate::util::test_util::{create_dir_structure, gen_unique_temp_dir, log_test};
115    use crate::util::tree::{FilterType, TreeConfig};
116
117    #[test]
118    fn tree_normal_case() {
119        let no_color_print = TreePrintConfig {
120            dir_color: None,
121            file_color: None,
122            symbol_color: None,
123            tree_color: None,
124        };
125        let (_tmp_dir, root) = gen_unique_temp_dir();
137        let structure: &[(Option<&str>, &[&str])] = &[
138            (Some("dir1"), &["file1", "file2"][..]),
139            (Some("dir2"), &["file3", "file4"][..]),
140            (Some("dir3"), &["file5"][..]),
141            (Some("dir3/dir4"), &[][..]),
142            (Some("dir3/dir4/dir5"), &[][..]),
143        ];
144        create_dir_structure(&root, structure);
145        cleanup!(
146            {
147                let config = TreeConfig {
148                    root: &root,
149                    target: "",
150                    filter_type: FilterType::Disable,
151                    filters: Vec::new(),
152                };
153                let bump = Bump::new();
154                let tree = DirTree::new(&config, &bump).unwrap();
155                let result = tree.print_tree(&no_color_print).unwrap();
156                assert_eq!(
157                    result,
158                    r#"├── dir1
159│   ├── file1
160│   └── file2
161├── dir2
162│   ├── file3
163│   └── file4
164└── dir3
165    ├── dir4
166    │   └── dir5
167    └── file5"#
168                );
169            },
170            {}
171        )
172    }
173
174    #[test]
175    fn test_filtered_case() {
176        let no_color_print = TreePrintConfig {
177            dir_color: None,
178            file_color: None,
179            symbol_color: None,
180            tree_color: None,
181        };
182        let (_tmp_dir, root) = gen_unique_temp_dir();
189        let structure: &[(Option<&str>, &[&str])] = &[
190            (Some("dir1"), &["file1"][..]),
191            (Some("dir2"), &["file2"][..]),
192            (Some("dir3"), &[][..]),
193        ];
194        create_dir_structure(&root, structure);
195
196        cleanup!(
198            {
199                let mut config = TreeConfig {
200                    root: &root,
201                    target: "",
202                    filter_type: FilterType::Exclude,
203                    filters: vec![Regex::new(r"dir2").unwrap(), Regex::new(r"file1").unwrap()],
204                };
205
206                let bump = Bump::new();
207                let tree = DirTree::new(&config, &bump).unwrap();
208                let result = tree.print_tree(&no_color_print).unwrap();
209                assert_eq!(
210                    result,
211                    r#"├── dir1
212└── dir3"#
213                );
214
215                config.filters = Vec::new();
217                config.filter_type = FilterType::Disable;
218                let tree = DirTree::new(&config, &bump).unwrap();
219                let result = tree.print_tree(&no_color_print).unwrap();
220                assert_eq!(
221                    result,
222                    r#"├── dir1
223│   └── file1
224├── dir2
225│   └── file2
226└── dir3"#
227                );
228
229                config.filter_type = FilterType::Exclude;
231                config.filters = vec![
232                    Regex::new(r"dir1").unwrap(),
233                    Regex::new(r"dir2").unwrap(),
234                    Regex::new(r"dir3").unwrap(),
235                ];
236                let tree = DirTree::new(&config, &bump).unwrap();
237                let result = tree.print_tree(&no_color_print).unwrap();
238                assert_eq!(result, "");
239
240                config.filter_type = FilterType::Include;
242                config.filters = vec![Regex::new(r"dir1").unwrap(), Regex::new(r"file2").unwrap()];
243                let tree = DirTree::new(&config, &bump).unwrap();
244                let result = tree.print_tree(&no_color_print).unwrap();
245                assert_eq!(
246                    result,
247                    r#"├── dir1
248│   └── file1
249└── dir2
250    └── file2"#
251                );
252            },
253            {}
254        )
255    }
256
257    #[test]
258    fn test_symbolic_link() {
259        let no_color_print = TreePrintConfig {
260            dir_color: None,
261            file_color: None,
262            symbol_color: None,
263            tree_color: None,
264        };
265        let (_tmp_dir, root1) = gen_unique_temp_dir();
279        let (_tmp_dir, root2) = gen_unique_temp_dir();
280        let structure1: &[(Option<&str>, &[&str])] = &[(Some("dir1"), &["file1"][..])];
281        let structure2: &[(Option<&str>, &[&str])] =
282            &[(Some("dir3"), &["file2"][..]), (Some("dir4"), &[][..])];
283        create_dir_structure(&root1, structure1);
284        create_dir_structure(&root2, structure2);
285        create_symlink(&root2, &root1.join("dir2")).unwrap();
286
287        cleanup!(
288            {
289                let config = TreeConfig {
290                    root: &root1,
291                    target: "",
292                    filter_type: FilterType::Exclude,
293                    filters: Vec::new(),
294                };
295                let bump = Bump::new();
296                let tree = DirTree::new(&config, &bump).unwrap();
297                let result = tree.print_tree(&no_color_print).unwrap();
298                assert_eq!(
299                    result,
300                    format!(
301                        r#"├── dir1
302│   └── file1
303└── dir2 -> {}
304    ├── dir3
305    │   └── file2
306    └── dir4"#,
307                        root2.to_str().unwrap()
308                    )
309                );
310            },
311            {}
312        );
313
314        let (_tmp_dir, root1) = gen_unique_temp_dir();
330        let (_tmp_dir, root2) = gen_unique_temp_dir();
331        let structure1: &[(Option<&str>, &[&str])] = &[
332            (Some("dir1"), &["file1"][..]),
333            (Some("dir3"), &[][..]),
334            (Some("dir4"), &[][..]),
335            (None, &["file114514"][..]),
336        ];
337        let structure2: &[(Option<&str>, &[&str])] =
338            &[(Some("dir3"), &["file2"][..]), (Some("dir4"), &[][..])];
339        create_dir_structure(&root1, structure1);
340        create_dir_structure(&root2, structure2);
341        create_symlink(&root2, &root1.join("dir2")).unwrap();
342        cleanup!(
343            {
344                let config = TreeConfig {
345                    root: &root1,
346                    target: "",
347                    filter_type: FilterType::Exclude,
348                    filters: Vec::new(),
349                };
350                let bump = Bump::new();
351                let tree = DirTree::new(&config, &bump).unwrap();
352                let result = tree.print_tree(&no_color_print).unwrap();
353                assert_eq!(
354                    result,
355                    format!(
356                        r#"├── dir1
357│   └── file1
358├── dir2 -> {}
359│   ├── dir3
360│   │   └── file2
361│   └── dir4
362├── dir3
363├── dir4
364└── file114514"#,
365                        root2.to_str().unwrap()
366                    )
367                );
368            },
369            {}
370        )
371    }
372
373    #[test]
374    fn test_recursive_detection() {
375        let no_color_print = TreePrintConfig {
376            dir_color: None,
377            file_color: None,
378            symbol_color: None,
379            tree_color: None,
380        };
381
382        {
384            let (_tmp_dir, root) = gen_unique_temp_dir();
385            let structure: &[(Option<&str>, &[&str])] =
386                &[(Some("dir1"), &["file1.txt"][..]), (None, &["root_file.txt"][..])];
387            create_dir_structure(&root, structure);
388
389            cleanup!(
390                {
391                    create_symlink(&root.join("dir1"), &root.join("dir1/loop")).unwrap();
393
394                    let config = TreeConfig {
395                        root: &root,
396                        target: "",
397                        filter_type: FilterType::Exclude,
398                        filters: Vec::new(),
399                    };
400                    let bump = Bump::new();
401                    let tree = DirTree::new(&config, &bump).unwrap();
402                    let result = tree.print_tree(&no_color_print).unwrap();
403                    log_test!("{}", result);
404
405                    assert_eq!(
406                        result,
407                        format!(
408                            r#"├── dir1
409│   ├── file1.txt
410│   └── loop -> {} [recursive, not followed]
411└── root_file.txt"#,
412                            root.join("dir1").to_str().unwrap()
413                        )
414                    );
415                },
416                {}
417            );
418        }
419
420        {
422            let (_tmp_dir, root) = gen_unique_temp_dir();
423            let structure: &[(Option<&str>, &[&str])] = &[
424                (Some("dirA"), &["fileA.txt"][..]),
425                (Some("dirB"), &["fileB.txt"][..]),
426                (Some("dirC"), &["fileC.txt"][..]),
427            ];
428            create_dir_structure(&root, structure);
429
430            cleanup!(
431                {
432                    create_symlink(&root.join("dirB"), &root.join("dirA/linkB")).unwrap();
434                    create_symlink(&root.join("dirC"), &root.join("dirB/linkC")).unwrap();
435                    create_symlink(&root.join("dirA"), &root.join("dirC/linkA")).unwrap();
436
437                    let config = TreeConfig {
438                        root: &root,
439                        target: "",
440                        filter_type: FilterType::Exclude,
441                        filters: Vec::new(),
442                    };
443                    let bump = Bump::new();
444                    let tree = DirTree::new(&config, &bump).unwrap();
445                    let result = tree.print_tree(&no_color_print).unwrap();
446                    log_test!("{}", result);
447                    assert_eq!(
448                        result,
449                        format!(
450                            r#"├── dirA
451│   ├── fileA.txt
452│   └── linkB -> {}
453│       ├── fileB.txt
454│       └── linkC -> {}
455│           ├── fileC.txt
456│           └── linkA -> {} [recursive, not followed]
457├── dirB
458│   ├── fileB.txt
459│   └── linkC -> {} [recursive, not followed]
460└── dirC
461    ├── fileC.txt
462    └── linkA -> {} [recursive, not followed]"#,
463                            root.join("dirB").to_str().unwrap(),
464                            root.join("dirC").to_str().unwrap(),
465                            root.join("dirA").to_str().unwrap(),
466                            root.join("dirC").to_str().unwrap(),
467                            root.join("dirA").to_str().unwrap(),
468                        )
469                    );
470                },
471                {}
472            );
473        }
474    }
475}