Skip to main content

fresh/input/quick_open/
mod.rs

1//! Quick Open Provider System
2//!
3//! A unified prompt system with prefix-based routing to different providers.
4//! Inspired by VSCode's Quick Open (Ctrl+P) which supports:
5//! - Empty prefix: file finder
6//! - `>`: command palette
7//! - `#`: symbol finder (future)
8//! - `@`: go to symbol in file (future)
9//! - `:`: go to line
10//!
11//! Providers are registered with a prefix and handle suggestion generation
12//! and selection for their domain.
13
14pub 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/// Result of confirming a selection in a provider
23#[derive(Debug, Clone)]
24pub enum QuickOpenResult {
25    /// Execute an editor action
26    ExecuteAction(Action),
27    /// Open a file at optional line/column
28    OpenFile {
29        path: String,
30        line: Option<usize>,
31        column: Option<usize>,
32    },
33    /// Show a buffer by ID
34    ShowBuffer(usize),
35    /// Go to a line in the current buffer
36    GotoLine(usize),
37    /// Do nothing (provider handled it internally)
38    None,
39    /// Show an error message
40    Error(String),
41}
42
43/// Context provided to providers when generating suggestions
44#[derive(Debug, Clone)]
45pub struct QuickOpenContext {
46    /// Current working directory
47    pub cwd: String,
48    /// List of open buffer paths
49    pub open_buffers: Vec<BufferInfo>,
50    /// Active buffer ID
51    pub active_buffer_id: usize,
52    /// Active buffer path (if any)
53    pub active_buffer_path: Option<String>,
54    /// Whether there's an active selection
55    pub has_selection: bool,
56    /// Current key context
57    pub key_context: crate::input::keybindings::KeyContext,
58    /// Active custom contexts (for command filtering)
59    pub custom_contexts: std::collections::HashSet<String>,
60    /// Active buffer mode (e.g., "vi_normal")
61    pub buffer_mode: Option<String>,
62    /// Whether the active buffer's language has an LSP server configured
63    pub has_lsp_config: bool,
64}
65
66/// Information about an open buffer
67#[derive(Debug, Clone)]
68pub struct BufferInfo {
69    pub id: usize,
70    pub path: String,
71    pub name: String,
72    pub modified: bool,
73}
74
75/// Trait for quick open providers
76///
77/// Each provider handles a specific prefix and provides suggestions
78/// for that domain (files, commands, buffers, etc.)
79pub trait QuickOpenProvider: Send + Sync {
80    /// The prefix that triggers this provider (e.g., ">", "#", ":")
81    /// Empty string means this is the default provider (no prefix)
82    fn prefix(&self) -> &str;
83
84    /// Human-readable name for this provider
85    fn name(&self) -> &str;
86
87    /// Short hint shown in the status bar (e.g., ">  Commands")
88    fn hint(&self) -> &str;
89
90    /// Generate suggestions for the given query
91    ///
92    /// The query has already had the prefix stripped.
93    fn suggestions(&self, query: &str, context: &QuickOpenContext) -> Vec<Suggestion>;
94
95    /// Handle selection of a suggestion
96    ///
97    /// `selected_index` is the index into the suggestions array returned by `suggestions()`.
98    /// `query` is the original query (without prefix).
99    fn on_select(
100        &self,
101        selected_index: Option<usize>,
102        query: &str,
103        context: &QuickOpenContext,
104    ) -> QuickOpenResult;
105
106    /// Optional: provide a preview for the selected suggestion
107    /// Returns a file path and optional line number for preview
108    fn preview(
109        &self,
110        _selected_index: usize,
111        _context: &QuickOpenContext,
112    ) -> Option<(String, Option<usize>)> {
113        None
114    }
115}
116
117/// Registry for quick open providers
118pub struct QuickOpenRegistry {
119    /// Providers indexed by their prefix
120    providers: HashMap<String, Box<dyn QuickOpenProvider>>,
121    /// Ordered list of prefixes for hint display
122    prefix_order: Vec<String>,
123}
124
125impl QuickOpenRegistry {
126    pub fn new() -> Self {
127        Self {
128            providers: HashMap::new(),
129            prefix_order: Vec::new(),
130        }
131    }
132
133    /// Register a provider
134    ///
135    /// If a provider with the same prefix exists, it will be replaced.
136    pub fn register(&mut self, provider: Box<dyn QuickOpenProvider>) {
137        let prefix = provider.prefix().to_string();
138        if !self.prefix_order.contains(&prefix) {
139            self.prefix_order.push(prefix.clone());
140        }
141        self.providers.insert(prefix, provider);
142    }
143
144    /// Get the provider for a given input
145    ///
146    /// Returns (provider, query_without_prefix)
147    pub fn get_provider_for_input<'a>(
148        &'a self,
149        input: &'a str,
150    ) -> Option<(&'a dyn QuickOpenProvider, &'a str)> {
151        // Try prefixes in order (longest first to handle overlapping prefixes)
152        let mut prefixes: Vec<_> = self.providers.keys().collect();
153        prefixes.sort_by(|a, b| b.len().cmp(&a.len()));
154
155        for prefix in prefixes {
156            if prefix.is_empty() {
157                continue; // Handle default provider last
158            }
159            if input.starts_with(prefix.as_str()) {
160                let query = &input[prefix.len()..];
161                return self.providers.get(prefix).map(|p| (p.as_ref(), query));
162            }
163        }
164
165        // Fall back to default provider (empty prefix)
166        self.providers.get("").map(|p| (p.as_ref(), input))
167    }
168
169    /// Get the default provider (empty prefix)
170    pub fn get_default_provider(&self) -> Option<&dyn QuickOpenProvider> {
171        self.providers.get("").map(|p| p.as_ref())
172    }
173
174    /// Get hints string for status bar display
175    pub fn get_hints(&self) -> String {
176        self.prefix_order
177            .iter()
178            .filter_map(|prefix| self.providers.get(prefix).map(|p| p.hint()))
179            .collect::<Vec<_>>()
180            .join("   ")
181    }
182
183    /// Get all registered prefixes
184    pub fn prefixes(&self) -> Vec<&str> {
185        self.providers.keys().map(|s| s.as_str()).collect()
186    }
187}
188
189impl Default for QuickOpenRegistry {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    struct TestProvider {
200        prefix: String,
201    }
202
203    impl QuickOpenProvider for TestProvider {
204        fn prefix(&self) -> &str {
205            &self.prefix
206        }
207
208        fn name(&self) -> &str {
209            "Test"
210        }
211
212        fn hint(&self) -> &str {
213            "Test hint"
214        }
215
216        fn suggestions(&self, _query: &str, _context: &QuickOpenContext) -> Vec<Suggestion> {
217            vec![]
218        }
219
220        fn on_select(
221            &self,
222            _selected_index: Option<usize>,
223            _query: &str,
224            _context: &QuickOpenContext,
225        ) -> QuickOpenResult {
226            QuickOpenResult::None
227        }
228    }
229
230    #[test]
231    fn test_provider_routing() {
232        let mut registry = QuickOpenRegistry::new();
233
234        registry.register(Box::new(TestProvider {
235            prefix: "".to_string(),
236        }));
237        registry.register(Box::new(TestProvider {
238            prefix: ">".to_string(),
239        }));
240        registry.register(Box::new(TestProvider {
241            prefix: "#".to_string(),
242        }));
243
244        // Default provider for no prefix
245        let (provider, query) = registry.get_provider_for_input("hello").unwrap();
246        assert_eq!(provider.prefix(), "");
247        assert_eq!(query, "hello");
248
249        // Command provider
250        let (provider, query) = registry.get_provider_for_input(">save").unwrap();
251        assert_eq!(provider.prefix(), ">");
252        assert_eq!(query, "save");
253
254        // Buffer provider
255        let (provider, query) = registry.get_provider_for_input("#main").unwrap();
256        assert_eq!(provider.prefix(), "#");
257        assert_eq!(query, "main");
258    }
259}