use ignore::WalkBuilder;
use regex::Regex;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub enum SymbolKind {
Function,
AsyncFunction,
Struct,
Enum,
Trait,
Impl,
Class,
Method,
Type,
Const,
Interface,
Module,
}
impl std::fmt::Display for SymbolKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
SymbolKind::Function => "fn",
SymbolKind::AsyncFunction => "async fn",
SymbolKind::Struct => "struct",
SymbolKind::Enum => "enum",
SymbolKind::Trait => "trait",
SymbolKind::Impl => "impl",
SymbolKind::Class => "class",
SymbolKind::Method => "method",
SymbolKind::Type => "type",
SymbolKind::Const => "const",
SymbolKind::Interface => "interface",
SymbolKind::Module => "mod",
};
write!(f, "{}", s)
}
}
#[derive(Debug, Clone)]
pub struct Symbol {
pub name: String,
pub kind: SymbolKind,
pub file: PathBuf,
pub line_start: usize,
pub line_end: usize,
}
pub struct SymbolIndex {
index: HashMap<String, Vec<Symbol>>,
workspace: PathBuf,
}
impl SymbolIndex {
pub fn build(workspace: &Path) -> Self {
let mut index: HashMap<String, Vec<Symbol>> = HashMap::new();
let walker = WalkBuilder::new(workspace)
.hidden(false)
.ignore(true)
.git_ignore(true)
.build();
for entry in walker.flatten() {
let path = entry.path();
if !path.is_file() {
continue;
}
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if !is_source_ext(ext) {
continue;
}
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => continue,
};
let rel = path.strip_prefix(workspace).unwrap_or(path).to_path_buf();
for sym in extract_symbols(&content, &rel, ext) {
index.entry(sym.name.to_lowercase()).or_default().push(sym);
}
}
Self {
index,
workspace: workspace.to_path_buf(),
}
}
pub fn find(&self, query: &str, limit: usize) -> Vec<&Symbol> {
let q = query.to_lowercase();
let mut results: Vec<&Symbol> = Vec::new();
if let Some(exact) = self.index.get(&q) {
results.extend(exact.iter());
}
if results.len() < limit {
for (key, syms) in &self.index {
if key == &q {
continue; }
if key.contains(q.as_str()) {
for sym in syms {
if results.len() >= limit {
break;
}
results.push(sym);
}
}
if results.len() >= limit {
break;
}
}
}
results.truncate(limit);
results
}
pub fn read_body(&self, name: &str) -> Option<String> {
let sym = self.index.get(&name.to_lowercase())?.first()?;
let full = self.workspace.join(&sym.file);
let content = std::fs::read_to_string(&full).ok()?;
let lines: Vec<&str> = content.lines().collect();
let start = sym.line_start.saturating_sub(1).min(lines.len());
let end = sym.line_end.min(lines.len());
if start > end {
return None;
}
Some(format!(
"// {}:{}\n{}",
sym.file.display(),
sym.line_start,
lines[start..end].join("\n")
))
}
pub fn symbol_count(&self) -> usize {
self.index.values().map(|v| v.len()).sum()
}
}
fn is_source_ext(ext: &str) -> bool {
matches!(
ext,
"rs" | "py"
| "js"
| "jsx"
| "ts"
| "tsx"
| "go"
| "java"
| "kt"
| "rb"
| "c"
| "cc"
| "cpp"
| "cxx"
| "h"
| "hpp"
| "hxx"
)
}
struct PatternDef {
re: Regex,
kind: SymbolKind,
}
fn extract_symbols(content: &str, file: &Path, ext: &str) -> Vec<Symbol> {
let pats = patterns_for(ext);
let lines: Vec<&str> = content.lines().collect();
let mut out: Vec<Symbol> = Vec::new();
let mut seen_positions = std::collections::HashSet::new();
for pd in pats {
for cap in pd.re.captures_iter(content) {
let name = match cap.get(1) {
Some(m) => m.as_str().to_string(),
None => continue,
};
if name.len() > 64 {
continue;
}
let byte_pos = cap.get(0).unwrap().start();
let line_start = content[..byte_pos].matches('\n').count() + 1;
if !seen_positions.insert(line_start) {
continue; }
let line_end = estimate_end(&lines, line_start - 1, ext);
out.push(Symbol {
name,
kind: pd.kind.clone(),
file: file.to_path_buf(),
line_start,
line_end,
});
}
}
out.sort_by_key(|s| s.line_start);
out
}
fn build_patterns(specs: &[(&str, SymbolKind)]) -> Vec<PatternDef> {
specs
.iter()
.filter_map(|(re_str, kind)| {
Regex::new(re_str).ok().map(|re| PatternDef {
re,
kind: kind.clone(),
})
})
.collect()
}
fn patterns_for(ext: &str) -> &'static [PatternDef] {
use std::sync::OnceLock;
static RS: OnceLock<Vec<PatternDef>> = OnceLock::new();
static PY: OnceLock<Vec<PatternDef>> = OnceLock::new();
static JS: OnceLock<Vec<PatternDef>> = OnceLock::new();
static GO: OnceLock<Vec<PatternDef>> = OnceLock::new();
static JV: OnceLock<Vec<PatternDef>> = OnceLock::new();
static CC: OnceLock<Vec<PatternDef>> = OnceLock::new();
match ext {
"rs" => RS.get_or_init(|| build_patterns(&[
(r"(?m)^[ \t]*(?:pub(?:\([^)]*\))?\s+)?async\s+fn\s+(\w+)", SymbolKind::AsyncFunction),
(r"(?m)^[ \t]*(?:pub(?:\([^)]*\))?\s+)?fn\s+(\w+)", SymbolKind::Function),
(r"(?m)^[ \t]*(?:pub(?:\([^)]*\))?\s+)?struct\s+(\w+)", SymbolKind::Struct),
(r"(?m)^[ \t]*(?:pub(?:\([^)]*\))?\s+)?enum\s+(\w+)", SymbolKind::Enum),
(r"(?m)^[ \t]*(?:pub(?:\([^)]*\))?\s+)?trait\s+(\w+)", SymbolKind::Trait),
(r"(?m)^[ \t]*impl(?:<[^>]*>)?\s+(\w+)", SymbolKind::Impl),
(r"(?m)^[ \t]*(?:pub(?:\([^)]*\))?\s+)?type\s+(\w+)", SymbolKind::Type),
(r"(?m)^[ \t]*(?:pub(?:\([^)]*\))?\s+)?(?:const|static)\s+(\w+)", SymbolKind::Const),
(r"(?m)^[ \t]*(?:pub(?:\([^)]*\))?\s+)?mod\s+(\w+)", SymbolKind::Module),
])),
"py" => PY.get_or_init(|| build_patterns(&[
(r"(?m)^[ \t]*async\s+def\s+(\w+)", SymbolKind::AsyncFunction),
(r"(?m)^[ \t]*def\s+(\w+)", SymbolKind::Function),
(r"(?m)^[ \t]*class\s+(\w+)", SymbolKind::Class),
])),
"js" | "jsx" | "ts" | "tsx" => JS.get_or_init(|| build_patterns(&[
(r"(?m)^[ \t]*(?:export\s+)?(?:default\s+)?async\s+function\s+(\w+)", SymbolKind::AsyncFunction),
(r"(?m)^[ \t]*(?:export\s+)?(?:default\s+)?function\s+(\w+)", SymbolKind::Function),
(r"(?m)^[ \t]*(?:export\s+)?(?:default\s+)?class\s+(\w+)", SymbolKind::Class),
(r"(?m)^[ \t]*(?:export\s+)?interface\s+(\w+)", SymbolKind::Interface),
(r"(?m)^[ \t]*(?:export\s+)?type\s+(\w+)\s*=", SymbolKind::Type),
(r"(?m)^[ \t]*(?:export\s+)?(?:const|let)\s+(\w+)\s*[=:]", SymbolKind::Const),
])),
"go" => GO.get_or_init(|| build_patterns(&[
(r"(?m)^func\s+(?:\([^)]*\)\s+)?(\w+)", SymbolKind::Function),
(r"(?m)^type\s+(\w+)\s+struct", SymbolKind::Struct),
(r"(?m)^type\s+(\w+)\s+interface", SymbolKind::Interface),
(r"(?m)^type\s+(\w+)\b", SymbolKind::Type),
(r"(?m)^const\s+(\w+)", SymbolKind::Const),
])),
"java" | "kt" | "rb" => JV.get_or_init(|| build_patterns(&[
(r"(?m)^[ \t]*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:abstract\s+)?(?:class|record)\s+(\w+)", SymbolKind::Class),
(r"(?m)^[ \t]*(?:public\s+|private\s+|protected\s+)?interface\s+(\w+)", SymbolKind::Interface),
])),
"c" | "cc" | "cpp" | "cxx" | "h" | "hpp" | "hxx" => CC.get_or_init(|| build_patterns(&[
(r"(?m)^[ \t]*struct\s+(\w+)", SymbolKind::Struct),
(r"(?m)^[ \t]*class\s+(\w+)", SymbolKind::Class),
(r"(?m)^[ \t]*enum\s+(?:class\s+)?(\w+)", SymbolKind::Enum),
(r"(?m)^[ \t]*union\s+(\w+)", SymbolKind::Struct),
(r"(?m)^[ \t]*namespace\s+(\w+)", SymbolKind::Module),
(r"(?m)^[ \t]*using\s+(\w+)\s*=", SymbolKind::Type),
(r"(?m)^[ \t]*typedef\s+[\w\s*&:<>]+\s+(\w+)\s*;", SymbolKind::Type),
(r"(?m)^[ \t]*template\s*<[^>]*>\s*(?:class|struct)\s+(\w+)", SymbolKind::Struct),
(r"(?m)^[ \t]*(?:(?:static|inline|extern|virtual|explicit|constexpr|override|friend|__attribute__\S*)\s+)*(?:[\w:*&<>\[\]~]+\s+)+(\w+)\s*\(", SymbolKind::Function),
])),
_ => {
static EMPTY: OnceLock<Vec<PatternDef>> = OnceLock::new();
EMPTY.get_or_init(Vec::new)
}
}
}
fn estimate_end(lines: &[&str], start_idx: usize, ext: &str) -> usize {
let total = lines.len();
if start_idx >= total {
return total;
}
if ext == "py" {
let base = leading_spaces(lines[start_idx]);
for i in (start_idx + 1)..total {
let line = lines[i];
if line.trim().is_empty() || line.trim().starts_with('#') {
continue;
}
if leading_spaces(line) <= base {
return i;
}
}
return total;
}
let mut depth: i32 = 0;
let mut found_open = false;
let limit = (start_idx + 250).min(total);
for i in start_idx..limit {
for ch in lines[i].chars() {
match ch {
'{' => {
depth += 1;
found_open = true;
}
'}' => {
depth -= 1;
if found_open && depth == 0 {
return i + 1;
}
}
_ => {}
}
}
}
limit
}
fn leading_spaces(s: &str) -> usize {
s.len() - s.trim_start().len()
}