Skip to main content

photon_ui/
autocomplete.rs

1use crate::fuzzy::fuzzy_filter;
2
3/// Trait for components that provide completion suggestions.
4pub trait AutocompleteProvider {
5    /// Return suggestions for the given query string.
6    fn suggest(&self, query: &str) -> Vec<String>;
7}
8
9/// A simple autocomplete provider backed by a static list of strings.
10///
11/// Uses fuzzy filtering to rank results. An empty query returns all items.
12pub struct SimpleAutocomplete {
13    items: Vec<String>,
14}
15
16impl SimpleAutocomplete {
17    /// Create a provider from a list of candidate strings.
18    pub fn new(items: Vec<String>) -> Self {
19        Self { items }
20    }
21}
22
23impl AutocompleteProvider for SimpleAutocomplete {
24    fn suggest(&self, query: &str) -> Vec<String> {
25        if query.is_empty() {
26            return self.items.clone();
27        }
28        let filtered = fuzzy_filter(&self.items, query, |s| s.as_str());
29        filtered.into_iter().map(|s| s.clone()).collect()
30    }
31}
32
33/// Combined autocomplete provider that handles slash-prefixed commands
34/// differently from plain text.
35///
36/// If the query starts with `/`, the slash is stripped before matching
37/// against the command list, then prepended back onto each result.
38pub struct CombinedAutocompleteProvider {
39    commands: Vec<String>,
40    _base_path: String,
41}
42
43impl CombinedAutocompleteProvider {
44    /// Create a combined provider.
45    pub fn new(commands: Vec<String>, base_path: impl Into<String>) -> Self {
46        Self {
47            commands,
48            _base_path: base_path.into(),
49        }
50    }
51}
52
53impl AutocompleteProvider for CombinedAutocompleteProvider {
54    fn suggest(&self, query: &str) -> Vec<String> {
55        if query.starts_with('/') {
56            fuzzy_filter(&self.commands, &query[1..], |s| s.as_str())
57                .into_iter()
58                .map(|s| format!("/{}", s))
59                .collect()
60        } else {
61            fuzzy_filter(&self.commands, query, |s| s.as_str())
62                .into_iter()
63                .map(|s| s.clone())
64                .collect()
65        }
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn simple_autocomplete_empty_query() {
75        let auto = SimpleAutocomplete::new(vec!["apple".into(), "banana".into()]);
76        let suggestions = auto.suggest("");
77        assert_eq!(suggestions.len(), 2);
78    }
79
80    #[test]
81    fn combined_autocomplete_non_slash() {
82        let auto = CombinedAutocompleteProvider::new(vec!["cmd".into()], "/base");
83        let suggestions = auto.suggest("c");
84        assert_eq!(suggestions.len(), 1);
85        assert_eq!(suggestions[0], "cmd");
86    }
87
88    #[test]
89    fn combined_autocomplete_slash() {
90        let auto = CombinedAutocompleteProvider::new(vec!["cmd".into()], "/base");
91        let suggestions = auto.suggest("/c");
92        assert_eq!(suggestions.len(), 1);
93        assert_eq!(suggestions[0], "/cmd");
94    }
95}