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}