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}
63
64/// Information about an open buffer
65#[derive(Debug, Clone)]
66pub struct BufferInfo {
67    pub id: usize,
68    pub path: String,
69    pub name: String,
70    pub modified: bool,
71}
72
73/// Trait for quick open providers
74///
75/// Each provider handles a specific prefix and provides suggestions
76/// for that domain (files, commands, buffers, etc.)
77pub trait QuickOpenProvider: Send + Sync {
78    /// The prefix that triggers this provider (e.g., ">", "#", ":")
79    /// Empty string means this is the default provider (no prefix)
80    fn prefix(&self) -> &str;
81
82    /// Human-readable name for this provider
83    fn name(&self) -> &str;
84
85    /// Short hint shown in the status bar (e.g., ">  Commands")
86    fn hint(&self) -> &str;
87
88    /// Generate suggestions for the given query
89    ///
90    /// The query has already had the prefix stripped.
91    fn suggestions(&self, query: &str, context: &QuickOpenContext) -> Vec<Suggestion>;
92
93    /// Handle selection of a suggestion
94    ///
95    /// `selected_index` is the index into the suggestions array returned by `suggestions()`.
96    /// `query` is the original query (without prefix).
97    fn on_select(
98        &self,
99        selected_index: Option<usize>,
100        query: &str,
101        context: &QuickOpenContext,
102    ) -> QuickOpenResult;
103
104    /// Optional: provide a preview for the selected suggestion
105    /// Returns a file path and optional line number for preview
106    fn preview(
107        &self,
108        _selected_index: usize,
109        _context: &QuickOpenContext,
110    ) -> Option<(String, Option<usize>)> {
111        None
112    }
113}
114
115/// Registry for quick open providers
116pub struct QuickOpenRegistry {
117    /// Providers indexed by their prefix
118    providers: HashMap<String, Box<dyn QuickOpenProvider>>,
119    /// Ordered list of prefixes for hint display
120    prefix_order: Vec<String>,
121}
122
123impl QuickOpenRegistry {
124    pub fn new() -> Self {
125        Self {
126            providers: HashMap::new(),
127            prefix_order: Vec::new(),
128        }
129    }
130
131    /// Register a provider
132    ///
133    /// If a provider with the same prefix exists, it will be replaced.
134    pub fn register(&mut self, provider: Box<dyn QuickOpenProvider>) {
135        let prefix = provider.prefix().to_string();
136        if !self.prefix_order.contains(&prefix) {
137            self.prefix_order.push(prefix.clone());
138        }
139        self.providers.insert(prefix, provider);
140    }
141
142    /// Get the provider for a given input
143    ///
144    /// Returns (provider, query_without_prefix)
145    pub fn get_provider_for_input<'a>(
146        &'a self,
147        input: &'a str,
148    ) -> Option<(&'a dyn QuickOpenProvider, &'a str)> {
149        // Try prefixes in order (longest first to handle overlapping prefixes)
150        let mut prefixes: Vec<_> = self.providers.keys().collect();
151        prefixes.sort_by(|a, b| b.len().cmp(&a.len()));
152
153        for prefix in prefixes {
154            if prefix.is_empty() {
155                continue; // Handle default provider last
156            }
157            if input.starts_with(prefix.as_str()) {
158                let query = &input[prefix.len()..];
159                return self.providers.get(prefix).map(|p| (p.as_ref(), query));
160            }
161        }
162
163        // Fall back to default provider (empty prefix)
164        self.providers.get("").map(|p| (p.as_ref(), input))
165    }
166
167    /// Get the default provider (empty prefix)
168    pub fn get_default_provider(&self) -> Option<&dyn QuickOpenProvider> {
169        self.providers.get("").map(|p| p.as_ref())
170    }
171
172    /// Get hints string for status bar display
173    pub fn get_hints(&self) -> String {
174        self.prefix_order
175            .iter()
176            .filter_map(|prefix| self.providers.get(prefix).map(|p| p.hint()))
177            .collect::<Vec<_>>()
178            .join("   ")
179    }
180
181    /// Get all registered prefixes
182    pub fn prefixes(&self) -> Vec<&str> {
183        self.providers.keys().map(|s| s.as_str()).collect()
184    }
185}
186
187impl Default for QuickOpenRegistry {
188    fn default() -> Self {
189        Self::new()
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    struct TestProvider {
198        prefix: String,
199    }
200
201    impl QuickOpenProvider for TestProvider {
202        fn prefix(&self) -> &str {
203            &self.prefix
204        }
205
206        fn name(&self) -> &str {
207            "Test"
208        }
209
210        fn hint(&self) -> &str {
211            "Test hint"
212        }
213
214        fn suggestions(&self, _query: &str, _context: &QuickOpenContext) -> Vec<Suggestion> {
215            vec![]
216        }
217
218        fn on_select(
219            &self,
220            _selected_index: Option<usize>,
221            _query: &str,
222            _context: &QuickOpenContext,
223        ) -> QuickOpenResult {
224            QuickOpenResult::None
225        }
226    }
227
228    #[test]
229    fn test_provider_routing() {
230        let mut registry = QuickOpenRegistry::new();
231
232        registry.register(Box::new(TestProvider {
233            prefix: "".to_string(),
234        }));
235        registry.register(Box::new(TestProvider {
236            prefix: ">".to_string(),
237        }));
238        registry.register(Box::new(TestProvider {
239            prefix: "#".to_string(),
240        }));
241
242        // Default provider for no prefix
243        let (provider, query) = registry.get_provider_for_input("hello").unwrap();
244        assert_eq!(provider.prefix(), "");
245        assert_eq!(query, "hello");
246
247        // Command provider
248        let (provider, query) = registry.get_provider_for_input(">save").unwrap();
249        assert_eq!(provider.prefix(), ">");
250        assert_eq!(query, "save");
251
252        // Buffer provider
253        let (provider, query) = registry.get_provider_for_input("#main").unwrap();
254        assert_eq!(provider.prefix(), "#");
255        assert_eq!(query, "main");
256    }
257}