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 pub is_virtual: bool,
126}
127
128pub fn parse_path_line_col(input: &str) -> (String, Option<usize>, Option<usize>) {
132 use std::path::{Component, Path};
133
134 let trimmed = input.trim();
135 if trimmed.is_empty() {
136 return (String::new(), None, None);
137 }
138
139 let has_drive = Path::new(trimmed)
141 .components()
142 .next()
143 .is_some_and(|c| matches!(c, Component::Prefix(_)));
144 let search_start = if has_drive {
145 trimmed.find(':').map(|i| i + 1).unwrap_or(0)
146 } else {
147 0
148 };
149
150 let suffix = &trimmed[search_start..];
151 let parts: Vec<&str> = suffix.rsplitn(3, ':').collect();
152
153 let rebuild_path = |rest: &str| {
155 if has_drive {
156 format!("{}{}", &trimmed[..search_start], rest)
157 } else {
158 rest.to_string()
159 }
160 };
161
162 let parsed = match parts.as_slice() {
164 [col_s, line_s, rest] if !rest.is_empty() => col_s
165 .parse::<usize>()
166 .ok()
167 .filter(|&c| c > 0)
168 .zip(line_s.parse::<usize>().ok().filter(|&l| l > 0))
169 .map(|(col, line)| (rebuild_path(rest), Some(line), Some(col))),
170 [line_s, rest] if !rest.is_empty() => line_s
171 .parse::<usize>()
172 .ok()
173 .filter(|&l| l > 0)
174 .map(|line| (rebuild_path(rest), Some(line), None)),
175 _ => None,
176 };
177
178 parsed.unwrap_or_else(|| (trimmed.to_string(), None, None))
179}
180
181pub trait QuickOpenProvider: Send + Sync {
186 fn prefix(&self) -> &str;
189
190 fn suggestions(&self, query: &str, context: &QuickOpenContext) -> Vec<Suggestion>;
194
195 fn on_select(
200 &self,
201 suggestion: Option<&Suggestion>,
202 query: &str,
203 context: &QuickOpenContext,
204 ) -> QuickOpenResult;
205
206 fn as_any(&self) -> &dyn std::any::Any;
208
209 fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
213}
214
215pub struct QuickOpenRegistry {
217 providers: HashMap<String, Box<dyn QuickOpenProvider>>,
219}
220
221impl QuickOpenRegistry {
222 pub fn new() -> Self {
223 Self {
224 providers: HashMap::new(),
225 }
226 }
227
228 pub fn register(&mut self, provider: Box<dyn QuickOpenProvider>) {
232 let prefix = provider.prefix().to_string();
233 self.providers.insert(prefix, provider);
234 }
235
236 pub fn set_file_backends(
245 &mut self,
246 filesystem: std::sync::Arc<dyn crate::model::filesystem::FileSystem + Send + Sync>,
247 process_spawner: std::sync::Arc<dyn crate::services::remote::ProcessSpawner>,
248 ) {
249 for provider in self.providers.values_mut() {
250 if let Some(fp) = provider.as_any_mut().downcast_mut::<FileProvider>() {
251 fp.set_backends(filesystem, process_spawner);
252 return;
253 }
254 }
255 }
256
257 pub fn get_provider_for_input<'a>(
261 &'a self,
262 input: &'a str,
263 ) -> Option<(&'a dyn QuickOpenProvider, &'a str)> {
264 let mut prefixes: Vec<_> = self.providers.keys().collect();
266 prefixes.sort_by_key(|b| std::cmp::Reverse(b.len()));
267
268 for prefix in prefixes {
269 if prefix.is_empty() {
270 continue; }
272 if input.starts_with(prefix.as_str()) {
273 let query = &input[prefix.len()..];
274 return self.providers.get(prefix).map(|p| (p.as_ref(), query));
275 }
276 }
277
278 self.providers.get("").map(|p| (p.as_ref(), input))
280 }
281}
282
283impl Default for QuickOpenRegistry {
284 fn default() -> Self {
285 Self::new()
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 struct TestProvider {
294 prefix: String,
295 }
296
297 impl QuickOpenProvider for TestProvider {
298 fn prefix(&self) -> &str {
299 &self.prefix
300 }
301
302 fn suggestions(&self, _query: &str, _context: &QuickOpenContext) -> Vec<Suggestion> {
303 vec![]
304 }
305
306 fn on_select(
307 &self,
308 _suggestion: Option<&Suggestion>,
309 _query: &str,
310 _context: &QuickOpenContext,
311 ) -> QuickOpenResult {
312 QuickOpenResult::None
313 }
314
315 fn as_any(&self) -> &dyn std::any::Any {
316 self
317 }
318
319 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
320 self
321 }
322 }
323
324 #[test]
325 fn parse_goto_line_input_absolute() {
326 assert_eq!(
327 parse_goto_line_input("500"),
328 Some(GotoLineTarget::Absolute(500))
329 );
330 assert_eq!(
331 parse_goto_line_input(" 42 "),
332 Some(GotoLineTarget::Absolute(42))
333 );
334 }
335
336 #[test]
337 fn parse_goto_line_input_relative() {
338 assert_eq!(
339 parse_goto_line_input("+3"),
340 Some(GotoLineTarget::Relative(3))
341 );
342 assert_eq!(
343 parse_goto_line_input("-3"),
344 Some(GotoLineTarget::Relative(-3))
345 );
346 assert_eq!(
347 parse_goto_line_input(" -10 "),
348 Some(GotoLineTarget::Relative(-10))
349 );
350 }
351
352 #[test]
353 fn parse_goto_line_input_rejects_invalid() {
354 assert_eq!(parse_goto_line_input(""), None);
355 assert_eq!(parse_goto_line_input(" "), None);
356 assert_eq!(parse_goto_line_input("0"), None);
357 assert_eq!(parse_goto_line_input("+0"), None);
358 assert_eq!(parse_goto_line_input("-0"), None);
359 assert_eq!(parse_goto_line_input("+"), None);
360 assert_eq!(parse_goto_line_input("-"), None);
361 assert_eq!(parse_goto_line_input("++3"), None);
362 assert_eq!(parse_goto_line_input("+-3"), None);
363 assert_eq!(parse_goto_line_input("abc"), None);
364 assert_eq!(parse_goto_line_input("3a"), None);
365 }
366
367 #[test]
368 fn test_provider_routing() {
369 let mut registry = QuickOpenRegistry::new();
370
371 registry.register(Box::new(TestProvider {
372 prefix: "".to_string(),
373 }));
374 registry.register(Box::new(TestProvider {
375 prefix: ">".to_string(),
376 }));
377 registry.register(Box::new(TestProvider {
378 prefix: "#".to_string(),
379 }));
380
381 let (provider, query) = registry.get_provider_for_input("hello").unwrap();
383 assert_eq!(provider.prefix(), "");
384 assert_eq!(query, "hello");
385
386 let (provider, query) = registry.get_provider_for_input(">save").unwrap();
388 assert_eq!(provider.prefix(), ">");
389 assert_eq!(query, "save");
390
391 let (provider, query) = registry.get_provider_for_input("#main").unwrap();
393 assert_eq!(provider.prefix(), "#");
394 assert_eq!(query, "main");
395 }
396}