use crate::engine::Symbol;
use crate::ops::{CallSite, CallSource, FileMap, SourceResult};
pub fn outline(syms: &[Symbol]) -> String {
let mut out = String::new();
for s in syms {
let owner = s.parent.clone().unwrap_or_default();
out.push_str(&format!(
"{:<10} {:<26} {:<18} {}:{:<4} {}\n",
s.kind, s.name, owner, s.line, s.col, s.signature
));
}
out
}
pub fn symbols(syms: &[Symbol]) -> String {
let mut out = String::new();
for s in syms {
let mark = if s.is_definition { "def" } else { "ref" };
out.push_str(&format!("{:<3} {:<10} {:<28} {}\n", mark, s.kind, s.name, s.id));
}
out
}
pub fn source(res: &SourceResult) -> String {
format!("{}\n", res.source)
}
pub fn callers(sites: &[CallSite]) -> String {
let mut out = String::new();
for s in sites {
let inf = s.in_function.as_deref().unwrap_or("<top-level>");
let tag = if s.source == CallSource::Structural { "S" } else { "T" };
out.push_str(&format!(
"{}:{}:{} {:<28} [{}] {}\n",
s.file, s.line, s.col, inf, tag, s.text
));
}
out
}
pub fn definition(defs: &[Symbol]) -> String {
let mut out = String::new();
for s in defs {
let owner = s.parent.clone().unwrap_or_default();
let loc = format!("{}:{}:{}", s.file, s.line, s.col);
out.push_str(&format!(
"{:<10} {:<26} {:<18} {:<28} {}\n",
s.kind, s.name, owner, loc, s.signature
));
}
out
}
pub fn map(maps: &[FileMap]) -> String {
let mut out = String::new();
for fm in maps {
out.push_str(&format!("{}\n", fm.file));
for e in &fm.entries {
let parent = e.parent.as_deref().unwrap_or("");
if e.references.is_empty() {
out.push_str(&format!(
" {:<10} {:<26} {:<18} {:<4} {}\n",
e.kind, e.name, parent, e.row, e.signature
));
} else {
out.push_str(&format!(
" {:<10} {:<26} {:<18} {:<4} {} → {}\n",
e.kind, e.name, parent, e.row, e.signature, e.references.join(", ")
));
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ops::MapEntry;
fn sym(kind: &str, name: &str, parent: Option<&str>, is_def: bool) -> Symbol {
Symbol {
id: format!("rust:x.rs#{name}@1"),
name: name.into(),
kind: kind.into(),
is_definition: is_def,
file: "x.rs".into(),
line: 1,
col: 0,
start_byte: 0,
end_byte: 1,
signature: format!("fn {name}()"),
parent: parent.map(String::from),
}
}
#[test]
fn outline_and_symbols_have_one_line_per_symbol() {
let syms = vec![sym("function", "foo", None, true), sym("method", "bar", Some("Baz"), true)];
let o = outline(&syms);
assert_eq!(o.lines().count(), 2);
assert!(o.contains("function") && o.contains("foo") && o.contains("1:0"));
assert!(o.contains("Baz"), "owner rendered");
let s = symbols(&syms);
assert!(s.starts_with("def "), "definition mark: {s}");
assert!(s.contains("rust:x.rs#foo@1"), "stable id: {s}");
}
#[test]
fn map_appends_references_arrow_only_when_present() {
let maps = vec![FileMap {
file: "x.rs".into(),
entries: vec![
MapEntry { id: "i1".into(), kind: "function".into(), name: "a".into(),
parent: None, row: 1, signature: "fn a()".into(), references: vec![] },
MapEntry { id: "i2".into(), kind: "function".into(), name: "b".into(),
parent: None, row: 2, signature: "fn b()".into(), references: vec!["a".into()] },
],
}];
let m = map(&maps);
assert!(m.starts_with("x.rs\n"), "file header first: {m}");
assert!(!m.lines().nth(1).unwrap().contains('→'), "no arrow without refs");
assert!(m.lines().nth(2).unwrap().contains("→ a"), "arrow with refs");
}
}