reflex/query/filter.rs
1//! Query filter types and stateless filtering helpers
2
3use regex::Regex;
4
5use crate::models::SymbolKind;
6
7/// Query filter options
8#[derive(Debug, Clone)]
9pub struct QueryFilter {
10 /// Language filter (None = all languages)
11 pub language: Option<crate::models::Language>,
12 /// Symbol kind filter (None = all kinds)
13 pub kind: Option<SymbolKind>,
14 /// Use AST pattern matching (vs lexical search)
15 pub use_ast: bool,
16 /// Use regex pattern matching
17 pub use_regex: bool,
18 /// Maximum number of results
19 pub limit: Option<usize>,
20 /// Search symbol definitions only (vs full-text)
21 pub symbols_mode: bool,
22 /// Show full symbol body (from span.start_line to span.end_line)
23 pub expand: bool,
24 /// File path filter (substring match)
25 pub file_pattern: Option<String>,
26 /// Exact symbol name match (no substring matching)
27 pub exact: bool,
28 /// Use substring matching instead of word-boundary matching (opt-in, expansive)
29 pub use_contains: bool,
30 /// Query timeout in seconds (0 = no timeout)
31 pub timeout_secs: u64,
32 /// Glob patterns to include (empty = all files)
33 pub glob_patterns: Vec<String>,
34 /// Glob patterns to exclude (applied after includes)
35 pub exclude_patterns: Vec<String>,
36 /// Return only unique file paths (deduplicated)
37 pub paths_only: bool,
38 /// Pagination offset (skip first N results after sorting)
39 pub offset: Option<usize>,
40 /// Force execution of potentially expensive queries (bypass broad query detection)
41 pub force: bool,
42 /// Suppress warning/info output (for --json mode to ensure pure JSON output)
43 pub suppress_output: bool,
44 /// Include dependency information in results
45 pub include_dependencies: bool,
46 /// Test-only: Override large index threshold (None = use default of 20,000)
47 #[doc(hidden)]
48 pub test_large_index_threshold: Option<usize>,
49 /// Test-only: Override short pattern threshold (None = use default of 4)
50 #[doc(hidden)]
51 pub test_short_pattern_threshold: Option<usize>,
52}
53
54impl Default for QueryFilter {
55 fn default() -> Self {
56 Self {
57 language: None,
58 kind: None,
59 use_ast: false,
60 use_regex: false,
61 limit: Some(100), // Default: limit to 100 results for token efficiency
62 symbols_mode: false,
63 expand: false,
64 file_pattern: None,
65 exact: false,
66 use_contains: false, // Default: word-boundary matching
67 timeout_secs: 30, // 30 seconds default timeout
68 glob_patterns: Vec::new(),
69 exclude_patterns: Vec::new(),
70 paths_only: false,
71 offset: None,
72 force: false, // Default: enable broad query detection
73 suppress_output: false, // Default: show warnings/info
74 include_dependencies: false, // Default: don't load dependencies for performance
75 test_large_index_threshold: None, // Default: use production threshold (20,000)
76 test_short_pattern_threshold: None, // Default: use production threshold (4)
77 }
78 }
79}
80
81/// Map a language keyword to its corresponding SymbolKind.
82///
83/// When users search for keywords like "class" or "function" with --symbols,
84/// automatically infer the kind filter to return only symbols of that type.
85pub fn keyword_to_kind(keyword: &str) -> Option<SymbolKind> {
86 match keyword {
87 "class" => Some(SymbolKind::Class),
88 "struct" => Some(SymbolKind::Struct),
89 "enum" => Some(SymbolKind::Enum),
90 "interface" => Some(SymbolKind::Interface),
91 "trait" => Some(SymbolKind::Trait),
92 "type" => Some(SymbolKind::Type),
93 "record" => Some(SymbolKind::Struct), // C# record types
94 "function" | "fn" | "def" | "func" => Some(SymbolKind::Function),
95 "const" | "static" => Some(SymbolKind::Constant),
96 "var" | "let" => Some(SymbolKind::Variable),
97 "mod" | "module" | "namespace" => Some(SymbolKind::Module),
98 "impl" | "async" => None,
99 _ => None,
100 }
101}
102
103/// Check if pattern appears at word boundaries in a line.
104///
105/// Used for default (restrictive) matching to find complete identifiers
106/// rather than substrings.
107pub fn has_word_boundary_match(line: &str, pattern: &str) -> bool {
108 let escaped_pattern = regex::escape(pattern);
109 let pattern_with_boundaries = format!(r"\b{}\b", escaped_pattern);
110
111 if let Ok(re) = Regex::new(&pattern_with_boundaries) {
112 re.is_match(line)
113 } else {
114 log::debug!("Word boundary regex failed for pattern '{}', falling back to substring", pattern);
115 line.contains(pattern)
116 }
117}