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(isize),
37 None,
39 Error(String),
41}
42
43#[derive(Debug, Clone)]
45pub struct QuickOpenContext {
46 pub cwd: String,
48 pub open_buffers: Vec<BufferInfo>,
50 pub active_buffer_id: usize,
52 pub active_buffer_path: Option<String>,
54 pub has_selection: bool,
56 pub key_context: crate::input::keybindings::KeyContext,
58 pub custom_contexts: std::collections::HashSet<String>,
60 pub buffer_mode: Option<String>,
62 pub has_lsp_config: bool,
64 pub relative_line_numbers: bool,
66}
67
68#[derive(Debug, Clone)]
70pub struct BufferInfo {
71 pub id: usize,
72 pub path: String,
73 pub name: String,
74 pub modified: bool,
75}
76
77pub fn parse_path_line_col(input: &str) -> (String, Option<usize>, Option<usize>) {
81 use std::path::{Component, Path};
82
83 let trimmed = input.trim();
84 if trimmed.is_empty() {
85 return (String::new(), None, None);
86 }
87
88 let has_drive = Path::new(trimmed)
90 .components()
91 .next()
92 .is_some_and(|c| matches!(c, Component::Prefix(_)));
93 let search_start = if has_drive {
94 trimmed.find(':').map(|i| i + 1).unwrap_or(0)
95 } else {
96 0
97 };
98
99 let suffix = &trimmed[search_start..];
100 let parts: Vec<&str> = suffix.rsplitn(3, ':').collect();
101
102 let rebuild_path = |rest: &str| {
104 if has_drive {
105 format!("{}{}", &trimmed[..search_start], rest)
106 } else {
107 rest.to_string()
108 }
109 };
110
111 let parsed = match parts.as_slice() {
113 [col_s, line_s, rest] if !rest.is_empty() => col_s
114 .parse::<usize>()
115 .ok()
116 .filter(|&c| c > 0)
117 .zip(line_s.parse::<usize>().ok().filter(|&l| l > 0))
118 .map(|(col, line)| (rebuild_path(rest), Some(line), Some(col))),
119 [line_s, rest] if !rest.is_empty() => line_s
120 .parse::<usize>()
121 .ok()
122 .filter(|&l| l > 0)
123 .map(|line| (rebuild_path(rest), Some(line), None)),
124 _ => None,
125 };
126
127 parsed.unwrap_or_else(|| (trimmed.to_string(), None, None))
128}
129
130pub trait QuickOpenProvider: Send + Sync {
135 fn prefix(&self) -> &str;
138
139 fn suggestions(&self, query: &str, context: &QuickOpenContext) -> Vec<Suggestion>;
143
144 fn on_select(
149 &self,
150 suggestion: Option<&Suggestion>,
151 query: &str,
152 context: &QuickOpenContext,
153 ) -> QuickOpenResult;
154
155 fn as_any(&self) -> &dyn std::any::Any;
157}
158
159pub struct QuickOpenRegistry {
161 providers: HashMap<String, Box<dyn QuickOpenProvider>>,
163}
164
165impl QuickOpenRegistry {
166 pub fn new() -> Self {
167 Self {
168 providers: HashMap::new(),
169 }
170 }
171
172 pub fn register(&mut self, provider: Box<dyn QuickOpenProvider>) {
176 let prefix = provider.prefix().to_string();
177 self.providers.insert(prefix, provider);
178 }
179
180 pub fn get_provider_for_input<'a>(
184 &'a self,
185 input: &'a str,
186 ) -> Option<(&'a dyn QuickOpenProvider, &'a str)> {
187 let mut prefixes: Vec<_> = self.providers.keys().collect();
189 prefixes.sort_by_key(|b| std::cmp::Reverse(b.len()));
190
191 for prefix in prefixes {
192 if prefix.is_empty() {
193 continue; }
195 if input.starts_with(prefix.as_str()) {
196 let query = &input[prefix.len()..];
197 return self.providers.get(prefix).map(|p| (p.as_ref(), query));
198 }
199 }
200
201 self.providers.get("").map(|p| (p.as_ref(), input))
203 }
204}
205
206impl Default for QuickOpenRegistry {
207 fn default() -> Self {
208 Self::new()
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 struct TestProvider {
217 prefix: String,
218 }
219
220 impl QuickOpenProvider for TestProvider {
221 fn prefix(&self) -> &str {
222 &self.prefix
223 }
224
225 fn suggestions(&self, _query: &str, _context: &QuickOpenContext) -> Vec<Suggestion> {
226 vec![]
227 }
228
229 fn on_select(
230 &self,
231 _suggestion: Option<&Suggestion>,
232 _query: &str,
233 _context: &QuickOpenContext,
234 ) -> QuickOpenResult {
235 QuickOpenResult::None
236 }
237
238 fn as_any(&self) -> &dyn std::any::Any {
239 self
240 }
241 }
242
243 #[test]
244 fn test_provider_routing() {
245 let mut registry = QuickOpenRegistry::new();
246
247 registry.register(Box::new(TestProvider {
248 prefix: "".to_string(),
249 }));
250 registry.register(Box::new(TestProvider {
251 prefix: ">".to_string(),
252 }));
253 registry.register(Box::new(TestProvider {
254 prefix: "#".to_string(),
255 }));
256
257 let (provider, query) = registry.get_provider_for_input("hello").unwrap();
259 assert_eq!(provider.prefix(), "");
260 assert_eq!(query, "hello");
261
262 let (provider, query) = registry.get_provider_for_input(">save").unwrap();
264 assert_eq!(provider.prefix(), ">");
265 assert_eq!(query, "save");
266
267 let (provider, query) = registry.get_provider_for_input("#main").unwrap();
269 assert_eq!(provider.prefix(), "#");
270 assert_eq!(query, "main");
271 }
272}