use super::budget::Continuation;
use super::dependency::{CrateOutlineResult, SymbolCandidate};
use super::enums::ReferenceView;
use super::graph::{BlastRadiusResult, TraceCallHierarchyResult};
use super::inspect::InspectSymbolResult;
use super::navigation::{EnclosingScopeResult, FileOutlineResult, FindReferencesResult};
use super::search::SearchSymbolsResult;
fn header(
tool: &str,
subject: &str,
summary: &str,
next: &Continuation,
notes: &[String],
extra: &[String],
) -> String {
let mut parts = vec![format!("{tool} {subject}: {summary}")];
parts.extend(extra.iter().cloned());
if next.complete == Some(true) {
parts.push("complete".to_string());
}
if let Some(offset) = next.next_offset {
parts.push(format!("TRUNCATED: call again with offset={offset}"));
}
if let Some(state) = &next.index_state {
parts.push(state.clone());
}
parts.extend(
notes
.iter()
.filter(|n| n.starts_with("CAVEAT"))
.map(|n| n.to_string()),
);
let mut out = parts.join(" | ");
for note in notes.iter().filter(|n| !n.starts_with("CAVEAT")) {
out.push_str("\nnote: ");
out.push_str(note);
}
out
}
fn range(offset: usize, shown: usize, total: usize) -> String {
if shown == 0 {
return format!("0 of {total}");
}
format!("{}-{} of {total}", offset + 1, offset + shown)
}
fn definitions(defs: &[SymbolCandidate]) -> String {
defs.iter()
.map(|d| {
let usages = d.usages.map_or(String::new(), |n| format!(" {n} usages"));
format!("\n{} {} {}{usages}", d.qualified_name, d.kind, d.location)
})
.collect()
}
fn plural(n: usize, one: &str) -> String {
format!("{n} {one}{}", if n == 1 { "" } else { "s" })
}
pub fn find_references(r: &FindReferencesResult) -> String {
let summary = format!(
"{} in {}",
plural(r.total_references, "usage"),
plural(r.total_files, "file")
);
if r.ambiguous && r.files.is_empty() {
let head = header(
"find_references",
&r.symbol,
&format!(
"AMBIGUOUS, {} definitions are used ({summary})",
r.definition_count
),
&r.next,
&r.notes,
&[],
);
return format!("{head}{}", definitions(&r.definitions));
}
let (view, shown) = match r.view {
Some(ReferenceView::Callers) => ("callers", r.callers.len()),
Some(ReferenceView::Files) => ("files", r.files.len()),
_ => ("sites", r.references.len()),
};
let total = match r.view {
Some(ReferenceView::Callers) => r.total_callers.unwrap_or(shown),
Some(ReferenceView::Files) => r.total_files,
_ => r.total_references,
};
let mut extra = vec![format!("{view} {}", range(r.offset, shown, total))];
if r.name_matched > 0 {
extra.push(format!(
"{} matched by name only (may be another symbol of the same name)",
r.name_matched
));
}
let mut out = header(
"find_references",
&r.symbol,
&summary,
&r.next,
&r.notes,
&extra,
);
if r.ambiguous {
out.push_str(&definitions(&r.definitions));
}
match r.view {
Some(ReferenceView::Callers) => {
for c in &r.callers {
out.push_str(&format!(
"\n{} {}:{} x{} {}",
c.symbol,
c.file_path,
c.line_number,
c.count,
c.text.as_deref().unwrap_or("")
));
}
}
Some(ReferenceView::Files) => {
for f in &r.files {
out.push_str(&format!("\n{} {}", f.file_path, f.count));
}
}
_ => {
for i in &r.references {
let via = match i.via.as_deref() {
Some("exact") | None => String::new(),
Some(v) => format!(" [{v}]"),
};
out.push_str(&format!(
"\n{}:{} {} {}{via} {}",
i.file_path,
i.line_number,
i.enclosing_symbol.as_deref().unwrap_or("-"),
i.role,
i.text.as_deref().unwrap_or("")
));
}
}
}
out
}
pub fn search_symbols(r: &SearchSymbolsResult) -> String {
let subject = r
.query
.as_deref()
.map(|q| format!("\"{q}\""))
.or_else(|| r.like_symbol.as_deref().map(|s| format!("like {s}")))
.unwrap_or_default();
let mut extra = vec![range(r.offset, r.symbols.len(), r.total)];
if r.confident {
extra.push("confident".to_string());
}
let mut out = header(
"search_symbols",
&subject,
&format!("{} ({})", plural(r.total, "match"), r.scope_level),
&r.next,
&[],
&extra,
);
for s in &r.symbols {
let place = match (&s.crate_name, &s.version) {
(Some(c), Some(v)) => format!("{c} {v} {}", s.file_path),
(Some(c), None) => format!("{c} {}", s.file_path),
_ => s.file_path.clone(),
};
out.push_str(&format!(
"\n{} {} {place}:{}-{}",
s.qualified_name, s.kind, s.start_line, s.end_line
));
if let Some(sig) = &s.signature {
out.push_str(&format!(" {sig}"));
}
if let Some(doc) = &s.doc {
out.push_str(&format!(" // {doc}"));
}
if let Some(snippet) = &s.snippet {
for line in snippet.lines() {
out.push_str(&format!("\n | {line}"));
}
}
}
out
}
pub fn file_outline(r: &FileOutlineResult) -> String {
let mut extra = vec![range(r.offset, r.symbols.len(), r.total_symbols)];
if !r.shed.is_empty() {
extra.push(format!("shed: {}", r.shed.join(", ")));
}
let mut out = header(
"get_file_outline",
&r.file_path,
&plural(r.total_symbols, "declaration"),
&r.next,
&r.notes,
&extra,
);
for s in &r.symbols {
let what = if s.signature.is_empty() {
&s.qualified_name
} else {
&s.signature
};
out.push_str(&format!("\n{what} L{}-{}", s.start_line, s.end_line));
if let Some(doc) = &s.docstring {
out.push_str(&format!(" // {doc}"));
}
}
out
}
pub fn trace_call_hierarchy(r: &TraceCallHierarchyResult) -> String {
let mut extra = vec![
format!("depth {}", r.depth),
range(r.offset, r.nodes.len(), r.total_nodes),
];
if let Some(q) = &r.qualified_name {
extra.insert(0, q.clone());
}
let summary = if r.ambiguous {
format!(
"AMBIGUOUS, {} definitions share the name",
r.candidates.len()
)
} else {
format!("{} ({})", plural(r.total_nodes, "node"), r.direction)
};
let mut out = header(
"trace_call_hierarchy",
&r.symbol,
&summary,
&r.next,
&r.notes,
if r.ambiguous { &[] } else { &extra },
);
out.push_str(&definitions(&r.candidates));
for n in &r.nodes {
out.push_str(&format!(
"\n{}{} {} {} {}",
" ".repeat(n.depth.saturating_sub(1)),
n.qualified_name,
n.file_path,
n.edge_type,
n.signature
));
}
out
}
pub fn crate_outline(r: &CrateOutlineResult) -> String {
let version = r
.version
.as_deref()
.map_or(String::new(), |v| format!(" {v}"));
let mut summary = format!(
"{}, {}",
plural(r.modules.len(), "module"),
plural(r.core_types.len(), "core type")
);
if let Some(n) = r.omitted {
summary.push_str(&format!(
" | {n} omitted to fit the budget; pass max_tokens for more"
));
}
let head = header(
"get_crate_outline",
&format!("{}{version}", r.crate_name),
&summary,
&Continuation::default(),
&[],
&[],
);
format!("{head}\n{}", r.formatted_markdown)
}
fn single_header(
tool: &str,
subject: &str,
summary: &str,
index_state: Option<String>,
notes: &[String],
extra: &[String],
) -> String {
let next = Continuation {
index_state,
..Continuation::default()
};
header(tool, subject, summary, &next, notes, extra)
}
pub fn enclosing_scope(r: &EnclosingScopeResult, index_state: Option<String>) -> String {
let subject = if r.file_path.is_empty() {
"(several files)"
} else {
&r.file_path
};
if !r.scopes.is_empty() {
let lines: usize = r.scopes.iter().map(|g| g.lines.len()).sum();
let mut out = single_header(
"get_enclosing_scope",
subject,
&format!(
"{} in {}",
plural(lines, "line"),
plural(r.scopes.len(), "symbol")
),
index_state,
&r.notes,
&[],
);
for g in &r.scopes {
let file = g.file_path.as_deref().unwrap_or(&r.file_path);
match (&g.qualified_name, g.start_line, g.end_line) {
(Some(name), Some(a), Some(b)) => out.push_str(&format!(
"\n{file}:{a}-{b} {name} {}",
g.kind.as_deref().unwrap_or("")
)),
_ => out.push_str(&format!("\n{file} (no symbol contains these lines)")),
}
for line in &g.lines {
let block = line
.block
.as_deref()
.map_or(String::new(), |b| format!(" [in {b}]"));
out.push_str(&format!(
"\n L{}: {}{block}",
line.line,
line.text.as_deref().unwrap_or("")
));
}
}
return out;
}
if !r.found {
return single_header(
"get_enclosing_scope",
subject,
"no enclosing symbol",
index_state,
&r.notes,
&[],
);
}
let name = r
.qualified_name
.as_deref()
.or(r.name.as_deref())
.unwrap_or("?");
let span = match (r.start_line, r.end_line) {
(Some(a), Some(b)) => format!(" L{a}-{b}"),
_ => String::new(),
};
let mut out = single_header(
"get_enclosing_scope",
subject,
&format!("{name} ({}){span}", r.kind.as_deref().unwrap_or("?")),
index_state,
&r.notes,
&[],
);
if let Some(sig) = &r.signature {
out.push_str(&format!("\nsignature: {sig}"));
}
if let Some(doc) = &r.doc {
out.push_str(&format!("\ndoc: {doc}"));
}
if let Some(scope) = &r.innermost_scope {
out.push_str(&format!(
"\ninnermost: {} {} L{}-{}",
scope.scope_kind,
scope.name.as_deref().unwrap_or(""),
scope.start_line,
scope.end_line
));
}
if let Some(snippet) = &r.snippet {
let first = r.snippet_start_line.unwrap_or(1);
for (i, line) in snippet.lines().enumerate() {
out.push_str(&format!("\n{:>5} {line}", first as usize + i));
}
if r.snippet_truncated {
out.push_str("\n ... (snippet shortened to fit the budget)");
}
}
out
}
pub fn blast_radius(r: &BlastRadiusResult, index_state: Option<String>) -> String {
let mut extra = Vec::new();
if r.truncated {
extra.push("TRUNCATED: raise `limit` for the rest".to_string());
}
if r.ambiguous {
let mut out = single_header(
"calculate_blast_radius",
&r.target,
&format!(
"AMBIGUOUS, {} definitions share the name",
r.candidates.len()
),
index_state,
&r.notes,
&[],
);
out.push_str(&definitions(&r.candidates));
return out;
}
let mut out = single_header(
"calculate_blast_radius",
&r.target,
&format!(
"{} in {}",
plural(r.affected_symbols_count, "affected symbol"),
plural(r.affected_files_count, "file")
),
index_state,
&r.notes,
&extra,
);
for file in &r.downstream_files {
out.push_str(&format!("\n{file}"));
}
if !r.affected_symbols.is_empty() {
out.push_str(&format!("\nsymbols: {}", r.affected_symbols.join(", ")));
}
out
}
pub fn inspect_symbol(r: &InspectSymbolResult, index_state: Option<String>) -> String {
if r.ambiguous {
let mut out = single_header(
"inspect_symbol",
&r.symbol,
&format!(
"AMBIGUOUS, {} definitions share the name",
r.candidate_count.unwrap_or(r.candidates.len())
),
index_state,
&r.notes,
&[],
);
out.push_str(&definitions(&r.candidates));
return out;
}
if !r.found {
let mut out = single_header(
"inspect_symbol",
&r.symbol,
"not found",
index_state,
&r.notes,
&[],
);
for hint in &r.see_also {
out.push_str(&format!("\nsee: {hint}"));
}
return out;
}
let location = match (&r.file_path, r.start_line, r.end_line) {
(Some(f), Some(a), Some(b)) => format!(" {f}:{a}-{b}"),
(Some(f), _, _) => format!(" {f}"),
_ => String::new(),
};
let mut out = single_header(
"inspect_symbol",
&r.symbol,
&format!(
"{} {}{location}",
r.qualified_name.as_deref().unwrap_or(&r.symbol),
r.kind.as_deref().unwrap_or("")
),
index_state,
&r.notes,
&r.reference_count
.map(|n| vec![plural(n, "reference")])
.unwrap_or_default(),
);
if let Some(sig) = &r.signature {
out.push_str(&format!("\nsignature: {sig}"));
}
if let Some(doc) = &r.docstring {
out.push_str(&format!("\ndoc: {doc}"));
}
if let Some(snippet) = &r.snippet {
let first = r.start_line.unwrap_or(1);
for (i, line) in snippet.lines().enumerate() {
out.push_str(&format!("\n{:>5} {line}", first as usize + i));
}
if let Some(rest) = &r.snippet_remaining {
out.push_str(&format!(
"\n ... rest: {}:{}-{}",
rest.file_path, rest.start_line, rest.end_line
));
}
}
for (label, items, count) in [
("callers", &r.callers, r.caller_count),
("callees", &r.callees, r.callee_count),
] {
if let Some(items) = items {
out.push_str(&format!("\n{label} ({}):", count.unwrap_or(items.len())));
for item in items {
out.push_str(&format!("\n {item}"));
}
}
}
if let Some(members) = &r.members {
let total = r.member_count.unwrap_or(members.len());
let count = if total > members.len() {
format!(
"{} of {total} shown, more in get_type_contract",
members.len()
)
} else {
total.to_string()
};
out.push_str(&format!("\nmembers ({count}): {}", members.join(", ")));
}
if let Some(invariants) = &r.invariants {
for inv in invariants {
let number = inv
.adr_number
.map_or(String::new(), |n| format!("ADR {n}: "));
out.push_str(&format!(
"\ninvariant: {number}{} ({})",
inv.title, inv.reason
));
}
}
for hint in &r.see_also {
out.push_str(&format!("\nsee: {hint}"));
}
out
}