Skip to main content

fff_query_parser/
lib.rs

1//! Fast 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//! - Single-pass parsing with minimal branching
6//! - Stack-allocated string buffers
7//!
8//! # Examples
9//!
10//! ```
11//! use fff_query_parser::{QueryParser, Constraint, FuzzyQuery};
12//!
13//! let parser = QueryParser::default();
14//!
15//! // Single-token queries return FFFQuery with Text fuzzy query and no constraints
16//! let result = parser.parse("hello");
17//! assert!(result.constraints.is_empty());
18//! assert_eq!(result.fuzzy_query, FuzzyQuery::Text("hello"));
19//!
20//! // Multi-token queries are parsed
21//! let result = parser.parse("name *.rs");
22//! match &result.fuzzy_query {
23//!     FuzzyQuery::Text(text) => assert_eq!(*text, "name"),
24//!     _ => panic!("Expected text"),
25//! }
26//! assert!(matches!(result.constraints[0], Constraint::Extension("rs")));
27//!
28//! // Parse glob pattern with text
29//! let result = parser.parse("**/*.rs foo");
30//! assert!(matches!(result.constraints[0], Constraint::Glob("**/*.rs")));
31//!
32//! // Parse negation
33//! let result = parser.parse("!*.rs foo");
34//! match &result.constraints[0] {
35//!     Constraint::Not(inner) => {
36//!         assert!(matches!(inner.as_ref(), Constraint::Extension("rs")));
37//!     }
38//!     _ => panic!("Expected Not constraint"),
39//! }
40//! ```
41
42mod config;
43mod constraints;
44pub mod glob_detect;
45pub mod location;
46mod parser;
47
48#[allow(deprecated)]
49pub use config::is_filename_constraint_token;
50pub use config::{
51    AiGrepConfig, DirSearchConfig, FileSearchConfig, GrepConfig, MixedSearchConfig, ParserConfig,
52};
53pub use constraints::{Constraint, GitStatusFilter};
54pub use location::Location;
55pub use parser::{FFFQuery, FuzzyQuery, QueryParser};
56
57pub type ConstraintVec<'a> = Vec<Constraint<'a>>;
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_small_constraint_count() {
216        let parser = QueryParser::default();
217        let result = parser.parse("*.rs *.toml !test");
218        assert_eq!(result.constraints.len(), 3);
219    }
220
221    #[test]
222    fn test_many_fuzzy_parts() {
223        let parser = QueryParser::default();
224        let result = parser.parse("one two three four five six");
225
226        match &result.fuzzy_query {
227            FuzzyQuery::Parts(parts) => {
228                assert_eq!(parts.len(), 6);
229                assert_eq!(parts[0], "one");
230                assert_eq!(parts[5], "six");
231            }
232            _ => panic!("Expected Parts fuzzy query"),
233        }
234    }
235}