Skip to main content

fff_query_parser/
config.rs

1use crate::constraints::Constraint;
2use crate::glob_detect::has_wildcards;
3
4/// Check if a token looks like a filename/path for use as a `FilePath` constraint.
5#[deprecated(note = "use `Constraint::is_filename_constraint_token`")]
6pub fn is_filename_constraint_token(token: &str) -> bool {
7    Constraint::is_filename_constraint_token(token)
8}
9
10/// Parser configuration trait - allows different picker types to customize parsing
11pub trait ParserConfig {
12    fn enable_glob(&self) -> bool {
13        true
14    }
15
16    /// Should parse extension shortcuts (e.g., *.rs)
17    fn enable_extension(&self) -> bool {
18        true
19    }
20
21    /// Should parse exclusion patterns (e.g., !test)
22    fn enable_exclude(&self) -> bool {
23        true
24    }
25
26    /// Should parse path segments (e.g., /src/)
27    fn enable_path_segments(&self) -> bool {
28        true
29    }
30
31    /// Should parse type constraints (e.g., type:rust)
32    fn enable_type_filter(&self) -> bool {
33        true
34    }
35
36    /// Should parse git status (e.g., status:modified)
37    fn enable_git_status(&self) -> bool {
38        true
39    }
40
41    /// Should parse location suffixes (e.g., file:12, file:12:4)
42    /// Disabled for grep modes where colon-number patterns like localhost:8080
43    /// are search text, not file locations.
44    fn enable_location(&self) -> bool {
45        true
46    }
47
48    /// Determine whether a token should be treated as a glob constraint.
49    ///
50    /// The default implementation delegates to `zlob::has_wildcards` with
51    /// `RECOMMENDED` flags, which recognises `*`, `?`, `[`, `{…}` etc.
52    ///
53    /// Override this in configs where some wildcard characters are common
54    /// in search text (e.g. grep mode where `?` and `[` appear in code).
55    fn is_glob_pattern(&self, token: &str) -> bool {
56        has_wildcards(token)
57    }
58
59    /// If `true`, a PathSegment constraint that is the ONLY token in the
60    /// query is demoted to fuzzy text to avoid over filtering
61    fn treat_lone_path_as_text(&self) -> bool {
62        true
63    }
64
65    /// If `true`, tokens that look like filenames (`score.rs`, `src/main.rs`)
66    /// become `FilePath` constraints that scope the search to matching paths.
67    /// Off by default: a partial filename like `vite.conf` would otherwise
68    /// filter out `vite.config.ts` and yield zero results.
69    fn enable_filename_constraint(&self) -> bool {
70        false
71    }
72
73    /// Custom constraint parsers for picker-specific needs.
74    ///
75    /// Returns `None` by default. Filename-token detection is handled
76    /// separately by the parser via `enable_filename_constraint`.
77    fn parse_custom<'a>(&self, _input: &'a str) -> Option<Constraint<'a>> {
78        None
79    }
80}
81
82/// Default configuration for the file picker.
83///
84/// Filename-constraint detection is off (trait default), so partial filenames
85/// like `vite.conf` don't silently filter out fuzzy matches. The Neovim layer
86/// overrides `enable_filename_constraint` to opt in based on user config.
87#[derive(Debug, Clone, Copy, Default)]
88pub struct FileSearchConfig;
89
90impl ParserConfig for FileSearchConfig {}
91
92/// Configuration for full-text search (grep) - file constraints enabled for
93/// filtering which files to search, git status disabled since it's not useful
94/// when searching file contents.
95///
96/// Glob detection is narrowed: only patterns containing a path separator (`/`)
97/// or brace expansion (`{…}`) are treated as globs. Characters like `?` and
98/// `[` are extremely common in source code and must remain literal search text.
99#[derive(Debug, Clone, Copy, Default)]
100pub struct GrepConfig;
101
102impl ParserConfig for GrepConfig {
103    fn enable_path_segments(&self) -> bool {
104        true
105    }
106
107    fn enable_git_status(&self) -> bool {
108        false
109    }
110
111    fn enable_location(&self) -> bool {
112        false
113    }
114
115    /// Only recognise globs that are clearly directory/path oriented.
116    ///
117    /// Characters like `?`, `[`, and bare `*` (without `/`) are extremely
118    /// common in source code (`foo?`, `arr[0]`, `*ptr`) and must NOT be
119    /// consumed as glob constraints. We only treat a token as a glob when
120    /// it contains path-oriented patterns:
121    ///
122    /// - Contains `/` → path glob (e.g. `src/**/*.rs`, `*/tests/*`)
123    /// - Contains `{…}` → brace expansion (e.g. `{src,lib}`)
124    fn is_glob_pattern(&self, token: &str) -> bool {
125        // Must contain at least one glob wildcard character
126        if !has_wildcards(token) {
127            return false;
128        }
129
130        let bytes = token.as_bytes();
131
132        // Contains path separator → clearly a path glob
133        if bytes.contains(&b'/') {
134            return true;
135        }
136
137        // Brace expansion → useful for directory alternatives.
138        // Require a comma between `{` and `}` AND at least one letter to
139        // distinguish real glob expansions like `{src,lib}` or `*.{ts,tsx}`
140        // from code patterns like `format!("{}")` and regex quantifiers `{2,3}`.
141        // Guard `open < close` so reversed braces like `}{` don't panic on slicing.
142        if let Some(open) = bytes.iter().position(|&b| b == b'{')
143            && let Some(close) = bytes.iter().rposition(|&b| b == b'}')
144            && open < close
145        {
146            let inner = &bytes[open + 1..close];
147            if inner.contains(&b',') && inner.iter().any(|b| b.is_ascii_alphabetic()) {
148                return true;
149            }
150        }
151
152        // Everything else (?, [, bare * without /) → treat as literal text
153        false
154    }
155}
156
157/// Configuration for AI-mode grep — extends `GrepConfig` behavior with
158/// automatic file-path constraint detection.
159///
160/// Bare filenames with valid extensions (`schema.rs`) and path-prefixed
161/// filenames (`libswscale/input.c`) are detected as `FilePath` constraints
162/// so the search is scoped to matching files. The caller validates the
163/// constraint against the index and drops it if no files match (fallback).
164#[derive(Debug, Clone, Copy, Default)]
165pub struct AiGrepConfig;
166
167/// Configuration for directory and mixed search modes.
168///
169/// Disables path segment parsing so that trailing `/` is kept as fuzzy text
170/// (e.g. `fff-core/` fuzzy-matches directory paths instead of becoming a
171/// `PathSegment("fff-core")` constraint with an empty query). Extension and
172/// filename constraints are also disabled since they don't apply to directories.
173#[derive(Debug, Clone, Copy, Default)]
174pub struct DirSearchConfig;
175
176impl ParserConfig for AiGrepConfig {
177    fn enable_path_segments(&self) -> bool {
178        true
179    }
180
181    fn enable_git_status(&self) -> bool {
182        false
183    }
184
185    fn enable_location(&self) -> bool {
186        false
187    }
188
189    fn enable_filename_constraint(&self) -> bool {
190        true
191    }
192
193    fn is_glob_pattern(&self, token: &str) -> bool {
194        // First check GrepConfig's strict rules (path globs, brace expansion)
195        if GrepConfig.is_glob_pattern(token) {
196            return true;
197        }
198
199        // AI agents use `*text*` to scope file searches (e.g. `*quote* TODO`).
200        // Recognise tokens that start AND end with `*` with non-empty text
201        // between them as glob constraints. Bare `*` or `**` are excluded.
202        if !has_wildcards(token) {
203            return false;
204        }
205        let bytes = token.as_bytes();
206        if bytes.len() >= 3
207            && bytes[0] == b'*'
208            && bytes[bytes.len() - 1] == b'*'
209            && bytes[1..bytes.len() - 1].iter().all(|&b| b != b'*')
210        {
211            return true;
212        }
213
214        false
215    }
216}
217
218impl ParserConfig for DirSearchConfig {
219    fn enable_path_segments(&self) -> bool {
220        false
221    }
222
223    fn enable_extension(&self) -> bool {
224        false
225    }
226
227    fn enable_type_filter(&self) -> bool {
228        false
229    }
230
231    fn enable_git_status(&self) -> bool {
232        false
233    }
234}
235
236/// Configuration for mixed (files + directories) search.
237///
238/// Like `DirSearchConfig`, disables path segment parsing so trailing `/`
239/// triggers dirs-only mode instead of becoming a constraint. Keeps git
240/// status and extension filters enabled since files are part of the results.
241#[derive(Debug, Clone, Copy, Default)]
242pub struct MixedSearchConfig;
243
244impl ParserConfig for MixedSearchConfig {
245    fn enable_path_segments(&self) -> bool {
246        false
247    }
248}