use crate::bm25::BM25Index;
use anyhow::Result;
use chrono::{DateTime, Utc};
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
static FN_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^\s*(pub(\(.*?\))?\s+)?(async\s+)?fn\s+(\w+)")
.unwrap_or_else(|e| panic!("Invalid regex pattern: {e}"))
});
static STRUCT_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^\s*(pub(\(.*?\))?\s+)?struct\s+(\w+)")
.unwrap_or_else(|e| panic!("Invalid regex pattern: {e}"))
});
static ENUM_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^\s*(pub(\(.*?\))?\s+)?enum\s+(\w+)")
.unwrap_or_else(|e| panic!("Invalid regex pattern: {e}"))
});
static TRAIT_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^\s*(pub(\(.*?\))?\s+)?trait\s+(\w+)")
.unwrap_or_else(|e| panic!("Invalid regex pattern: {e}"))
});
static IMPL_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^\s*impl(<.*?>)?\s+(\w+)").unwrap_or_else(|e| panic!("Invalid regex pattern: {e}"))
});
static CONST_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^\s*(pub(\(.*?\))?\s+)?const\s+(\w+)")
.unwrap_or_else(|e| panic!("Invalid regex pattern: {e}"))
});
static TYPE_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^\s*(pub(\(.*?\))?\s+)?type\s+(\w+)")
.unwrap_or_else(|e| panic!("Invalid regex pattern: {e}"))
});
static MACRO_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^\s*(pub(\(.*?\))?\s+)?macro_rules!\s+(\w+)")
.unwrap_or_else(|e| panic!("Invalid regex pattern: {e}"))
});
static MOD_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^\s*(pub(\(.*?\))?\s+)?mod\s+(\w+)")
.unwrap_or_else(|e| panic!("Invalid regex pattern: {e}"))
});
#[derive(Debug)]
pub struct ProjectIntelligence {
root: PathBuf,
symbols: Arc<RwLock<SymbolIndex>>,
dependencies: Arc<RwLock<DependencyGraph>>,
git_state: Arc<RwLock<GitState>>,
files: Arc<RwLock<FileIndex>>,
patterns: Arc<RwLock<PatternDetector>>,
last_update: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SymbolKind {
Function,
Struct,
Enum,
Trait,
Impl,
Const,
Static,
Type,
Macro,
Module,
}
impl SymbolKind {
pub fn icon(&self) -> &'static str {
match self {
SymbolKind::Function => "ƒ",
SymbolKind::Struct => "◇",
SymbolKind::Enum => "◆",
SymbolKind::Trait => "▸",
SymbolKind::Impl => "▹",
SymbolKind::Const => "C",
SymbolKind::Static => "S",
SymbolKind::Type => "T",
SymbolKind::Macro => "M",
SymbolKind::Module => "◫",
}
}
pub fn color(&self) -> &'static str {
match self {
SymbolKind::Function => "\x1b[33m", SymbolKind::Struct => "\x1b[36m", SymbolKind::Enum => "\x1b[35m", SymbolKind::Trait => "\x1b[34m", SymbolKind::Impl => "\x1b[32m", SymbolKind::Const | SymbolKind::Static => "\x1b[31m", SymbolKind::Type => "\x1b[94m", SymbolKind::Macro => "\x1b[95m", SymbolKind::Module => "\x1b[37m", }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Symbol {
pub name: String,
pub kind: SymbolKind,
pub file: PathBuf,
pub line: usize,
pub column: usize,
pub signature: String,
pub doc: Option<String>,
pub visibility: Visibility,
pub parent: Option<String>,
}
impl Symbol {
pub fn new(name: String, kind: SymbolKind, file: PathBuf, line: usize) -> Self {
Self {
name,
kind,
file,
line,
column: 1,
signature: String::new(),
doc: None,
visibility: Visibility::Private,
parent: None,
}
}
pub fn with_signature(mut self, signature: String) -> Self {
self.signature = signature;
self
}
pub fn with_doc(mut self, doc: String) -> Self {
self.doc = Some(doc);
self
}
pub fn with_visibility(mut self, visibility: Visibility) -> Self {
self.visibility = visibility;
self
}
pub fn with_parent(mut self, parent: String) -> Self {
self.parent = Some(parent);
self
}
pub fn with_column(mut self, column: usize) -> Self {
self.column = column;
self
}
pub fn display(&self) -> String {
format!(
"{} {} {}:{}",
self.kind.icon(),
self.name,
self.file.display(),
self.line
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum Visibility {
#[default]
Private,
Pub,
PubCrate,
PubSuper,
PubIn(String),
}
impl Visibility {
pub fn parse(s: &str) -> Self {
if s.starts_with("pub(crate)") {
Visibility::PubCrate
} else if s.starts_with("pub(super)") {
Visibility::PubSuper
} else if s.starts_with("pub(in") {
if let Some(start) = s.find("pub(in ") {
if let Some(end) = s[start..].find(')') {
let path = s[start + 7..start + end].to_string();
return Visibility::PubIn(path);
}
}
Visibility::Pub
} else if s.starts_with("pub") {
Visibility::Pub
} else {
Visibility::Private
}
}
pub fn is_public(&self) -> bool {
matches!(self, Visibility::Pub)
}
}
#[derive(Debug, Default)]
pub struct SymbolIndex {
by_name: HashMap<String, Vec<Symbol>>,
by_file: HashMap<PathBuf, Vec<Symbol>>,
by_kind: HashMap<SymbolKind, Vec<Symbol>>,
count: usize,
bm25: BM25Index,
}
impl SymbolIndex {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, symbol: Symbol) {
let searchable = format!(
"{} {} {}",
symbol.name,
symbol.signature,
symbol.doc.as_deref().unwrap_or("")
);
let doc_id = Self::make_doc_id(&symbol.file, symbol.line, &symbol.name);
self.bm25.add(&doc_id, searchable);
self.by_name
.entry(symbol.name.clone())
.or_default()
.push(symbol.clone());
self.by_file
.entry(symbol.file.clone())
.or_default()
.push(symbol.clone());
self.by_kind
.entry(symbol.kind.clone())
.or_default()
.push(symbol);
self.count += 1;
}
pub fn search(&mut self, query: &str) -> Vec<&Symbol> {
let bm25_results = self.bm25.search(query, 100);
let mut results = Vec::new();
for result in bm25_results {
if let Some((file_str, line, name)) = Self::parse_doc_id(&result.id) {
if let Some(symbols) = self.by_name.get(name) {
for symbol in symbols {
if symbol.file.to_string_lossy() == file_str && symbol.line == line {
results.push(symbol);
break;
}
}
}
}
}
results
}
pub fn get(&self, name: &str) -> Option<&Vec<Symbol>> {
self.by_name.get(name)
}
pub fn in_file(&self, file: &Path) -> Option<&Vec<Symbol>> {
self.by_file.get(file)
}
pub fn of_kind(&self, kind: &SymbolKind) -> Option<&Vec<Symbol>> {
self.by_kind.get(kind)
}
pub fn functions(&self) -> Vec<&Symbol> {
self.by_kind
.get(&SymbolKind::Function)
.map(|v| v.iter().collect())
.unwrap_or_default()
}
pub fn structs(&self) -> Vec<&Symbol> {
self.by_kind
.get(&SymbolKind::Struct)
.map(|v| v.iter().collect())
.unwrap_or_default()
}
pub fn len(&self) -> usize {
self.count
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn clear(&mut self) {
self.by_name.clear();
self.by_file.clear();
self.by_kind.clear();
self.bm25.clear();
self.count = 0;
}
pub fn remove_file(&mut self, file: &Path) {
if let Some(symbols) = self.by_file.remove(file) {
let removed_count = symbols.len();
for symbol in &symbols {
let doc_id = Self::make_doc_id(&symbol.file, symbol.line, &symbol.name);
self.bm25.remove_all(&doc_id);
if let Some(by_name) = self.by_name.get_mut(&symbol.name) {
by_name.retain(|s| s.file != file);
}
if let Some(by_kind) = self.by_kind.get_mut(&symbol.kind) {
by_kind.retain(|s| s.file != file);
}
}
self.count = self.count.saturating_sub(removed_count);
}
}
fn make_doc_id(file: &Path, line: usize, name: &str) -> String {
format!("{}\x00{}\x00{}", file.display(), line, name)
}
fn parse_doc_id(doc_id: &str) -> Option<(&str, usize, &str)> {
let parts: Vec<&str> = doc_id.splitn(3, '\x00').collect();
if parts.len() == 3 {
let line = parts[1].parse().ok()?;
Some((parts[0], line, parts[2]))
} else {
None
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dependency {
pub name: String,
pub version: String,
pub features: Vec<String>,
pub optional: bool,
pub dev: bool,
pub build: bool,
}
impl Dependency {
pub fn new(name: String, version: String) -> Self {
Self {
name,
version,
features: Vec::new(),
optional: false,
dev: false,
build: false,
}
}
pub fn with_features(mut self, features: Vec<String>) -> Self {
self.features = features;
self
}
pub fn optional(mut self) -> Self {
self.optional = true;
self
}
pub fn dev(mut self) -> Self {
self.dev = true;
self
}
pub fn build(mut self) -> Self {
self.build = true;
self
}
}
#[derive(Debug, Default)]
pub struct DependencyGraph {
pub dependencies: Vec<Dependency>,
pub dev_dependencies: Vec<Dependency>,
pub build_dependencies: Vec<Dependency>,
pub package_name: Option<String>,
pub package_version: Option<String>,
pub features: HashMap<String, Vec<String>>,
}
impl DependencyGraph {
pub fn new() -> Self {
Self::default()
}
pub fn parse(content: &str) -> Result<Self> {
let value: toml::Value = toml::from_str(content)?;
let mut graph = Self::new();
if let Some(package) = value.get("package") {
if let Some(name) = package.get("name").and_then(|v| v.as_str()) {
graph.package_name = Some(name.to_string());
}
if let Some(version) = package.get("version").and_then(|v| v.as_str()) {
graph.package_version = Some(version.to_string());
}
}
if let Some(deps) = value.get("dependencies") {
graph.dependencies = Self::parse_deps(deps)?;
}
if let Some(deps) = value.get("dev-dependencies") {
graph.dev_dependencies = Self::parse_deps(deps)?;
for dep in &mut graph.dev_dependencies {
dep.dev = true;
}
}
if let Some(deps) = value.get("build-dependencies") {
graph.build_dependencies = Self::parse_deps(deps)?;
for dep in &mut graph.build_dependencies {
dep.build = true;
}
}
if let Some(features) = value.get("features").and_then(|v| v.as_table()) {
for (name, value) in features {
if let Some(arr) = value.as_array() {
let deps: Vec<String> = arr
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
graph.features.insert(name.clone(), deps);
}
}
}
Ok(graph)
}
fn parse_deps(deps: &toml::Value) -> Result<Vec<Dependency>> {
let mut result = Vec::new();
if let Some(table) = deps.as_table() {
for (name, value) in table {
let dep = match value {
toml::Value::String(version) => Dependency::new(name.clone(), version.clone()),
toml::Value::Table(t) => {
let version = t
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("*")
.to_string();
let mut dep = Dependency::new(name.clone(), version);
if let Some(features) = t.get("features").and_then(|v| v.as_array()) {
dep.features = features
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
}
if let Some(optional) = t.get("optional").and_then(|v| v.as_bool()) {
dep.optional = optional;
}
dep
}
_ => continue,
};
result.push(dep);
}
}
Ok(result)
}
pub fn all(&self) -> Vec<&Dependency> {
self.dependencies
.iter()
.chain(self.dev_dependencies.iter())
.chain(self.build_dependencies.iter())
.collect()
}
pub fn find(&self, name: &str) -> Option<&Dependency> {
self.all().into_iter().find(|d| d.name == name)
}
pub fn count(&self) -> usize {
self.dependencies.len() + self.dev_dependencies.len() + self.build_dependencies.len()
}
}
#[derive(Debug, Default)]
pub struct GitState {
pub branch: Option<String>,
pub commit: Option<String>,
pub dirty: bool,
pub untracked: Vec<PathBuf>,
pub modified: Vec<PathBuf>,
pub staged: Vec<PathBuf>,
pub remote: Option<String>,
pub ahead: usize,
pub behind: usize,
}
impl GitState {
pub fn new() -> Self {
Self::default()
}
pub fn update(&mut self, repo_path: &Path) -> Result<()> {
let repo = git2::Repository::open(repo_path)?;
if let Ok(head) = repo.head() {
if head.is_branch() {
self.branch = head.shorthand().ok().map(String::from);
}
if let Some(oid) = head.target() {
self.commit = Some(oid.to_string());
}
}
let statuses = repo.statuses(None)?;
self.untracked.clear();
self.modified.clear();
self.staged.clear();
for entry in statuses.iter() {
if let Ok(path) = entry.path() {
let path = PathBuf::from(path);
let status = entry.status();
if status.is_wt_new() {
self.untracked.push(path.clone());
}
if status.is_wt_modified() || status.is_wt_deleted() {
self.modified.push(path.clone());
}
if status.is_index_new() || status.is_index_modified() || status.is_index_deleted()
{
self.staged.push(path);
}
}
}
self.dirty =
!self.untracked.is_empty() || !self.modified.is_empty() || !self.staged.is_empty();
Ok(())
}
pub fn summary(&self) -> String {
let mut parts = Vec::new();
if let Some(ref branch) = self.branch {
parts.push(format!("on {}", branch));
}
if self.dirty {
let changes = self.modified.len() + self.staged.len();
parts.push(format!("{} changes", changes));
}
if !self.untracked.is_empty() {
parts.push(format!("{} untracked", self.untracked.len()));
}
if self.ahead > 0 {
parts.push(format!("↑{}", self.ahead));
}
if self.behind > 0 {
parts.push(format!("↓{}", self.behind));
}
parts.join(", ")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileEntry {
pub path: PathBuf,
pub size: u64,
pub modified: DateTime<Utc>,
pub extension: Option<String>,
pub language: Option<String>,
pub lines: Option<usize>,
}
impl FileEntry {
pub fn from_path(path: PathBuf) -> Result<Self> {
let metadata = std::fs::metadata(&path)?;
let modified = metadata.modified()?.into();
let extension = path.extension().map(|e| e.to_string_lossy().to_string());
let language = extension.as_ref().and_then(|e| detect_language(e));
Ok(Self {
path,
size: metadata.len(),
modified,
extension,
language,
lines: None,
})
}
pub fn count_lines(&mut self) -> Result<usize> {
let content = std::fs::read_to_string(&self.path)?;
let count = content.lines().count();
self.lines = Some(count);
Ok(count)
}
}
pub fn detect_language(ext: &str) -> Option<String> {
let lang = match ext.to_lowercase().as_str() {
"rs" => "Rust",
"py" => "Python",
"js" => "JavaScript",
"ts" => "TypeScript",
"tsx" | "jsx" => "React",
"go" => "Go",
"java" => "Java",
"c" | "h" => "C",
"cpp" | "hpp" | "cc" | "cxx" => "C++",
"rb" => "Ruby",
"php" => "PHP",
"swift" => "Swift",
"kt" | "kts" => "Kotlin",
"scala" => "Scala",
"hs" => "Haskell",
"ml" | "mli" => "OCaml",
"ex" | "exs" => "Elixir",
"erl" | "hrl" => "Erlang",
"clj" | "cljs" => "Clojure",
"lua" => "Lua",
"r" => "R",
"sql" => "SQL",
"sh" | "bash" | "zsh" => "Shell",
"md" | "markdown" => "Markdown",
"json" => "JSON",
"yaml" | "yml" => "YAML",
"toml" => "TOML",
"xml" => "XML",
"html" | "htm" => "HTML",
"css" => "CSS",
"scss" | "sass" => "SASS",
"vue" => "Vue",
"svelte" => "Svelte",
_ => return None,
};
Some(lang.to_string())
}
#[derive(Debug, Default)]
pub struct FileIndex {
files: HashMap<PathBuf, FileEntry>,
by_extension: HashMap<String, Vec<PathBuf>>,
by_language: HashMap<String, Vec<PathBuf>>,
}
impl FileIndex {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, entry: FileEntry) {
let path = entry.path.clone();
if let Some(ext) = &entry.extension {
self.by_extension
.entry(ext.clone())
.or_default()
.push(path.clone());
}
if let Some(lang) = &entry.language {
self.by_language
.entry(lang.clone())
.or_default()
.push(path.clone());
}
self.files.insert(path, entry);
}
pub fn get(&self, path: &Path) -> Option<&FileEntry> {
self.files.get(path)
}
pub fn by_extension(&self, ext: &str) -> Vec<&FileEntry> {
self.by_extension
.get(ext)
.map(|paths| paths.iter().filter_map(|p| self.files.get(p)).collect())
.unwrap_or_default()
}
pub fn by_language(&self, lang: &str) -> Vec<&FileEntry> {
self.by_language
.get(lang)
.map(|paths| paths.iter().filter_map(|p| self.files.get(p)).collect())
.unwrap_or_default()
}
pub fn remove(&mut self, path: &Path) {
if let Some(entry) = self.files.remove(path) {
if let Some(ext) = &entry.extension {
if let Some(paths) = self.by_extension.get_mut(ext) {
paths.retain(|p| p != path);
}
}
if let Some(lang) = &entry.language {
if let Some(paths) = self.by_language.get_mut(lang) {
paths.retain(|p| p != path);
}
}
}
}
pub fn len(&self) -> usize {
self.files.len()
}
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
pub fn clear(&mut self) {
self.files.clear();
self.by_extension.clear();
self.by_language.clear();
}
pub fn language_stats(&self) -> HashMap<String, usize> {
self.by_language
.iter()
.map(|(lang, paths)| (lang.clone(), paths.len()))
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodePattern {
pub name: String,
pub description: String,
pub category: PatternCategory,
pub locations: Vec<PatternLocation>,
}
impl CodePattern {
pub fn new(name: String, description: String, category: PatternCategory) -> Self {
Self {
name,
description,
category,
locations: Vec::new(),
}
}
pub fn add_location(&mut self, file: PathBuf, line: usize, snippet: String) {
self.locations.push(PatternLocation {
file,
line,
snippet,
});
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PatternCategory {
Design, AntiPattern, Convention, Security, Performance, Testing, }
impl PatternCategory {
pub fn icon(&self) -> &'static str {
match self {
PatternCategory::Design => "🏗️",
PatternCategory::AntiPattern => "⚠️",
PatternCategory::Convention => "📏",
PatternCategory::Security => "🔒",
PatternCategory::Performance => "⚡",
PatternCategory::Testing => "🧪",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatternLocation {
pub file: PathBuf,
pub line: usize,
pub snippet: String,
}
#[derive(Debug, Default)]
pub struct PatternDetector {
patterns: Vec<CodePattern>,
rules: Vec<PatternRule>,
}
#[derive(Debug, Clone)]
pub struct PatternRule {
pub name: String,
pub category: PatternCategory,
pub description: String,
pub regex: Regex,
}
impl PatternRule {
pub fn new(
name: &str,
category: PatternCategory,
description: &str,
pattern: &str,
) -> Result<Self> {
Ok(Self {
name: name.to_string(),
category,
description: description.to_string(),
regex: Regex::new(pattern)?,
})
}
}
impl PatternDetector {
pub fn new() -> Self {
let mut detector = Self::default();
detector.add_default_rules();
detector
}
fn add_default_rules(&mut self) {
if let Ok(rule) = PatternRule::new(
"unwrap_usage",
PatternCategory::AntiPattern,
"Direct .unwrap() calls can panic",
r"\.unwrap\(\)",
) {
self.rules.push(rule);
}
if let Ok(rule) = PatternRule::new(
"todo_comment",
PatternCategory::Convention,
"TODO comments indicate unfinished work",
r"(?i)//\s*TODO:",
) {
self.rules.push(rule);
}
if let Ok(rule) = PatternRule::new(
"fixme_comment",
PatternCategory::Convention,
"FIXME comments indicate bugs or issues",
r"(?i)//\s*FIXME:",
) {
self.rules.push(rule);
}
if let Ok(rule) = PatternRule::new(
"unsafe_block",
PatternCategory::Security,
"Unsafe blocks require careful review",
r"unsafe\s*\{",
) {
self.rules.push(rule);
}
if let Ok(rule) = PatternRule::new(
"clone_in_loop",
PatternCategory::Performance,
"Cloning in loops can be expensive",
r"for\s+.*\{[^}]*\.clone\(\)",
) {
self.rules.push(rule);
}
if let Ok(rule) = PatternRule::new(
"test_function",
PatternCategory::Testing,
"Test functions",
r"#\[test\]",
) {
self.rules.push(rule);
}
}
pub fn analyze(&mut self, file: &Path, content: &str) {
for rule in &self.rules {
for (line_num, line) in content.lines().enumerate() {
if rule.regex.is_match(line) {
let pattern = self.patterns.iter_mut().find(|p| p.name == rule.name);
if let Some(pattern) = pattern {
pattern.add_location(file.to_path_buf(), line_num + 1, line.to_string());
} else {
let mut pattern = CodePattern::new(
rule.name.clone(),
rule.description.clone(),
rule.category.clone(),
);
pattern.add_location(file.to_path_buf(), line_num + 1, line.to_string());
self.patterns.push(pattern);
}
}
}
}
}
pub fn patterns(&self) -> &[CodePattern] {
&self.patterns
}
pub fn by_category(&self, category: &PatternCategory) -> Vec<&CodePattern> {
self.patterns
.iter()
.filter(|p| &p.category == category)
.collect()
}
pub fn anti_patterns(&self) -> Vec<&CodePattern> {
self.by_category(&PatternCategory::AntiPattern)
}
pub fn clear(&mut self) {
self.patterns.clear();
}
pub fn add_rule(&mut self, rule: PatternRule) {
self.rules.push(rule);
}
pub fn summary(&self) -> HashMap<PatternCategory, usize> {
let mut result = HashMap::new();
for pattern in &self.patterns {
*result.entry(pattern.category.clone()).or_insert(0) += pattern.locations.len();
}
result
}
}
impl ProjectIntelligence {
pub fn new(root: PathBuf) -> Self {
Self {
root,
symbols: Arc::new(RwLock::new(SymbolIndex::new())),
dependencies: Arc::new(RwLock::new(DependencyGraph::new())),
git_state: Arc::new(RwLock::new(GitState::new())),
files: Arc::new(RwLock::new(FileIndex::new())),
patterns: Arc::new(RwLock::new(PatternDetector::new())),
last_update: Utc::now(),
}
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn symbols(&self) -> &Arc<RwLock<SymbolIndex>> {
&self.symbols
}
pub fn dependencies(&self) -> &Arc<RwLock<DependencyGraph>> {
&self.dependencies
}
pub fn git_state(&self) -> &Arc<RwLock<GitState>> {
&self.git_state
}
pub fn files(&self) -> &Arc<RwLock<FileIndex>> {
&self.files
}
pub fn patterns(&self) -> &Arc<RwLock<PatternDetector>> {
&self.patterns
}
pub fn refresh(&mut self) -> Result<()> {
if let Ok(mut git) = self.git_state.write() {
let _ = git.update(&self.root);
}
let cargo_path = self.root.join("Cargo.toml");
if cargo_path.exists() {
if let Ok(content) = std::fs::read_to_string(&cargo_path) {
if let Ok(deps) = DependencyGraph::parse(&content) {
if let Ok(mut graph) = self.dependencies.write() {
*graph = deps;
}
}
}
}
self.index_files()?;
self.last_update = Utc::now();
Ok(())
}
fn index_files(&mut self) -> Result<()> {
use walkdir::WalkDir;
let mut file_index = self
.files
.write()
.map_err(|_| anyhow::anyhow!("Lock error"))?;
let mut symbol_index = self
.symbols
.write()
.map_err(|_| anyhow::anyhow!("Lock error"))?;
let mut pattern_detector = self
.patterns
.write()
.map_err(|_| anyhow::anyhow!("Lock error"))?;
file_index.clear();
symbol_index.clear();
pattern_detector.clear();
for entry in WalkDir::new(&self.root)
.into_iter()
.filter_entry(|e| {
if e.depth() == 0 {
return true;
}
let name = e.file_name().to_string_lossy();
!name.starts_with('.') && name != "target" && name != "node_modules"
})
.filter_map(|e| e.ok())
{
if entry.file_type().is_file() {
let path = entry.path().to_path_buf();
if let Ok(file_entry) = FileEntry::from_path(path.clone()) {
file_index.add(file_entry);
if path.extension().map(|e| e == "rs").unwrap_or(false) {
if let Ok(content) = std::fs::read_to_string(&path) {
self.index_rust_symbols(&mut symbol_index, &path, &content);
pattern_detector.analyze(&path, &content);
}
}
}
}
}
Ok(())
}
fn index_rust_symbols(&self, index: &mut SymbolIndex, file: &Path, content: &str) {
for (line_num, line) in content.lines().enumerate() {
let line_num = line_num + 1;
if let Some(caps) = FN_REGEX.captures(line) {
let vis = Visibility::parse(caps.get(1).map(|m| m.as_str()).unwrap_or(""));
if let Some(name_match) = caps.get(4) {
let symbol = Symbol::new(
name_match.as_str().to_string(),
SymbolKind::Function,
file.to_path_buf(),
line_num,
)
.with_visibility(vis)
.with_signature(line.trim().to_string());
index.add(symbol);
}
}
else if let Some(caps) = STRUCT_REGEX.captures(line) {
let vis = Visibility::parse(caps.get(1).map(|m| m.as_str()).unwrap_or(""));
if let Some(name_match) = caps.get(3) {
let symbol = Symbol::new(
name_match.as_str().to_string(),
SymbolKind::Struct,
file.to_path_buf(),
line_num,
)
.with_visibility(vis)
.with_signature(line.trim().to_string());
index.add(symbol);
}
}
else if let Some(caps) = ENUM_REGEX.captures(line) {
let vis = Visibility::parse(caps.get(1).map(|m| m.as_str()).unwrap_or(""));
if let Some(name_match) = caps.get(3) {
let symbol = Symbol::new(
name_match.as_str().to_string(),
SymbolKind::Enum,
file.to_path_buf(),
line_num,
)
.with_visibility(vis)
.with_signature(line.trim().to_string());
index.add(symbol);
}
}
else if let Some(caps) = TRAIT_REGEX.captures(line) {
let vis = Visibility::parse(caps.get(1).map(|m| m.as_str()).unwrap_or(""));
if let Some(name_match) = caps.get(3) {
let symbol = Symbol::new(
name_match.as_str().to_string(),
SymbolKind::Trait,
file.to_path_buf(),
line_num,
)
.with_visibility(vis)
.with_signature(line.trim().to_string());
index.add(symbol);
}
}
else if let Some(caps) = IMPL_REGEX.captures(line) {
if let Some(name_match) = caps.get(2) {
let symbol = Symbol::new(
name_match.as_str().to_string(),
SymbolKind::Impl,
file.to_path_buf(),
line_num,
)
.with_signature(line.trim().to_string());
index.add(symbol);
}
}
else if let Some(caps) = CONST_REGEX.captures(line) {
let vis = Visibility::parse(caps.get(1).map(|m| m.as_str()).unwrap_or(""));
if let Some(name_match) = caps.get(3) {
let symbol = Symbol::new(
name_match.as_str().to_string(),
SymbolKind::Const,
file.to_path_buf(),
line_num,
)
.with_visibility(vis)
.with_signature(line.trim().to_string());
index.add(symbol);
}
}
else if let Some(caps) = TYPE_REGEX.captures(line) {
let vis = Visibility::parse(caps.get(1).map(|m| m.as_str()).unwrap_or(""));
if let Some(name_match) = caps.get(3) {
let symbol = Symbol::new(
name_match.as_str().to_string(),
SymbolKind::Type,
file.to_path_buf(),
line_num,
)
.with_visibility(vis)
.with_signature(line.trim().to_string());
index.add(symbol);
}
}
else if let Some(caps) = MACRO_REGEX.captures(line) {
let vis = Visibility::parse(caps.get(1).map(|m| m.as_str()).unwrap_or(""));
if let Some(name_match) = caps.get(3) {
let symbol = Symbol::new(
name_match.as_str().to_string(),
SymbolKind::Macro,
file.to_path_buf(),
line_num,
)
.with_visibility(vis)
.with_signature(line.trim().to_string());
index.add(symbol);
}
}
else if let Some(caps) = MOD_REGEX.captures(line) {
let vis = Visibility::parse(caps.get(1).map(|m| m.as_str()).unwrap_or(""));
if let Some(name_match) = caps.get(3) {
if !line.contains(';') || line.contains('{') {
let symbol = Symbol::new(
name_match.as_str().to_string(),
SymbolKind::Module,
file.to_path_buf(),
line_num,
)
.with_visibility(vis)
.with_signature(line.trim().to_string());
index.add(symbol);
}
}
}
}
}
pub fn last_update(&self) -> DateTime<Utc> {
self.last_update
}
pub fn search(&self, query: &str) -> Vec<SearchResult> {
let mut results = Vec::new();
if let Ok(mut symbols) = self.symbols.write() {
for symbol in symbols.search(query) {
results.push(SearchResult::Symbol(symbol.clone()));
}
}
if let Ok(files) = self.files.read() {
let query_lower = query.to_lowercase();
for (path, entry) in &files.files {
if path.to_string_lossy().to_lowercase().contains(&query_lower) {
results.push(SearchResult::File(entry.clone()));
}
}
}
results
}
}
#[derive(Debug, Clone)]
pub enum SearchResult {
Symbol(Symbol),
File(FileEntry),
Pattern(CodePattern),
}
impl SearchResult {
pub fn display(&self) -> String {
match self {
SearchResult::Symbol(s) => s.display(),
SearchResult::File(f) => format!("📄 {}", f.path.display()),
SearchResult::Pattern(p) => format!(
"{} {} ({} matches)",
p.category.icon(),
p.name,
p.locations.len()
),
}
}
}
#[cfg(test)]
#[path = "../../tests/unit/cognitive/intelligence/intelligence_test.rs"]
mod tests;