1use crate::engine::Symbol;
10use crate::ops::{CallSite, CallSource, FileMap, SourceResult};
11
12pub fn outline(syms: &[Symbol]) -> String {
14 let mut out = String::new();
15 for s in syms {
16 let owner = s.parent.clone().unwrap_or_default();
17 out.push_str(&format!(
18 "{:<10} {:<26} {:<18} {}:{:<4} {}\n",
19 s.kind, s.name, owner, s.line, s.col, s.signature
20 ));
21 }
22 out
23}
24
25pub fn symbols(syms: &[Symbol]) -> String {
27 let mut out = String::new();
28 for s in syms {
29 let mark = if s.is_definition { "def" } else { "ref" };
30 out.push_str(&format!("{:<3} {:<10} {:<28} {}\n", mark, s.kind, s.name, s.id));
31 }
32 out
33}
34
35pub fn source(res: &SourceResult) -> String {
38 format!("{}\n", res.source)
39}
40
41pub fn callers(sites: &[CallSite]) -> String {
44 let mut out = String::new();
45 for s in sites {
46 let inf = s.in_function.as_deref().unwrap_or("<top-level>");
47 let tag = if s.source == CallSource::Structural { "S" } else { "T" };
48 out.push_str(&format!(
49 "{}:{}:{} {:<28} [{}] {}\n",
50 s.file, s.line, s.col, inf, tag, s.text
51 ));
52 }
53 out
54}
55
56pub fn definition(defs: &[Symbol]) -> String {
59 let mut out = String::new();
60 for s in defs {
61 let owner = s.parent.clone().unwrap_or_default();
62 let loc = format!("{}:{}:{}", s.file, s.line, s.col);
63 out.push_str(&format!(
64 "{:<10} {:<26} {:<18} {:<28} {}\n",
65 s.kind, s.name, owner, loc, s.signature
66 ));
67 }
68 out
69}
70
71pub fn map(maps: &[FileMap]) -> String {
73 let mut out = String::new();
74 for fm in maps {
75 out.push_str(&format!("{}\n", fm.file));
76 for e in &fm.entries {
77 let parent = e.parent.as_deref().unwrap_or("");
78 if e.references.is_empty() {
79 out.push_str(&format!(
80 " {:<10} {:<26} {:<18} {:<4} {}\n",
81 e.kind, e.name, parent, e.row, e.signature
82 ));
83 } else {
84 out.push_str(&format!(
85 " {:<10} {:<26} {:<18} {:<4} {} → {}\n",
86 e.kind, e.name, parent, e.row, e.signature, e.references.join(", ")
87 ));
88 }
89 }
90 }
91 out
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::ops::MapEntry;
98
99 fn sym(kind: &str, name: &str, parent: Option<&str>, is_def: bool) -> Symbol {
100 Symbol {
101 id: format!("rust:x.rs#{name}@1"),
102 name: name.into(),
103 kind: kind.into(),
104 is_definition: is_def,
105 file: "x.rs".into(),
106 line: 1,
107 col: 0,
108 start_byte: 0,
109 end_byte: 1,
110 signature: format!("fn {name}()"),
111 parent: parent.map(String::from),
112 }
113 }
114
115 #[test]
116 fn outline_and_symbols_have_one_line_per_symbol() {
117 let syms = vec![sym("function", "foo", None, true), sym("method", "bar", Some("Baz"), true)];
118 let o = outline(&syms);
119 assert_eq!(o.lines().count(), 2);
120 assert!(o.contains("function") && o.contains("foo") && o.contains("1:0"));
121 assert!(o.contains("Baz"), "owner rendered");
122
123 let s = symbols(&syms);
124 assert!(s.starts_with("def "), "definition mark: {s}");
125 assert!(s.contains("rust:x.rs#foo@1"), "stable id: {s}");
126 }
127
128 #[test]
129 fn map_appends_references_arrow_only_when_present() {
130 let maps = vec![FileMap {
131 file: "x.rs".into(),
132 entries: vec![
133 MapEntry { id: "i1".into(), kind: "function".into(), name: "a".into(),
134 parent: None, row: 1, signature: "fn a()".into(), references: vec![] },
135 MapEntry { id: "i2".into(), kind: "function".into(), name: "b".into(),
136 parent: None, row: 2, signature: "fn b()".into(), references: vec!["a".into()] },
137 ],
138 }];
139 let m = map(&maps);
140 assert!(m.starts_with("x.rs\n"), "file header first: {m}");
141 assert!(!m.lines().nth(1).unwrap().contains('→'), "no arrow without refs");
142 assert!(m.lines().nth(2).unwrap().contains("→ a"), "arrow with refs");
143 }
144}