Treat space as a seperator instead of a character in fuzzy picker

pull/4192/head
Pascal Kuthe 2 years ago committed by Blaž Hrastnik
parent c388e16e09
commit 7af599e0af

@ -0,0 +1,74 @@
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
#[cfg(test)]
mod test;
pub struct FuzzyQuery {
queries: Vec<String>,
}
impl FuzzyQuery {
pub fn new(query: &str) -> FuzzyQuery {
let mut saw_backslash = false;
let queries = query
.split(|c| {
saw_backslash = match c {
' ' if !saw_backslash => return true,
'\\' => true,
_ => false,
};
false
})
.filter_map(|query| {
if query.is_empty() {
None
} else {
Some(query.replace("\\ ", " "))
}
})
.collect();
FuzzyQuery { queries }
}
pub fn fuzzy_match(&self, item: &str, matcher: &Matcher) -> Option<i64> {
// use the rank of the first query for the rank, because merging ranks is not really possible
// this behaviour matches fzf and skim
let score = matcher.fuzzy_match(item, self.queries.get(0)?)?;
if self
.queries
.iter()
.any(|query| matcher.fuzzy_match(item, query).is_none())
{
return None;
}
Some(score)
}
pub fn fuzzy_indicies(&self, item: &str, matcher: &Matcher) -> Option<(i64, Vec<usize>)> {
if self.queries.len() == 1 {
return matcher.fuzzy_indices(item, &self.queries[0]);
}
// use the rank of the first query for the rank, because merging ranks is not really possible
// this behaviour matches fzf and skim
let (score, mut indicies) = matcher.fuzzy_indices(item, self.queries.get(0)?)?;
// fast path for the common case of not using a space
// during matching this branch should be free thanks to branch prediction
if self.queries.len() == 1 {
return Some((score, indicies));
}
for query in &self.queries[1..] {
let (_, matched_indicies) = matcher.fuzzy_indices(item, query)?;
indicies.extend_from_slice(&matched_indicies);
}
// deadup and remove duplicate matches
indicies.sort_unstable();
indicies.dedup();
Some((score, indicies))
}
}

@ -0,0 +1,47 @@
use crate::ui::fuzzy_match::FuzzyQuery;
use crate::ui::fuzzy_match::Matcher;
fn run_test<'a>(query: &str, items: &'a [&'a str]) -> Vec<String> {
let query = FuzzyQuery::new(query);
let matcher = Matcher::default();
items
.iter()
.filter_map(|item| {
let (_, indicies) = query.fuzzy_indicies(item, &matcher)?;
let matched_string = indicies
.iter()
.map(|&pos| item.chars().nth(pos).unwrap())
.collect();
Some(matched_string)
})
.collect()
}
#[test]
fn match_single_value() {
let matches = run_test("foo", &["foobar", "foo", "bar"]);
assert_eq!(matches, &["foo", "foo"])
}
#[test]
fn match_multiple_values() {
let matches = run_test(
"foo bar",
&["foo bar", "foo bar", "bar foo", "bar", "foo"],
);
assert_eq!(matches, &["foobar", "foobar", "barfoo"])
}
#[test]
fn space_escape() {
let matches = run_test(r"foo\ bar", &["bar foo", "foo bar", "foobar"]);
assert_eq!(matches, &["foo bar"])
}
#[test]
fn trim() {
let matches = run_test(r" foo bar ", &["bar foo", "foo bar", "foobar"]);
assert_eq!(matches, &["barfoo", "foobar", "foobar"]);
let matches = run_test(r" foo bar\ ", &["bar foo", "foo bar", "foobar"]);
assert_eq!(matches, &["bar foo"])
}

@ -1,5 +1,6 @@
mod completion; mod completion;
pub(crate) mod editor; pub(crate) mod editor;
mod fuzzy_match;
mod info; mod info;
pub mod lsp; pub mod lsp;
mod markdown; mod markdown;

@ -1,7 +1,7 @@
use crate::{ use crate::{
compositor::{Component, Compositor, Context, Event, EventResult}, compositor::{Component, Compositor, Context, Event, EventResult},
ctrl, key, shift, ctrl, key, shift,
ui::{self, EditorView}, ui::{self, fuzzy_match::FuzzyQuery, EditorView},
}; };
use tui::{ use tui::{
buffer::Buffer as Surface, buffer::Buffer as Surface,
@ -9,7 +9,6 @@ use tui::{
}; };
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use tui::widgets::Widget; use tui::widgets::Widget;
use std::time::Instant; use std::time::Instant;
@ -389,13 +388,14 @@ impl<T: Item> Picker<T> {
.map(|(index, _option)| (index, 0)), .map(|(index, _option)| (index, 0)),
); );
} else if pattern.starts_with(&self.previous_pattern) { } else if pattern.starts_with(&self.previous_pattern) {
let query = FuzzyQuery::new(pattern);
// optimization: if the pattern is a more specific version of the previous one // optimization: if the pattern is a more specific version of the previous one
// then we can score the filtered set. // then we can score the filtered set.
self.matches.retain_mut(|(index, score)| { self.matches.retain_mut(|(index, score)| {
let option = &self.options[*index]; let option = &self.options[*index];
let text = option.sort_text(&self.editor_data); let text = option.sort_text(&self.editor_data);
match self.matcher.fuzzy_match(&text, pattern) { match query.fuzzy_match(&text, &self.matcher) {
Some(s) => { Some(s) => {
// Update the score // Update the score
*score = s; *score = s;
@ -408,6 +408,7 @@ impl<T: Item> Picker<T> {
self.matches self.matches
.sort_unstable_by_key(|(_, score)| Reverse(*score)); .sort_unstable_by_key(|(_, score)| Reverse(*score));
} else { } else {
let query = FuzzyQuery::new(pattern);
self.matches.clear(); self.matches.clear();
self.matches.extend( self.matches.extend(
self.options self.options
@ -423,8 +424,8 @@ impl<T: Item> Picker<T> {
let text = option.filter_text(&self.editor_data); let text = option.filter_text(&self.editor_data);
self.matcher query
.fuzzy_match(&text, pattern) .fuzzy_match(&text, &self.matcher)
.map(|score| (index, score)) .map(|score| (index, score))
}), }),
); );
@ -657,9 +658,8 @@ impl<T: Item + 'static> Component for Picker<T> {
} }
let spans = option.label(&self.editor_data); let spans = option.label(&self.editor_data);
let (_score, highlights) = self let (_score, highlights) = FuzzyQuery::new(self.prompt.line())
.matcher .fuzzy_indicies(&String::from(&spans), &self.matcher)
.fuzzy_indices(&String::from(&spans), self.prompt.line())
.unwrap_or_default(); .unwrap_or_default();
spans.0.into_iter().fold(inner, |pos, span| { spans.0.into_iter().fold(inner, |pos, span| {

Loading…
Cancel
Save