use base64::Engine;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::graph::CodeGraph;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectSummary {
pub name: String,
pub version: String,
pub language: String,
pub total_files: usize,
pub total_symbols: usize,
pub symbol_counts: SymbolCounts,
pub entry_points: Vec<String>,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SymbolCounts {
pub functions: usize,
pub methods: usize,
pub structs: usize,
pub traits: usize,
pub enums: usize,
pub modules: usize,
pub other: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileContext {
pub path: String,
pub language: String,
pub symbol_count: usize,
pub public_symbols: Vec<String>,
pub symbol_counts: SymbolCounts,
pub imports: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SymbolDetail {
pub name: String,
pub kind: String,
pub file: String,
pub line: usize,
pub signature: Option<String>,
pub documentation: Option<String>,
pub callers: Vec<String>,
pub callees: Vec<String>,
pub related: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginatedResult<T> {
pub page: usize,
pub total_pages: usize,
pub page_size: usize,
pub total_items: usize,
pub next_cursor: Option<String>,
pub prev_cursor: Option<String>,
pub items: Vec<T>,
}
impl<T> PaginatedResult<T> {
pub fn new(items: Vec<T>, page: usize, page_size: usize, total_items: usize) -> Self {
let total_pages = total_items.div_ceil(page_size);
let next_cursor = if page < total_pages {
Some(base64::engine::general_purpose::STANDARD.encode(format!("page={}", page + 1)))
} else {
None
};
let prev_cursor = if page > 1 {
Some(base64::engine::general_purpose::STANDARD.encode(format!("page={}", page - 1)))
} else {
None
};
let start_idx = (page.saturating_sub(1)) * page_size;
let end_idx = (start_idx + page_size).min(items.len());
let paged_items = if start_idx < items.len() {
items
.into_iter()
.skip(start_idx)
.take(end_idx - start_idx)
.collect()
} else {
Vec::new()
};
Self {
page,
total_pages,
page_size,
total_items,
next_cursor,
prev_cursor,
items: paged_items,
}
}
pub fn empty(page: usize, page_size: usize) -> Self {
Self::new(Vec::new(), page, page_size, 0)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ListQuery {
pub kind: Option<String>,
pub file_pattern: Option<String>,
pub page: Option<usize>,
pub page_size: Option<usize>,
pub cursor: Option<String>,
}
impl Default for ListQuery {
fn default() -> Self {
Self {
kind: None,
file_pattern: None,
page: Some(1),
page_size: Some(50),
cursor: None,
}
}
}
pub fn get_project_summary(graph: &mut CodeGraph) -> Result<ProjectSummary> {
let total_files = graph.count_files()?;
let total_symbols = graph.count_symbols()?;
let mut counts = SymbolCounts::default();
for label in &["fn", "method", "struct", "trait", "enum", "mod"] {
let symbols = graph.get_symbols_by_label(label)?;
let count = symbols.len();
match *label {
"fn" => counts.functions = count,
"method" => counts.methods = count,
"struct" => counts.structs = count,
"trait" => counts.traits = count,
"enum" => counts.enums = count,
"mod" => counts.modules = count,
_ => counts.other += count,
}
}
let (name, version) = detect_project_info()?;
let language = detect_primary_language(graph)?;
let entry_points = find_entry_points(graph)?;
let description = format!(
"{} {} written in {}, {} files, {} symbols ({} functions, {} structs)",
name, version, language, total_files, total_symbols, counts.functions, counts.structs
);
Ok(ProjectSummary {
name,
version,
language,
total_files,
total_symbols,
symbol_counts: counts,
entry_points,
description,
})
}
pub fn get_file_context(graph: &mut CodeGraph, file_path: &str) -> Result<FileContext> {
let symbols = graph.symbols_in_file(file_path)?;
let mut counts = SymbolCounts::default();
let mut public_symbols = Vec::new();
for symbol in &symbols {
let kind = symbol.kind_normalized.as_str();
match kind {
"fn" => counts.functions += 1,
"method" => counts.methods += 1,
"struct" => counts.structs += 1,
"trait" => counts.traits += 1,
"enum" => counts.enums += 1,
"mod" => counts.modules += 1,
_ => counts.other += 1,
}
if let Some(ref name) = symbol.name {
if !name.starts_with('_') {
public_symbols.push(format!("{}:{}", kind, name));
}
}
}
let language = crate::common::detect_language_from_path(file_path);
let imports = Vec::new();
Ok(FileContext {
path: file_path.to_string(),
language,
symbol_count: symbols.len(),
public_symbols,
symbol_counts: counts,
imports,
})
}
pub fn get_symbol_detail(
graph: &mut CodeGraph,
symbol_name: &str,
file_path: Option<&str>,
) -> Result<SymbolDetail> {
let symbols = if let Some(file) = file_path {
graph
.symbols_in_file(file)?
.into_iter()
.filter(|s| s.name.as_deref() == Some(symbol_name))
.collect::<Vec<_>>()
} else {
let results = graph.get_symbols_by_label(symbol_name)?;
results
.into_iter()
.filter_map(|r| {
graph.symbols_in_file(&r.file_path).ok().and_then(|syms| {
syms.into_iter()
.find(|s| s.name.as_deref() == Some(symbol_name))
})
})
.collect::<Vec<_>>()
};
let symbol = symbols
.first()
.ok_or_else(|| anyhow::anyhow!("Symbol '{}' not found", symbol_name))?;
let callers = graph
.callers_of_symbol(&symbol.file_path.to_string_lossy(), symbol_name)?
.iter()
.map(|c| c.caller.clone())
.collect();
let callees = graph
.calls_from_symbol(&symbol.file_path.to_string_lossy(), symbol_name)?
.iter()
.map(|c| c.callee.clone())
.collect();
let related = graph
.symbols_in_file(&symbol.file_path.to_string_lossy())?
.iter()
.filter(|s| s.name.as_deref() != Some(symbol_name))
.filter_map(|s| s.name.clone())
.take(10)
.collect();
Ok(SymbolDetail {
name: symbol_name.to_string(),
kind: symbol.kind_normalized.clone(),
file: symbol.file_path.to_string_lossy().to_string(),
line: symbol.start_line,
signature: None, documentation: None, callers,
callees,
related,
})
}
pub fn get_callers(
graph: &mut CodeGraph,
symbol_name: &str,
file_path: Option<&str>,
) -> Result<Vec<SymbolListItem>> {
let symbols = if let Some(file) = file_path {
graph
.symbols_in_file(file)?
.into_iter()
.filter(|s| s.name.as_deref() == Some(symbol_name))
.collect::<Vec<_>>()
} else {
let results = graph.get_symbols_by_label(symbol_name)?;
results
.into_iter()
.filter_map(|r| {
graph.symbols_in_file(&r.file_path).ok().and_then(|syms| {
syms.into_iter()
.find(|s| s.name.as_deref() == Some(symbol_name))
})
})
.collect::<Vec<_>>()
};
let symbol = symbols
.first()
.ok_or_else(|| anyhow::anyhow!("Symbol '{}' not found", symbol_name))?;
let callers = graph.callers_of_symbol(&symbol.file_path.to_string_lossy(), symbol_name)?;
let items = callers
.into_iter()
.map(|c| SymbolListItem {
name: c.caller,
kind: "function".to_string(),
file: c.file_path.to_string_lossy().to_string(),
line: c.start_line,
})
.collect();
Ok(items)
}
pub fn get_callees(
graph: &mut CodeGraph,
symbol_name: &str,
file_path: Option<&str>,
) -> Result<Vec<SymbolListItem>> {
let symbols = if let Some(file) = file_path {
graph
.symbols_in_file(file)?
.into_iter()
.filter(|s| s.name.as_deref() == Some(symbol_name))
.collect::<Vec<_>>()
} else {
let results = graph.get_symbols_by_label(symbol_name)?;
results
.into_iter()
.filter_map(|r| {
graph.symbols_in_file(&r.file_path).ok().and_then(|syms| {
syms.into_iter()
.find(|s| s.name.as_deref() == Some(symbol_name))
})
})
.collect::<Vec<_>>()
};
let symbol = symbols
.first()
.ok_or_else(|| anyhow::anyhow!("Symbol '{}' not found", symbol_name))?;
let callees = graph.calls_from_symbol(&symbol.file_path.to_string_lossy(), symbol_name)?;
let items = callees
.into_iter()
.map(|c| SymbolListItem {
name: c.callee,
kind: "function".to_string(),
file: c.file_path.to_string_lossy().to_string(),
line: c.start_line,
})
.collect();
Ok(items)
}
pub fn list_symbols(
graph: &mut CodeGraph,
query: &ListQuery,
) -> Result<PaginatedResult<SymbolListItem>> {
let page = query
.cursor
.as_ref()
.and_then(|c| base64::engine::general_purpose::STANDARD.decode(c).ok())
.and_then(|d| String::from_utf8(d).ok())
.and_then(|s| {
s.strip_prefix("page=")
.and_then(|p| p.parse::<usize>().ok())
})
.unwrap_or(query.page.unwrap_or(1));
let page_size = query.page_size.unwrap_or(50);
let all_symbols = if let Some(ref kind) = query.kind {
graph
.get_symbols_by_label(kind)?
.into_iter()
.map(|r| SymbolListItem {
name: r.name,
kind: kind.clone(),
file: r.file_path,
line: 0, })
.collect::<Vec<_>>()
} else {
let files = graph.all_file_nodes()?;
let mut items = Vec::new();
for (file_path, _) in files {
if let Ok(symbols) = graph.symbols_in_file(&file_path) {
for symbol in symbols {
if let Some(ref name) = symbol.name {
items.push(SymbolListItem {
name: name.clone(),
kind: symbol.kind_normalized.clone(),
file: file_path.clone(),
line: symbol.start_line,
});
}
}
}
}
items
};
let total_items = all_symbols.len();
Ok(PaginatedResult::new(
all_symbols,
page,
page_size,
total_items,
))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SymbolListItem {
pub name: String,
pub kind: String,
pub file: String,
pub line: usize,
}
fn detect_project_info() -> Result<(String, String)> {
let cargo_toml = std::path::Path::new("Cargo.toml");
if cargo_toml.exists() {
if let Ok(content) = std::fs::read_to_string(cargo_toml) {
let name = content
.lines()
.find(|l| l.starts_with("name = "))
.and_then(|l| l.split('"').nth(1))
.unwrap_or("unknown")
.to_string();
let version = content
.lines()
.find(|l| l.starts_with("version = "))
.and_then(|l| l.split('"').nth(1))
.unwrap_or("0.1.0")
.to_string();
return Ok((name, version));
}
}
let dir_name = std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
.unwrap_or_else(|| "unknown".to_string());
Ok((dir_name, "0.1.0".to_string()))
}
fn detect_primary_language(graph: &mut CodeGraph) -> Result<String> {
let files = graph.all_file_nodes()?;
let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for (path, _) in files {
if let Some(ext) = std::path::Path::new(&path)
.extension()
.and_then(|e| e.to_str())
{
*counts.entry(ext.to_string()).or_insert(0) += 1;
}
}
let primary = counts
.iter()
.max_by_key(|(_, &count)| count)
.map(|(ext, _)| ext.as_str())
.unwrap_or("unknown");
let language = match primary {
"rs" => "Rust",
"py" => "Python",
"c" | "h" => "C",
"cpp" | "hpp" | "cc" => "C++",
"java" => "Java",
"js" | "mjs" => "JavaScript",
"ts" | "tsx" => "TypeScript",
_ => "Unknown",
};
Ok(language.to_string())
}
fn find_entry_points(graph: &mut CodeGraph) -> Result<Vec<String>> {
let mut entry_points = Vec::new();
if let Ok(mains) = graph.get_symbols_by_label("main") {
for m in mains {
entry_points.push(format!("{} ({})", m.name, m.file_path));
}
}
if let Ok(libs) = graph.get_symbols_by_label("lib") {
for l in libs {
entry_points.push(format!("{} ({})", l.name, l.file_path));
}
}
Ok(entry_points)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_paginated_result_creation() {
let items: Vec<i32> = (1..101).collect();
let result = PaginatedResult::new(items, 1, 50, 100);
assert_eq!(result.page, 1);
assert_eq!(result.total_pages, 2);
assert_eq!(result.page_size, 50);
assert_eq!(result.total_items, 100);
assert!(result.next_cursor.is_some());
assert!(result.prev_cursor.is_none());
assert_eq!(result.items.len(), 50);
}
#[test]
fn test_list_query_default() {
let query = ListQuery::default();
assert_eq!(query.page, Some(1));
assert_eq!(query.page_size, Some(50));
assert!(query.kind.is_none());
}
}