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 /// Number of context lines to show before and after each match (default: 0 = disabled)
47 pub context_lines: usize,
48 /// Test-only: Override large index threshold (None = use default of 20,000)
49 #[doc(hidden)]
50 pub test_large_index_threshold: Option<usize>,
51 /// Test-only: Override short pattern threshold (None = use default of 4)
52 #[doc(hidden)]
53 pub test_short_pattern_threshold: Option<usize>,
54}
55
56impl Default for QueryFilter {
57 fn default() -> Self {
58 Self {
59 language: None,
60 kind: None,
61 use_ast: false,
62 use_regex: false,
63 limit: Some(100), // Default: limit to 100 results for token efficiency
64 symbols_mode: false,
65 expand: false,
66 file_pattern: None,
67 exact: false,
68 use_contains: false, // Default: word-boundary matching
69 timeout_secs: 30, // 30 seconds default timeout
70 glob_patterns: Vec::new(),
71 exclude_patterns: Vec::new(),
72 paths_only: false,
73 offset: None,
74 force: false, // Default: enable broad query detection
75 suppress_output: false, // Default: show warnings/info
76 include_dependencies: false, // Default: don't load dependencies for performance
77 context_lines: 0, // Default: no context lines shown
78 test_large_index_threshold: None, // Default: use production threshold (20,000)
79 test_short_pattern_threshold: None, // Default: use production threshold (4)
80 }
81 }
82}
83
84/// Map a language keyword to its corresponding SymbolKind.
85///
86/// When users search for keywords like "class" or "function" with --symbols,
87/// automatically infer the kind filter to return only symbols of that type.
88pub fn keyword_to_kind(keyword: &str) -> Option<SymbolKind> {
89 match keyword {
90 "class" => Some(SymbolKind::Class),
91 "struct" => Some(SymbolKind::Struct),
92 "enum" => Some(SymbolKind::Enum),
93 "interface" => Some(SymbolKind::Interface),
94 "trait" => Some(SymbolKind::Trait),
95 "type" => Some(SymbolKind::Type),
96 "record" => Some(SymbolKind::Struct), // C# record types
97 "function" | "fn" | "def" | "func" => Some(SymbolKind::Function),
98 "const" | "static" => Some(SymbolKind::Constant),
99 "var" | "let" => Some(SymbolKind::Variable),
100 "mod" | "module" | "namespace" => Some(SymbolKind::Module),
101 "impl" | "async" => None,
102 _ => None,
103 }
104}
105
106/// Check if pattern appears at word boundaries in a line.
107///
108/// Used for default (restrictive) matching to find complete identifiers
109/// rather than substrings.
110pub fn has_word_boundary_match(line: &str, pattern: &str) -> bool {
111 let escaped_pattern = regex::escape(pattern);
112 let pattern_with_boundaries = format!(r"\b{}\b", escaped_pattern);
113
114 if let Ok(re) = Regex::new(&pattern_with_boundaries) {
115 re.is_match(line)
116 } else {
117 log::debug!(
118 "Word boundary regex failed for pattern '{}', falling back to substring",
119 pattern
120 );
121 line.contains(pattern)
122 }
123}