Skip to main content

fff_query_parser/
lib.rs

1//! Fast, zero-allocation query parser for file search
2//!
3//! This parser takes a search query and extracts structured constraints
4//! while preserving text for fuzzy matching. Designed for maximum performance:
5//! - Zero allocations for queries with ≤8 constraints (SmallVec)
6//! - Single-pass parsing with minimal branching
7//! - Stack-allocated string buffers
8//!
9//! # Examples
10//!
11//! ```
12//! use fff_query_parser::{QueryParser, Constraint, FuzzyQuery};
13//!
14//! let parser = QueryParser::default();
15//!
16//! // Single-token queries return FFFQuery with Text fuzzy query and no constraints
17//! let result = parser.parse("hello");
18//! assert!(result.constraints.is_empty());
19//! assert_eq!(result.fuzzy_query, FuzzyQuery::Text("hello"));
20//!
21//! // Multi-token queries are parsed
22//! let result = parser.parse("name *.rs");
23//! match &result.fuzzy_query {
24//!     FuzzyQuery::Text(text) => assert_eq!(*text, "name"),
25//!     _ => panic!("Expected text"),
26//! }
27//! assert!(matches!(result.constraints[0], Constraint::Extension("rs")));
28//!
29//! // Parse glob pattern with text
30//! let result = parser.parse("**/*.rs foo");
31//! assert!(matches!(result.constraints[0], Constraint::Glob("**/*.rs")));
32//!
33//! // Parse negation
34//! let result = parser.parse("!*.rs foo");
35//! match &result.constraints[0] {
36//!     Constraint::Not(inner) => {
37//!         assert!(matches!(inner.as_ref(), Constraint::Extension("rs")));
38//!     }
39//!     _ => panic!("Expected Not constraint"),
40//! }
41//! ```
42
43mod config;
44mod constraints;
45pub mod glob_detect;
46pub mod location;
47mod parser;
48
49pub use config::{AiGrepConfig, FilePickerConfig, GrepConfig, ParserConfig};
50pub use constraints::{Constraint, GitStatusFilter};
51pub use location::Location;
52pub use parser::{FFFQuery, FuzzyQuery, QueryParser};
53
54// Re-export SmallVec for convenience
55pub use smallvec::SmallVec;
56
57/// Type alias for constraint vector - stack-allocated for ≤8 constraints
58pub type ConstraintVec<'a> = SmallVec<[Constraint<'a>; 8]>;
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn test_empty_query() {
66        let parser = QueryParser::default();
67        let result = parser.parse("");
68        assert!(result.constraints.is_empty());
69        assert_eq!(result.fuzzy_query, FuzzyQuery::Empty);
70    }
71
72    #[test]
73    fn test_whitespace_only() {
74        let parser = QueryParser::default();
75        let result = parser.parse("   ");
76        assert!(result.constraints.is_empty());
77        assert_eq!(result.fuzzy_query, FuzzyQuery::Empty);
78    }
79
80    #[test]
81    fn test_single_token() {
82        let parser = QueryParser::default();
83        let result = parser.parse("hello");
84        assert!(result.constraints.is_empty());
85        assert_eq!(result.fuzzy_query, FuzzyQuery::Text("hello"));
86    }
87
88    #[test]
89    fn test_simple_text() {
90        let parser = QueryParser::default();
91        let result = parser.parse("hello world");
92
93        match &result.fuzzy_query {
94            FuzzyQuery::Parts(parts) => {
95                assert_eq!(parts.len(), 2);
96                assert_eq!(parts[0], "hello");
97                assert_eq!(parts[1], "world");
98            }
99            _ => panic!("Expected Parts fuzzy query"),
100        }
101
102        assert_eq!(result.constraints.len(), 0);
103    }
104
105    #[test]
106    fn test_extension_only() {
107        let parser = QueryParser::default();
108        // Single constraint token - returns Some so constraint can be applied
109        let result = parser.parse("*.rs");
110        assert!(matches!(result.fuzzy_query, FuzzyQuery::Empty));
111        assert_eq!(result.constraints.len(), 1);
112        assert!(matches!(result.constraints[0], Constraint::Extension("rs")));
113    }
114
115    #[test]
116    fn test_glob_pattern() {
117        let parser = QueryParser::default();
118        let result = parser.parse("**/*.rs foo");
119        assert_eq!(result.constraints.len(), 1);
120        // Glob patterns with ** are treated as globs, not extensions
121        match &result.constraints[0] {
122            Constraint::Glob(pattern) => assert_eq!(*pattern, "**/*.rs"),
123            other => panic!("Expected Glob constraint, got {:?}", other),
124        }
125    }
126
127    #[test]
128    fn test_negation_pattern() {
129        let parser = QueryParser::default();
130        let result = parser.parse("!test foo");
131        assert_eq!(result.constraints.len(), 1);
132        match &result.constraints[0] {
133            Constraint::Not(inner) => {
134                assert!(matches!(**inner, Constraint::Text("test")));
135            }
136            _ => panic!("Expected Not constraint"),
137        }
138    }
139
140    #[test]
141    fn test_path_segment() {
142        let parser = QueryParser::default();
143        let result = parser.parse("/src/ foo");
144        assert_eq!(result.constraints.len(), 1);
145        assert!(matches!(
146            result.constraints[0],
147            Constraint::PathSegment("src")
148        ));
149    }
150
151    #[test]
152    fn test_git_status() {
153        let parser = QueryParser::default();
154        let result = parser.parse("status:modified foo");
155        assert_eq!(result.constraints.len(), 1);
156        assert!(matches!(
157            result.constraints[0],
158            Constraint::GitStatus(GitStatusFilter::Modified)
159        ));
160    }
161
162    #[test]
163    fn test_file_type() {
164        let parser = QueryParser::default();
165        let result = parser.parse("type:rust foo");
166        assert_eq!(result.constraints.len(), 1);
167        assert!(matches!(
168            result.constraints[0],
169            Constraint::FileType("rust")
170        ));
171    }
172
173    #[test]
174    fn test_complex_query() {
175        let parser = QueryParser::default();
176        let result = parser.parse("src name *.rs !test /lib/ status:modified");
177
178        // Verify we have fuzzy text
179        match &result.fuzzy_query {
180            FuzzyQuery::Parts(parts) => {
181                assert_eq!(parts.len(), 2);
182                assert_eq!(parts[0], "src");
183                assert_eq!(parts[1], "name");
184            }
185            _ => panic!("Expected Parts fuzzy query"),
186        }
187
188        // Should have multiple constraints
189        assert!(result.constraints.len() >= 4);
190
191        // Verify specific constraints exist
192        let has_extension = result
193            .constraints
194            .iter()
195            .any(|c| matches!(c, Constraint::Extension("rs")));
196        let has_not = result
197            .constraints
198            .iter()
199            .any(|c| matches!(c, Constraint::Not(_)));
200        let has_path = result
201            .constraints
202            .iter()
203            .any(|c| matches!(c, Constraint::PathSegment("lib")));
204        let has_git_status = result
205            .constraints
206            .iter()
207            .any(|c| matches!(c, Constraint::GitStatus(_)));
208
209        assert!(has_extension, "Should have Extension constraint");
210        assert!(has_not, "Should have Not constraint");
211        assert!(has_path, "Should have PathSegment constraint");
212        assert!(has_git_status, "Should have GitStatus constraint");
213    }
214
215    #[test]
216    fn test_no_heap_allocation_for_small_queries() {
217        let parser = QueryParser::default();
218        let result = parser.parse("*.rs *.toml !test");
219        // SmallVec should not have spilled to heap
220        assert!(!result.constraints.spilled());
221    }
222
223    #[test]
224    fn test_many_fuzzy_parts() {
225        let parser = QueryParser::default();
226        let result = parser.parse("one two three four five six");
227
228        match &result.fuzzy_query {
229            FuzzyQuery::Parts(parts) => {
230                assert_eq!(parts.len(), 6);
231                assert_eq!(parts[0], "one");
232                assert_eq!(parts[5], "six");
233            }
234            _ => panic!("Expected Parts fuzzy query"),
235        }
236    }
237}