Treat space as a seperator instead of a character in fuzzy picker
This commit is contained in:
parent
c388e16e09
commit
7af599e0af
74
helix-term/src/ui/fuzzy_match.rs
Normal file
74
helix-term/src/ui/fuzzy_match.rs
Normal file
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
47
helix-term/src/ui/fuzzy_match/test.rs
Normal file
47
helix-term/src/ui/fuzzy_match/test.rs
Normal file
|
@ -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 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 @@ pub fn score(&mut self) {
|
||||||
.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 @@ pub fn score(&mut self) {
|
||||||
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 @@ pub fn score(&mut self) {
|
||||||
|
|
||||||
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 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
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…
Reference in a new issue