on master help sign in
Treat space as a seperator instead of a character in fuzzy picker
7af599ePascal Kuthe
helix-term/src/ui/fuzzy_match.rsnew file+74
@@ -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.rsnew file+47
@@ -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+1
@@ -1,5 +1,6 @@
1mod completion;
1mod completion;
2pub(crate) mod editor;
2pub(crate) mod editor;
3+mod fuzzy_match;
3mod info;
4mod info;
4pub mod lsp;
5pub mod lsp;
5mod markdown;
6mod markdown;
helix-term/src/ui/picker.rs+8−8
@@ -1,7 +1,7 @@
1use crate::{
1use crate::{
2 compositor::{Component, Compositor, Context, Event, EventResult},
2 compositor::{Component, Compositor, Context, Event, EventResult},
3 ctrl, key, shift,
3 ctrl, key, shift,
4− ui::{self, EditorView},
4+ ui::{self, fuzzy_match::FuzzyQuery, EditorView},
5};
5};
6use tui::{
6use tui::{
7 buffer::Buffer as Surface,
7 buffer::Buffer as Surface,
@@ -9,7 +9,6 @@ use tui::{
9};
9};
10
10
11use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
11use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
12−use fuzzy_matcher::FuzzyMatcher;
13use tui::widgets::Widget;
12use tui::widgets::Widget;
14
13
15use std::time::Instant;
14use std::time::Instant;
@@ -389,13 +388,14 @@ impl<T: Item> Picker<T> {
389 .map(|(index, _option)| (index, 0)),
388 .map(|(index, _option)| (index, 0)),
390 );
389 );
391 } else if pattern.starts_with(&self.previous_pattern) {
390 } else if pattern.starts_with(&self.previous_pattern) {
391+ let query = FuzzyQuery::new(pattern);
392 // optimization: if the pattern is a more specific version of the previous one
392 // optimization: if the pattern is a more specific version of the previous one
393 // then we can score the filtered set.
393 // then we can score the filtered set.
394 self.matches.retain_mut(|(index, score)| {
394 self.matches.retain_mut(|(index, score)| {
395 let option = &self.options[*index];
395 let option = &self.options[*index];
396 let text = option.sort_text(&self.editor_data);
396 let text = option.sort_text(&self.editor_data);
397
397
398− match self.matcher.fuzzy_match(&text, pattern) {
398+ match query.fuzzy_match(&text, &self.matcher) {
399 Some(s) => {
399 Some(s) => {
400 // Update the score
400 // Update the score
401 *score = s;
401 *score = s;
@@ -408,6 +408,7 @@ impl<T: Item> Picker<T> {
408 self.matches
408 self.matches
409 .sort_unstable_by_key(|(_, score)| Reverse(*score));
409 .sort_unstable_by_key(|(_, score)| Reverse(*score));
410 } else {
410 } else {
411+ let query = FuzzyQuery::new(pattern);
411 self.matches.clear();
412 self.matches.clear();
412 self.matches.extend(
413 self.matches.extend(
413 self.options
414 self.options
@@ -423,8 +424,8 @@ impl<T: Item> Picker<T> {
423
424
424 let text = option.filter_text(&self.editor_data);
425 let text = option.filter_text(&self.editor_data);
425
426
426− self.matcher
427+ query
427− .fuzzy_match(&text, pattern)
428+ .fuzzy_match(&text, &self.matcher)
428 .map(|score| (index, score))
429 .map(|score| (index, score))
429 }),
430 }),
430 );
431 );
@@ -657,9 +658,8 @@ impl<T: Item + 'static> Component for Picker<T> {
657 }
658 }
658
659
659 let spans = option.label(&self.editor_data);
660 let spans = option.label(&self.editor_data);
660− let (_score, highlights) = self
661+ let (_score, highlights) = FuzzyQuery::new(self.prompt.line())
661− .matcher
662− .fuzzy_indices(&String::from(&spans), self.prompt.line())
662+ .fuzzy_indicies(&String::from(&spans), &self.matcher)
663 .unwrap_or_default();
663 .unwrap_or_default();
664
664
665 spans.0.into_iter().fold(inner, |pos, span| {
665 spans.0.into_iter().fold(inner, |pos, span| {