Skip to content

Commit b43741d

Browse files
committed
Treat space as a seperator instead of a character in fuzzy picker
1 parent e8f0886 commit b43741d

File tree

4 files changed

+130
-8
lines changed

4 files changed

+130
-8
lines changed

helix-term/src/ui/fuzzy_match.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
2+
use fuzzy_matcher::FuzzyMatcher;
3+
4+
#[cfg(test)]
5+
mod test;
6+
7+
pub struct FuzzyQuery {
8+
queries: Vec<String>,
9+
}
10+
11+
impl FuzzyQuery {
12+
pub fn new(query: &str) -> FuzzyQuery {
13+
let mut saw_backslash = false;
14+
let queries = query
15+
.split(|c| {
16+
saw_backslash = match c {
17+
' ' if !saw_backslash => return true,
18+
'\\' => true,
19+
_ => false,
20+
};
21+
false
22+
})
23+
.filter_map(|query| {
24+
if query.is_empty() {
25+
None
26+
} else {
27+
Some(query.replace("\\ ", " "))
28+
}
29+
})
30+
.collect();
31+
FuzzyQuery { queries }
32+
}
33+
34+
pub fn fuzzy_match(&self, item: &str, matcher: &Matcher) -> Option<i64> {
35+
// use the rank of the first query for the rank, because merging ranks is not really possible
36+
// this behaviour matches fzf and skim
37+
let score = matcher.fuzzy_match(item, self.queries.get(0)?)?;
38+
if self
39+
.queries
40+
.iter()
41+
.any(|query| matcher.fuzzy_match(item, query).is_none())
42+
{
43+
return None;
44+
}
45+
Some(score)
46+
}
47+
48+
pub fn fuzzy_indicies(&self, item: &str, matcher: &Matcher) -> Option<(i64, Vec<usize>)> {
49+
if self.queries.len() == 1 {
50+
return matcher.fuzzy_indices(item, &self.queries[0]);
51+
}
52+
53+
// use the rank of the first query for the rank, because merging ranks is not really possible
54+
// this behaviour matches fzf and skim
55+
let (score, mut indicies) = matcher.fuzzy_indices(item, self.queries.get(0)?)?;
56+
57+
// fast path for the common case of not using a space
58+
// during matching this branch should be free thanks to branch prediction
59+
if self.queries.len() == 1 {
60+
return Some((score, indicies));
61+
}
62+
63+
for query in &self.queries[1..] {
64+
let (_, matched_indicies) = matcher.fuzzy_indices(item, query)?;
65+
indicies.extend_from_slice(&matched_indicies);
66+
}
67+
68+
// deadup and remove duplicate matches
69+
indicies.sort_unstable();
70+
indicies.dedup();
71+
72+
Some((score, indicies))
73+
}
74+
}

helix-term/src/ui/fuzzy_match/test.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
use crate::ui::fuzzy_match::FuzzyQuery;
2+
use crate::ui::fuzzy_match::Matcher;
3+
4+
fn run_test<'a>(query: &str, items: &'a [&'a str]) -> Vec<String> {
5+
let query = FuzzyQuery::new(query);
6+
let matcher = Matcher::default();
7+
items
8+
.iter()
9+
.filter_map(|item| {
10+
let (_, indicies) = query.fuzzy_indicies(item, &matcher)?;
11+
let matched_string = indicies
12+
.iter()
13+
.map(|&pos| item.chars().nth(pos).unwrap())
14+
.collect();
15+
Some(matched_string)
16+
})
17+
.collect()
18+
}
19+
20+
#[test]
21+
fn match_single_value() {
22+
let matches = run_test("foo", &["foobar", "foo", "bar"]);
23+
assert_eq!(matches, &["foo", "foo"])
24+
}
25+
26+
#[test]
27+
fn match_multiple_values() {
28+
let matches = run_test(
29+
"foo bar",
30+
&["foo bar", "foo bar", "bar foo", "bar", "foo"],
31+
);
32+
assert_eq!(matches, &["foobar", "foobar", "barfoo"])
33+
}
34+
35+
#[test]
36+
fn space_escape() {
37+
let matches = run_test(r"foo\ bar", &["bar foo", "foo bar", "foobar"]);
38+
assert_eq!(matches, &["foo bar"])
39+
}
40+
41+
#[test]
42+
fn trim() {
43+
let matches = run_test(r" foo bar ", &["bar foo", "foo bar", "foobar"]);
44+
assert_eq!(matches, &["barfoo", "foobar", "foobar"]);
45+
let matches = run_test(r" foo bar\ ", &["bar foo", "foo bar", "foobar"]);
46+
assert_eq!(matches, &["bar foo"])
47+
}

helix-term/src/ui/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod completion;
22
pub(crate) mod editor;
3+
mod fuzzy_match;
34
mod info;
45
pub mod lsp;
56
mod markdown;

helix-term/src/ui/picker.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
use crate::{
22
compositor::{Component, Compositor, Context, Event, EventResult},
33
ctrl, key, shift,
4-
ui::{self, EditorView},
4+
ui::{self, fuzzy_match::FuzzyQuery, EditorView},
55
};
66
use tui::{
77
buffer::Buffer as Surface,
88
widgets::{Block, BorderType, Borders},
99
};
1010

1111
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
12-
use fuzzy_matcher::FuzzyMatcher;
1312
use tui::widgets::Widget;
1413

1514
use std::time::Instant;
@@ -365,13 +364,14 @@ impl<T: Item> Picker<T> {
365364
.map(|(index, _option)| (index, 0)),
366365
);
367366
} else if pattern.starts_with(&self.previous_pattern) {
367+
let query = FuzzyQuery::new(pattern);
368368
// optimization: if the pattern is a more specific version of the previous one
369369
// then we can score the filtered set.
370370
self.matches.retain_mut(|(index, score)| {
371371
let option = &self.options[*index];
372372
let text = option.sort_text(&self.editor_data);
373373

374-
match self.matcher.fuzzy_match(&text, pattern) {
374+
match query.fuzzy_match(&text, &self.matcher) {
375375
Some(s) => {
376376
// Update the score
377377
*score = s;
@@ -384,6 +384,7 @@ impl<T: Item> Picker<T> {
384384
self.matches
385385
.sort_unstable_by_key(|(_, score)| Reverse(*score));
386386
} else {
387+
let query = FuzzyQuery::new(pattern);
387388
self.matches.clear();
388389
self.matches.extend(
389390
self.options
@@ -399,8 +400,8 @@ impl<T: Item> Picker<T> {
399400

400401
let text = option.filter_text(&self.editor_data);
401402

402-
self.matcher
403-
.fuzzy_match(&text, pattern)
403+
query
404+
.fuzzy_match(&text, &self.matcher)
404405
.map(|score| (index, score))
405406
}),
406407
);
@@ -630,9 +631,8 @@ impl<T: Item + 'static> Component for Picker<T> {
630631
}
631632

632633
let spans = option.label(&self.editor_data);
633-
let (_score, highlights) = self
634-
.matcher
635-
.fuzzy_indices(&String::from(&spans), self.prompt.line())
634+
let (_score, highlights) = FuzzyQuery::new(self.prompt.line())
635+
.fuzzy_indicies(&String::from(&spans), &self.matcher)
636636
.unwrap_or_default();
637637

638638
spans.0.into_iter().fold(inner, |pos, span| {

0 commit comments

Comments
 (0)