pub mod providers;
pub use providers::{BufferProvider, CommandProvider, FileProvider, GotoLineProvider};
use crate::input::commands::Suggestion;
use crate::input::keybindings::Action;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum QuickOpenResult {
ExecuteAction(Action),
OpenFile {
path: String,
line: Option<usize>,
column: Option<usize>,
},
ShowBuffer(usize),
GotoLine(isize),
None,
Error(String),
}
#[derive(Debug, Clone)]
pub struct QuickOpenContext {
pub cwd: String,
pub open_buffers: Vec<BufferInfo>,
pub active_buffer_id: usize,
pub active_buffer_path: Option<String>,
pub has_selection: bool,
pub key_context: crate::input::keybindings::KeyContext,
pub custom_contexts: std::collections::HashSet<String>,
pub buffer_mode: Option<String>,
pub has_lsp_config: bool,
pub relative_line_numbers: bool,
}
#[derive(Debug, Clone)]
pub struct BufferInfo {
pub id: usize,
pub path: String,
pub name: String,
pub modified: bool,
}
pub fn parse_path_line_col(input: &str) -> (String, Option<usize>, Option<usize>) {
use std::path::{Component, Path};
let trimmed = input.trim();
if trimmed.is_empty() {
return (String::new(), None, None);
}
let has_drive = Path::new(trimmed)
.components()
.next()
.is_some_and(|c| matches!(c, Component::Prefix(_)));
let search_start = if has_drive {
trimmed.find(':').map(|i| i + 1).unwrap_or(0)
} else {
0
};
let suffix = &trimmed[search_start..];
let parts: Vec<&str> = suffix.rsplitn(3, ':').collect();
let rebuild_path = |rest: &str| {
if has_drive {
format!("{}{}", &trimmed[..search_start], rest)
} else {
rest.to_string()
}
};
let parsed = match parts.as_slice() {
[col_s, line_s, rest] if !rest.is_empty() => col_s
.parse::<usize>()
.ok()
.filter(|&c| c > 0)
.zip(line_s.parse::<usize>().ok().filter(|&l| l > 0))
.map(|(col, line)| (rebuild_path(rest), Some(line), Some(col))),
[line_s, rest] if !rest.is_empty() => line_s
.parse::<usize>()
.ok()
.filter(|&l| l > 0)
.map(|line| (rebuild_path(rest), Some(line), None)),
_ => None,
};
parsed.unwrap_or_else(|| (trimmed.to_string(), None, None))
}
pub trait QuickOpenProvider: Send + Sync {
fn prefix(&self) -> &str;
fn suggestions(&self, query: &str, context: &QuickOpenContext) -> Vec<Suggestion>;
fn on_select(
&self,
suggestion: Option<&Suggestion>,
query: &str,
context: &QuickOpenContext,
) -> QuickOpenResult;
fn as_any(&self) -> &dyn std::any::Any;
}
pub struct QuickOpenRegistry {
providers: HashMap<String, Box<dyn QuickOpenProvider>>,
}
impl QuickOpenRegistry {
pub fn new() -> Self {
Self {
providers: HashMap::new(),
}
}
pub fn register(&mut self, provider: Box<dyn QuickOpenProvider>) {
let prefix = provider.prefix().to_string();
self.providers.insert(prefix, provider);
}
pub fn get_provider_for_input<'a>(
&'a self,
input: &'a str,
) -> Option<(&'a dyn QuickOpenProvider, &'a str)> {
let mut prefixes: Vec<_> = self.providers.keys().collect();
prefixes.sort_by_key(|b| std::cmp::Reverse(b.len()));
for prefix in prefixes {
if prefix.is_empty() {
continue; }
if input.starts_with(prefix.as_str()) {
let query = &input[prefix.len()..];
return self.providers.get(prefix).map(|p| (p.as_ref(), query));
}
}
self.providers.get("").map(|p| (p.as_ref(), input))
}
}
impl Default for QuickOpenRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestProvider {
prefix: String,
}
impl QuickOpenProvider for TestProvider {
fn prefix(&self) -> &str {
&self.prefix
}
fn suggestions(&self, _query: &str, _context: &QuickOpenContext) -> Vec<Suggestion> {
vec![]
}
fn on_select(
&self,
_suggestion: Option<&Suggestion>,
_query: &str,
_context: &QuickOpenContext,
) -> QuickOpenResult {
QuickOpenResult::None
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[test]
fn test_provider_routing() {
let mut registry = QuickOpenRegistry::new();
registry.register(Box::new(TestProvider {
prefix: "".to_string(),
}));
registry.register(Box::new(TestProvider {
prefix: ">".to_string(),
}));
registry.register(Box::new(TestProvider {
prefix: "#".to_string(),
}));
let (provider, query) = registry.get_provider_for_input("hello").unwrap();
assert_eq!(provider.prefix(), "");
assert_eq!(query, "hello");
let (provider, query) = registry.get_provider_for_input(">save").unwrap();
assert_eq!(provider.prefix(), ">");
assert_eq!(query, "save");
let (provider, query) = registry.get_provider_for_input("#main").unwrap();
assert_eq!(provider.prefix(), "#");
assert_eq!(query, "main");
}
}