2022-02-19 13:55:21 +00:00
|
|
|
use crate::Graph;
|
|
|
|
use std::{collections::HashSet, io, io::Write, str::FromStr};
|
|
|
|
|
2023-01-17 02:11:27 +00:00
|
|
|
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
|
2022-02-19 13:55:21 +00:00
|
|
|
pub enum Charset {
|
2023-01-17 02:11:27 +00:00
|
|
|
// when operating in a console on windows non-UTF-8 byte sequences are not supported on
|
|
|
|
// stdout, See also [`StdoutLock`]
|
|
|
|
#[cfg_attr(not(target_os = "windows"), default)]
|
2023-01-17 02:14:38 +00:00
|
|
|
Utf8,
|
|
|
|
#[cfg_attr(target_os = "windows", default)]
|
2022-02-19 13:55:21 +00:00
|
|
|
Ascii,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FromStr for Charset {
|
|
|
|
type Err = String;
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
match s {
|
|
|
|
"utf8" => Ok(Charset::Utf8),
|
|
|
|
"ascii" => Ok(Charset::Ascii),
|
2022-11-07 23:43:11 +00:00
|
|
|
s => Err(format!("invalid charset: {s}")),
|
2022-02-19 13:55:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Options to configure formatting
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
|
|
pub struct TreeOptions {
|
|
|
|
/// The style of characters to use.
|
|
|
|
pub charset: Charset,
|
|
|
|
/// If `true`, duplicate imports will be repeated.
|
|
|
|
/// If `false`, duplicates are suffixed with `(*)`, and their imports
|
|
|
|
/// won't be shown.
|
|
|
|
pub no_dedupe: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Internal helper type for symbols
|
|
|
|
struct Symbols {
|
|
|
|
down: &'static str,
|
|
|
|
tee: &'static str,
|
|
|
|
ell: &'static str,
|
|
|
|
right: &'static str,
|
|
|
|
}
|
|
|
|
|
|
|
|
static UTF8_SYMBOLS: Symbols = Symbols { down: "│", tee: "├", ell: "└", right: "─" };
|
|
|
|
|
|
|
|
static ASCII_SYMBOLS: Symbols = Symbols { down: "|", tee: "|", ell: "`", right: "-" };
|
|
|
|
|
|
|
|
pub fn print(graph: &Graph, opts: &TreeOptions, out: &mut dyn Write) -> io::Result<()> {
|
|
|
|
let symbols = match opts.charset {
|
|
|
|
Charset::Utf8 => &UTF8_SYMBOLS,
|
|
|
|
Charset::Ascii => &ASCII_SYMBOLS,
|
|
|
|
};
|
|
|
|
|
|
|
|
// used to determine whether to display `(*)`
|
|
|
|
let mut visited_imports = HashSet::new();
|
|
|
|
|
|
|
|
// A stack of bools used to determine where | symbols should appear
|
|
|
|
// when printing a line.
|
|
|
|
let mut levels_continue = Vec::new();
|
|
|
|
// used to detect dependency cycles when --no-dedupe is used.
|
|
|
|
// contains a `Node` for each level.
|
|
|
|
let mut write_stack = Vec::new();
|
|
|
|
|
|
|
|
for (node_index, _) in graph.input_nodes().enumerate() {
|
|
|
|
print_node(
|
|
|
|
graph,
|
|
|
|
node_index,
|
|
|
|
symbols,
|
|
|
|
opts.no_dedupe,
|
|
|
|
&mut visited_imports,
|
|
|
|
&mut levels_continue,
|
|
|
|
&mut write_stack,
|
|
|
|
out,
|
|
|
|
)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
fn print_node(
|
|
|
|
graph: &Graph,
|
|
|
|
node_index: usize,
|
|
|
|
symbols: &Symbols,
|
|
|
|
no_dedupe: bool,
|
|
|
|
visited_imports: &mut HashSet<usize>,
|
|
|
|
levels_continue: &mut Vec<bool>,
|
|
|
|
write_stack: &mut Vec<usize>,
|
|
|
|
out: &mut dyn Write,
|
|
|
|
) -> io::Result<()> {
|
|
|
|
let new_node = no_dedupe || visited_imports.insert(node_index);
|
|
|
|
|
|
|
|
if let Some((last_continues, rest)) = levels_continue.split_last() {
|
|
|
|
for continues in rest {
|
|
|
|
let c = if *continues { symbols.down } else { " " };
|
2022-11-07 23:43:11 +00:00
|
|
|
write!(out, "{c} ")?;
|
2022-02-19 13:55:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let c = if *last_continues { symbols.tee } else { symbols.ell };
|
|
|
|
write!(out, "{0}{1}{1} ", c, symbols.right)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
let in_cycle = write_stack.contains(&node_index);
|
|
|
|
// if this node does not have any outgoing edges, don't include the (*)
|
|
|
|
// since there isn't really anything "deduplicated", and it generally just
|
|
|
|
// adds noise.
|
|
|
|
let has_deps = graph.has_outgoing_edges(node_index);
|
|
|
|
let star = if (new_node && !in_cycle) || !has_deps { "" } else { " (*)" };
|
|
|
|
|
2022-11-07 23:43:11 +00:00
|
|
|
writeln!(out, "{}{star}", graph.display_node(node_index))?;
|
2022-02-19 13:55:21 +00:00
|
|
|
|
|
|
|
if !new_node || in_cycle {
|
|
|
|
return Ok(())
|
|
|
|
}
|
|
|
|
write_stack.push(node_index);
|
|
|
|
|
|
|
|
print_imports(
|
|
|
|
graph,
|
|
|
|
node_index,
|
|
|
|
symbols,
|
|
|
|
no_dedupe,
|
|
|
|
visited_imports,
|
|
|
|
levels_continue,
|
|
|
|
write_stack,
|
|
|
|
out,
|
|
|
|
)?;
|
|
|
|
|
|
|
|
write_stack.pop();
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Prints all the imports of a node
|
|
|
|
#[allow(clippy::too_many_arguments, clippy::ptr_arg)]
|
|
|
|
fn print_imports(
|
|
|
|
graph: &Graph,
|
|
|
|
node_index: usize,
|
|
|
|
symbols: &Symbols,
|
|
|
|
no_dedupe: bool,
|
|
|
|
visited_imports: &mut HashSet<usize>,
|
|
|
|
levels_continue: &mut Vec<bool>,
|
|
|
|
write_stack: &mut Vec<usize>,
|
|
|
|
out: &mut dyn Write,
|
|
|
|
) -> io::Result<()> {
|
|
|
|
let imports = graph.imported_nodes(node_index);
|
|
|
|
if imports.is_empty() {
|
|
|
|
return Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut iter = imports.iter().peekable();
|
|
|
|
|
|
|
|
while let Some(import) = iter.next() {
|
|
|
|
levels_continue.push(iter.peek().is_some());
|
|
|
|
print_node(
|
|
|
|
graph,
|
|
|
|
*import,
|
|
|
|
symbols,
|
|
|
|
no_dedupe,
|
|
|
|
visited_imports,
|
|
|
|
levels_continue,
|
|
|
|
write_stack,
|
|
|
|
out,
|
|
|
|
)?;
|
|
|
|
levels_continue.pop();
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|