fresh/input/quick_open/
mod.rs1pub 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(usize),
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}
65
66#[derive(Debug, Clone)]
68pub struct BufferInfo {
69 pub id: usize,
70 pub path: String,
71 pub name: String,
72 pub modified: bool,
73}
74
75pub fn parse_path_line_col(input: &str) -> (String, Option<usize>, Option<usize>) {
79 use std::path::{Component, Path};
80
81 let trimmed = input.trim();
82 if trimmed.is_empty() {
83 return (String::new(), None, None);
84 }
85
86 let has_drive = Path::new(trimmed)
88 .components()
89 .next()
90 .is_some_and(|c| matches!(c, Component::Prefix(_)));
91 let search_start = if has_drive {
92 trimmed.find(':').map(|i| i + 1).unwrap_or(0)
93 } else {
94 0
95 };
96
97 let suffix = &trimmed[search_start..];
98 let parts: Vec<&str> = suffix.rsplitn(3, ':').collect();
99
100 let rebuild_path = |rest: &str| {
102 if has_drive {
103 format!("{}{}", &trimmed[..search_start], rest)
104 } else {
105 rest.to_string()
106 }
107 };
108
109 let parsed = match parts.as_slice() {
111 [col_s, line_s, rest] if !rest.is_empty() => col_s
112 .parse::<usize>()
113 .ok()
114 .filter(|&c| c > 0)
115 .zip(line_s.parse::<usize>().ok().filter(|&l| l > 0))
116 .map(|(col, line)| (rebuild_path(rest), Some(line), Some(col))),
117 [line_s, rest] if !rest.is_empty() => line_s
118 .parse::<usize>()
119 .ok()
120 .filter(|&l| l > 0)
121 .map(|line| (rebuild_path(rest), Some(line), None)),
122 _ => None,
123 };
124
125 parsed.unwrap_or_else(|| (trimmed.to_string(), None, None))
126}
127
128pub trait QuickOpenProvider: Send + Sync {
133 fn prefix(&self) -> &str;
136
137 fn suggestions(&self, query: &str, context: &QuickOpenContext) -> Vec<Suggestion>;
141
142 fn on_select(
147 &self,
148 suggestion: Option<&Suggestion>,
149 query: &str,
150 context: &QuickOpenContext,
151 ) -> QuickOpenResult;
152
153 fn as_any(&self) -> &dyn std::any::Any;
155}
156
157pub struct QuickOpenRegistry {
159 providers: HashMap<String, Box<dyn QuickOpenProvider>>,
161}
162
163impl QuickOpenRegistry {
164 pub fn new() -> Self {
165 Self {
166 providers: HashMap::new(),
167 }
168 }
169
170 pub fn register(&mut self, provider: Box<dyn QuickOpenProvider>) {
174 let prefix = provider.prefix().to_string();
175 self.providers.insert(prefix, provider);
176 }
177
178 pub fn get_provider_for_input<'a>(
182 &'a self,
183 input: &'a str,
184 ) -> Option<(&'a dyn QuickOpenProvider, &'a str)> {
185 let mut prefixes: Vec<_> = self.providers.keys().collect();
187 prefixes.sort_by_key(|b| std::cmp::Reverse(b.len()));
188
189 for prefix in prefixes {
190 if prefix.is_empty() {
191 continue; }
193 if input.starts_with(prefix.as_str()) {
194 let query = &input[prefix.len()..];
195 return self.providers.get(prefix).map(|p| (p.as_ref(), query));
196 }
197 }
198
199 self.providers.get("").map(|p| (p.as_ref(), input))
201 }
202}
203
204impl Default for QuickOpenRegistry {
205 fn default() -> Self {
206 Self::new()
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 struct TestProvider {
215 prefix: String,
216 }
217
218 impl QuickOpenProvider for TestProvider {
219 fn prefix(&self) -> &str {
220 &self.prefix
221 }
222
223 fn suggestions(&self, _query: &str, _context: &QuickOpenContext) -> Vec<Suggestion> {
224 vec![]
225 }
226
227 fn on_select(
228 &self,
229 _suggestion: Option<&Suggestion>,
230 _query: &str,
231 _context: &QuickOpenContext,
232 ) -> QuickOpenResult {
233 QuickOpenResult::None
234 }
235
236 fn as_any(&self) -> &dyn std::any::Any {
237 self
238 }
239 }
240
241 #[test]
242 fn test_provider_routing() {
243 let mut registry = QuickOpenRegistry::new();
244
245 registry.register(Box::new(TestProvider {
246 prefix: "".to_string(),
247 }));
248 registry.register(Box::new(TestProvider {
249 prefix: ">".to_string(),
250 }));
251 registry.register(Box::new(TestProvider {
252 prefix: "#".to_string(),
253 }));
254
255 let (provider, query) = registry.get_provider_for_input("hello").unwrap();
257 assert_eq!(provider.prefix(), "");
258 assert_eq!(query, "hello");
259
260 let (provider, query) = registry.get_provider_for_input(">save").unwrap();
262 assert_eq!(provider.prefix(), ">");
263 assert_eq!(query, "save");
264
265 let (provider, query) = registry.get_provider_for_input("#main").unwrap();
267 assert_eq!(provider.prefix(), "#");
268 assert_eq!(query, "main");
269 }
270}