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