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 fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
209}
210
211pub struct QuickOpenRegistry {
213 providers: HashMap<String, Box<dyn QuickOpenProvider>>,
215}
216
217impl QuickOpenRegistry {
218 pub fn new() -> Self {
219 Self {
220 providers: HashMap::new(),
221 }
222 }
223
224 pub fn register(&mut self, provider: Box<dyn QuickOpenProvider>) {
228 let prefix = provider.prefix().to_string();
229 self.providers.insert(prefix, provider);
230 }
231
232 pub fn set_file_backends(
241 &mut self,
242 filesystem: std::sync::Arc<dyn crate::model::filesystem::FileSystem + Send + Sync>,
243 process_spawner: std::sync::Arc<dyn crate::services::remote::ProcessSpawner>,
244 ) {
245 for provider in self.providers.values_mut() {
246 if let Some(fp) = provider.as_any_mut().downcast_mut::<FileProvider>() {
247 fp.set_backends(filesystem, process_spawner);
248 return;
249 }
250 }
251 }
252
253 pub fn get_provider_for_input<'a>(
257 &'a self,
258 input: &'a str,
259 ) -> Option<(&'a dyn QuickOpenProvider, &'a str)> {
260 let mut prefixes: Vec<_> = self.providers.keys().collect();
262 prefixes.sort_by_key(|b| std::cmp::Reverse(b.len()));
263
264 for prefix in prefixes {
265 if prefix.is_empty() {
266 continue; }
268 if input.starts_with(prefix.as_str()) {
269 let query = &input[prefix.len()..];
270 return self.providers.get(prefix).map(|p| (p.as_ref(), query));
271 }
272 }
273
274 self.providers.get("").map(|p| (p.as_ref(), input))
276 }
277}
278
279impl Default for QuickOpenRegistry {
280 fn default() -> Self {
281 Self::new()
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 struct TestProvider {
290 prefix: String,
291 }
292
293 impl QuickOpenProvider for TestProvider {
294 fn prefix(&self) -> &str {
295 &self.prefix
296 }
297
298 fn suggestions(&self, _query: &str, _context: &QuickOpenContext) -> Vec<Suggestion> {
299 vec![]
300 }
301
302 fn on_select(
303 &self,
304 _suggestion: Option<&Suggestion>,
305 _query: &str,
306 _context: &QuickOpenContext,
307 ) -> QuickOpenResult {
308 QuickOpenResult::None
309 }
310
311 fn as_any(&self) -> &dyn std::any::Any {
312 self
313 }
314
315 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
316 self
317 }
318 }
319
320 #[test]
321 fn parse_goto_line_input_absolute() {
322 assert_eq!(
323 parse_goto_line_input("500"),
324 Some(GotoLineTarget::Absolute(500))
325 );
326 assert_eq!(
327 parse_goto_line_input(" 42 "),
328 Some(GotoLineTarget::Absolute(42))
329 );
330 }
331
332 #[test]
333 fn parse_goto_line_input_relative() {
334 assert_eq!(
335 parse_goto_line_input("+3"),
336 Some(GotoLineTarget::Relative(3))
337 );
338 assert_eq!(
339 parse_goto_line_input("-3"),
340 Some(GotoLineTarget::Relative(-3))
341 );
342 assert_eq!(
343 parse_goto_line_input(" -10 "),
344 Some(GotoLineTarget::Relative(-10))
345 );
346 }
347
348 #[test]
349 fn parse_goto_line_input_rejects_invalid() {
350 assert_eq!(parse_goto_line_input(""), None);
351 assert_eq!(parse_goto_line_input(" "), None);
352 assert_eq!(parse_goto_line_input("0"), None);
353 assert_eq!(parse_goto_line_input("+0"), None);
354 assert_eq!(parse_goto_line_input("-0"), None);
355 assert_eq!(parse_goto_line_input("+"), None);
356 assert_eq!(parse_goto_line_input("-"), None);
357 assert_eq!(parse_goto_line_input("++3"), None);
358 assert_eq!(parse_goto_line_input("+-3"), None);
359 assert_eq!(parse_goto_line_input("abc"), None);
360 assert_eq!(parse_goto_line_input("3a"), None);
361 }
362
363 #[test]
364 fn test_provider_routing() {
365 let mut registry = QuickOpenRegistry::new();
366
367 registry.register(Box::new(TestProvider {
368 prefix: "".to_string(),
369 }));
370 registry.register(Box::new(TestProvider {
371 prefix: ">".to_string(),
372 }));
373 registry.register(Box::new(TestProvider {
374 prefix: "#".to_string(),
375 }));
376
377 let (provider, query) = registry.get_provider_for_input("hello").unwrap();
379 assert_eq!(provider.prefix(), "");
380 assert_eq!(query, "hello");
381
382 let (provider, query) = registry.get_provider_for_input(">save").unwrap();
384 assert_eq!(provider.prefix(), ">");
385 assert_eq!(query, "save");
386
387 let (provider, query) = registry.get_provider_for_input("#main").unwrap();
389 assert_eq!(provider.prefix(), "#");
390 assert_eq!(query, "main");
391 }
392}