on master help sign in
sort fuzzy matches with equal score by length in picker (#4698)
4b89177Pascal Kuthe
helix-term/src/ui/picker.rs+55−27
@@ -11,9 +11,8 @@ use tui::{
11use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
11use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
12use tui::widgets::Widget;
12use tui::widgets::Widget;
13
13
14−use std::time::Instant;
14+use std::{cmp::Ordering, time::Instant};
15use std::{
15use std::{
16− cmp::Reverse,
17 collections::HashMap,
16 collections::HashMap,
18 io::Read,
17 io::Read,
19 path::{Path, PathBuf},
18 path::{Path, PathBuf},
@@ -309,13 +308,34 @@ impl<T: Item + 'static> Component for FilePicker<T> {
309 }
308 }
310}
309}
311
310
311+#[derive(PartialEq, Eq, Debug)]
312+struct PickerMatch {
313+ index: usize,
314+ score: i64,
315+ len: usize,
316+}
317+
318+impl PartialOrd for PickerMatch {
319+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
320+ Some(self.cmp(other))
321+ }
322+}
323+
324+impl Ord for PickerMatch {
325+ fn cmp(&self, other: &Self) -> Ordering {
326+ self.score
327+ .cmp(&other.score)
328+ .reverse()
329+ .then_with(|| self.len.cmp(&other.len))
330+ }
331+}
332+
312pub struct Picker<T: Item> {
333pub struct Picker<T: Item> {
313 options: Vec<T>,
334 options: Vec<T>,
314 editor_data: T::Data,
335 editor_data: T::Data,
315 // filter: String,
336 // filter: String,
316 matcher: Box<Matcher>,
337 matcher: Box<Matcher>,
317− /// (index, score)
318− matches: Vec<(usize, i64)>,
338+ matches: Vec<PickerMatch>,
319
339
320 /// Current height of the completions box
340 /// Current height of the completions box
321 completion_height: u16,
341 completion_height: u16,
@@ -361,13 +381,16 @@ impl<T: Item> Picker<T> {
361
381
362 // scoring on empty input:
382 // scoring on empty input:
363 // TODO: just reuse score()
383 // TODO: just reuse score()
384+ picker
364− picker.matches.extend(
385+ .matches
365− picker
386+ .extend(picker.options.iter().enumerate().map(|(index, option)| {
366− .options
387+ let text = option.filter_text(&picker.editor_data);
367− .iter()
388+ PickerMatch {
368− .enumerate()
389+ index,
369− .map(|(index, _option)| (index, 0)),
390+ score: 0,
391+ len: text.chars().count(),
392+ }
370− );
393+ }));
371
394
372 picker
395 picker
373 }
396 }
@@ -384,32 +407,34 @@ impl<T: Item> Picker<T> {
384 if pattern.is_empty() {
407 if pattern.is_empty() {
385 // Fast path for no pattern.
408 // Fast path for no pattern.
386 self.matches.clear();
409 self.matches.clear();
387− self.matches.extend(
410+ self.matches
388− self.options
411+ .extend(self.options.iter().enumerate().map(|(index, option)| {
389− .iter()
412+ let text = option.filter_text(&self.editor_data);
390− .enumerate()
413+ PickerMatch {
391− .map(|(index, _option)| (index, 0)),
414+ index,
415+ score: 0,
416+ len: text.chars().count(),
417+ }
392− );
418+ }));
393 } else if pattern.starts_with(&self.previous_pattern) {
419 } else if pattern.starts_with(&self.previous_pattern) {
394 let query = FuzzyQuery::new(pattern);
420 let query = FuzzyQuery::new(pattern);
395 // optimization: if the pattern is a more specific version of the previous one
421 // optimization: if the pattern is a more specific version of the previous one
396 // then we can score the filtered set.
422 // then we can score the filtered set.
397− self.matches.retain_mut(|(index, score)| {
423+ self.matches.retain_mut(|pmatch| {
398− let option = &self.options[*index];
424+ let option = &self.options[pmatch.index];
399 let text = option.sort_text(&self.editor_data);
425 let text = option.sort_text(&self.editor_data);
400
426
401 match query.fuzzy_match(&text, &self.matcher) {
427 match query.fuzzy_match(&text, &self.matcher) {
402 Some(s) => {
428 Some(s) => {
403 // Update the score
429 // Update the score
404− *score = s;
430+ pmatch.score = s;
405 true
431 true
406 }
432 }
407 None => false,
433 None => false,
408 }
434 }
409 });
435 });
410
436
411− self.matches
437+ self.matches.sort_unstable();
412− .sort_unstable_by_key(|(_, score)| Reverse(*score));
413 } else {
438 } else {
414 let query = FuzzyQuery::new(pattern);
439 let query = FuzzyQuery::new(pattern);
415 self.matches.clear();
440 self.matches.clear();
@@ -422,11 +447,14 @@ impl<T: Item> Picker<T> {
422
447
423 query
448 query
424 .fuzzy_match(&text, &self.matcher)
449 .fuzzy_match(&text, &self.matcher)
425− .map(|score| (index, score))
450+ .map(|score| PickerMatch {
451+ index,
452+ score,
453+ len: text.chars().count(),
454+ })
426 }),
455 }),
427 );
456 );
428− self.matches
457+ self.matches.sort_unstable();
429− .sort_unstable_by_key(|(_, score)| Reverse(*score));
430 }
458 }
431
459
432 log::debug!("picker score {:?}", Instant::now().duration_since(now));
460 log::debug!("picker score {:?}", Instant::now().duration_since(now));
@@ -478,7 +506,7 @@ impl<T: Item> Picker<T> {
478 pub fn selection(&self) -> Option<&T> {
506 pub fn selection(&self) -> Option<&T> {
479 self.matches
507 self.matches
480 .get(self.cursor)
508 .get(self.cursor)
481− .map(|(index, _score)| &self.options[*index])
509+ .map(|pmatch| &self.options[pmatch.index])
482 }
510 }
483
511
484 pub fn toggle_preview(&mut self) {
512 pub fn toggle_preview(&mut self) {
@@ -625,7 +653,7 @@ impl<T: Item + 'static> Component for Picker<T> {
625 .matches
653 .matches
626 .iter()
654 .iter()
627 .skip(offset)
655 .skip(offset)
628− .map(|(index, _score)| (*index, self.options.get(*index).unwrap()));
656+ .map(|pmatch| (pmatch.index, self.options.get(pmatch.index).unwrap()));
629
657
630 for (i, (_index, option)) in files.take(rows as usize).enumerate() {
658 for (i, (_index, option)) in files.take(rows as usize).enumerate() {
631 let is_active = i == (self.cursor - offset);
659 let is_active = i == (self.cursor - offset);