fff_query_parser/constraints.rs
1use crate::glob_detect::has_wildcards;
2
3/// Constraint types that can be extracted from a query
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum Constraint<'a> {
6 /// Match file extension: *.rs -> Extension("rs")
7 Extension(&'a str),
8
9 /// Glob pattern: **/*.rs -> Glob("**/*.rs")
10 Glob(&'a str),
11
12 /// Multiple text search parts: ["src", "name"]
13 /// Uses slice to avoid allocation
14 Parts(&'a [&'a str]),
15
16 /// Single text token (optimized case)
17 Text(&'a str),
18
19 /// Exclude pattern: !test -> Exclude(&["test"])
20 Exclude(&'a [&'a str]),
21
22 /// Path constraint: /src/ -> PathSegment("src")
23 PathSegment(&'a str),
24
25 /// File path constraint (AI mode): "libswscale/input.c" → FilePath("libswscale/input.c")
26 /// Matches files whose relative path ends with this suffix at a `/` boundary.
27 FilePath(&'a str),
28
29 /// File type constraint: type:rust -> FileType("rust")
30 FileType(&'a str),
31
32 /// Git status constraint: status:modified -> GitStatus(Modified)
33 GitStatus(GitStatusFilter),
34
35 /// Negation constraint: !extension:rs -> Not(Extension("rs"))
36 /// Negates the inner constraint
37 Not(Box<Constraint<'a>>),
38}
39
40impl Constraint<'_> {
41 #[inline(always)]
42 pub fn is_filename_constraint_token(token: &str) -> bool {
43 let bytes = token.as_bytes();
44
45 // Must NOT end with / or .
46 if token.is_empty() || (bytes.last() == Some(&b'/') && bytes.first() != Some(&b'.')) {
47 return false;
48 }
49
50 // Must NOT contain wildcards (those are globs)
51 if has_wildcards(token) {
52 return false;
53 }
54
55 // Get the filename component (after last /)
56 let filename = token.rsplit('/').next().unwrap_or(token);
57
58 // Extension must exist and look like a real file extension:
59 // starts with an ASCII letter (rejects version numbers like "v2.0"),
60 // followed by alphanumeric chars, max 10 chars total.
61 match filename.rfind('.') {
62 None => false,
63 Some(dot_idx) => {
64 let extension = &filename[dot_idx + 1..];
65
66 !extension.is_empty()
67 && extension.len() <= 10 // just an sassumption
68 && extension.bytes().all(|b| b.is_ascii_alphanumeric())
69 }
70 }
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum GitStatusFilter {
76 Modified,
77 Untracked,
78 Staged,
79 Unmodified,
80}
81
82/// Buffer for text parts during query parsing.
83pub(crate) type TextPartsBuffer<'a> = Vec<&'a str>;