1pub mod providers;
15
16pub use providers::{BufferProvider, CommandProvider, FileProvider, GotoLineProvider};
17
18use crate::input::commands::Suggestion;
19use crate::input::keybindings::Action;
20use std::collections::HashMap;
21
22#[derive(Debug, Clone)]
24pub enum QuickOpenResult {
25 ExecuteAction(Action),
27 OpenFile {
29 path: String,
30 line: Option<usize>,
31 column: Option<usize>,
32 },
33 ShowBuffer(usize),
35 GotoLine(GotoLineTarget),
37 None,
39 Error(String),
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum GotoLineTarget {
48 Absolute(usize),
50 Relative(isize),
52}
53
54pub fn parse_goto_line_input(input: &str) -> Option<GotoLineTarget> {
64 let trimmed = input.trim();
65 if trimmed.is_empty() {
66 return None;
67 }
68 if let Some(rest) = trimmed
69 .strip_prefix('+')
70 .or_else(|| trimmed.strip_prefix('-'))
71 {
72 if rest.is_empty() || rest.starts_with('+') || rest.starts_with('-') {
74 return None;
75 }
76 let delta = trimmed.parse::<isize>().ok()?;
77 if delta == 0 {
78 return None;
79 }
80 Some(GotoLineTarget::Relative(delta))
81 } else {
82 let n = trimmed.parse::<usize>().ok()?;
83 if n == 0 {
84 return None;
85 }
86 Some(GotoLineTarget::Absolute(n))
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct QuickOpenContext {
93 pub cwd: String,
95 pub open_buffers: Vec<BufferInfo>,
97 pub active_buffer_id: usize,
99 pub active_buffer_path: Option<String>,
101 pub has_selection: bool,
103 pub key_context: crate::input::keybindings::KeyContext,
105 pub custom_contexts: std::collections::HashSet<String>,
107 pub buffer_mode: Option<String>,
109 pub has_lsp_config: bool,
111 pub relative_line_numbers: bool,
113}
114
115#[derive(Debug, Clone)]
117pub struct BufferInfo {
118 pub id: usize,
119 pub path: String,
120 pub name: String,
121 pub modified: bool,
122}
123
124pub fn parse_path_line_col(input: &str) -> (String, Option<usize>, Option<usize>) {
128 use std::path::{Component, Path};
129
130 let trimmed = input.trim();
131 if trimmed.is_empty() {
132 return (String::new(), None, None);
133 }
134
135 let has_drive = Path::new(trimmed)
137 .components()
138 .next()
139 .is_some_and(|c| matches!(c, Component::Prefix(_)));
140 let search_start = if has_drive {
141 trimmed.find(':').map(|i| i + 1).unwrap_or(0)
142 } else {
143 0
144 };
145
146 let suffix = &trimmed[search_start..];
147 let parts: Vec<&str> = suffix.rsplitn(3, ':').collect();
148
149 let rebuild_path = |rest: &str| {
151 if has_drive {
152 format!("{}{}", &trimmed[..search_start], rest)
153 } else {
154 rest.to_string()
155 }
156 };
157
158 let parsed = match parts.as_slice() {
160 [col_s, line_s, rest] if !rest.is_empty() => col_s
161 .parse::<usize>()
162 .ok()
163 .filter(|&c| c > 0)
164 .zip(line_s.parse::<usize>().ok().filter(|&l| l > 0))
165 .map(|(col, line)| (rebuild_path(rest), Some(line), Some(col))),
166 [line_s, rest] if !rest.is_empty() => line_s
167 .parse::<usize>()
168 .ok()
169 .filter(|&l| l > 0)
170 .map(|line| (rebuild_path(rest), Some(line), None)),
171 _ => None,
172 };
173
174 parsed.unwrap_or_else(|| (trimmed.to_string(), None, None))
175}
176
177pub trait QuickOpenProvider: Send + Sync {
182 fn prefix(&self) -> &str;
185
186 fn suggestions(&self, query: &str, context: &QuickOpenContext) -> Vec<Suggestion>;
190
191 fn on_select(
196 &self,
197 suggestion: Option<&Suggestion>,
198 query: &str,
199 context: &QuickOpenContext,
200 ) -> QuickOpenResult;
201
202 fn as_any(&self) -> &dyn std::any::Any;
204}
205
206pub struct QuickOpenRegistry {
208 providers: HashMap<String, Box<dyn QuickOpenProvider>>,
210}
211
212impl QuickOpenRegistry {
213 pub fn new() -> Self {
214 Self {
215 providers: HashMap::new(),
216 }
217 }
218
219 pub fn register(&mut self, provider: Box<dyn QuickOpenProvider>) {
223 let prefix = provider.prefix().to_string();
224 self.providers.insert(prefix, provider);
225 }
226
227 pub fn get_provider_for_input<'a>(
231 &'a self,
232 input: &'a str,
233 ) -> Option<(&'a dyn QuickOpenProvider, &'a str)> {
234 let mut prefixes: Vec<_> = self.providers.keys().collect();
236 prefixes.sort_by_key(|b| std::cmp::Reverse(b.len()));
237
238 for prefix in prefixes {
239 if prefix.is_empty() {
240 continue; }
242 if input.starts_with(prefix.as_str()) {
243 let query = &input[prefix.len()..];
244 return self.providers.get(prefix).map(|p| (p.as_ref(), query));
245 }
246 }
247
248 self.providers.get("").map(|p| (p.as_ref(), input))
250 }
251}
252
253impl Default for QuickOpenRegistry {
254 fn default() -> Self {
255 Self::new()
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 struct TestProvider {
264 prefix: String,
265 }
266
267 impl QuickOpenProvider for TestProvider {
268 fn prefix(&self) -> &str {
269 &self.prefix
270 }
271
272 fn suggestions(&self, _query: &str, _context: &QuickOpenContext) -> Vec<Suggestion> {
273 vec![]
274 }
275
276 fn on_select(
277 &self,
278 _suggestion: Option<&Suggestion>,
279 _query: &str,
280 _context: &QuickOpenContext,
281 ) -> QuickOpenResult {
282 QuickOpenResult::None
283 }
284
285 fn as_any(&self) -> &dyn std::any::Any {
286 self
287 }
288 }
289
290 #[test]
291 fn parse_goto_line_input_absolute() {
292 assert_eq!(
293 parse_goto_line_input("500"),
294 Some(GotoLineTarget::Absolute(500))
295 );
296 assert_eq!(
297 parse_goto_line_input(" 42 "),
298 Some(GotoLineTarget::Absolute(42))
299 );
300 }
301
302 #[test]
303 fn parse_goto_line_input_relative() {
304 assert_eq!(
305 parse_goto_line_input("+3"),
306 Some(GotoLineTarget::Relative(3))
307 );
308 assert_eq!(
309 parse_goto_line_input("-3"),
310 Some(GotoLineTarget::Relative(-3))
311 );
312 assert_eq!(
313 parse_goto_line_input(" -10 "),
314 Some(GotoLineTarget::Relative(-10))
315 );
316 }
317
318 #[test]
319 fn parse_goto_line_input_rejects_invalid() {
320 assert_eq!(parse_goto_line_input(""), None);
321 assert_eq!(parse_goto_line_input(" "), None);
322 assert_eq!(parse_goto_line_input("0"), None);
323 assert_eq!(parse_goto_line_input("+0"), None);
324 assert_eq!(parse_goto_line_input("-0"), None);
325 assert_eq!(parse_goto_line_input("+"), None);
326 assert_eq!(parse_goto_line_input("-"), None);
327 assert_eq!(parse_goto_line_input("++3"), None);
328 assert_eq!(parse_goto_line_input("+-3"), None);
329 assert_eq!(parse_goto_line_input("abc"), None);
330 assert_eq!(parse_goto_line_input("3a"), None);
331 }
332
333 #[test]
334 fn test_provider_routing() {
335 let mut registry = QuickOpenRegistry::new();
336
337 registry.register(Box::new(TestProvider {
338 prefix: "".to_string(),
339 }));
340 registry.register(Box::new(TestProvider {
341 prefix: ">".to_string(),
342 }));
343 registry.register(Box::new(TestProvider {
344 prefix: "#".to_string(),
345 }));
346
347 let (provider, query) = registry.get_provider_for_input("hello").unwrap();
349 assert_eq!(provider.prefix(), "");
350 assert_eq!(query, "hello");
351
352 let (provider, query) = registry.get_provider_for_input(">save").unwrap();
354 assert_eq!(provider.prefix(), ">");
355 assert_eq!(query, "save");
356
357 let (provider, query) = registry.get_provider_for_input("#main").unwrap();
359 assert_eq!(provider.prefix(), "#");
360 assert_eq!(query, "main");
361 }
362}