Skip to main content

permissive_search/
lib.rs

1//! # Get your search bar right.
2//!
3//! `permissive-search` provides a set of utilities for implementing search interfaces. Check out
4//! the examples in the repo.
5
6#![no_std]
7
8extern crate alloc;
9
10pub mod lookalikes;
11
12use {
13    alloc::{boxed::Box, string::String, vec, vec::Vec},
14    core::mem::swap,
15};
16
17/// A tree that associates a string key with an `usize` index.
18#[derive(Debug, Default)]
19pub struct SearchTree {
20    nodes: Vec<(char, SearchTree)>,
21    end: Option<usize>,
22}
23
24impl<'key> FromIterator<(usize, &'key str)> for SearchTree {
25    fn from_iter<T: IntoIterator<Item = (usize, &'key str)>>(iter: T) -> Self {
26        let mut res = Self::default();
27        for (index, key) in iter {
28            res.push(key, index);
29        }
30        res
31    }
32}
33
34impl SearchTree {
35    /// Get an immediate child node associated with the provided character.
36    pub fn get(&self, index: char) -> Option<&Self> {
37        if self.nodes.last().is_none_or(|(last, _)| index > *last) {
38            return None;
39        }
40
41        self.nodes
42            .binary_search_by_key(&index, |(ch, _)| *ch)
43            .ok()
44            .map(|i| &self.nodes[i].1)
45    }
46
47    /// Add a key to the tree
48    pub fn push(&mut self, key: &str, index: usize) {
49        let mut iter = key.chars();
50        let Some(ch) = iter.next() else {
51            self.end = Some(index);
52            return;
53        };
54
55        let i = match self.nodes.binary_search_by_key(&ch, |(ch, _)| *ch) {
56            Ok(i) => i,
57            Err(i) => {
58                self.nodes.insert(i, (ch, Self::default()));
59                i
60            }
61        };
62
63        self.nodes[i].1.push(iter.as_str(), index);
64    }
65
66    fn for_each_base<E>(&self, f: &mut impl FnMut(usize) -> Result<(), E>) -> Result<(), E> {
67        self.end.map(&mut *f).transpose()?;
68        self.nodes
69            .iter()
70            .try_for_each(|(_, node)| node.for_each_base(f))
71    }
72
73    /// Calls a function on all the keys reachable from this tree node.
74    ///
75    /// # Errors
76    /// The function doesn't fail itself, but it does propagate errors from the callback
77    pub fn for_each<E>(&self, mut f: impl FnMut(usize) -> Result<(), E>) -> Result<(), E> {
78        self.for_each_base(&mut f)
79    }
80}
81
82/// Storage for the state of a search through a [`SearchTree`].
83pub struct Searcher<'tree> {
84    root: &'tree SearchTree,
85    input: String,
86    /// Nodes in consideration
87    considered: Vec<&'tree SearchTree>,
88    /// To be swapped with `considered` after every char input
89    new: Vec<&'tree SearchTree>,
90    /// Temporary buffer for similar chars gathered from `lookalike_gen`
91    lookalikes_buf: Vec<char>,
92    #[allow(clippy::type_complexity, reason = "it's not lol")]
93    lookalike_gen: Box<dyn FnMut(char, &mut Vec<char>)>,
94}
95
96impl Extend<char> for Searcher<'_> {
97    fn extend<T: IntoIterator<Item = char>>(&mut self, iter: T) {
98        for ch in iter {
99            self.push(ch);
100        }
101    }
102}
103
104impl<'tree> Searcher<'tree> {
105    /// Create a new searcher.
106    /// - `root` is the root of the tree to be searched.
107    /// - `iter_gen` is the function that returns an iterator over characters similar to the input one.
108    ///
109    /// The definition of similarity is defined by `iter_gen`.
110    ///
111    /// # Example
112    /// ```rust
113    /// // An example of creating a `Searcher` that only accounts for QWERTY misclicks.
114    /// use permissive_search::*;
115    ///
116    /// # let root = SearchTree::default();
117    /// let searcher = Searcher::new(&root, lookalikes::qwerty_misclicks);
118    /// # _ = searcher;
119    /// ```
120    pub fn new<I: Iterator<Item = char>>(
121        root: &'tree SearchTree,
122        mut iter_gen: impl 'static + FnMut(char) -> I,
123    ) -> Self {
124        Self {
125            root,
126            input: String::new(),
127            considered: vec![root],
128            new: vec![],
129            lookalikes_buf: vec![],
130            lookalike_gen: Box::new(move |ch, dst| dst.extend(iter_gen(ch))),
131        }
132    }
133
134    pub const fn root(&self) -> &'tree SearchTree {
135        self.root
136    }
137
138    pub fn input(&self) -> &str {
139        self.input.as_str()
140    }
141
142    /// Push a character into the searched string
143    pub fn push(&mut self, ch: char) {
144        self.input.push(ch);
145        Self::compute_considerations(
146            ch,
147            &mut self.lookalikes_buf,
148            &mut self.lookalike_gen,
149            &mut self.considered,
150            &mut self.new,
151        );
152    }
153
154    /// Common impl for [`Searcher::push`] & [`Searcher::pop`]
155    fn compute_considerations(
156        ch: char,
157        lookalikes_buf: &mut Vec<char>,
158        lookalike_gen: &mut dyn FnMut(char, &mut Vec<char>),
159        considered: &mut Vec<&'tree SearchTree>,
160        new: &mut Vec<&'tree SearchTree>,
161    ) {
162        lookalikes_buf.clear();
163        lookalikes_buf.push(ch);
164        lookalike_gen(ch, lookalikes_buf);
165
166        new.clear();
167        new.extend(
168            considered
169                .iter()
170                .flat_map(|n| lookalikes_buf.iter().filter_map(|&ch| n.get(ch))),
171        );
172
173        if !new.is_empty() {
174            swap(new, considered);
175        }
176    }
177
178    /// Remove the last character from the searched string, if present.
179    pub fn pop(&mut self) {
180        if self.input.pop().is_none() {
181            return;
182        }
183        self.considered.clear();
184        self.considered.push(self.root);
185        for ch in self.input.chars() {
186            Self::compute_considerations(
187                ch,
188                &mut self.lookalikes_buf,
189                &mut self.lookalike_gen,
190                &mut self.considered,
191                &mut self.new,
192            );
193        }
194    }
195
196    /// Calls a function on every key that could've been referred to by the current input.
197    ///
198    /// # Errors
199    /// The function doesn't fail itself, but it does propagate errors from the callback
200    pub fn for_each_candidate<E>(
201        &self,
202        mut f: impl FnMut(usize) -> Result<(), E>,
203    ) -> Result<(), E> {
204        self.considered
205            .iter()
206            .try_for_each(|n| n.for_each_base(&mut f))
207    }
208}