use std::collections::BTreeMap;
use crate::check::{CheckReport, DiskMap};
pub struct TreeOpts {
pub ascii: bool,
pub depth: Option<usize>,
}
pub fn render(report: &CheckReport, opts: &TreeOpts) -> String {
let maps = &report.maps;
if maps.is_empty() {
return "no maps - run `radar map` first\n".to_string();
}
let mut kids: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for scope in maps.keys() {
if scope.is_empty() {
continue;
}
let parent = nearest_ancestor(maps, scope).unwrap_or("");
kids.entry(parent).or_default().push(scope);
}
let mut out = String::new();
let shared_fidelity = maps
.values()
.next()
.filter(|first| {
(opts.depth.is_none()
|| opts.depth.is_some_and(|depth| {
depth > 0 && kids.get("").is_some_and(|children| children.len() >= 2)
}))
&& maps.len() >= 3
&& maps.contains_key("")
&& maps.values().all(|map| map.fidelity == first.fidelity)
})
.map(|map| map.fidelity.as_str());
if let Some(fidelity) = shared_fidelity {
out.push_str(&format!("fidelity: {fidelity}\n"));
}
let name_width = maps
.keys()
.map(|s| display_name(s, maps).len() + s.matches('/').count() * 4)
.max()
.unwrap_or(8)
.max(8);
render_node(
report,
&kids,
"",
"",
true,
true,
opts,
shared_fidelity,
name_width,
0,
&mut out,
);
out
}
fn display_name<'a>(scope: &'a str, maps: &BTreeMap<String, DiskMap>) -> &'a str {
if scope.is_empty() {
return ".";
}
match nearest_ancestor(maps, scope) {
Some(p) if !p.is_empty() => scope
.strip_prefix(p)
.map_or(scope, |s| s.trim_start_matches('/')),
_ => scope,
}
}
fn nearest_ancestor<'m>(maps: &'m BTreeMap<String, DiskMap>, scope: &str) -> Option<&'m str> {
let mut cur = scope;
loop {
let parent = match cur.rfind('/') {
Some(i) => &cur[..i],
None if !cur.is_empty() => "",
None => return None,
};
if let Some((k, _)) = maps.get_key_value(parent) {
return Some(k.as_str());
}
if parent.is_empty() {
return None;
}
cur = parent;
}
}
#[allow(clippy::too_many_arguments)]
fn render_node(
report: &CheckReport,
kids: &BTreeMap<&str, Vec<&str>>,
scope: &str,
prefix: &str,
is_last: bool,
is_root: bool,
opts: &TreeOpts,
shared_fidelity: Option<&str>,
name_width: usize,
depth: usize,
out: &mut String,
) {
let Some(map) = report.maps.get(scope) else {
return;
};
let (tee, ell, pipe) = if opts.ascii {
("|-- ", "`-- ", "| ")
} else {
("├── ", "└── ", "│ ")
};
let connector = if is_root {
""
} else if is_last {
ell
} else {
tee
};
let name = display_name(scope, &report.maps);
let stale = report.stale.get(scope).copied().unwrap_or(false);
let marker = match (stale, opts.ascii) {
(true, false) => "✗ stale",
(true, true) => "x stale",
(false, false) => "✓",
(false, true) => "ok",
};
let tokens = map.tokens.as_deref().unwrap_or("?");
let label = format!("{prefix}{connector}{name}");
let pad = name_width.saturating_sub(label.chars().count()) + 2;
if shared_fidelity.is_some() {
out.push_str(&format!("{label}{:pad$}{:>7} {marker}\n", "", tokens));
} else {
out.push_str(&format!(
"{label}{:pad$}{:<8} {:>7} {marker}\n",
"", map.fidelity, tokens,
));
}
if opts.depth.is_some_and(|d| depth + 1 > d) {
return;
}
let children = kids.get(scope).cloned().unwrap_or_default();
let child_prefix = if is_root {
String::new()
} else if is_last {
format!("{prefix} ")
} else {
format!("{prefix}{pipe}")
};
for (i, child) in children.iter().enumerate() {
let last = i + 1 == children.len();
render_node(
report,
kids,
child,
&child_prefix,
last,
false,
opts,
shared_fidelity,
name_width,
depth + 1,
out,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn map(fidelity: &str) -> DiskMap {
DiskMap {
parent_link: None,
children_links: Vec::new(),
api_hash: None,
bytes_actual: 0,
tokens: Some("~10".to_string()),
fidelity: fidelity.to_string(),
slot_filled: true,
}
}
#[test]
fn shared_fidelity_requires_three_visible_uniform_rows() {
let mut report = CheckReport::default();
report.maps.insert(String::new(), map("syntax"));
report.maps.insert("a".to_string(), map("syntax"));
report.maps.insert("b".to_string(), map("syntax"));
report.stale.insert("a".to_string(), true);
let mut opts = TreeOpts {
ascii: true,
depth: None,
};
let compact = render(&report, &opts);
assert!(compact.starts_with("fidelity: syntax\n"));
assert_eq!(compact.matches("syntax").count(), 1);
assert!(compact.contains("|-- a"));
assert!(compact.contains("`-- b"));
assert!(compact.contains("x stale"));
opts.depth = Some(1);
let bounded = render(&report, &opts);
assert!(bounded.starts_with("fidelity: syntax\n"));
assert_eq!(bounded.matches("syntax").count(), 1);
opts.depth = Some(0);
assert!(!render(&report, &opts).starts_with("fidelity:"));
report.maps.remove("b");
opts.depth = Some(1);
assert!(!render(&report, &opts).starts_with("fidelity:"));
report.maps.insert("b".to_string(), map("lsp"));
opts.depth = None;
let mixed = render(&report, &opts);
assert!(!mixed.starts_with("fidelity:"));
assert!(mixed.contains("syntax"));
assert!(mixed.contains("lsp"));
report.maps.remove("");
assert_eq!(render(&report, &opts), "");
}
}