From 4b89177e53a467ceda2e3ee95fc77874f77d2b85 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Thu, 17 Nov 2022 00:28:20 +0100 Subject: [PATCH] sort fuzzy matches with equal score by length in picker (#4698) --- helix-term/src/ui/picker.rs | 82 +++++++++++++++++++++++++------------ 1 file changed, 55 insertions(+), 27 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 2505f2197..549119241 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -11,9 +11,8 @@ use tui::{ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use tui::widgets::Widget; -use std::time::Instant; +use std::{cmp::Ordering, time::Instant}; use std::{ - cmp::Reverse, collections::HashMap, io::Read, path::{Path, PathBuf}, @@ -309,13 +308,34 @@ impl Component for FilePicker { } } +#[derive(PartialEq, Eq, Debug)] +struct PickerMatch { + index: usize, + score: i64, + len: usize, +} + +impl PartialOrd for PickerMatch { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PickerMatch { + fn cmp(&self, other: &Self) -> Ordering { + self.score + .cmp(&other.score) + .reverse() + .then_with(|| self.len.cmp(&other.len)) + } +} + pub struct Picker { options: Vec, editor_data: T::Data, // filter: String, matcher: Box, - /// (index, score) - matches: Vec<(usize, i64)>, + matches: Vec, /// Current height of the completions box completion_height: u16, @@ -361,13 +381,16 @@ impl Picker { // scoring on empty input: // TODO: just reuse score() - picker.matches.extend( - picker - .options - .iter() - .enumerate() - .map(|(index, _option)| (index, 0)), - ); + picker + .matches + .extend(picker.options.iter().enumerate().map(|(index, option)| { + let text = option.filter_text(&picker.editor_data); + PickerMatch { + index, + score: 0, + len: text.chars().count(), + } + })); picker } @@ -384,32 +407,34 @@ impl Picker { if pattern.is_empty() { // Fast path for no pattern. self.matches.clear(); - self.matches.extend( - self.options - .iter() - .enumerate() - .map(|(index, _option)| (index, 0)), - ); + self.matches + .extend(self.options.iter().enumerate().map(|(index, option)| { + let text = option.filter_text(&self.editor_data); + PickerMatch { + index, + score: 0, + len: text.chars().count(), + } + })); } 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 // then we can score the filtered set. - self.matches.retain_mut(|(index, score)| { - let option = &self.options[*index]; + self.matches.retain_mut(|pmatch| { + let option = &self.options[pmatch.index]; let text = option.sort_text(&self.editor_data); match query.fuzzy_match(&text, &self.matcher) { Some(s) => { // Update the score - *score = s; + pmatch.score = s; true } None => false, } }); - self.matches - .sort_unstable_by_key(|(_, score)| Reverse(*score)); + self.matches.sort_unstable(); } else { let query = FuzzyQuery::new(pattern); self.matches.clear(); @@ -422,11 +447,14 @@ impl Picker { query .fuzzy_match(&text, &self.matcher) - .map(|score| (index, score)) + .map(|score| PickerMatch { + index, + score, + len: text.chars().count(), + }) }), ); - self.matches - .sort_unstable_by_key(|(_, score)| Reverse(*score)); + self.matches.sort_unstable(); } log::debug!("picker score {:?}", Instant::now().duration_since(now)); @@ -478,7 +506,7 @@ impl Picker { pub fn selection(&self) -> Option<&T> { self.matches .get(self.cursor) - .map(|(index, _score)| &self.options[*index]) + .map(|pmatch| &self.options[pmatch.index]) } pub fn toggle_preview(&mut self) { @@ -625,7 +653,7 @@ impl Component for Picker { .matches .iter() .skip(offset) - .map(|(index, _score)| (*index, self.options.get(*index).unwrap())); + .map(|pmatch| (pmatch.index, self.options.get(pmatch.index).unwrap())); for (i, (_index, option)) in files.take(rows as usize).enumerate() { let is_active = i == (self.cursor - offset);