use heapless::{String, Vec};
pub struct Autocomplete<'a, const NC: usize, const FNL: usize> {
candidates: Vec<&'a str, NC>,
filtered: Vec<&'a str, NC>,
input: String<FNL>,
tab_index: usize,
}
impl<'a, const NC: usize, const FNL: usize> Autocomplete<'a, NC, FNL> {
pub fn new(candidates: Vec<&'a str, NC>) -> Self {
Self {
candidates,
filtered: Vec::new(),
input: String::new(),
tab_index: 0,
}
}
pub fn update_input(&mut self, new_input: String<FNL>) {
self.input = new_input;
self.filtered.clear();
for c in self.candidates.iter().copied() {
if c.starts_with(self.input.as_str()) {
let _ = self.filtered.push(c); }
}
self.tab_index = 0;
if self.filtered.len() == 1 {
self.input.clear();
let _ = self.input.push_str(self.filtered[0]);
let _ = self.input.push(' ');
} else if self.filtered.len() > 1 {
self.input = Self::longest_common_prefix(&self.filtered);
}
}
pub fn cycle_forward(&mut self) {
if self.filtered.is_empty() {
return;
}
self.tab_index = (self.tab_index + 1) % self.filtered.len();
self.input.clear();
let _ = self.input.push_str(self.filtered[self.tab_index]);
let _ = self.input.push(' ');
}
pub fn cycle_backward(&mut self) {
if self.filtered.is_empty() {
return;
}
self.tab_index = if self.tab_index == 0 {
self.filtered.len() - 1
} else {
self.tab_index - 1
};
self.input.clear();
let _ = self.input.push_str(self.filtered[self.tab_index]);
let _ = self.input.push(' ');
}
pub fn current_input(&self) -> &str {
&self.input
}
fn longest_common_prefix(strings: &[&str]) -> String<FNL> {
if strings.is_empty() {
return String::new();
}
let mut prefix = strings[0];
for s in strings.iter().skip(1) {
while !s.starts_with(prefix) {
if prefix.is_empty() {
break;
}
prefix = &prefix[..prefix.len() - 1];
}
}
let mut result = String::new();
let _ = result.push_str(prefix); result
}
pub fn reset(&mut self) {
self.input.clear();
self.filtered.clear();
self.tab_index = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
use heapless::{String, Vec};
const NC: usize = 8;
const FNL: usize = 32;
fn make_candidates() -> Vec<&'static str, NC> {
let mut v: Vec<&'static str, NC> = Vec::new();
v.push("alpha").unwrap();
v.push("alpine").unwrap();
v.push("beta").unwrap();
v.push("gamma").unwrap();
v.push("gamut").unwrap();
v.push("gambit").unwrap();
v.push("zeta").unwrap();
v
}
#[test]
fn test_new() {
let ac: Autocomplete<NC, FNL> = Autocomplete::new(make_candidates());
assert_eq!(ac.current_input(), "");
assert_eq!(ac.filtered.len(), 0);
assert_eq!(ac.tab_index, 0);
}
#[test]
fn test_filter_multiple() {
let mut ac = Autocomplete::<NC, FNL>::new(make_candidates());
let mut s: String<FNL> = String::new();
s.push_str("ga").unwrap();
ac.update_input(s);
assert_eq!(ac.filtered.len(), 3); assert!(ac.filtered.contains(&"gamma"));
assert!(ac.filtered.contains(&"gamut"));
assert!(ac.filtered.contains(&"gambit"));
}
#[test]
fn test_filter_none() {
let mut ac = Autocomplete::<NC, FNL>::new(make_candidates());
let mut s = String::<FNL>::new();
s.push_str("xyz").unwrap();
ac.update_input(s);
assert_eq!(ac.filtered.len(), 0);
assert_eq!(ac.current_input(), "xyz");
}
#[test]
fn test_full_match_auto_complete() {
let mut ac = Autocomplete::<NC, FNL>::new(make_candidates());
let mut s = String::<FNL>::new();
s.push_str("alp").unwrap();
ac.update_input(s);
assert_eq!(ac.filtered.len(), 2);
assert_eq!(ac.current_input(), "alp");
}
#[test]
fn test_single_match_auto_complete() {
let mut ac = Autocomplete::<NC, FNL>::new(make_candidates());
let mut s = String::<FNL>::new();
s.push_str("bet").unwrap();
ac.update_input(s);
assert_eq!(ac.filtered.len(), 1);
assert_eq!(ac.current_input(), "beta ");
}
#[test]
fn test_lcp_no_common_prefix() {
let strings = ["alpha", "beta", "gamma"];
let result = Autocomplete::<NC, FNL>::longest_common_prefix(&strings);
assert_eq!(result, "");
}
#[test]
fn test_lcp_entire_word_common() {
let strings = ["test", "testing", "tester"];
let result = Autocomplete::<NC, FNL>::longest_common_prefix(&strings);
assert_eq!(result, "test");
}
#[test]
fn test_lcp_one_string() {
let strings = ["hello"];
let result = Autocomplete::<NC, FNL>::longest_common_prefix(&strings);
assert_eq!(result, "hello");
}
#[test]
fn test_cycle_forward_wrap() {
let mut ac = Autocomplete::<NC, FNL>::new(make_candidates());
let mut s = String::<FNL>::new();
s.push_str("ga").unwrap();
ac.update_input(s);
ac.cycle_forward(); ac.cycle_forward(); ac.cycle_forward();
assert_eq!(ac.current_input(), "gamma ");
}
#[test]
fn test_cycle_backward_wrap() {
let mut ac = Autocomplete::<NC, FNL>::new(make_candidates());
let mut s = String::<FNL>::new();
s.push_str("ga").unwrap();
ac.update_input(s);
ac.cycle_backward(); assert_eq!(ac.current_input(), "gambit ");
}
#[test]
fn test_cycle_no_filtered_candidates() {
let mut ac = Autocomplete::<NC, FNL>::new(make_candidates());
ac.cycle_forward(); ac.cycle_backward(); assert_eq!(ac.current_input(), "");
}
#[test]
fn test_empty_candidate_list() {
let empty: Vec<&'static str, NC> = Vec::new();
let mut ac = Autocomplete::<NC, FNL>::new(empty);
let mut s = String::<FNL>::new();
s.push_str("a").unwrap();
ac.update_input(s);
assert_eq!(ac.filtered.len(), 0);
assert_eq!(ac.current_input(), "a");
}
#[test]
fn test_reset() {
let mut ac = Autocomplete::<NC, FNL>::new(make_candidates());
let mut s = String::<FNL>::new();
s.push_str("alp").unwrap();
ac.update_input(s);
ac.reset();
assert_eq!(ac.current_input(), "");
assert_eq!(ac.filtered.len(), 0);
assert_eq!(ac.tab_index, 0);
}
#[test]
fn test_filtered_overflow_graceful() {
let mut v: Vec<&'static str, 4> = Vec::new();
v.push("abc").unwrap();
v.push("abcd").unwrap();
v.push("abcde").unwrap();
v.push("abcdef").unwrap();
let mut ac = Autocomplete::<4, FNL>::new(v);
let mut s = String::<FNL>::new();
s.push_str("a").unwrap();
ac.update_input(s);
assert_eq!(ac.filtered.len(), 4);
assert_eq!(ac.current_input(), "abc"); }
#[test]
fn test_candidate_list_overflow_handling() {
let mut v: Vec<&'static str, 2> = Vec::new();
v.push("alpha").unwrap();
v.push("beta").unwrap();
let overflow_attempt = v.push("gamma");
assert!(overflow_attempt.is_err());
}
#[test]
fn test_fuzz_random_sequences() {
let mut ac = Autocomplete::<NC, FNL>::new(make_candidates());
let test_inputs = [
"a", "al", "alp", "alpi", "g", "ga", "gam", "gamb", "z", "ze", "zet", "zeta",
];
for inp in test_inputs {
let mut s = String::<FNL>::new();
s.push_str(inp).unwrap();
ac.update_input(s);
let prefix = inp;
for f in ac.filtered.iter() {
assert!(f.starts_with(prefix));
}
if ac.filtered.len() > 0 {
assert!(ac.tab_index < ac.filtered.len());
} else {
assert_eq!(ac.tab_index, 0);
}
}
}
}