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, FileSearchConfig, 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
57pub type ConstraintVec<'a> = SmallVec<[Constraint<'a>; 8]>;
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn test_empty_query() {
65        let parser = QueryParser::default();
66        let result = parser.parse("");
67        assert!(result.constraints.is_empty());
68        assert_eq!(result.fuzzy_query, FuzzyQuery::Empty);
69    }
70
71    #[test]
72    fn test_whitespace_only() {
73        let parser = QueryParser::default();
74        let result = parser.parse("   ");
75        assert!(result.constraints.is_empty());
76        assert_eq!(result.fuzzy_query, FuzzyQuery::Empty);
77    }
78
79    #[test]
80    fn test_single_token() {
81        let parser = QueryParser::default();
82        let result = parser.parse("hello");
83        assert!(result.constraints.is_empty());
84        assert_eq!(result.fuzzy_query, FuzzyQuery::Text("hello"));
85    }
86
87    #[test]
88    fn test_simple_text() {
89        let parser = QueryParser::default();
90        let result = parser.parse("hello world");
91
92        match &result.fuzzy_query {
93            FuzzyQuery::Parts(parts) => {
94                assert_eq!(parts.len(), 2);
95                assert_eq!(parts[0], "hello");
96                assert_eq!(parts[1], "world");
97            }
98            _ => panic!("Expected Parts fuzzy query"),
99        }
100
101        assert_eq!(result.constraints.len(), 0);
102    }
103
104    #[test]
105    fn test_extension_only() {
106        let parser = QueryParser::default();
107        // Single constraint token - returns Some so constraint can be applied
108        let result = parser.parse("*.rs");
109        assert!(matches!(result.fuzzy_query, FuzzyQuery::Empty));
110        assert_eq!(result.constraints.len(), 1);
111        assert!(matches!(result.constraints[0], Constraint::Extension("rs")));
112    }
113
114    #[test]
115    fn test_glob_pattern() {
116        let parser = QueryParser::default();
117        let result = parser.parse("**/*.rs foo");
118        assert_eq!(result.constraints.len(), 1);
119        // Glob patterns with ** are treated as globs, not extensions
120        match &result.constraints[0] {
121            Constraint::Glob(pattern) => assert_eq!(*pattern, "**/*.rs"),
122            other => panic!("Expected Glob constraint, got {:?}", other),
123        }
124    }
125
126    #[test]
127    fn test_negation_pattern() {
128        let parser = QueryParser::default();
129        let result = parser.parse("!test foo");
130        assert_eq!(result.constraints.len(), 1);
131        match &result.constraints[0] {
132            Constraint::Not(inner) => {
133                assert!(matches!(**inner, Constraint::Text("test")));
134            }
135            _ => panic!("Expected Not constraint"),
136        }
137    }
138
139    #[test]
140    fn test_path_segment() {
141        let parser = QueryParser::default();
142        let result = parser.parse("/src/ foo");
143        assert_eq!(result.constraints.len(), 1);
144        assert!(matches!(
145            result.constraints[0],
146            Constraint::PathSegment("src")
147        ));
148    }
149
150    #[test]
151    fn test_git_status() {
152        let parser = QueryParser::default();
153        let result = parser.parse("status:modified foo");
154        assert_eq!(result.constraints.len(), 1);
155        assert!(matches!(
156            result.constraints[0],
157            Constraint::GitStatus(GitStatusFilter::Modified)
158        ));
159    }
160
161    #[test]
162    fn test_file_type() {
163        let parser = QueryParser::default();
164        let result = parser.parse("type:rust foo");
165        assert_eq!(result.constraints.len(), 1);
166        assert!(matches!(
167            result.constraints[0],
168            Constraint::FileType("rust")
169        ));
170    }
171
172    #[test]
173    fn test_complex_query() {
174        let parser = QueryParser::default();
175        let result = parser.parse("src name *.rs !test /lib/ status:modified");
176
177        // Verify we have fuzzy text
178        match &result.fuzzy_query {
179            FuzzyQuery::Parts(parts) => {
180                assert_eq!(parts.len(), 2);
181                assert_eq!(parts[0], "src");
182                assert_eq!(parts[1], "name");
183            }
184            _ => panic!("Expected Parts fuzzy query"),
185        }
186
187        // Should have multiple constraints
188        assert!(result.constraints.len() >= 4);
189
190        // Verify specific constraints exist
191        let has_extension = result
192            .constraints
193            .iter()
194            .any(|c| matches!(c, Constraint::Extension("rs")));
195        let has_not = result
196            .constraints
197            .iter()
198            .any(|c| matches!(c, Constraint::Not(_)));
199        let has_path = result
200            .constraints
201            .iter()
202            .any(|c| matches!(c, Constraint::PathSegment("lib")));
203        let has_git_status = result
204            .constraints
205            .iter()
206            .any(|c| matches!(c, Constraint::GitStatus(_)));
207
208        assert!(has_extension, "Should have Extension constraint");
209        assert!(has_not, "Should have Not constraint");
210        assert!(has_path, "Should have PathSegment constraint");
211        assert!(has_git_status, "Should have GitStatus constraint");
212    }
213
214    #[test]
215    fn test_no_heap_allocation_for_small_queries() {
216        let parser = QueryParser::default();
217        let result = parser.parse("*.rs *.toml !test");
218        // SmallVec should not have spilled to heap
219        assert!(!result.constraints.spilled());
220    }
221
222    #[test]
223    fn test_many_fuzzy_parts() {
224        let parser = QueryParser::default();
225        let result = parser.parse("one two three four five six");
226
227        match &result.fuzzy_query {
228            FuzzyQuery::Parts(parts) => {
229                assert_eq!(parts.len(), 6);
230                assert_eq!(parts[0], "one");
231                assert_eq!(parts[5], "six");
232            }
233            _ => panic!("Expected Parts fuzzy query"),
234        }
235    }
236}