use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Span {
pub start_line: usize,
pub end_line: usize,
}
impl Span {
pub fn new(start_line: usize, start_col: usize, end_line: usize, end_col: usize) -> Self {
let _ = (start_col, end_col);
Self {
start_line,
end_line,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, EnumString, Display)]
#[strum(serialize_all = "PascalCase")]
pub enum SymbolKind {
Function,
Class,
Struct,
Enum,
Interface,
Trait,
Constant,
Variable,
Method,
Module,
Namespace,
Type,
Macro,
Property,
Event,
Import,
Export,
Attribute,
#[strum(default)]
Unknown(String),
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum Language {
#[default]
Rust,
Python,
JavaScript,
TypeScript,
Vue,
Svelte,
Go,
Java,
PHP,
C,
Cpp,
CSharp,
Ruby,
Kotlin,
Swift,
Zig,
Unknown,
}
impl Language {
pub fn from_extension(ext: &str) -> Self {
match ext {
"rs" => Language::Rust,
"py" => Language::Python,
"js" | "mjs" | "cjs" | "jsx" => Language::JavaScript,
"ts" | "mts" | "cts" | "tsx" => Language::TypeScript,
"vue" => Language::Vue,
"svelte" => Language::Svelte,
"go" => Language::Go,
"java" => Language::Java,
"php" => Language::PHP,
"c" | "h" => Language::C,
"cpp" | "cc" | "cxx" | "hpp" | "hxx" | "C" | "H" => Language::Cpp,
"cs" => Language::CSharp,
"rb" | "rake" | "gemspec" => Language::Ruby,
"kt" | "kts" => Language::Kotlin,
"swift" => Language::Swift,
"zig" => Language::Zig,
_ => Language::Unknown,
}
}
pub fn from_name(name: &str) -> Option<Self> {
match name.to_lowercase().as_str() {
"rust" | "rs" => Some(Language::Rust),
"python" | "py" => Some(Language::Python),
"javascript" | "js" => Some(Language::JavaScript),
"typescript" | "ts" => Some(Language::TypeScript),
"vue" => Some(Language::Vue),
"svelte" => Some(Language::Svelte),
"go" => Some(Language::Go),
"java" => Some(Language::Java),
"php" => Some(Language::PHP),
"c" => Some(Language::C),
"cpp" | "c++" => Some(Language::Cpp),
"csharp" | "cs" | "c#" => Some(Language::CSharp),
"ruby" | "rb" => Some(Language::Ruby),
"kotlin" | "kt" => Some(Language::Kotlin),
"zig" => Some(Language::Zig),
_ => None,
}
}
pub fn supported_names_help() -> &'static str {
"rust (rs), python (py), javascript (js), typescript (ts), vue, svelte, \
go, java, php, c, cpp (c++), csharp (cs, c#), ruby (rb), kotlin (kt), zig"
}
pub fn is_supported(&self) -> bool {
match self {
Language::Rust => true,
Language::TypeScript => true,
Language::JavaScript => true,
Language::Vue => true,
Language::Svelte => true,
Language::Python => true,
Language::Go => true,
Language::Java => true,
Language::PHP => true,
Language::C => true,
Language::Cpp => true,
Language::CSharp => true,
Language::Ruby => true,
Language::Kotlin => true,
Language::Swift => false, Language::Zig => true,
Language::Unknown => false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ImportType {
Internal,
External,
Stdlib,
#[serde(rename = "mod_decl")]
ModDecl,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyInfo {
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbols: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct Dependency {
pub file_id: i64,
pub imported_path: String,
pub resolved_file_id: Option<i64>,
pub import_type: ImportType,
pub line_number: usize,
pub imported_symbols: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SymbolRef {
pub name: String,
pub kind: SymbolKind,
pub span: Span,
}
fn is_unknown_kind(kind: &SymbolKind) -> bool {
matches!(kind, SymbolKind::Unknown(_))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResult {
pub path: String,
#[serde(skip)]
pub lang: Language,
#[serde(skip_serializing_if = "is_unknown_kind")]
pub kind: SymbolKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol: Option<String>,
pub span: Span,
pub preview: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub dependencies: Option<Vec<DependencyInfo>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MatchResult {
#[serde(skip_serializing_if = "is_unknown_kind")]
pub kind: SymbolKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol: Option<String>,
pub span: Span,
pub preview: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub context_before: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub context_after: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileGroupedResult {
pub path: String,
pub language: Language,
#[serde(skip_serializing_if = "Option::is_none")]
pub dependencies: Option<Vec<DependencyInfo>>,
pub matches: Vec<MatchResult>,
}
impl SearchResult {
pub fn new(
path: String,
lang: Language,
kind: SymbolKind,
symbol: Option<String>,
span: Span,
scope: Option<String>,
preview: String,
) -> Self {
let _ = scope;
Self {
path,
lang,
kind,
symbol,
span,
preview,
dependencies: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexConfig {
pub languages: Vec<Language>,
pub include_patterns: Vec<String>,
pub exclude_patterns: Vec<String>,
pub follow_symlinks: bool,
pub max_file_size: usize,
pub parallel_threads: usize,
pub query_timeout_secs: u64,
pub max_posting_list_entries: usize,
}
impl Default for IndexConfig {
fn default() -> Self {
Self {
languages: vec![],
include_patterns: vec![],
exclude_patterns: vec![],
follow_symlinks: false,
max_file_size: 10 * 1024 * 1024, parallel_threads: 0, query_timeout_secs: 30, max_posting_list_entries: 500_000, }
}
}
fn is_zero(v: &usize) -> bool {
*v == 0
}
fn is_zero_u64(v: &u64) -> bool {
*v == 0
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct IndexStats {
pub total_files: usize,
pub index_size_bytes: u64,
pub last_updated: String,
pub files_by_language: std::collections::HashMap<String, usize>,
pub lines_by_language: std::collections::HashMap<String, usize>,
#[serde(default, skip_serializing_if = "is_zero")]
pub new_files: usize,
#[serde(default, skip_serializing_if = "is_zero")]
pub modified_files: usize,
#[serde(default, skip_serializing_if = "is_zero")]
pub unchanged_files: usize,
#[serde(default, skip_serializing_if = "is_zero")]
pub skipped_too_large: usize,
#[serde(default, skip_serializing_if = "is_zero_u64")]
pub skipped_bytes_too_large: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexedFile {
pub path: String,
pub language: String,
pub last_indexed: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum IndexStatus {
Fresh,
Stale,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexWarning {
pub reason: String,
pub action_required: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub files_modified: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<IndexWarningDetails>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexWarningDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub current_branch: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub indexed_branch: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_commit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub indexed_commit: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginationInfo {
pub total: usize,
pub count: usize,
pub offset: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
pub has_more: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub ai_instruction: Option<String>,
pub status: IndexStatus,
pub can_trust_results: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub warning: Option<IndexWarning>,
pub pagination: PaginationInfo,
pub results: Vec<FileGroupedResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompactionReport {
pub files_removed: usize,
pub space_saved_bytes: u64,
pub duration_ms: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_symbol_ref_json_shape() {
let sym = SymbolRef {
name: "my_function".to_string(),
kind: SymbolKind::Function,
span: Span {
start_line: 10,
end_line: 20,
},
};
let json = serde_json::to_value(&sym).unwrap();
assert_eq!(json["name"], "my_function");
assert_eq!(json["kind"], "Function");
assert_eq!(json["span"]["start_line"], 10);
assert_eq!(json["span"]["end_line"], 20);
assert!(json.as_array().is_none());
}
#[test]
fn test_symbol_ref_roundtrip() {
let original = SymbolRef {
name: "MyStruct".to_string(),
kind: SymbolKind::Struct,
span: Span {
start_line: 1,
end_line: 5,
},
};
let json = serde_json::to_string(&original).unwrap();
let decoded: SymbolRef = serde_json::from_str(&json).unwrap();
assert_eq!(original, decoded);
}
#[test]
fn test_symbol_ref_exact_json() {
let sym = SymbolRef {
name: "Foo".to_string(),
kind: SymbolKind::Class,
span: Span {
start_line: 3,
end_line: 7,
},
};
let json = serde_json::to_string(&sym).unwrap();
assert_eq!(
json,
r#"{"name":"Foo","kind":"Class","span":{"start_line":3,"end_line":7}}"#
);
}
}