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