Skip to main content

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>;