1use serde::{Deserialize, Serialize};
7use strum::{Display, EnumString};
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11pub struct Span {
12 pub start_line: usize,
14 pub end_line: usize,
16}
17
18impl Span {
19 pub fn new(start_line: usize, start_col: usize, end_line: usize, end_col: usize) -> Self {
20 let _ = (start_col, end_col);
22 Self {
23 start_line,
24 end_line,
25 }
26 }
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, EnumString, Display)]
31#[strum(serialize_all = "PascalCase")]
32pub enum SymbolKind {
33 Function,
34 Class,
35 Struct,
36 Enum,
37 Interface,
38 Trait,
39 Constant,
40 Variable,
41 Method,
42 Module,
43 Namespace,
44 Type,
45 Macro,
46 Property,
47 Event,
48 Import,
49 Export,
50 Attribute,
51 #[strum(default)]
55 Unknown(String),
56}
57
58#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
60#[serde(rename_all = "lowercase")]
61pub enum Language {
62 #[default]
63 Rust,
64 Python,
65 JavaScript,
66 TypeScript,
67 Vue,
68 Svelte,
69 Go,
70 Java,
71 PHP,
72 C,
73 Cpp,
74 CSharp,
75 Ruby,
76 Kotlin,
77 Swift,
78 Zig,
79 Text,
88 Unknown,
89}
90
91const TEXT_EXTENSIONS: &[&str] = &[
96 "md", "mdx", "txt", "yaml", "yml", "toml", "json", "proto", "html", "htm", "sh", "bash", "ini",
97 "cfg", "sql", "graphql",
98];
99
100const TEXT_FILENAME_EXCLUSIONS: &[&str] = &[
106 "package-lock.json",
107 "composer.lock",
108 "yarn.lock",
109 "pnpm-lock.yaml",
110 "Cargo.lock",
111 "poetry.lock",
112 "Gemfile.lock",
113];
114
115pub fn is_text_tier_file(file_name: &str) -> bool {
119 if TEXT_FILENAME_EXCLUSIONS
120 .iter()
121 .any(|n| n.eq_ignore_ascii_case(file_name))
122 {
123 return false;
124 }
125 if file_name.ends_with("-lock.json") || file_name.ends_with(".lock") {
127 return false;
128 }
129 match file_name.rsplit_once('.') {
130 Some((_, ext)) => TEXT_EXTENSIONS.contains(&ext.to_ascii_lowercase().as_str()),
131 None => false,
132 }
133}
134
135impl Language {
136 pub fn from_extension(ext: &str) -> Self {
137 match ext {
138 "rs" => Language::Rust,
139 "py" => Language::Python,
140 "js" | "mjs" | "cjs" | "jsx" => Language::JavaScript,
141 "ts" | "mts" | "cts" | "tsx" => Language::TypeScript,
142 "vue" => Language::Vue,
143 "svelte" => Language::Svelte,
144 "go" => Language::Go,
145 "java" => Language::Java,
146 "php" => Language::PHP,
147 "c" | "h" => Language::C,
148 "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "C" | "H" => Language::Cpp,
149 "cs" => Language::CSharp,
150 "rb" | "rake" | "gemspec" => Language::Ruby,
151 "kt" | "kts" => Language::Kotlin,
152 "swift" => Language::Swift,
153 "zig" => Language::Zig,
154 ext if TEXT_EXTENSIONS.contains(&ext.to_ascii_lowercase().as_str()) => Language::Text,
157 _ => Language::Unknown,
158 }
159 }
160
161 pub fn from_name(name: &str) -> Option<Self> {
166 match name.to_lowercase().as_str() {
167 "rust" | "rs" => Some(Language::Rust),
168 "python" | "py" => Some(Language::Python),
169 "javascript" | "js" => Some(Language::JavaScript),
170 "typescript" | "ts" => Some(Language::TypeScript),
171 "vue" => Some(Language::Vue),
172 "svelte" => Some(Language::Svelte),
173 "go" => Some(Language::Go),
174 "java" => Some(Language::Java),
175 "php" => Some(Language::PHP),
176 "c" => Some(Language::C),
177 "cpp" | "c++" => Some(Language::Cpp),
178 "csharp" | "cs" | "c#" => Some(Language::CSharp),
179 "ruby" | "rb" => Some(Language::Ruby),
180 "kotlin" | "kt" => Some(Language::Kotlin),
181 "zig" => Some(Language::Zig),
182 "text" | "txt" | "plaintext" | "plain" => Some(Language::Text),
183 _ => None,
184 }
185 }
186
187 pub fn supported_names_help() -> &'static str {
189 "rust (rs), python (py), javascript (js), typescript (ts), vue, svelte, \
190 go, java, php, c, cpp (c++), csharp (cs, c#), ruby (rb), kotlin (kt), zig, \
191 text (docs and config: md, yaml, toml, json, proto, html, sh, sql, graphql)"
192 }
193
194 pub fn is_supported(&self) -> bool {
199 match self {
200 Language::Rust => true,
201 Language::TypeScript => true,
202 Language::JavaScript => true,
203 Language::Vue => true,
204 Language::Svelte => true,
205 Language::Python => true,
206 Language::Go => true,
207 Language::Java => true,
208 Language::PHP => true,
209 Language::C => true,
210 Language::Cpp => true,
211 Language::CSharp => true,
212 Language::Ruby => true,
213 Language::Kotlin => true,
214 Language::Swift => false, Language::Zig => true,
216 Language::Text => false,
218 Language::Unknown => false,
219 }
220 }
221
222 pub fn is_text(&self) -> bool {
224 matches!(self, Language::Text)
225 }
226
227 pub fn is_indexable(&self) -> bool {
233 self.is_supported() || self.is_text()
234 }
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
239#[serde(rename_all = "lowercase")]
240pub enum ImportType {
241 Internal,
243 External,
245 Stdlib,
247 #[serde(rename = "mod_decl")]
249 ModDecl,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct DependencyInfo {
255 pub path: String,
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub line: Option<usize>,
260 #[serde(skip_serializing_if = "Option::is_none")]
262 pub symbols: Option<Vec<String>>,
263}
264
265#[derive(Debug, Clone)]
267pub struct Dependency {
268 pub file_id: i64,
270 pub imported_path: String,
272 pub resolved_file_id: Option<i64>,
274 pub import_type: ImportType,
276 pub line_number: usize,
278 pub imported_symbols: Option<Vec<String>>,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
289pub struct SymbolRef {
290 pub name: String,
292 pub kind: SymbolKind,
294 pub span: Span,
296}
297
298fn is_unknown_kind(kind: &SymbolKind) -> bool {
300 matches!(kind, SymbolKind::Unknown(_))
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct SearchResult {
306 pub path: String,
308 #[serde(skip)]
310 pub lang: Language,
311 #[serde(skip_serializing_if = "is_unknown_kind")]
313 pub kind: SymbolKind,
314 #[serde(skip_serializing_if = "Option::is_none")]
317 pub symbol: Option<String>,
318 pub span: Span,
320 pub preview: String,
322 #[serde(skip_serializing_if = "Option::is_none")]
325 pub dependencies: Option<Vec<DependencyInfo>>,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct MatchResult {
331 #[serde(skip_serializing_if = "is_unknown_kind")]
333 pub kind: SymbolKind,
334 #[serde(skip_serializing_if = "Option::is_none")]
336 pub symbol: Option<String>,
337 pub span: Span,
339 pub preview: String,
341 #[serde(skip_serializing_if = "Vec::is_empty")]
343 pub context_before: Vec<String>,
344 #[serde(skip_serializing_if = "Vec::is_empty")]
346 pub context_after: Vec<String>,
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct FileGroupedResult {
352 pub path: String,
354 pub language: Language,
356 #[serde(skip_serializing_if = "Option::is_none")]
358 pub dependencies: Option<Vec<DependencyInfo>>,
359 pub matches: Vec<MatchResult>,
361}
362
363impl SearchResult {
364 pub fn new(
365 path: String,
366 lang: Language,
367 kind: SymbolKind,
368 symbol: Option<String>,
369 span: Span,
370 scope: Option<String>,
371 preview: String,
372 ) -> Self {
373 let _ = scope;
375 Self {
376 path,
377 lang,
378 kind,
379 symbol,
380 span,
381 preview,
382 dependencies: None,
383 }
384 }
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct IndexConfig {
390 pub languages: Vec<Language>,
392 pub include_patterns: Vec<String>,
394 pub exclude_patterns: Vec<String>,
396 pub follow_symlinks: bool,
398 pub max_file_size: usize,
400 pub parallel_threads: usize,
402 pub query_timeout_secs: u64,
404 pub max_posting_list_entries: usize,
407 #[serde(default, skip_serializing_if = "is_zero_u64")]
411 pub lock_wait_secs: u64,
412 #[serde(default = "default_true")]
424 pub text_tier: bool,
425}
426
427fn default_true() -> bool {
429 true
430}
431
432impl Default for IndexConfig {
433 fn default() -> Self {
434 Self {
435 languages: vec![],
436 include_patterns: vec![],
437 exclude_patterns: vec![],
438 follow_symlinks: false,
439 max_file_size: 10 * 1024 * 1024, parallel_threads: 0, query_timeout_secs: 30, max_posting_list_entries: 500_000, text_tier: true, lock_wait_secs: 0, }
446 }
447}
448
449fn is_zero(v: &usize) -> bool {
450 *v == 0
451}
452fn is_zero_u64(v: &u64) -> bool {
453 *v == 0
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize, Default)]
458pub struct IndexStats {
459 pub total_files: usize,
461 pub index_size_bytes: u64,
463 pub last_updated: String,
465 pub files_by_language: std::collections::HashMap<String, usize>,
467 pub lines_by_language: std::collections::HashMap<String, usize>,
469 #[serde(default, skip_serializing_if = "is_zero")]
471 pub new_files: usize,
472 #[serde(default, skip_serializing_if = "is_zero")]
474 pub modified_files: usize,
475 #[serde(default, skip_serializing_if = "is_zero")]
477 pub unchanged_files: usize,
478 #[serde(default, skip_serializing_if = "is_zero")]
480 pub skipped_too_large: usize,
481 #[serde(default, skip_serializing_if = "is_zero_u64")]
483 pub skipped_bytes_too_large: u64,
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct IndexedFile {
489 pub path: String,
491 pub language: String,
493 pub last_indexed: String,
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
499#[serde(rename_all = "snake_case")]
500pub enum IndexStatus {
501 Fresh,
503 Stale,
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize)]
509pub struct IndexWarning {
510 pub reason: String,
512 pub action_required: String,
514 #[serde(skip_serializing_if = "Option::is_none")]
521 pub files_modified: Option<Vec<String>>,
522 #[serde(skip_serializing_if = "Option::is_none")]
524 pub files_added: Option<Vec<String>>,
525 #[serde(skip_serializing_if = "Option::is_none")]
527 pub files_deleted: Option<Vec<String>>,
528 #[serde(skip_serializing_if = "Option::is_none")]
530 pub changed_count: Option<usize>,
531 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
533 pub truncated: bool,
534 #[serde(skip_serializing_if = "Option::is_none")]
536 pub details: Option<IndexWarningDetails>,
537}
538
539impl IndexWarning {
540 pub fn new(reason: impl Into<String>, action_required: impl Into<String>) -> Self {
542 Self {
543 reason: reason.into(),
544 action_required: action_required.into(),
545 files_modified: None,
546 files_added: None,
547 files_deleted: None,
548 changed_count: None,
549 truncated: false,
550 details: None,
551 }
552 }
553
554 pub fn with_details(mut self, details: IndexWarningDetails) -> Self {
556 self.details = Some(details);
557 self
558 }
559}
560
561#[derive(Debug, Clone, Serialize, Deserialize)]
563pub struct IndexWarningDetails {
564 #[serde(skip_serializing_if = "Option::is_none")]
566 pub current_branch: Option<String>,
567 #[serde(skip_serializing_if = "Option::is_none")]
569 pub indexed_branch: Option<String>,
570 #[serde(skip_serializing_if = "Option::is_none")]
572 pub current_commit: Option<String>,
573 #[serde(skip_serializing_if = "Option::is_none")]
575 pub indexed_commit: Option<String>,
576}
577
578#[derive(Debug, Clone, Serialize, Deserialize)]
580pub struct PaginationInfo {
581 pub total: usize,
583 pub count: usize,
585 pub offset: usize,
587 #[serde(skip_serializing_if = "Option::is_none")]
589 pub limit: Option<usize>,
590 pub has_more: bool,
592}
593
594#[derive(Debug, Clone, Serialize, Deserialize)]
596pub struct QueryResponse {
597 #[serde(skip_serializing_if = "Option::is_none")]
601 pub ai_instruction: Option<String>,
602 pub status: IndexStatus,
604 pub can_trust_results: bool,
606 #[serde(skip_serializing_if = "Option::is_none")]
608 pub warning: Option<IndexWarning>,
609 pub pagination: PaginationInfo,
611 pub results: Vec<FileGroupedResult>,
614}
615
616#[derive(Debug, Clone, Serialize, Deserialize)]
618pub struct CompactionReport {
619 pub files_removed: usize,
621 pub space_saved_bytes: u64,
623 pub duration_ms: u64,
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630
631 #[test]
632 fn test_symbol_ref_json_shape() {
633 let sym = SymbolRef {
634 name: "my_function".to_string(),
635 kind: SymbolKind::Function,
636 span: Span {
637 start_line: 10,
638 end_line: 20,
639 },
640 };
641 let json = serde_json::to_value(&sym).unwrap();
642 assert_eq!(json["name"], "my_function");
643 assert_eq!(json["kind"], "Function");
644 assert_eq!(json["span"]["start_line"], 10);
645 assert_eq!(json["span"]["end_line"], 20);
646 assert!(json.as_array().is_none());
647 }
648
649 #[test]
650 fn test_symbol_ref_roundtrip() {
651 let original = SymbolRef {
652 name: "MyStruct".to_string(),
653 kind: SymbolKind::Struct,
654 span: Span {
655 start_line: 1,
656 end_line: 5,
657 },
658 };
659 let json = serde_json::to_string(&original).unwrap();
660 let decoded: SymbolRef = serde_json::from_str(&json).unwrap();
661 assert_eq!(original, decoded);
662 }
663
664 #[test]
665 fn test_symbol_ref_exact_json() {
666 let sym = SymbolRef {
667 name: "Foo".to_string(),
668 kind: SymbolKind::Class,
669 span: Span {
670 start_line: 3,
671 end_line: 7,
672 },
673 };
674 let json = serde_json::to_string(&sym).unwrap();
675 assert_eq!(
676 json,
677 r#"{"name":"Foo","kind":"Class","span":{"start_line":3,"end_line":7}}"#
678 );
679 }
680}