use std::collections::HashMap;
use crate::commands::entrypoints::EntrypointInfo;
use crate::commands::flow::FlowPath;
use crate::commands::map::{CoreSymbol, DirStats, MapStats};
use crate::core::graph::{
CallerInfo, ClassRelationships, Dependency, ImpactResult, Reference, Symbol, TraceResult,
};
use crate::core::searcher::SearchResult;
pub const SEPARATOR: &str =
"──────────────────────────────────────────────────────────────────────────────";
pub fn normalize_path(path: &str) -> String {
path.replace('\\', "/")
}
pub fn format_line_range(start: u32, end: u32) -> String {
if start == end {
format!("{start}")
} else {
format!("{start}-{end}")
}
}
fn print_header(symbol: &Symbol) {
let path = normalize_path(&symbol.file_path);
let line_range = format_line_range(symbol.line_start, symbol.line_end);
println!(
"{:<50}{} {}:{}",
symbol.name, symbol.kind, path, line_range
);
println!("{SEPARATOR}");
}
pub fn print_class_sketch(
symbol: &Symbol,
methods: &[Symbol],
caller_counts: &HashMap<String, usize>,
relationships: &ClassRelationships,
limit: usize,
show_docs: bool,
) {
print_header(symbol);
if !relationships.dependencies.is_empty() {
println!("deps: {}", relationships.dependencies.join(", "));
}
if !relationships.extends.is_empty() {
println!("extends: {}", relationships.extends.join(", "));
}
if !relationships.implements.is_empty() {
println!("implements: {}", relationships.implements.join(", "));
}
let (method_syms, field_syms): (Vec<&Symbol>, Vec<&Symbol>) = methods
.iter()
.partition(|m| m.kind == "method" || m.kind == "function");
if !method_syms.is_empty() {
println!();
println!("methods:");
let display_methods = if method_syms.len() > limit {
&method_syms[..limit]
} else {
&method_syms
};
for method in display_methods {
if show_docs {
if let Some(ref doc) = method.docstring {
let first_line = doc.lines().next().unwrap_or("").trim();
let clean = first_line
.trim_start_matches("///")
.trim_start_matches("//")
.trim_start_matches("/**")
.trim_start_matches("*")
.trim_start_matches("*/")
.trim();
if !clean.is_empty() {
println!(" /// {clean}");
}
}
}
let sig = method_display_line(method);
let count = caller_counts.get(&method.id).copied().unwrap_or(0);
let count_label = if count > 0 {
format!("[{count} caller{}]", if count == 1 { "" } else { "s" })
} else {
"[internal]".to_string()
};
let padding = SEPARATOR
.chars()
.count()
.saturating_sub(2 + sig.chars().count() + count_label.chars().count());
println!(
" {sig}{:>width$}",
count_label,
width = padding + count_label.len()
);
}
if method_syms.len() > limit {
println!(
" ... {} more (use --limit to show more)",
method_syms.len() - limit
);
}
}
let field_syms: Vec<&Symbol> = field_syms
.into_iter()
.filter(|s| s.kind == "property")
.collect();
if !field_syms.is_empty() {
println!();
println!("fields:");
for field in &field_syms {
let sig = field.signature.as_deref().unwrap_or(&field.name);
println!(" {sig}");
}
}
}
pub fn print_method_sketch(
symbol: &Symbol,
outgoing_calls: &[String],
incoming_callers: &[CallerInfo],
) {
print_header(symbol);
let enrichment = extract_enrichment_prefix(&symbol.language, &symbol.metadata);
if !enrichment.is_empty() {
println!("{enrichment}");
}
let modifiers = extract_modifiers(&symbol.metadata);
if !modifiers.is_empty() {
println!("{}", modifiers.join(" "));
}
if let Some(sig) = &symbol.signature {
println!("signature: {sig}");
}
if !outgoing_calls.is_empty() {
println!("calls: {}", outgoing_calls.join(", "));
}
if !incoming_callers.is_empty() {
let caller_parts: Vec<String> = incoming_callers
.iter()
.map(|c| {
if c.count > 1 {
format!("{} [x{}]", c.name, c.count)
} else {
c.name.clone()
}
})
.collect();
println!("called by: {}", caller_parts.join(", "));
}
}
pub fn print_interface_sketch(
symbol: &Symbol,
methods: &[Symbol],
implementors: &[String],
limit: usize,
) {
print_header(symbol);
if !implementors.is_empty() {
println!("implemented by: {}", implementors.join(", "));
}
if !methods.is_empty() {
println!();
println!("methods:");
let display_methods = if methods.len() > limit {
&methods[..limit]
} else {
methods
};
for method in display_methods {
let sig = method_display_line(method);
println!(" {sig}");
}
if methods.len() > limit {
println!(
" ... {} more (use --limit to show more)",
methods.len() - limit
);
}
}
}
pub fn print_file_sketch(
file_path: &str,
symbols: &[Symbol],
caller_counts: &HashMap<String, usize>,
) {
let path = normalize_path(file_path);
println!("{path}");
println!("{SEPARATOR}");
for sym in symbols {
let line_range = format_line_range(sym.line_start, sym.line_end);
let count = caller_counts.get(&sym.id).copied().unwrap_or(0);
let count_label = if count > 0 {
format!("[{count} caller{}]", if count == 1 { "" } else { "s" })
} else {
"[internal]".to_string()
};
println!(
" {:<24}{:<10}{:<9}{}",
sym.name, sym.kind, line_range, count_label
);
}
}
pub fn print_enum_sketch(symbol: &Symbol, variants: &[&Symbol], caller_count: usize) {
print_header(symbol);
if !variants.is_empty() {
println!("variants:");
for v in variants {
let display = v.signature.as_deref().unwrap_or(&v.name);
println!(" {display}");
}
}
println!();
if caller_count > 0 {
println!(
"[{caller_count} caller{}]",
if caller_count == 1 { "" } else { "s" }
);
} else {
println!("[internal]");
}
}
pub fn print_generic_sketch(symbol: &Symbol) {
print_header(symbol);
if let Some(sig) = &symbol.signature {
println!("signature: {sig}");
}
}
fn method_display_line(method: &Symbol) -> String {
let sig = method.signature.as_deref().unwrap_or(&method.name);
let modifiers = extract_modifiers(&method.metadata);
let base = if modifiers.is_empty() {
sig.to_string()
} else {
let missing: Vec<&str> = modifiers
.iter()
.filter(|m| !sig.contains(m.as_str()))
.map(|m| m.as_str())
.collect();
if missing.is_empty() {
sig.to_string()
} else {
format!("{} {}", missing.join(" "), sig)
}
};
let enrichment = extract_enrichment_prefix(&method.language, &method.metadata);
if enrichment.is_empty() {
base
} else {
format!("{enrichment} {base}")
}
}
fn extract_enrichment_prefix(language: &str, metadata_json: &str) -> String {
let parsed: serde_json::Value = match serde_json::from_str(metadata_json) {
Ok(v) => v,
Err(_) => return String::new(),
};
match language {
"java" => {
if let Some(annotations) = parsed.get("annotations").and_then(|v| v.as_array()) {
let prefixes: Vec<String> = annotations
.iter()
.filter_map(|a| a.as_str())
.filter(|a| !a.is_empty())
.map(|a| format!("@{a}"))
.collect();
if !prefixes.is_empty() {
return prefixes.join(" ");
}
}
String::new()
}
"python" => {
if let Some(decorators) = parsed.get("decorators").and_then(|v| v.as_array()) {
let prefixes: Vec<String> = decorators
.iter()
.filter_map(|d| d.as_str())
.filter(|d| !d.is_empty())
.map(|d| format!("@{d}"))
.collect();
if !prefixes.is_empty() {
return prefixes.join(" ");
}
}
String::new()
}
"go" => {
if let Some(receiver) = parsed.get("receiver").and_then(|v| v.as_str()) {
if !receiver.is_empty() {
let is_pointer = parsed
.get("is_pointer_receiver")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let var_name = receiver
.chars()
.next()
.map(|c| c.to_lowercase().to_string())
.unwrap_or_default();
let type_display = if is_pointer {
format!("*{receiver}")
} else {
receiver.to_string()
};
return format!("({var_name} {type_display})");
}
}
String::new()
}
_ => String::new(),
}
}
fn extract_modifiers(metadata_json: &str) -> Vec<String> {
let parsed: serde_json::Value = match serde_json::from_str(metadata_json) {
Ok(v) => v,
Err(_) => return Vec::new(),
};
let mut mods = Vec::new();
if let Some(access) = parsed.get("access").and_then(|v| v.as_str()) {
match access {
"private" | "protected" | "internal" | "protected internal" => {
mods.push(access.to_string());
}
_ => {} }
}
if parsed
.get("is_async")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
mods.push("async".to_string());
}
if parsed
.get("is_static")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
mods.push("static".to_string());
}
if parsed
.get("is_abstract")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
mods.push("abstract".to_string());
}
if parsed
.get("is_virtual")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
mods.push("virtual".to_string());
}
if parsed
.get("is_override")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
mods.push("override".to_string());
}
mods
}
pub fn print_refs(symbol_name: &str, refs: &[Reference], total: usize) {
println!(
"{} \u{2014} {} reference{}",
symbol_name,
total,
if total == 1 { "" } else { "s" }
);
println!("{SEPARATOR}");
for r in refs {
let path = normalize_path(&r.file_path);
let location = if let Some(line) = r.line {
format!("{path}:{line}")
} else {
path
};
let display_text = r.snippet_line.as_deref().unwrap_or(&r.context);
let truncated_text = truncate_str(display_text.trim(), 80);
println!("{:<40}{}", location, truncated_text);
if let Some(ref snippet) = r.snippet {
print_snippet_context(snippet, r.line);
}
}
if refs.len() < total {
println!("... {} more (use --limit to show more)", total - refs.len());
}
}
pub fn print_refs_grouped(symbol_name: &str, groups: &[(String, Vec<Reference>)], total: usize) {
println!(
"{} \u{2014} {} reference{}",
symbol_name,
total,
if total == 1 { "" } else { "s" }
);
println!("{SEPARATOR}");
let mut shown = 0;
for (kind, refs) in groups {
let kind_label = humanize_edge_kind(kind);
println!("{kind_label} ({}):", refs.len());
for r in refs {
let path = normalize_path(&r.file_path);
let location = if let Some(line) = r.line {
format!("{path}:{line}")
} else {
path
};
let display_text = r.snippet_line.as_deref().unwrap_or(&r.context);
let truncated_text = truncate_str(display_text.trim(), 80);
println!(" {:<38}{}", location, truncated_text);
if let Some(ref snippet) = r.snippet {
print_snippet_context(snippet, r.line);
}
}
shown += refs.len();
println!();
}
if shown < total {
println!("... {} more (use --limit to show more)", total - shown);
}
}
pub fn print_file_refs(file_path: &str, refs: &[Reference], total: usize) {
let path = normalize_path(file_path);
println!(
"{} \u{2014} {} reference{}",
path,
total,
if total == 1 { "" } else { "s" }
);
println!("{SEPARATOR}");
for r in refs {
let rpath = normalize_path(&r.file_path);
let location = if let Some(line) = r.line {
format!("{rpath}:{line}")
} else {
rpath
};
let display_text = r.snippet_line.as_deref().unwrap_or(&r.context);
let truncated_text = truncate_str(display_text.trim(), 80);
println!("{:<40}{}", location, truncated_text);
if let Some(ref snippet) = r.snippet {
print_snippet_context(snippet, r.line);
}
}
if refs.len() < total {
println!("... {} more (use --limit to show more)", total - refs.len());
}
}
pub fn print_deps(symbol_name: &str, deps: &[Dependency], max_depth: usize) {
let depth_label = if max_depth <= 1 {
"direct dependencies".to_string()
} else {
format!("transitive dependencies (depth {max_depth})")
};
println!("{} \u{2014} {}", symbol_name, depth_label);
println!("{SEPARATOR}");
if deps.is_empty() {
println!("(no dependencies found)");
return;
}
let mut groups: Vec<(String, Vec<&Dependency>)> = Vec::new();
for dep in deps {
if let Some(group) = groups.iter_mut().find(|(k, _)| *k == dep.kind) {
group.1.push(dep);
} else {
let kind = dep.kind.clone();
groups.push((kind, vec![dep]));
}
}
for (kind, group_deps) in &groups {
let all_external = group_deps.iter().all(|d| d.is_external);
let kind_label = if all_external {
format!("{kind} (external):")
} else {
format!("{kind}:")
};
println!("{kind_label}");
for dep in group_deps {
if dep.is_external {
println!(" {:<24}(external)", dep.name);
} else if let Some(fp) = &dep.file_path {
let path = normalize_path(fp);
println!(" {:<24}{}", dep.name, path);
} else {
println!(" {}", dep.name);
}
}
println!();
}
}
pub fn print_file_deps(file_path: &str, deps: &[Dependency], max_depth: usize) {
let path = normalize_path(file_path);
let depth_label = if max_depth <= 1 {
"direct dependencies".to_string()
} else {
format!("transitive dependencies (depth {max_depth})")
};
println!("{} \u{2014} {}", path, depth_label);
println!("{SEPARATOR}");
if deps.is_empty() {
println!("(no dependencies found)");
return;
}
let mut groups: Vec<(String, Vec<&Dependency>)> = Vec::new();
for dep in deps {
if let Some(group) = groups.iter_mut().find(|(k, _)| *k == dep.kind) {
group.1.push(dep);
} else {
let kind = dep.kind.clone();
groups.push((kind, vec![dep]));
}
}
for (kind, group_deps) in &groups {
let all_external = group_deps.iter().all(|d| d.is_external);
let kind_label = if all_external {
format!("{kind} (external):")
} else {
format!("{kind}:")
};
println!("{kind_label}");
for dep in group_deps {
if dep.is_external {
println!(" {:<24}(external)", dep.name);
} else if let Some(fp) = &dep.file_path {
let fpath = normalize_path(fp);
println!(" {:<24}{}", dep.name, fpath);
} else {
println!(" {}", dep.name);
}
}
println!();
}
}
pub fn print_impact(symbol_name: &str, result: &ImpactResult) {
println!("Impact analysis: {symbol_name}");
println!("{SEPARATOR}");
if result.nodes_by_depth.is_empty() && result.test_files.is_empty() {
println!("(no impact detected)");
return;
}
for (depth, nodes) in &result.nodes_by_depth {
let depth_label = impact_depth_label(*depth);
println!("{depth_label} ({}):", nodes.len());
let max_display = 10;
let display_nodes = if nodes.len() > max_display {
&nodes[..max_display]
} else {
nodes
};
for node in display_nodes {
let path = normalize_path(&node.file_path);
println!(" {:<40}{}", node.name, path);
}
if nodes.len() > max_display {
println!(" ... ({} more)", nodes.len() - max_display);
}
println!();
}
if !result.test_files.is_empty() {
println!("Test files affected: {}", result.test_files.len());
let max_display = 10;
let display_tests = if result.test_files.len() > max_display {
&result.test_files[..max_display]
} else {
&result.test_files
};
for node in display_tests {
let path = normalize_path(&node.file_path);
println!(" {path}");
}
if result.test_files.len() > max_display {
println!(" ... ({} more)", result.test_files.len() - max_display);
}
}
}
pub fn print_trace(symbol_name: &str, result: &TraceResult, total: usize, truncated: bool) {
let path_count = result.paths.len();
let path_word = if path_count == 1 { "path" } else { "paths" };
let display_count = if truncated { total } else { path_count };
println!(
"{} \u{2014} {} entry {}",
symbol_name, display_count, path_word
);
println!("{SEPARATOR}");
if result.paths.is_empty() {
println!("(no entry paths found)");
return;
}
for (i, call_path) in result.paths.iter().enumerate() {
if call_path.steps.is_empty() {
continue;
}
let entry = &call_path.steps[0];
let entry_name = entry.symbol_name.clone();
println!("Path {}: {}", i + 1, entry_name);
for (step_idx, step) in call_path.steps.iter().enumerate().skip(1) {
let indent = " ".repeat(step_idx);
let step_name = step.symbol_name.clone();
let path = normalize_path(&step.file_path);
let location = format!("{path}:{}", step.line);
println!(
"{indent}\u{2514}\u{2500}\u{2192} {:<40}{}",
step_name, location
);
}
if i < path_count - 1 {
println!();
}
}
if truncated {
println!(
"... {} more paths (use --limit to show more)",
total - path_count
);
}
}
pub fn print_flow(start: &str, end: &str, paths: &[FlowPath], total: usize, depth_limit: usize) {
if paths.is_empty() {
println!(
"No path found from {} to {} within depth {}.",
start, end, depth_limit
);
return;
}
for (i, path) in paths.iter().enumerate() {
let names: Vec<&str> = path.steps.iter().map(|s| s.name.as_str()).collect();
println!("{}", names.join(" \u{2192} "));
let locations: Vec<String> = path
.steps
.iter()
.map(|s| format!("{}:{}", normalize_path(&s.file_path), s.line_start))
.collect();
println!(" {}", locations.join(" \u{2192} "));
if i < paths.len() - 1 {
println!();
}
}
let path_word = if total == 1 { "path" } else { "paths" };
println!(
"\n\u{2500} {} {} found (depth limit: {})",
total, path_word, depth_limit
);
}
pub fn print_entrypoints(
groups: &[(String, Vec<EntrypointInfo>)],
total: usize,
file_count: usize,
) {
let file_word = if file_count == 1 { "file" } else { "files" };
println!(
"Entrypoints \u{2014} {} across {} {}",
total, file_count, file_word
);
println!("{SEPARATOR}");
if groups.is_empty() {
println!("(no entry points found)");
return;
}
for (i, (group_name, entries)) in groups.iter().enumerate() {
println!("{group_name}:");
let max_name_len = entries
.iter()
.map(|e| e.name.chars().count())
.max()
.unwrap_or(0);
let name_width = max_name_len.max(20) + 2;
for entry in entries {
let path = normalize_path(&entry.file_path);
let suffix = if entry.method_count > 0 {
format!(
" \u{2192} {} method{}",
entry.method_count,
if entry.method_count == 1 { "" } else { "s" }
)
} else {
String::new()
};
println!(
" {:<width$}{}{}",
entry.name,
path,
suffix,
width = name_width
);
}
if i < groups.len() - 1 {
println!();
}
}
}
pub fn print_map(
project_name: &str,
stats: &MapStats,
entrypoints: &[(String, Vec<EntrypointInfo>)],
core_symbols: &[CoreSymbol],
directories: &[DirStats],
) {
println!(
"{} \u{2014} {} files, {} symbols, {} edges",
project_name,
format_number(stats.file_count),
format_number(stats.symbol_count),
format_number(stats.edge_count),
);
println!("{SEPARATOR}");
if !stats.languages.is_empty() {
println!("Languages: {}", stats.languages.join(", "));
}
let mut ep_count = 0usize;
let mut ep_lines: Vec<String> = Vec::new();
for (_group_name, entries) in entrypoints {
for entry in entries {
let path = normalize_path(&entry.file_path);
let dir = if let Some(pos) = path.rfind('/') {
format!("{}/", &path[..pos])
} else {
String::new()
};
let display_dir = dir.strip_prefix("src/").unwrap_or(&dir).to_string();
let suffix = if entry.method_count > 0 {
format!(
" \u{2192} {} method{}",
entry.method_count,
if entry.method_count == 1 { "" } else { "s" }
)
} else {
String::new()
};
ep_lines.push(format!(" {:<32}{:<32}{}", entry.name, display_dir, suffix));
ep_count += 1;
}
}
if !ep_lines.is_empty() {
println!();
println!("Entry points:");
let max_display = 8;
for line in ep_lines.iter().take(max_display) {
println!("{line}");
}
if ep_count > max_display {
println!(" ... {} more", ep_count - max_display);
}
}
if !core_symbols.is_empty() {
println!();
println!("Core symbols (by caller count):");
for sym in core_symbols {
let path = normalize_path(&sym.file_path);
let display_path = path.strip_prefix("src/").unwrap_or(&path).to_string();
let caller_label = format!(
"{} caller{}",
sym.caller_count,
if sym.caller_count == 1 { "" } else { "s" }
);
println!(" {:<32}{:<14}{}", sym.name, caller_label, display_path);
}
}
if !directories.is_empty() {
println!();
println!("Architecture:");
for dir in directories {
let file_label = format!(
"{} file{}",
dir.file_count,
if dir.file_count == 1 { "" } else { "s" }
);
let sym_label = format!(
"{} symbol{}",
dir.symbol_count,
if dir.symbol_count == 1 { "" } else { "s" }
);
println!(" {:<24}{:<14}{}", dir.directory, file_label, sym_label);
}
}
}
fn impact_depth_label(depth: usize) -> &'static str {
match depth {
1 => "Direct callers",
2 => "Second-degree",
3 => "Third-degree",
_ => "Further impact",
}
}
pub fn print_incremental_result(
modified: &[String],
added: &[String],
deleted: &[String],
duration_secs: f64,
) {
let total = modified.len() + added.len() + deleted.len();
eprintln!(
"{} file{} changed. Re-indexing...",
total,
if total == 1 { "" } else { "s" }
);
for path in modified {
eprintln!(" Modified: {}", normalize_path(path));
}
for path in added {
eprintln!(" Added: {}", normalize_path(path));
}
for path in deleted {
eprintln!(" Deleted: {}", normalize_path(path));
}
eprintln!("Updated in {duration_secs:.1}s.");
}
pub fn print_find_results(query: &str, results: &[SearchResult]) {
println!("Results for: \"{query}\"");
println!("{SEPARATOR}");
if results.is_empty() {
println!("(no results found)");
return;
}
for result in results {
let path = normalize_path(&result.file_path);
let location = format!("{path}:{}", result.line_start);
println!(
"{:.2} {:<40}{:<36} {}",
result.score, result.name, location, result.kind
);
}
}
pub fn print_status(
status_label: &str,
symbol_count: usize,
file_count: usize,
edge_count: usize,
last_indexed: Option<&str>,
) {
println!("Index status: {status_label}");
println!(" Symbols: {}", format_number(symbol_count));
println!(" Files: {}", format_number(file_count));
println!(" Edges: {}", format_number(edge_count));
if let Some(relative) = last_indexed {
println!(" Last index: {relative}");
} else {
println!(" Last index: never");
}
}
fn format_number(n: usize) -> String {
let s = n.to_string();
let bytes = s.as_bytes();
let len = bytes.len();
if len <= 3 {
return s;
}
let mut result = String::with_capacity(len + len / 3);
for (i, ch) in s.chars().enumerate() {
if i > 0 && (len - i).is_multiple_of(3) {
result.push(',');
}
result.push(ch);
}
result
}
fn truncate_str(s: &str, max_chars: usize) -> String {
if s.chars().count() <= max_chars {
s.to_string()
} else {
let truncated: String = s.chars().take(max_chars.saturating_sub(3)).collect();
format!("{truncated}...")
}
}
fn print_snippet_context(snippet: &[String], ref_line: Option<i64>) {
let Some(line_num) = ref_line else { return };
let ref_idx_in_snippet = snippet.len() / 2; let start_line = (line_num as usize).saturating_sub(ref_idx_in_snippet);
for (i, code) in snippet.iter().enumerate() {
let current_line = start_line + i;
let marker = if current_line == line_num as usize {
">"
} else {
" "
};
println!(" {marker} {current_line:>4} | {code}");
}
}
pub fn print_workspace_list(
workspace_name: &str,
members: &[crate::commands::workspace::MemberStatus],
) {
println!("Workspace: {workspace_name}");
println!("{SEPARATOR}");
if members.is_empty() {
println!(" (no members)");
return;
}
let max_name = members
.iter()
.map(|m| m.name.len())
.max()
.unwrap_or(4)
.max(4);
let max_path = members
.iter()
.map(|m| m.path.len())
.max()
.unwrap_or(4)
.max(4);
println!(
" {:<name_w$} {:<path_w$} {:<15} {:>5} {:>7}",
"Name",
"Path",
"Status",
"Files",
"Symbols",
name_w = max_name,
path_w = max_path,
);
for member in members {
println!(
" {:<name_w$} {:<path_w$} {:<15} {:>5} {:>7}",
member.name,
normalize_path(&member.path),
member.status,
if member.file_count > 0 {
format_number(member.file_count)
} else {
"─".to_string()
},
if member.symbol_count > 0 {
format_number(member.symbol_count)
} else {
"─".to_string()
},
name_w = max_name,
path_w = max_path,
);
}
}
fn humanize_edge_kind(kind: &str) -> &str {
match kind {
"instantiates" => "instantiated",
"extends" => "extended",
"implements" => "implemented",
"references_type" => "used as type",
"imports" => "imported",
"calls" => "called",
"references" => "referenced",
_ => kind,
}
}
pub fn print_workspace_status(
workspace_name: &str,
members: &[crate::commands::status::MemberStatusData],
total_symbols: usize,
total_files: usize,
total_edges: usize,
) {
println!("Workspace: {workspace_name}");
println!("{SEPARATOR}");
for m in members {
let status_label = if m.status.index_exists {
if m.status.symbol_count == 0 {
"empty"
} else {
"indexed"
}
} else {
"not indexed"
};
let last = m.status.last_indexed_relative.as_deref().unwrap_or("never");
println!(
" {:<16}{:<14}{:>6} files {:>7} symbols {:>7} edges {}",
m.name,
status_label,
format_number(m.status.file_count),
format_number(m.status.symbol_count),
format_number(m.status.edge_count),
last,
);
}
println!("{SEPARATOR}");
println!(
" {:<16}{:<14}{:>6} files {:>7} symbols {:>7} edges",
"Total",
"",
format_number(total_files),
format_number(total_symbols),
format_number(total_edges),
);
}
pub fn print_workspace_refs(
symbol_name: &str,
refs: &[crate::core::workspace_graph::WorkspaceRef],
total: usize,
) {
println!(
"{} \u{2014} {} reference{} (workspace)",
symbol_name,
total,
if total == 1 { "" } else { "s" }
);
println!("{SEPARATOR}");
for wr in refs {
let r = &wr.reference;
let path = normalize_path(&r.file_path);
let location = if let Some(line) = r.line {
format!("{path}:{line}")
} else {
path
};
let display_text = r.snippet_line.as_deref().unwrap_or(&r.context);
let truncated_text = truncate_str(display_text.trim(), 70);
println!("[{:<12}] {:<36}{}", wr.project, location, truncated_text);
}
}
pub fn print_workspace_find_results(
query: &str,
results: &[crate::commands::find::WorkspaceSearchResult],
) {
println!(
"find \"{}\" \u{2014} {} result{}",
query,
results.len(),
if results.len() == 1 { "" } else { "s" }
);
println!("{SEPARATOR}");
for r in results {
let path = normalize_path(&r.result.file_path);
let line_range = format_line_range(r.result.line_start, r.result.line_end);
println!(
"[{:<12}] {:<32}{:<8} {path}:{line_range} ({:.2})",
r.project, r.result.name, r.result.kind, r.result.score
);
}
}