From edfd3933dbdfe30e05533beff625e0ddd446f41f Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Fri, 18 Dec 2020 16:43:15 +0900 Subject: picker: Implement fuzzy search. --- helix-term/src/ui/picker.rs | 67 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 10 deletions(-) (limited to 'helix-term') diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 5046ef74..82dbdd2e 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -21,6 +21,8 @@ pub struct Picker { files: Vec, // filter: String, matcher: Box, + /// (index, score) + matches: Vec<(usize, i64)>, cursor: usize, // pattern: String, @@ -51,20 +53,46 @@ impl Picker { const MAX: usize = 1024; - Self { + let mut picker = Self { files: files.take(MAX).collect(), matcher: Box::new(Matcher::default()), + matches: Vec::new(), cursor: 0, prompt, - } + }; + + // TODO: scoring on empty input should just use a fastpath + picker.score(); + + picker } - pub fn score(&mut self, pattern: &str) { - self.files.iter().filter_map(|path| match path.to_str() { - // TODO: using fuzzy_indices could give us the char idx for match highlighting - Some(path) => (self.matcher.fuzzy_match(path, pattern)), - None => None, - }); + pub fn score(&mut self) { + // need to borrow via pattern match otherwise it complains about simultaneous borrow + let Self { + ref mut files, + ref mut matcher, + ref mut matches, + .. + } = *self; + + let pattern = &self.prompt.line; + + // reuse the matches allocation + matches.clear(); + matches.extend(self.files.iter().enumerate().filter_map(|(index, path)| { + match path.to_str() { + // TODO: using fuzzy_indices could give us the char idx for match highlighting + Some(path) => matcher + .fuzzy_match(path, pattern) + .map(|score| (index, score)), + None => None, + } + })); + matches.sort_unstable_by_key(|(_, score)| -score); + + // reset cursor position + self.cursor = 0; } pub fn move_up(&mut self) { @@ -77,6 +105,12 @@ impl Picker { self.cursor += 1; } } + + pub fn selection(&self) -> Option<&PathBuf> { + self.matches + .get(self.cursor) + .map(|(index, _score)| &self.files[*index]) + } } // process: @@ -125,7 +159,15 @@ impl Component for Picker { } => { return close_fn; } - _ => return self.prompt.handle_event(event, cx), + _ => { + match self.prompt.handle_event(event, cx) { + EventResult::Consumed(_) => { + // TODO: recalculate only if pattern changed + self.score(); + } + _ => (), + } + } } EventResult::Consumed(None) @@ -184,7 +226,12 @@ impl Component for Picker { let selected = Style::default().fg(Color::Rgb(255, 255, 255)); let rows = inner.height - 2; // -1 for search bar - for (i, file) in self.files.iter().take(rows as usize).enumerate() { + + let files = self.matches.iter().map(|(index, _score)| { + (index, self.files.get(*index).unwrap()) // get_unchecked + }); + + for (i, (_index, file)) in files.take(rows as usize).enumerate() { if i == self.cursor { surface.set_string(inner.x + 1, inner.y + 2 + i as u16, ">", selected); } -- cgit v1.2.3-70-g09d2