use std::collections::HashMap;
pub struct CrateIndex {
pub crate_name: String,
pub version: String,
pub items: HashMap<String, IndexedItem>,
pub modules: HashMap<String, Vec<String>>,
pub impl_blocks: HashMap<String, Vec<ImplBlock>>,
pub root_items: Vec<String>,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct IndexedItem {
pub path: String,
pub name: String,
pub kind: ItemKind,
pub signature: String,
pub short_doc: String,
pub doc: String,
pub detail: ItemDetail,
pub parent_module: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ItemKind {
Module,
Struct,
Enum,
Trait,
Function,
TypeAlias,
Constant,
Static,
Macro,
Union,
}
impl std::fmt::Display for ItemKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ItemKind::Module => write!(f, "mod"),
ItemKind::Struct => write!(f, "struct"),
ItemKind::Enum => write!(f, "enum"),
ItemKind::Trait => write!(f, "trait"),
ItemKind::Function => write!(f, "fn"),
ItemKind::TypeAlias => write!(f, "type"),
ItemKind::Constant => write!(f, "const"),
ItemKind::Static => write!(f, "static"),
ItemKind::Macro => write!(f, "macro"),
ItemKind::Union => write!(f, "union"),
}
}
}
#[derive(Debug, Clone, Default)]
#[allow(dead_code)]
pub struct ItemDetail {
pub fields: Vec<FieldInfo>,
pub variants: Vec<VariantInfo>,
pub methods: Vec<MethodInfo>,
pub derives: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct FieldInfo {
pub name: String,
pub type_str: String,
pub doc: String,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct VariantInfo {
pub name: String,
pub signature: String,
pub doc: String,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct MethodInfo {
pub name: String,
pub signature: String,
pub doc: String,
pub is_required: bool,
}
#[derive(Debug, Clone)]
pub struct ImplBlock {
pub header: String,
pub trait_name: Option<String>,
pub methods: Vec<MethodInfo>,
}
pub struct SearchResult {
pub item: IndexedItem,
pub score: SearchScore,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SearchScore {
Exact = 4,
Prefix = 3,
NameContains = 2,
PathContains = 1,
DocContains = 0,
}
impl CrateIndex {
pub fn search(&self, query: &str, limit: usize) -> Vec<SearchResult> {
let query_lower = query.to_lowercase();
let mut results: Vec<SearchResult> = self
.items
.values()
.filter_map(|item| {
let name_lower = item.name.to_lowercase();
let path_lower = item.path.to_lowercase();
let doc_lower = item.doc.to_lowercase();
let score = if name_lower == query_lower {
SearchScore::Exact
} else if name_lower.starts_with(&query_lower) {
SearchScore::Prefix
} else if name_lower.contains(&query_lower) {
SearchScore::NameContains
} else if path_lower.contains(&query_lower) {
SearchScore::PathContains
} else if doc_lower.contains(&query_lower) {
SearchScore::DocContains
} else {
return None;
};
Some(SearchResult {
item: item.clone(),
score,
})
})
.collect();
results.sort_by(|a, b| {
b.score
.cmp(&a.score)
.then_with(|| a.item.path.cmp(&b.item.path))
});
results.truncate(limit);
results
}
pub fn get_module_items(&self, module_path: Option<&str>) -> Vec<&IndexedItem> {
let children = match module_path {
Some(path) => self.modules.get(path),
None => Some(&self.root_items),
};
let Some(children) = children else {
return Vec::new();
};
let mut items: Vec<&IndexedItem> = children
.iter()
.filter_map(|path| self.items.get(path))
.collect();
items.sort_by(|a, b| {
let a_is_mod = a.kind == ItemKind::Module;
let b_is_mod = b.kind == ItemKind::Module;
b_is_mod
.cmp(&a_is_mod)
.then_with(|| a.kind.to_string().cmp(&b.kind.to_string()))
.then_with(|| a.name.cmp(&b.name))
});
items
}
pub fn get_item(&self, item_path: &str) -> Option<&IndexedItem> {
if let Some(item) = self.items.get(item_path) {
return Some(item);
}
let full_path = format!("{}::{}", self.crate_name, item_path);
self.items.get(&full_path)
}
pub fn get_impl_blocks(&self, item_path: &str) -> Vec<&ImplBlock> {
let mut result = Vec::new();
if let Some(impls) = self.impl_blocks.get(item_path) {
result.extend(impls.iter());
}
let full_path = format!("{}::{}", self.crate_name, item_path);
if let Some(impls) = self.impl_blocks.get(&full_path) {
result.extend(impls.iter());
}
result
}
pub fn suggest_similar(&self, query: &str, max_suggestions: usize) -> Vec<String> {
let query_lower = query.to_lowercase();
let mut scored: Vec<(String, usize)> = self
.items
.keys()
.map(|path| {
let name = path.rsplit("::").next().unwrap_or(path);
let name_lower = name.to_lowercase();
let path_lower = path.to_lowercase();
let d1 = levenshtein(&query_lower, &name_lower);
let d2 = levenshtein(&query_lower, &path_lower);
(path.clone(), d1.min(d2))
})
.collect();
scored.sort_by_key(|(_, d)| *d);
scored.truncate(max_suggestions);
let threshold = query.len() / 2 + 3;
scored
.into_iter()
.filter(|(_, d)| *d <= threshold)
.map(|(path, _)| path)
.collect()
}
}
fn levenshtein(a: &str, b: &str) -> usize {
let a_len = a.len();
let b_len = b.len();
if a_len == 0 {
return b_len;
}
if b_len == 0 {
return a_len;
}
let mut prev: Vec<usize> = (0..=b_len).collect();
let mut curr = vec![0; b_len + 1];
for (i, a_ch) in a.chars().enumerate() {
curr[0] = i + 1;
for (j, b_ch) in b.chars().enumerate() {
let cost = if a_ch == b_ch { 0 } else { 1 };
curr[j + 1] = (prev[j + 1] + 1).min(curr[j] + 1).min(prev[j] + cost);
}
std::mem::swap(&mut prev, &mut curr);
}
prev[b_len]
}