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 search_contains(&self, query: &str) -> Vec<&Symbol> {
let query_lower = query.to_lowercase();
let mut results: Vec<_> = self
.by_name
.iter()
.filter(|(name, _)| name.to_lowercase().contains(&query_lower))
.flat_map(|(_, symbols)| symbols.iter())
.collect();
results.sort_by(|a, b| a.name.cmp(&b.name));
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().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 Some(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| {
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)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_symbol_kind_icons() {
assert_eq!(SymbolKind::Function.icon(), "ƒ");
assert_eq!(SymbolKind::Struct.icon(), "◇");
assert_eq!(SymbolKind::Enum.icon(), "◆");
assert_eq!(SymbolKind::Trait.icon(), "▸");
assert_eq!(SymbolKind::Impl.icon(), "▹");
assert_eq!(SymbolKind::Const.icon(), "C");
assert_eq!(SymbolKind::Static.icon(), "S");
assert_eq!(SymbolKind::Type.icon(), "T");
assert_eq!(SymbolKind::Macro.icon(), "M");
assert_eq!(SymbolKind::Module.icon(), "◫");
}
#[test]
fn test_symbol_kind_colors() {
assert!(SymbolKind::Function.color().contains("33"));
assert!(SymbolKind::Struct.color().contains("36"));
}
#[test]
fn test_symbol_creation() {
let sym = Symbol::new(
"test_fn".to_string(),
SymbolKind::Function,
PathBuf::from("src/lib.rs"),
10,
);
assert_eq!(sym.name, "test_fn");
assert_eq!(sym.kind, SymbolKind::Function);
assert_eq!(sym.line, 10);
}
#[test]
fn test_symbol_builder() {
let sym = Symbol::new(
"test".to_string(),
SymbolKind::Function,
PathBuf::from("test.rs"),
1,
)
.with_signature("fn test() -> Result<()>".to_string())
.with_doc("Test function".to_string())
.with_visibility(Visibility::Pub)
.with_parent("TestStruct".to_string())
.with_column(5);
assert_eq!(sym.signature, "fn test() -> Result<()>");
assert_eq!(sym.doc, Some("Test function".to_string()));
assert_eq!(sym.visibility, Visibility::Pub);
assert_eq!(sym.parent, Some("TestStruct".to_string()));
assert_eq!(sym.column, 5);
}
#[test]
fn test_symbol_display() {
let sym = Symbol::new(
"my_func".to_string(),
SymbolKind::Function,
PathBuf::from("src/lib.rs"),
42,
);
let display = sym.display();
assert!(display.contains("ƒ"));
assert!(display.contains("my_func"));
assert!(display.contains("42"));
}
#[test]
fn test_visibility_parse() {
assert_eq!(Visibility::parse("pub fn"), Visibility::Pub);
assert_eq!(Visibility::parse("pub(crate) fn"), Visibility::PubCrate);
assert_eq!(Visibility::parse("pub(super) fn"), Visibility::PubSuper);
assert_eq!(Visibility::parse("fn"), Visibility::Private);
}
#[test]
fn test_visibility_is_public() {
assert!(Visibility::Pub.is_public());
assert!(!Visibility::Private.is_public());
assert!(!Visibility::PubCrate.is_public());
}
#[test]
fn test_visibility_default() {
let v: Visibility = Default::default();
assert_eq!(v, Visibility::Private);
}
#[test]
fn test_symbol_index_add_search() {
let mut index = SymbolIndex::new();
index.add(Symbol::new(
"test_function".to_string(),
SymbolKind::Function,
PathBuf::from("test.rs"),
1,
));
index.add(Symbol::new(
"TestStruct".to_string(),
SymbolKind::Struct,
PathBuf::from("test.rs"),
10,
));
assert_eq!(index.len(), 2);
assert!(!index.is_empty());
let results = index.search("test");
assert_eq!(results.len(), 2);
let results = index.search("Struct");
assert_eq!(results.len(), 1);
}
#[test]
fn test_symbol_index_get() {
let mut index = SymbolIndex::new();
index.add(Symbol::new(
"my_fn".to_string(),
SymbolKind::Function,
PathBuf::from("test.rs"),
1,
));
assert!(index.get("my_fn").is_some());
assert!(index.get("nonexistent").is_none());
}
#[test]
fn test_symbol_index_in_file() {
let mut index = SymbolIndex::new();
let path = PathBuf::from("src/lib.rs");
index.add(Symbol::new(
"fn1".to_string(),
SymbolKind::Function,
path.clone(),
1,
));
index.add(Symbol::new(
"fn2".to_string(),
SymbolKind::Function,
path.clone(),
5,
));
let symbols = index.in_file(&path).unwrap();
assert_eq!(symbols.len(), 2);
}
#[test]
fn test_symbol_index_of_kind() {
let mut index = SymbolIndex::new();
index.add(Symbol::new(
"fn1".to_string(),
SymbolKind::Function,
PathBuf::from("test.rs"),
1,
));
index.add(Symbol::new(
"Struct1".to_string(),
SymbolKind::Struct,
PathBuf::from("test.rs"),
5,
));
let funcs = index.of_kind(&SymbolKind::Function).unwrap();
assert_eq!(funcs.len(), 1);
}
#[test]
fn test_symbol_index_functions_structs() {
let mut index = SymbolIndex::new();
index.add(Symbol::new(
"fn1".to_string(),
SymbolKind::Function,
PathBuf::from("test.rs"),
1,
));
index.add(Symbol::new(
"Struct1".to_string(),
SymbolKind::Struct,
PathBuf::from("test.rs"),
5,
));
assert_eq!(index.functions().len(), 1);
assert_eq!(index.structs().len(), 1);
}
#[test]
fn test_symbol_index_remove_file() {
let mut index = SymbolIndex::new();
let path = PathBuf::from("test.rs");
index.add(Symbol::new(
"fn1".to_string(),
SymbolKind::Function,
path.clone(),
1,
));
index.add(Symbol::new(
"fn2".to_string(),
SymbolKind::Function,
path.clone(),
5,
));
assert_eq!(index.len(), 2);
index.remove_file(&path);
assert_eq!(index.len(), 0);
}
#[test]
fn test_symbol_index_clear() {
let mut index = SymbolIndex::new();
index.add(Symbol::new(
"fn1".to_string(),
SymbolKind::Function,
PathBuf::from("test.rs"),
1,
));
index.clear();
assert!(index.is_empty());
}
#[test]
fn test_dependency_creation() {
let dep = Dependency::new("serde".to_string(), "1.0".to_string());
assert_eq!(dep.name, "serde");
assert_eq!(dep.version, "1.0");
assert!(!dep.optional);
assert!(!dep.dev);
assert!(!dep.build);
}
#[test]
fn test_dependency_builder() {
let dep = Dependency::new("tokio".to_string(), "1.0".to_string())
.with_features(vec!["full".to_string()])
.optional()
.dev();
assert!(dep.optional);
assert!(dep.dev);
assert_eq!(dep.features, vec!["full".to_string()]);
}
#[test]
fn test_dependency_build() {
let dep = Dependency::new("proc-macro2".to_string(), "1.0".to_string()).build();
assert!(dep.build);
}
#[test]
fn test_dependency_graph_parse() {
let toml_content = r#"
[package]
name = "test"
version = "0.1.0"
[dependencies]
serde = "1.0"
tokio = { version = "1.35", features = ["full"] }
[dev-dependencies]
tempfile = "3.9"
[features]
default = []
full = ["tokio/full"]
"#;
let graph = DependencyGraph::parse(toml_content).unwrap();
assert_eq!(graph.package_name, Some("test".to_string()));
assert_eq!(graph.package_version, Some("0.1.0".to_string()));
assert_eq!(graph.dependencies.len(), 2);
assert_eq!(graph.dev_dependencies.len(), 1);
assert!(graph.features.contains_key("full"));
}
#[test]
fn test_dependency_graph_find() {
let toml_content = r#"
[package]
name = "test"
version = "0.1.0"
[dependencies]
serde = "1.0"
"#;
let graph = DependencyGraph::parse(toml_content).unwrap();
assert!(graph.find("serde").is_some());
assert!(graph.find("nonexistent").is_none());
}
#[test]
fn test_dependency_graph_count() {
let toml_content = r#"
[package]
name = "test"
version = "0.1.0"
[dependencies]
a = "1.0"
b = "1.0"
[dev-dependencies]
c = "1.0"
"#;
let graph = DependencyGraph::parse(toml_content).unwrap();
assert_eq!(graph.count(), 3);
}
#[test]
fn test_git_state_new() {
let state = GitState::new();
assert!(state.branch.is_none());
assert!(state.commit.is_none());
assert!(!state.dirty);
}
#[test]
fn test_git_state_summary() {
let mut state = GitState::new();
state.branch = Some("main".to_string());
state.dirty = true;
state.modified = vec![PathBuf::from("file.rs")];
state.ahead = 2;
let summary = state.summary();
assert!(summary.contains("main"));
assert!(summary.contains("changes"));
assert!(summary.contains("↑2"));
}
#[test]
fn test_detect_language() {
assert_eq!(detect_language("rs"), Some("Rust".to_string()));
assert_eq!(detect_language("py"), Some("Python".to_string()));
assert_eq!(detect_language("js"), Some("JavaScript".to_string()));
assert_eq!(detect_language("ts"), Some("TypeScript".to_string()));
assert_eq!(detect_language("go"), Some("Go".to_string()));
assert_eq!(detect_language("unknown"), None);
}
#[test]
fn test_detect_language_case_insensitive() {
assert_eq!(detect_language("RS"), Some("Rust".to_string()));
assert_eq!(detect_language("Py"), Some("Python".to_string()));
}
#[test]
fn test_file_entry_from_path() {
let temp = TempDir::new().unwrap();
let file_path = temp.path().join("test.rs");
std::fs::write(&file_path, "fn main() {}").unwrap();
let entry = FileEntry::from_path(file_path).unwrap();
assert_eq!(entry.extension, Some("rs".to_string()));
assert_eq!(entry.language, Some("Rust".to_string()));
assert!(entry.size > 0);
}
#[test]
fn test_file_entry_count_lines() {
let temp = TempDir::new().unwrap();
let file_path = temp.path().join("test.rs");
std::fs::write(&file_path, "line1\nline2\nline3").unwrap();
let mut entry = FileEntry::from_path(file_path).unwrap();
let count = entry.count_lines().unwrap();
assert_eq!(count, 3);
assert_eq!(entry.lines, Some(3));
}
#[test]
fn test_file_index_add_get() {
let mut index = FileIndex::new();
let entry = FileEntry {
path: PathBuf::from("src/lib.rs"),
size: 100,
modified: Utc::now(),
extension: Some("rs".to_string()),
language: Some("Rust".to_string()),
lines: None,
};
index.add(entry);
assert_eq!(index.len(), 1);
assert!(index.get(Path::new("src/lib.rs")).is_some());
}
#[test]
fn test_file_index_by_extension() {
let mut index = FileIndex::new();
index.add(FileEntry {
path: PathBuf::from("a.rs"),
size: 100,
modified: Utc::now(),
extension: Some("rs".to_string()),
language: Some("Rust".to_string()),
lines: None,
});
index.add(FileEntry {
path: PathBuf::from("b.rs"),
size: 100,
modified: Utc::now(),
extension: Some("rs".to_string()),
language: Some("Rust".to_string()),
lines: None,
});
let rust_files = index.by_extension("rs");
assert_eq!(rust_files.len(), 2);
}
#[test]
fn test_file_index_by_language() {
let mut index = FileIndex::new();
index.add(FileEntry {
path: PathBuf::from("a.rs"),
size: 100,
modified: Utc::now(),
extension: Some("rs".to_string()),
language: Some("Rust".to_string()),
lines: None,
});
let rust_files = index.by_language("Rust");
assert_eq!(rust_files.len(), 1);
}
#[test]
fn test_file_index_remove() {
let mut index = FileIndex::new();
let path = PathBuf::from("test.rs");
index.add(FileEntry {
path: path.clone(),
size: 100,
modified: Utc::now(),
extension: Some("rs".to_string()),
language: Some("Rust".to_string()),
lines: None,
});
assert_eq!(index.len(), 1);
index.remove(&path);
assert_eq!(index.len(), 0);
}
#[test]
fn test_file_index_clear() {
let mut index = FileIndex::new();
index.add(FileEntry {
path: PathBuf::from("test.rs"),
size: 100,
modified: Utc::now(),
extension: Some("rs".to_string()),
language: Some("Rust".to_string()),
lines: None,
});
index.clear();
assert!(index.is_empty());
}
#[test]
fn test_file_index_language_stats() {
let mut index = FileIndex::new();
index.add(FileEntry {
path: PathBuf::from("a.rs"),
size: 100,
modified: Utc::now(),
extension: Some("rs".to_string()),
language: Some("Rust".to_string()),
lines: None,
});
index.add(FileEntry {
path: PathBuf::from("b.py"),
size: 100,
modified: Utc::now(),
extension: Some("py".to_string()),
language: Some("Python".to_string()),
lines: None,
});
let stats = index.language_stats();
assert_eq!(stats.get("Rust"), Some(&1));
assert_eq!(stats.get("Python"), Some(&1));
}
#[test]
fn test_pattern_category_icons() {
assert_eq!(PatternCategory::Design.icon(), "🏗️");
assert_eq!(PatternCategory::AntiPattern.icon(), "⚠️");
assert_eq!(PatternCategory::Convention.icon(), "📏");
assert_eq!(PatternCategory::Security.icon(), "🔒");
assert_eq!(PatternCategory::Performance.icon(), "⚡");
assert_eq!(PatternCategory::Testing.icon(), "🧪");
}
#[test]
fn test_code_pattern_creation() {
let mut pattern = CodePattern::new(
"unwrap_usage".to_string(),
"Direct unwrap calls".to_string(),
PatternCategory::AntiPattern,
);
pattern.add_location(PathBuf::from("test.rs"), 10, ".unwrap()".to_string());
assert_eq!(pattern.locations.len(), 1);
}
#[test]
fn test_pattern_rule_creation() {
let rule = PatternRule::new(
"test_rule",
PatternCategory::Convention,
"Test description",
r"fn\s+test",
)
.unwrap();
assert_eq!(rule.name, "test_rule");
assert!(rule.regex.is_match("fn test()"));
}
#[test]
fn test_pattern_detector_new() {
let detector = PatternDetector::new();
assert!(!detector.rules.is_empty());
}
#[test]
fn test_pattern_detector_analyze() {
let mut detector = PatternDetector::new();
let content = r#"
fn main() {
let x = Some(1).unwrap();
// TODO: fix this
}
"#;
detector.analyze(Path::new("test.rs"), content);
let patterns = detector.patterns();
assert!(!patterns.is_empty());
}
#[test]
fn test_pattern_detector_by_category() {
let mut detector = PatternDetector::new();
let content = "let x = val.unwrap();";
detector.analyze(Path::new("test.rs"), content);
let anti = detector.by_category(&PatternCategory::AntiPattern);
assert!(!anti.is_empty());
}
#[test]
fn test_pattern_detector_anti_patterns() {
let mut detector = PatternDetector::new();
detector.analyze(Path::new("test.rs"), ".unwrap()");
let anti = detector.anti_patterns();
assert!(!anti.is_empty());
}
#[test]
fn test_pattern_detector_clear() {
let mut detector = PatternDetector::new();
detector.analyze(Path::new("test.rs"), ".unwrap()");
detector.clear();
assert!(detector.patterns().is_empty());
}
#[test]
fn test_pattern_detector_add_rule() {
let mut detector = PatternDetector::new();
let rule = PatternRule::new(
"custom",
PatternCategory::Convention,
"Custom rule",
r"custom_pattern",
)
.unwrap();
let initial_count = detector.rules.len();
detector.add_rule(rule);
assert_eq!(detector.rules.len(), initial_count + 1);
}
#[test]
fn test_pattern_detector_summary() {
let mut detector = PatternDetector::new();
detector.analyze(Path::new("test.rs"), "val.unwrap(); // TODO: fix");
let summary = detector.summary();
assert!(summary.contains_key(&PatternCategory::AntiPattern));
assert!(summary.contains_key(&PatternCategory::Convention));
}
#[test]
fn test_project_intelligence_new() {
let intel = ProjectIntelligence::new(PathBuf::from("/tmp/test"));
assert_eq!(intel.root(), Path::new("/tmp/test"));
}
#[test]
fn test_project_intelligence_accessors() {
let intel = ProjectIntelligence::new(PathBuf::from("/tmp/test"));
assert!(intel
.symbols()
.read()
.unwrap_or_else(|e| e.into_inner())
.is_empty());
assert!(intel
.files()
.read()
.unwrap_or_else(|e| e.into_inner())
.is_empty());
}
#[test]
fn test_project_intelligence_search_empty() {
let intel = ProjectIntelligence::new(PathBuf::from("/tmp/test"));
let results = intel.search("test");
assert!(results.is_empty());
}
#[test]
fn test_search_result_display() {
let sym = Symbol::new(
"test".to_string(),
SymbolKind::Function,
PathBuf::from("test.rs"),
1,
);
let result = SearchResult::Symbol(sym);
assert!(result.display().contains("test"));
}
#[test]
fn test_search_result_file_display() {
let entry = FileEntry {
path: PathBuf::from("src/lib.rs"),
size: 100,
modified: Utc::now(),
extension: Some("rs".to_string()),
language: Some("Rust".to_string()),
lines: None,
};
let result = SearchResult::File(entry);
assert!(result.display().contains("lib.rs"));
}
#[test]
fn test_search_result_pattern_display() {
let pattern = CodePattern::new(
"test_pattern".to_string(),
"Test".to_string(),
PatternCategory::Testing,
);
let result = SearchResult::Pattern(pattern);
assert!(result.display().contains("test_pattern"));
}
#[test]
fn test_project_intelligence_refresh_in_temp() {
let temp = TempDir::new().unwrap();
let cargo_path = temp.path().join("Cargo.toml");
std::fs::write(
&cargo_path,
r#"
[package]
name = "test"
version = "0.1.0"
[dependencies]
serde = "1.0"
"#,
)
.unwrap();
let src_dir = temp.path().join("src");
std::fs::create_dir(&src_dir).unwrap();
std::fs::write(src_dir.join("lib.rs"), "pub fn hello() {}").unwrap();
let mut intel = ProjectIntelligence::new(temp.path().to_path_buf());
intel.refresh().unwrap();
let deps = intel
.dependencies()
.read()
.unwrap_or_else(|e| e.into_inner());
assert!(deps.find("serde").is_some());
}
#[test]
fn test_project_intelligence_index_files_manually() {
let intel = ProjectIntelligence::new(PathBuf::from("/tmp/test"));
let mut files = intel.files().write().unwrap_or_else(|e| e.into_inner());
files.add(FileEntry {
path: PathBuf::from("test.rs"),
size: 100,
modified: Utc::now(),
extension: Some("rs".to_string()),
language: Some("Rust".to_string()),
lines: Some(10),
});
assert_eq!(files.len(), 1);
}
#[test]
fn test_rust_symbol_indexing() {
let intel = ProjectIntelligence::new(PathBuf::from("/tmp/test"));
let mut index = SymbolIndex::new();
let content = r#"
pub fn public_function() {}
fn private_function() {}
pub struct MyStruct {}
pub enum MyEnum {}
pub trait MyTrait {}
impl MyStruct {}
pub const MY_CONST: u32 = 1;
pub type MyType = u32;
macro_rules! my_macro { () => {} }
"#;
intel.index_rust_symbols(&mut index, Path::new("test.rs"), content);
assert!(!index.functions().is_empty());
assert!(!index.structs().is_empty());
assert!(index.get("MyEnum").is_some());
assert!(index.get("MyTrait").is_some());
assert!(index.get("MyStruct").is_some()); }
}