From 8b80ed71ed45e713218638524fe0c8b9eb354ba7 Mon Sep 17 00:00:00 2001 From: ste Date: Sun, 10 Sep 2023 23:30:06 +0200 Subject: [PATCH 01/11] Working fuzzy search on Select and Mselect --- inquire/Cargo.toml | 1 + inquire/src/prompts/multiselect/mod.rs | 34 ++++++++++---- inquire/src/prompts/multiselect/prompt.rs | 56 +++++++++++++---------- inquire/src/prompts/select/mod.rs | 27 +++++++---- inquire/src/prompts/select/prompt.rs | 56 +++++++++++++---------- inquire/src/type_aliases.rs | 2 +- 6 files changed, 107 insertions(+), 69 deletions(-) diff --git a/inquire/Cargo.toml b/inquire/Cargo.toml index a7a457e7..ef4b4538 100644 --- a/inquire/Cargo.toml +++ b/inquire/Cargo.toml @@ -47,6 +47,7 @@ newline-converter = "0.3" once_cell = "1.18.0" unicode-segmentation = "1" unicode-width = "0.1" +fuzzy-matcher = { version = "0.3.7", default-features = false } [[example]] name = "form" diff --git a/inquire/src/prompts/multiselect/mod.rs b/inquire/src/prompts/multiselect/mod.rs index 025ee52c..2d0c506d 100644 --- a/inquire/src/prompts/multiselect/mod.rs +++ b/inquire/src/prompts/multiselect/mod.rs @@ -6,6 +6,7 @@ mod prompt; mod test; pub use action::*; +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use std::fmt::Display; @@ -16,7 +17,7 @@ use crate::{ list_option::ListOption, prompts::prompt::Prompt, terminal::get_default_terminal, - type_aliases::Filter, + type_aliases::Scorer, ui::{Backend, MultiSelectBackend, RenderConfig}, validator::MultiOptionValidator, }; @@ -79,7 +80,10 @@ pub struct MultiSelect<'a, T> { /// Function called with the current user input to filter the provided /// options. - pub filter: Filter<'a, T>, + // pub filter: Filter<'a, T>, + + /// TODO Docs + pub scorer: Scorer<'a, T>, /// Whether the current filter typed by the user is kept or cleaned after a selection is made. pub keep_filter: bool, @@ -157,11 +161,23 @@ where /// assert_eq!(false, filter("sa", &"Jacksonville", "Jacksonville", 11)); /// assert_eq!(true, filter("sa", &"San Jose", "San Jose", 12)); /// ``` - pub const DEFAULT_FILTER: Filter<'a, T> = &|filter, _, string_value, _| -> bool { - let filter = filter.to_lowercase(); + // pub const DEFAULT_FILTER: Filter<'a, T> = &|filter, _, string_value, _| -> bool { + // let filter = filter.to_lowercase(); - string_value.to_lowercase().contains(&filter) - }; + // string_value.to_lowercase().contains(&filter) + // }; + + /// TODO Docs + pub const DEFAULT_SCORER: Scorer<'a, T> = + &|filter, _option, string_value, _idx| -> Option { + let matcher = SkimMatcherV2::default().ignore_case(); + + match matcher.fuzzy_match(string_value, filter) { + Some(t) if t <= usize::MAX as i64 => Some(t as usize), + Some(t) if t > usize::MAX as i64 => Some(usize::MAX), + Some(_) | None => None, + } + }; /// Default page size, equal to the global default page size [config::DEFAULT_PAGE_SIZE] pub const DEFAULT_PAGE_SIZE: usize = crate::config::DEFAULT_PAGE_SIZE; @@ -190,7 +206,7 @@ where vim_mode: Self::DEFAULT_VIM_MODE, starting_cursor: Self::DEFAULT_STARTING_CURSOR, keep_filter: Self::DEFAULT_KEEP_FILTER, - filter: Self::DEFAULT_FILTER, + scorer: Self::DEFAULT_SCORER, formatter: Self::DEFAULT_FORMATTER, validator: None, render_config: get_configuration(), @@ -228,8 +244,8 @@ where } /// Sets the filter function. - pub fn with_filter(mut self, filter: Filter<'a, T>) -> Self { - self.filter = filter; + pub fn with_scorer(mut self, scorer: Scorer<'a, T>) -> Self { + self.scorer = scorer; self } diff --git a/inquire/src/prompts/multiselect/prompt.rs b/inquire/src/prompts/multiselect/prompt.rs index d568c2b9..16c70ba0 100644 --- a/inquire/src/prompts/multiselect/prompt.rs +++ b/inquire/src/prompts/multiselect/prompt.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeSet, fmt::Display}; +use std::{cmp::Reverse, collections::BTreeSet, fmt::Display}; use crate::{ error::InquireResult, @@ -6,7 +6,7 @@ use crate::{ input::{Input, InputActionResult}, list_option::ListOption, prompts::prompt::{ActionResult, Prompt}, - type_aliases::Filter, + type_aliases::Scorer, ui::MultiSelectBackend, utils::paginate, validator::{ErrorMessage, MultiOptionValidator, Validation}, @@ -24,8 +24,8 @@ pub struct MultiSelectPrompt<'a, T> { cursor_index: usize, checked: BTreeSet, input: Input, - filtered_options: Vec, - filter: Filter<'a, T>, + scored_options: Vec, + scorer: Scorer<'a, T>, formatter: MultiOptionFormatter<'a, T>, validator: Option>>, error: Option, @@ -54,7 +54,7 @@ where } let string_options = mso.options.iter().map(T::to_string).collect(); - let filtered_options = (0..mso.options.len()).collect(); + let scored_options = (0..mso.options.len()).collect(); let checked_options = mso .default .as_ref() @@ -71,11 +71,11 @@ where config: (&mso).into(), options: mso.options, string_options, - filtered_options, + scored_options, help_message: mso.help_message, cursor_index: mso.starting_cursor, input: Input::new(), - filter: mso.filter, + scorer: mso.scorer, formatter: mso.formatter, validator: mso.validator, error: None, @@ -83,16 +83,20 @@ where }) } - fn filter_options(&self) -> Vec { + fn score_options(&self) -> Vec<(usize, usize)> { self.options .iter() .enumerate() - .filter_map(|(i, opt)| match self.input.content() { - val if val.is_empty() => Some(i), - val if (self.filter)(val, opt, self.string_options.get(i).unwrap(), i) => Some(i), - _ => None, + .filter_map(|(i, opt)| { + (self.scorer)( + self.input.content(), + opt, + self.string_options.get(i).unwrap(), + i, + ) + .map(|score| (i, score)) }) - .collect() + .collect::>() } fn move_cursor_up(&mut self, qty: usize, wrap: bool) -> ActionResult { @@ -100,7 +104,7 @@ where let after_wrap = qty.saturating_sub(self.cursor_index); self.cursor_index .checked_sub(qty) - .unwrap_or_else(|| self.filtered_options.len().saturating_sub(after_wrap)) + .unwrap_or_else(|| self.scored_options.len().saturating_sub(after_wrap)) } else { self.cursor_index.saturating_sub(qty) }; @@ -111,13 +115,13 @@ where fn move_cursor_down(&mut self, qty: usize, wrap: bool) -> ActionResult { let mut new_position = self.cursor_index.saturating_add(qty); - if new_position >= self.filtered_options.len() { - new_position = if self.filtered_options.is_empty() { + if new_position >= self.scored_options.len() { + new_position = if self.scored_options.is_empty() { 0 } else if wrap { - new_position % self.filtered_options.len() + new_position % self.scored_options.len() } else { - self.filtered_options.len().saturating_sub(1) + self.scored_options.len().saturating_sub(1) } } @@ -134,7 +138,7 @@ where } fn toggle_cursor_selection(&mut self) -> ActionResult { - let idx = match self.filtered_options.get(self.cursor_index) { + let idx = match self.scored_options.get(self.cursor_index) { Some(val) => val, None => return ActionResult::Clean, }; @@ -236,7 +240,7 @@ where MultiSelectPromptAction::ToggleCurrentOption => self.toggle_cursor_selection(), MultiSelectPromptAction::SelectAll => { self.checked.clear(); - for idx in &self.filtered_options { + for idx in &self.scored_options { self.checked.insert(*idx); } @@ -259,11 +263,13 @@ where let result = self.input.handle(input_action); if let InputActionResult::ContentChanged = result { - let options = self.filter_options(); - self.filtered_options = options; - if self.filtered_options.len() <= self.cursor_index { + let mut options = self.score_options(); + options.sort_unstable_by_key(|(_idx, score)| Reverse(*score)); + + self.scored_options = options.into_iter().map(|(idx, _)| idx).collect(); + if self.scored_options.len() <= self.cursor_index { let _ = self - .update_cursor_position(self.filtered_options.len().saturating_sub(1)); + .update_cursor_position(self.scored_options.len().saturating_sub(1)); } } @@ -284,7 +290,7 @@ where backend.render_multiselect_prompt(prompt, &self.input)?; let choices = self - .filtered_options + .scored_options .iter() .cloned() .map(|i| ListOption::new(i, self.options.get(i).unwrap())) diff --git a/inquire/src/prompts/select/mod.rs b/inquire/src/prompts/select/mod.rs index be538bf0..a246848d 100644 --- a/inquire/src/prompts/select/mod.rs +++ b/inquire/src/prompts/select/mod.rs @@ -6,6 +6,7 @@ mod prompt; mod test; pub use action::*; +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use std::fmt::Display; @@ -16,7 +17,7 @@ use crate::{ list_option::ListOption, prompts::prompt::Prompt, terminal::get_default_terminal, - type_aliases::Filter, + type_aliases::Scorer, ui::{Backend, RenderConfig, SelectBackend}, }; @@ -86,7 +87,10 @@ pub struct Select<'a, T> { /// Function called with the current user input to filter the provided /// options. - pub filter: Filter<'a, T>, + // pub filter: Filter<'a, T>, + + /// TODO Docs + pub scorer: Scorer<'a, T>, /// Function that formats the user input and presents it to the user as the final rendering of the prompt. pub formatter: OptionFormatter<'a, T>, @@ -144,11 +148,16 @@ where /// assert_eq!(false, filter("sa", &"Jacksonville", "Jacksonville", 11)); /// assert_eq!(true, filter("sa", &"San Jose", "San Jose", 12)); /// ``` - pub const DEFAULT_FILTER: Filter<'a, T> = &|filter, _, string_value, _| -> bool { - let filter = filter.to_lowercase(); + pub const DEFAULT_SCORER: Scorer<'a, T> = + &|filter, _option, string_value, _idx| -> Option { + let matcher = SkimMatcherV2::default().ignore_case(); - string_value.to_lowercase().contains(&filter) - }; + match matcher.fuzzy_match(string_value, filter) { + Some(t) if t <= usize::MAX as i64 => Some(t as usize), + Some(t) if t > usize::MAX as i64 => Some(usize::MAX), + Some(_) | None => None, + } + }; /// Default page size. pub const DEFAULT_PAGE_SIZE: usize = crate::config::DEFAULT_PAGE_SIZE; @@ -172,7 +181,7 @@ where page_size: Self::DEFAULT_PAGE_SIZE, vim_mode: Self::DEFAULT_VIM_MODE, starting_cursor: Self::DEFAULT_STARTING_CURSOR, - filter: Self::DEFAULT_FILTER, + scorer: Self::DEFAULT_SCORER, formatter: Self::DEFAULT_FORMATTER, render_config: get_configuration(), } @@ -203,8 +212,8 @@ where } /// Sets the filter function. - pub fn with_filter(mut self, filter: Filter<'a, T>) -> Self { - self.filter = filter; + pub fn with_scorer(mut self, scorer: Scorer<'a, T>) -> Self { + self.scorer = scorer; self } diff --git a/inquire/src/prompts/select/prompt.rs b/inquire/src/prompts/select/prompt.rs index efb0dffa..2848f8bf 100644 --- a/inquire/src/prompts/select/prompt.rs +++ b/inquire/src/prompts/select/prompt.rs @@ -1,4 +1,4 @@ -use std::fmt::Display; +use std::{cmp::Reverse, fmt::Display}; use crate::{ error::InquireResult, @@ -6,7 +6,7 @@ use crate::{ input::{Input, InputActionResult}, list_option::ListOption, prompts::prompt::{ActionResult, Prompt}, - type_aliases::Filter, + type_aliases::Scorer, ui::SelectBackend, utils::paginate, InquireError, Select, @@ -19,11 +19,11 @@ pub struct SelectPrompt<'a, T> { config: SelectConfig, options: Vec, string_options: Vec, - filtered_options: Vec, + scored_options: Vec, help_message: Option<&'a str>, cursor_index: usize, input: Input, - filter: Filter<'a, T>, + scorer: Scorer<'a, T>, formatter: OptionFormatter<'a, T>, } @@ -47,32 +47,36 @@ where } let string_options = so.options.iter().map(T::to_string).collect(); - let filtered_options = (0..so.options.len()).collect(); + let scored_options = (0..so.options.len()).collect(); Ok(Self { message: so.message, config: (&so).into(), options: so.options, string_options, - filtered_options, + scored_options, help_message: so.help_message, cursor_index: so.starting_cursor, input: Input::new(), - filter: so.filter, + scorer: so.scorer, formatter: so.formatter, }) } - fn filter_options(&self) -> Vec { + fn score_options(&self) -> Vec<(usize, usize)> { self.options .iter() .enumerate() - .filter_map(|(i, opt)| match self.input.content() { - val if val.is_empty() => Some(i), - val if (self.filter)(val, opt, self.string_options.get(i).unwrap(), i) => Some(i), - _ => None, + .filter_map(|(i, opt)| { + (self.scorer)( + self.input.content(), + opt, + self.string_options.get(i).unwrap(), + i, + ) + .map(|score| (i, score)) }) - .collect() + .collect::>() } fn move_cursor_up(&mut self, qty: usize, wrap: bool) -> ActionResult { @@ -80,7 +84,7 @@ where let after_wrap = qty.saturating_sub(self.cursor_index); self.cursor_index .checked_sub(qty) - .unwrap_or_else(|| self.filtered_options.len().saturating_sub(after_wrap)) + .unwrap_or_else(|| self.scored_options.len().saturating_sub(after_wrap)) } else { self.cursor_index.saturating_sub(qty) }; @@ -91,13 +95,13 @@ where fn move_cursor_down(&mut self, qty: usize, wrap: bool) -> ActionResult { let mut new_position = self.cursor_index.saturating_add(qty); - if new_position >= self.filtered_options.len() { - new_position = if self.filtered_options.is_empty() { + if new_position >= self.scored_options.len() { + new_position = if self.scored_options.is_empty() { 0 } else if wrap { - new_position % self.filtered_options.len() + new_position % self.scored_options.len() } else { - self.filtered_options.len().saturating_sub(1) + self.scored_options.len().saturating_sub(1) } } @@ -114,14 +118,14 @@ where } fn has_answer_highlighted(&mut self) -> bool { - self.filtered_options.get(self.cursor_index).is_some() + self.scored_options.get(self.cursor_index).is_some() } fn get_final_answer(&mut self) -> ListOption { // should only be called after current cursor index is validated // on has_answer_highlighted - let index = *self.filtered_options.get(self.cursor_index).unwrap(); + let index = *self.scored_options.get(self.cursor_index).unwrap(); let value = self.options.swap_remove(index); ListOption::new(index, value) @@ -170,11 +174,13 @@ where let result = self.input.handle(input_action); if let InputActionResult::ContentChanged = result { - let options = self.filter_options(); - self.filtered_options = options; - if self.filtered_options.len() <= self.cursor_index { + let mut options = self.score_options(); + options.sort_unstable_by_key(|(_idx, score)| Reverse(*score)); + + self.scored_options = options.into_iter().map(|(idx, _)| idx).collect(); + if self.scored_options.len() <= self.cursor_index { let _ = self - .update_cursor_position(self.filtered_options.len().saturating_sub(1)); + .update_cursor_position(self.scored_options.len().saturating_sub(1)); } } @@ -191,7 +197,7 @@ where backend.render_select_prompt(prompt, &self.input)?; let choices = self - .filtered_options + .scored_options .iter() .cloned() .map(|i| ListOption::new(i, self.options.get(i).unwrap())) diff --git a/inquire/src/type_aliases.rs b/inquire/src/type_aliases.rs index 6093bf09..3f74b259 100644 --- a/inquire/src/type_aliases.rs +++ b/inquire/src/type_aliases.rs @@ -36,7 +36,7 @@ use crate::error::CustomUserError; /// assert_eq!(false, filter("san", "Jacksonville", "Jacksonville", 11)); /// assert_eq!(true, filter("san", "San Jose", "San Jose", 12)); /// ``` -pub type Filter<'a, T> = &'a dyn Fn(&str, &T, &str, usize) -> bool; +pub type Scorer<'a, T> = &'a dyn Fn(&str, &T, &str, usize) -> Option; /// Type alias to represent the function used to retrieve text input suggestions. /// The function receives the current input and should return a collection of strings From 1a3efe2a6e62d1416541ec7daa01980a836c835a Mon Sep 17 00:00:00 2001 From: ste Date: Mon, 11 Sep 2023 22:30:51 +0200 Subject: [PATCH 02/11] Improve implementation, missing updated docs --- inquire/src/prompts/multiselect/action.rs | 4 ++-- inquire/src/prompts/multiselect/mod.rs | 10 +++------- inquire/src/prompts/multiselect/prompt.rs | 6 +++--- inquire/src/prompts/select/action.rs | 4 ++-- inquire/src/prompts/select/mod.rs | 10 +++------- inquire/src/prompts/select/prompt.rs | 6 +++--- inquire/src/type_aliases.rs | 2 +- 7 files changed, 17 insertions(+), 25 deletions(-) diff --git a/inquire/src/prompts/multiselect/action.rs b/inquire/src/prompts/multiselect/action.rs index 87f15000..4869581c 100644 --- a/inquire/src/prompts/multiselect/action.rs +++ b/inquire/src/prompts/multiselect/action.rs @@ -9,7 +9,7 @@ use super::config::MultiSelectConfig; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum MultiSelectPromptAction { /// Action on the value text input handler. - FilterInput(InputAction), + ScoreInput(InputAction), /// Moves the cursor to the option above. MoveUp, /// Moves the cursor to the option below. @@ -59,7 +59,7 @@ impl InnerAction for MultiSelectPromptAction { Key::Right(KeyModifiers::NONE) => Self::SelectAll, Key::Left(KeyModifiers::NONE) => Self::ClearSelections, key => match InputAction::from_key(key, &()) { - Some(action) => Self::FilterInput(action), + Some(action) => Self::ScoreInput(action), None => return None, }, }; diff --git a/inquire/src/prompts/multiselect/mod.rs b/inquire/src/prompts/multiselect/mod.rs index 2d0c506d..f0c00fd9 100644 --- a/inquire/src/prompts/multiselect/mod.rs +++ b/inquire/src/prompts/multiselect/mod.rs @@ -169,14 +169,10 @@ where /// TODO Docs pub const DEFAULT_SCORER: Scorer<'a, T> = - &|filter, _option, string_value, _idx| -> Option { + &|input, _option, string_value, _idx| -> Option { + // TODO Figure out how to move matcher instantiation out of scoring function. once_ cell/lock or member on Type? let matcher = SkimMatcherV2::default().ignore_case(); - - match matcher.fuzzy_match(string_value, filter) { - Some(t) if t <= usize::MAX as i64 => Some(t as usize), - Some(t) if t > usize::MAX as i64 => Some(usize::MAX), - Some(_) | None => None, - } + matcher.fuzzy_match(string_value, input) }; /// Default page size, equal to the global default page size [config::DEFAULT_PAGE_SIZE] diff --git a/inquire/src/prompts/multiselect/prompt.rs b/inquire/src/prompts/multiselect/prompt.rs index 16c70ba0..97858018 100644 --- a/inquire/src/prompts/multiselect/prompt.rs +++ b/inquire/src/prompts/multiselect/prompt.rs @@ -83,7 +83,7 @@ where }) } - fn score_options(&self) -> Vec<(usize, usize)> { + fn score_options(&self) -> Vec<(usize, i64)> { self.options .iter() .enumerate() @@ -96,7 +96,7 @@ where ) .map(|score| (i, score)) }) - .collect::>() + .collect::>() } fn move_cursor_up(&mut self, qty: usize, wrap: bool) -> ActionResult { @@ -259,7 +259,7 @@ where ActionResult::NeedsRedraw } - MultiSelectPromptAction::FilterInput(input_action) => { + MultiSelectPromptAction::ScoreInput(input_action) => { let result = self.input.handle(input_action); if let InputActionResult::ContentChanged = result { diff --git a/inquire/src/prompts/select/action.rs b/inquire/src/prompts/select/action.rs index 41c777c4..e8145e0c 100644 --- a/inquire/src/prompts/select/action.rs +++ b/inquire/src/prompts/select/action.rs @@ -9,7 +9,7 @@ use super::config::SelectConfig; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum SelectPromptAction { /// Action on the value text input handler. - FilterInput(InputAction), + ScoreInput(InputAction), /// Moves the cursor to the option above. MoveUp, /// Moves the cursor to the option below. @@ -50,7 +50,7 @@ impl InnerAction for SelectPromptAction { Key::End => Self::MoveToEnd, key => match InputAction::from_key(key, &()) { - Some(action) => Self::FilterInput(action), + Some(action) => Self::ScoreInput(action), None => return None, }, }; diff --git a/inquire/src/prompts/select/mod.rs b/inquire/src/prompts/select/mod.rs index a246848d..8609bc50 100644 --- a/inquire/src/prompts/select/mod.rs +++ b/inquire/src/prompts/select/mod.rs @@ -149,14 +149,10 @@ where /// assert_eq!(true, filter("sa", &"San Jose", "San Jose", 12)); /// ``` pub const DEFAULT_SCORER: Scorer<'a, T> = - &|filter, _option, string_value, _idx| -> Option { + &|input, _option, string_value, _idx| -> Option { + // TODO Figure out how to move matcher instantiation out of scoring function. once_ cell/lock or member on Type? let matcher = SkimMatcherV2::default().ignore_case(); - - match matcher.fuzzy_match(string_value, filter) { - Some(t) if t <= usize::MAX as i64 => Some(t as usize), - Some(t) if t > usize::MAX as i64 => Some(usize::MAX), - Some(_) | None => None, - } + matcher.fuzzy_match(string_value, input) }; /// Default page size. diff --git a/inquire/src/prompts/select/prompt.rs b/inquire/src/prompts/select/prompt.rs index 2848f8bf..a9a61f10 100644 --- a/inquire/src/prompts/select/prompt.rs +++ b/inquire/src/prompts/select/prompt.rs @@ -63,7 +63,7 @@ where }) } - fn score_options(&self) -> Vec<(usize, usize)> { + fn score_options(&self) -> Vec<(usize, i64)> { self.options .iter() .enumerate() @@ -76,7 +76,7 @@ where ) .map(|score| (i, score)) }) - .collect::>() + .collect::>() } fn move_cursor_up(&mut self, qty: usize, wrap: bool) -> ActionResult { @@ -170,7 +170,7 @@ where SelectPromptAction::PageDown => self.move_cursor_down(self.config.page_size, false), SelectPromptAction::MoveToStart => self.move_cursor_up(usize::MAX, false), SelectPromptAction::MoveToEnd => self.move_cursor_down(usize::MAX, false), - SelectPromptAction::FilterInput(input_action) => { + SelectPromptAction::ScoreInput(input_action) => { let result = self.input.handle(input_action); if let InputActionResult::ContentChanged = result { diff --git a/inquire/src/type_aliases.rs b/inquire/src/type_aliases.rs index 3f74b259..c171a647 100644 --- a/inquire/src/type_aliases.rs +++ b/inquire/src/type_aliases.rs @@ -36,7 +36,7 @@ use crate::error::CustomUserError; /// assert_eq!(false, filter("san", "Jacksonville", "Jacksonville", 11)); /// assert_eq!(true, filter("san", "San Jose", "San Jose", 12)); /// ``` -pub type Scorer<'a, T> = &'a dyn Fn(&str, &T, &str, usize) -> Option; +pub type Scorer<'a, T> = &'a dyn Fn(&str, &T, &str, usize) -> Option; /// Type alias to represent the function used to retrieve text input suggestions. /// The function receives the current input and should return a collection of strings From fa0f986c6e4303601a43dec02a3f1fd2314137a0 Mon Sep 17 00:00:00 2001 From: ste Date: Mon, 11 Sep 2023 22:33:41 +0200 Subject: [PATCH 03/11] Update Documentation and Readmes --- README.md | 14 ++--- inquire/src/prompts/multiselect/mod.rs | 49 +++++++---------- inquire/src/prompts/select/mod.rs | 41 +++++++-------- inquire/src/type_aliases.rs | 73 +++++++++++++++++++------- 4 files changed, 99 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 7045fe56..21ca6522 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ It provides several different prompts in order to interactively ask the user for - Help messages; - Autocompletion for [`Text`] prompts; - Confirmation messages for [`Password`] prompts; - - Custom list filters for Select and [`MultiSelect`] prompts; + - Custom list filters for [`Select`] and [`MultiSelect`] prompts; - Custom parsers for [`Confirm`] and [`CustomType`] prompts; - Custom extensions for files created by [`Editor`] prompts; - and many others! @@ -154,13 +154,13 @@ The default parser for [`CustomType`] prompts calls the `parse::()` method on In the [demo](#demo) you can see this behavior in action with the _amount_ (CustomType) prompt. -## Filtering +## Scoring -Filtering is applicable to two prompts: [`Select`] and [`MultiSelect`]. They provide the user the ability to filter the options based on their text input. This is specially useful when there are a lot of options for the user to choose from, allowing them to quickly find their expected options. +Scoring is applicable to two prompts: [`Select`] and [`MultiSelect`]. They provide the user the ability to sort and filter the list of options based on their text input. This is specially useful when there are a lot of options for the user to choose from, allowing them to quickly find their expected options. -Filter functions receive three arguments: the current user input, the option string value and the option index. They must return a `bool` value indicating whether the option should be part of the results or not. +Scoring functions receive four arguments: the current user input, the option, the option string value and the option index. They must return a `Option` value indicating whether the option should be part of the results or not. -The default filter function does a naive case-insensitive comparison between the option string value and the current user input, returning `true` if the option string value contains the user input as a substring. +The default scoring function calculates a match value with the current user input and each option using SkimV2 from [fuzzy_matcher](https://crates.io/crates/fuzzy-matcher), resulting in fuzzy searching and filtering, returning `Some(_i64)` if SkimV2 detects a match. In the [demo](#demo) you can see this behavior in action with the _account_ (Select) and _tags_ (MultiSelect) prompts. @@ -313,7 +313,7 @@ Like all others, this prompt also allows you to customize several aspects of it: - Prints the selected option string value by default. - **Page size**: Number of options displayed at once, 7 by default. - **Display option indexes**: On long lists, it might be helpful to display the indexes of the options to the user. Via the `RenderConfig`, you can set the display mode of the indexes as a prefix of an option. The default configuration is `None`, to not render any index when displaying the options. -- **Filter function**: Function that defines if an option is displayed or not based on the current filter input. +- **Scoring function**: Function that defines the order of options and if an option is displayed or not based on the current user input. ## MultiSelect @@ -344,7 +344,7 @@ Customizable options: - No validators are on by default. - **Page size**: Number of options displayed at once, 7 by default. - **Display option indexes**: On long lists, it might be helpful to display the indexes of the options to the user. Via the `RenderConfig`, you can set the display mode of the indexes as a prefix of an option. The default configuration is `None`, to not render any index when displaying the options. -- **Filter function**: Function that defines if an option is displayed or not based on the current filter input. +- **Scoring function**: Function that defines the order of options and if an option is displayed or not based on the current user input. - **Keep filter flag**: Whether the current filter input should be cleared or not after a selection is made. Defaults to true. ## Editor diff --git a/inquire/src/prompts/multiselect/mod.rs b/inquire/src/prompts/multiselect/mod.rs index f0c00fd9..19ddba3d 100644 --- a/inquire/src/prompts/multiselect/mod.rs +++ b/inquire/src/prompts/multiselect/mod.rs @@ -46,7 +46,7 @@ use self::prompt::MultiSelectPrompt; /// - No validators are on by default. /// - **Page size**: Number of options displayed at once, 7 by default. /// - **Display option indexes**: On long lists, it might be helpful to display the indexes of the options to the user. Via the `RenderConfig`, you can set the display mode of the indexes as a prefix of an option. The default configuration is `None`, to not render any index when displaying the options. -/// - **Filter function**: Function that defines if an option is displayed or not based on the current filter input. +/// - **Scorer function**: Function that defines the order of options and if displayed as all. /// - **Keep filter flag**: Whether the current filter input should be cleared or not after a selection is made. Defaults to true. /// /// # Example @@ -78,11 +78,9 @@ pub struct MultiSelect<'a, T> { /// Starting cursor index of the selection. pub starting_cursor: usize, - /// Function called with the current user input to filter the provided + /// Function called with the current user input to score the provided /// options. - // pub filter: Filter<'a, T>, - - /// TODO Docs + /// The list of options is sorted in descending order (highest score first) pub scorer: Scorer<'a, T>, /// Whether the current filter typed by the user is kept or cleaned after a selection is made. @@ -138,36 +136,29 @@ where .join(", ") }; - /// Default filter function, which checks if the current filter value is a substring of the option value. - /// If it is, the option is displayed. + /// Default scoring function, which will create a score for the current option using the input value. + /// The return will be sorted in Descending order, leaving options with None as a score. /// /// # Examples /// /// ``` /// use inquire::MultiSelect; /// - /// let filter = MultiSelect::<&str>::DEFAULT_FILTER; - /// assert_eq!(false, filter("sa", &"New York", "New York", 0)); - /// assert_eq!(true, filter("sa", &"Sacramento", "Sacramento", 1)); - /// assert_eq!(true, filter("sa", &"Kansas", "Kansas", 2)); - /// assert_eq!(true, filter("sa", &"Mesa", "Mesa", 3)); - /// assert_eq!(false, filter("sa", &"Phoenix", "Phoenix", 4)); - /// assert_eq!(false, filter("sa", &"Philadelphia", "Philadelphia", 5)); - /// assert_eq!(true, filter("sa", &"San Antonio", "San Antonio", 6)); - /// assert_eq!(true, filter("sa", &"San Diego", "San Diego", 7)); - /// assert_eq!(false, filter("sa", &"Dallas", "Dallas", 8)); - /// assert_eq!(true, filter("sa", &"San Francisco", "San Francisco", 9)); - /// assert_eq!(false, filter("sa", &"Austin", "Austin", 10)); - /// assert_eq!(false, filter("sa", &"Jacksonville", "Jacksonville", 11)); - /// assert_eq!(true, filter("sa", &"San Jose", "San Jose", 12)); + /// let scorer = MultiSelect::<&str>::DEFAULT_SCORER; + /// assert_eq!(None, scorer("sa", &"New York", "New York", 0)); + /// assert_eq!(Some(49), scorer("sa", &"Sacramento", "Sacramento", 1)); + /// assert_eq!(Some(35), scorer("sa", &"Kansas", "Kansas", 2)); + /// assert_eq!(Some(35), scorer("sa", &"Mesa", "Mesa", 3)); + /// assert_eq!(None, scorer("sa", &"Phoenix", "Phoenix", 4)); + /// assert_eq!(None, scorer("sa", &"Philadelphia", "Philadelphia", 5)); + /// assert_eq!(Some(49), scorer("sa", &"San Antonio", "San Antonio", 6)); + /// assert_eq!(Some(49), scorer("sa", &"San Diego", "San Diego", 7)); + /// assert_eq!(None, scorer("sa", &"Dallas", "Dallas", 8)); + /// assert_eq!(Some(49), scorer("sa", &"San Francisco", "San Francisco", 9)); + /// assert_eq!(None, scorer("sa", &"Austin", "Austin", 10)); + /// assert_eq!(None, scorer("sa", &"Jacksonville", "Jacksonville", 11)); + /// assert_eq!(Some(49), scorer("sa", &"San Jose", "San Jose", 12)); /// ``` - // pub const DEFAULT_FILTER: Filter<'a, T> = &|filter, _, string_value, _| -> bool { - // let filter = filter.to_lowercase(); - - // string_value.to_lowercase().contains(&filter) - // }; - - /// TODO Docs pub const DEFAULT_SCORER: Scorer<'a, T> = &|input, _option, string_value, _idx| -> Option { // TODO Figure out how to move matcher instantiation out of scoring function. once_ cell/lock or member on Type? @@ -239,7 +230,7 @@ where self } - /// Sets the filter function. + /// Sets the scoring function. pub fn with_scorer(mut self, scorer: Scorer<'a, T>) -> Self { self.scorer = scorer; self diff --git a/inquire/src/prompts/select/mod.rs b/inquire/src/prompts/select/mod.rs index 8609bc50..58d15c80 100644 --- a/inquire/src/prompts/select/mod.rs +++ b/inquire/src/prompts/select/mod.rs @@ -44,7 +44,7 @@ use self::prompt::SelectPrompt; /// - Prints the selected option string value by default. /// - **Page size**: Number of options displayed at once, 7 by default. /// - **Display option indexes**: On long lists, it might be helpful to display the indexes of the options to the user. Via the `RenderConfig`, you can set the display mode of the indexes as a prefix of an option. The default configuration is `None`, to not render any index when displaying the options. -/// - **Filter function**: Function that defines if an option is displayed or not based on the current filter input. +/// - **Scorer function**: Function that defines the order of options and if displayed as all. /// /// # Example /// @@ -85,11 +85,8 @@ pub struct Select<'a, T> { /// Starting cursor index of the selection. pub starting_cursor: usize, - /// Function called with the current user input to filter the provided + /// Function called with the current user input to score the provided /// options. - // pub filter: Filter<'a, T>, - - /// TODO Docs pub scorer: Scorer<'a, T>, /// Function that formats the user input and presents it to the user as the final rendering of the prompt. @@ -125,28 +122,28 @@ where /// ``` pub const DEFAULT_FORMATTER: OptionFormatter<'a, T> = &|ans| ans.to_string(); - /// Default filter function, which checks if the current filter value is a substring of the option value. - /// If it is, the option is displayed. + /// Default scoring function, which will create a score for the current option using the input value. + /// The return will be sorted in Descending order, leaving options with None as a score. /// /// # Examples /// /// ``` /// use inquire::Select; /// - /// let filter = Select::<&str>::DEFAULT_FILTER; - /// assert_eq!(false, filter("sa", &"New York", "New York", 0)); - /// assert_eq!(true, filter("sa", &"Sacramento", "Sacramento", 1)); - /// assert_eq!(true, filter("sa", &"Kansas", "Kansas", 2)); - /// assert_eq!(true, filter("sa", &"Mesa", "Mesa", 3)); - /// assert_eq!(false, filter("sa", &"Phoenix", "Phoenix", 4)); - /// assert_eq!(false, filter("sa", &"Philadelphia", "Philadelphia", 5)); - /// assert_eq!(true, filter("sa", &"San Antonio", "San Antonio", 6)); - /// assert_eq!(true, filter("sa", &"San Diego", "San Diego", 7)); - /// assert_eq!(false, filter("sa", &"Dallas", "Dallas", 8)); - /// assert_eq!(true, filter("sa", &"San Francisco", "San Francisco", 9)); - /// assert_eq!(false, filter("sa", &"Austin", "Austin", 10)); - /// assert_eq!(false, filter("sa", &"Jacksonville", "Jacksonville", 11)); - /// assert_eq!(true, filter("sa", &"San Jose", "San Jose", 12)); + /// let scorer = Select::<&str>::DEFAULT_SCORER; + /// assert_eq!(None, scorer("sa", &"New York", "New York", 0)); + /// assert_eq!(Some(49), scorer("sa", &"Sacramento", "Sacramento", 1)); + /// assert_eq!(Some(35), scorer("sa", &"Kansas", "Kansas", 2)); + /// assert_eq!(Some(35), scorer("sa", &"Mesa", "Mesa", 3)); + /// assert_eq!(None, scorer("sa", &"Phoenix", "Phoenix", 4)); + /// assert_eq!(None, scorer("sa", &"Philadelphia", "Philadelphia", 5)); + /// assert_eq!(Some(49), scorer("sa", &"San Antonio", "San Antonio", 6)); + /// assert_eq!(Some(49), scorer("sa", &"San Diego", "San Diego", 7)); + /// assert_eq!(None, scorer("sa", &"Dallas", "Dallas", 8)); + /// assert_eq!(Some(49), scorer("sa", &"San Francisco", "San Francisco", 9)); + /// assert_eq!(None, scorer("sa", &"Austin", "Austin", 10)); + /// assert_eq!(None, scorer("sa", &"Jacksonville", "Jacksonville", 11)); + /// assert_eq!(Some(49), scorer("sa", &"San Jose", "San Jose", 12)); /// ``` pub const DEFAULT_SCORER: Scorer<'a, T> = &|input, _option, string_value, _idx| -> Option { @@ -207,7 +204,7 @@ where self } - /// Sets the filter function. + /// Sets the scoring function. pub fn with_scorer(mut self, scorer: Scorer<'a, T>) -> Self { self.scorer = scorer; self diff --git a/inquire/src/type_aliases.rs b/inquire/src/type_aliases.rs index c171a647..0b6999b7 100644 --- a/inquire/src/type_aliases.rs +++ b/inquire/src/type_aliases.rs @@ -2,7 +2,7 @@ use crate::error::CustomUserError; -/// Type alias to represent the function used to filter options. +/// Type alias to represent the function used to Score and filter options. /// /// The function receives: /// - Current user input, filter value @@ -10,31 +10,64 @@ use crate::error::CustomUserError; /// - String value of the current option /// - Index of the current option in the original list /// -/// The return type should be whether the current option should be displayed to the user. +/// The return type should be a score determining the order options should be displayed to the user. +/// The greater the score, the higher on the list it will be displayed. /// /// # Examples /// /// ``` -/// use inquire::type_aliases::Filter; +/// use inquire::type_aliases::Scorer; /// -/// let filter: Filter = &|filter, _, string_value, _| -> bool { -/// let filter = filter.to_lowercase(); -/// -/// string_value.to_lowercase().starts_with(&filter) +/// // Implement simpler 'contains' filter that maintains the current order +/// // If all scores are the same, no sorting will occur +/// let filter: Scorer = +/// &|input, _option, string_value, _idx| -> Option { +/// let filter = input.to_lowercase(); +/// match string_value.to_lowercase().contains(&filter) { +/// true => Some(0), +/// false => None, +/// } +/// } /// }; -/// assert_eq!(false, filter("san", "New York", "New York", 0)); -/// assert_eq!(false, filter("san", "Los Angeles", "Los Angeles", 1)); -/// assert_eq!(false, filter("san", "Chicago", "Chicago", 2)); -/// assert_eq!(false, filter("san", "Houston", "Houston", 3)); -/// assert_eq!(false, filter("san", "Phoenix", "Phoenix", 4)); -/// assert_eq!(false, filter("san", "Philadelphia", "Philadelphia", 5)); -/// assert_eq!(true, filter("san", "San Antonio", "San Antonio", 6)); -/// assert_eq!(true, filter("san", "San Diego", "San Diego", 7)); -/// assert_eq!(false, filter("san", "Dallas", "Dallas", 8)); -/// assert_eq!(true, filter("san", "San Francisco", "San Francisco", 9)); -/// assert_eq!(false, filter("san", "Austin", "Austin", 10)); -/// assert_eq!(false, filter("san", "Jacksonville", "Jacksonville", 11)); -/// assert_eq!(true, filter("san", "San Jose", "San Jose", 12)); +/// +/// assert_eq!(Some(0), scorer("san", "New York", "New York", 0)); +/// assert_eq!(Some(0), scorer("san", "Los Angeles", "Los Angeles", 1)); +/// assert_eq!(Some(0), scorer("san", "Chicago", "Chicago", 2)); +/// assert_eq!(Some(0), scorer("san", "Houston", "Houston", 3)); +/// assert_eq!(Some(0), scorer("san", "Phoenix", "Phoenix", 4)); +/// assert_eq!(Some(0), scorer("san", "Philadelphia", "Philadelphia", 5)); +/// assert_eq!(Some(0), scorer("san", "San Antonio", "San Antonio", 6)); +/// assert_eq!(Some(0), scorer("san", "San Diego", "San Diego", 7)); +/// assert_eq!(Some(0), scorer("san", "Dallas", "Dallas", 8)); +/// assert_eq!(Some(0), scorer("san", "San Francisco", "San Francisco", 9)); +/// assert_eq!(Some(0), scorer("san", "Austin", "Austin", 10)); +/// assert_eq!(Some(0), scorer("san", "Jacksonville", "Jacksonville", 11)); +/// assert_eq!(Some(0), scorer("san", "San Jose", "San Jose", 12)); +/// +/// +/// // Default implementation for fuzzy search +/// use inquire::type_aliases::Scorer; +/// use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; +/// +/// pub const DEFAULT_SCORER: Scorer<'a, T> = +/// &|input, _option, string_value, _idx| -> Option { +/// let matcher = SkimMatcherV2::default().ignore_case(); +/// matcher.fuzzy_match(string_value, input) +/// }; +/// +/// assert_eq!(None, scorer("sa", &"New York", "New York", 0)); +/// assert_eq!(Some(49), scorer("sa", &"Sacramento", "Sacramento", 1)); +/// assert_eq!(Some(35), scorer("sa", &"Kansas", "Kansas", 2)); +/// assert_eq!(Some(35), scorer("sa", &"Mesa", "Mesa", 3)); +/// assert_eq!(None, scorer("sa", &"Phoenix", "Phoenix", 4)); +/// assert_eq!(None, scorer("sa", &"Philadelphia", "Philadelphia", 5)); +/// assert_eq!(Some(49), scorer("sa", &"San Antonio", "San Antonio", 6)); +/// assert_eq!(Some(49), scorer("sa", &"San Diego", "San Diego", 7)); +/// assert_eq!(None, scorer("sa", &"Dallas", "Dallas", 8)); +/// assert_eq!(Some(49), scorer("sa", &"San Francisco", "San Francisco", 9)); +/// assert_eq!(None, scorer("sa", &"Austin", "Austin", 10)); +/// assert_eq!(None, scorer("sa", &"Jacksonville", "Jacksonville", 11)); +/// assert_eq!(Some(49), scorer("sa", &"San Jose", "San Jose", 12)); /// ``` pub type Scorer<'a, T> = &'a dyn Fn(&str, &T, &str, usize) -> Option; From 0ece33e0276326a01d18a058309d4b8dba1d98b1 Mon Sep 17 00:00:00 2001 From: ste Date: Mon, 11 Sep 2023 23:03:26 +0200 Subject: [PATCH 04/11] Avoid intantiation SkimMatcher on every option --- inquire/src/prompts/multiselect/mod.rs | 6 +++--- inquire/src/prompts/select/mod.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/inquire/src/prompts/multiselect/mod.rs b/inquire/src/prompts/multiselect/mod.rs index 19ddba3d..70781d59 100644 --- a/inquire/src/prompts/multiselect/mod.rs +++ b/inquire/src/prompts/multiselect/mod.rs @@ -7,6 +7,7 @@ mod test; pub use action::*; use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; +use once_cell::sync::Lazy; use std::fmt::Display; @@ -24,6 +25,7 @@ use crate::{ use self::prompt::MultiSelectPrompt; +static DEFAULT_MATCHER: Lazy = Lazy::new(|| SkimMatcherV2::default().ignore_case()); /// Prompt suitable for when you need the user to select many options (including none if applicable) among a list of them. /// /// The user can select (or deselect) the current highlighted option by pressing space, clean all selections by pressing the left arrow and select all options by pressing the right arrow. @@ -161,9 +163,7 @@ where /// ``` pub const DEFAULT_SCORER: Scorer<'a, T> = &|input, _option, string_value, _idx| -> Option { - // TODO Figure out how to move matcher instantiation out of scoring function. once_ cell/lock or member on Type? - let matcher = SkimMatcherV2::default().ignore_case(); - matcher.fuzzy_match(string_value, input) + DEFAULT_MATCHER.fuzzy_match(string_value, input) }; /// Default page size, equal to the global default page size [config::DEFAULT_PAGE_SIZE] diff --git a/inquire/src/prompts/select/mod.rs b/inquire/src/prompts/select/mod.rs index 58d15c80..c883f318 100644 --- a/inquire/src/prompts/select/mod.rs +++ b/inquire/src/prompts/select/mod.rs @@ -7,6 +7,7 @@ mod test; pub use action::*; use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; +use once_cell::sync::Lazy; use std::fmt::Display; @@ -23,6 +24,7 @@ use crate::{ use self::prompt::SelectPrompt; +static DEFAULT_MATCHER: Lazy = Lazy::new(|| SkimMatcherV2::default().ignore_case()); /// Prompt suitable for when you need the user to select one option among many. /// /// The user can select and submit the current highlighted option by pressing enter. @@ -147,9 +149,7 @@ where /// ``` pub const DEFAULT_SCORER: Scorer<'a, T> = &|input, _option, string_value, _idx| -> Option { - // TODO Figure out how to move matcher instantiation out of scoring function. once_ cell/lock or member on Type? - let matcher = SkimMatcherV2::default().ignore_case(); - matcher.fuzzy_match(string_value, input) + DEFAULT_MATCHER.fuzzy_match(string_value, input) }; /// Default page size. From f1b00f8836963ce1db7af2b960426efdc2220804 Mon Sep 17 00:00:00 2001 From: ste Date: Tue, 12 Sep 2023 21:03:40 +0200 Subject: [PATCH 05/11] Fix doc tests --- inquire/src/type_aliases.rs | 62 +++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/inquire/src/type_aliases.rs b/inquire/src/type_aliases.rs index 0b6999b7..0469bcbe 100644 --- a/inquire/src/type_aliases.rs +++ b/inquire/src/type_aliases.rs @@ -27,47 +27,49 @@ use crate::error::CustomUserError; /// true => Some(0), /// false => None, /// } -/// } -/// }; +/// }; +/// +/// assert_eq!(None, filter("sa", "New York", "New York", 0)); +/// assert_eq!(None, filter("sa", "Los Angeles", "Los Angeles", 1)); +/// assert_eq!(None, filter("sa", "Chicago", "Chicago", 2)); +/// assert_eq!(None, filter("sa", "Houston", "Houston", 3)); +/// assert_eq!(None, filter("sa", "Phoenix", "Phoenix", 4)); +/// assert_eq!(None, filter("sa", "Philadelphia", "Philadelphia", 5)); +/// assert_eq!(Some(0), filter("sa", "San Antonio", "San Antonio", 6)); +/// assert_eq!(Some(0), filter("sa", "San Diego", "San Diego", 7)); +/// assert_eq!(None, filter("sa", "Dallas", "Dallas", 8)); +/// assert_eq!(Some(0), filter("sa", "San Francisco", "San Francisco", 9)); +/// assert_eq!(None, filter("sa", "Austin", "Austin", 10)); +/// assert_eq!(None, filter("sa", "Jacksonville", "Jacksonville", 11)); +/// assert_eq!(Some(0), filter("sa", "San Jose", "San Jose", 12)); +///``` /// -/// assert_eq!(Some(0), scorer("san", "New York", "New York", 0)); -/// assert_eq!(Some(0), scorer("san", "Los Angeles", "Los Angeles", 1)); -/// assert_eq!(Some(0), scorer("san", "Chicago", "Chicago", 2)); -/// assert_eq!(Some(0), scorer("san", "Houston", "Houston", 3)); -/// assert_eq!(Some(0), scorer("san", "Phoenix", "Phoenix", 4)); -/// assert_eq!(Some(0), scorer("san", "Philadelphia", "Philadelphia", 5)); -/// assert_eq!(Some(0), scorer("san", "San Antonio", "San Antonio", 6)); -/// assert_eq!(Some(0), scorer("san", "San Diego", "San Diego", 7)); -/// assert_eq!(Some(0), scorer("san", "Dallas", "Dallas", 8)); -/// assert_eq!(Some(0), scorer("san", "San Francisco", "San Francisco", 9)); -/// assert_eq!(Some(0), scorer("san", "Austin", "Austin", 10)); -/// assert_eq!(Some(0), scorer("san", "Jacksonville", "Jacksonville", 11)); -/// assert_eq!(Some(0), scorer("san", "San Jose", "San Jose", 12)); /// /// -/// // Default implementation for fuzzy search +/// Default implementation for fuzzy search +///``` /// use inquire::type_aliases::Scorer; /// use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; /// -/// pub const DEFAULT_SCORER: Scorer<'a, T> = +/// pub const DEFAULT_SCORER: Scorer = /// &|input, _option, string_value, _idx| -> Option { /// let matcher = SkimMatcherV2::default().ignore_case(); /// matcher.fuzzy_match(string_value, input) /// }; /// -/// assert_eq!(None, scorer("sa", &"New York", "New York", 0)); -/// assert_eq!(Some(49), scorer("sa", &"Sacramento", "Sacramento", 1)); -/// assert_eq!(Some(35), scorer("sa", &"Kansas", "Kansas", 2)); -/// assert_eq!(Some(35), scorer("sa", &"Mesa", "Mesa", 3)); -/// assert_eq!(None, scorer("sa", &"Phoenix", "Phoenix", 4)); -/// assert_eq!(None, scorer("sa", &"Philadelphia", "Philadelphia", 5)); -/// assert_eq!(Some(49), scorer("sa", &"San Antonio", "San Antonio", 6)); -/// assert_eq!(Some(49), scorer("sa", &"San Diego", "San Diego", 7)); -/// assert_eq!(None, scorer("sa", &"Dallas", "Dallas", 8)); -/// assert_eq!(Some(49), scorer("sa", &"San Francisco", "San Francisco", 9)); -/// assert_eq!(None, scorer("sa", &"Austin", "Austin", 10)); -/// assert_eq!(None, scorer("sa", &"Jacksonville", "Jacksonville", 11)); -/// assert_eq!(Some(49), scorer("sa", &"San Jose", "San Jose", 12)); +/// assert_eq!(None, DEFAULT_SCORER("sa", &"New York", "New York", 0)); +/// assert_eq!(Some(49), DEFAULT_SCORER("sa", &"Sacramento", "Sacramento", 1)); +/// assert_eq!(Some(35), DEFAULT_SCORER("sa", &"Kansas", "Kansas", 2)); +/// assert_eq!(Some(35), DEFAULT_SCORER("sa", &"Mesa", "Mesa", 3)); +/// assert_eq!(None, DEFAULT_SCORER("sa", &"Phoenix", "Phoenix", 4)); +/// assert_eq!(None, DEFAULT_SCORER("sa", &"Philadelphia", "Philadelphia", 5)); +/// assert_eq!(Some(49), DEFAULT_SCORER("sa", &"San Antonio", "San Antonio", 6)); +/// assert_eq!(Some(49), DEFAULT_SCORER("sa", &"San Diego", "San Diego", 7)); +/// assert_eq!(None, DEFAULT_SCORER("sa", &"Dallas", "Dallas", 8)); +/// assert_eq!(Some(49), DEFAULT_SCORER("sa", &"San Francisco", "San Francisco", 9)); +/// assert_eq!(None, DEFAULT_SCORER("sa", &"Austin", "Austin", 10)); +/// assert_eq!(None, DEFAULT_SCORER("sa", &"Jacksonville", "Jacksonville", 11)); +/// assert_eq!(Some(49), DEFAULT_SCORER("sa", &"San Jose", "San Jose", 12)); /// ``` pub type Scorer<'a, T> = &'a dyn Fn(&str, &T, &str, usize) -> Option; From e31b5b41e28eb2889e091bd0d6ec63d798f245ad Mon Sep 17 00:00:00 2001 From: ste Date: Tue, 12 Sep 2023 21:13:54 +0200 Subject: [PATCH 06/11] Feature flag toggling fuzzy matcher dependency --- inquire/Cargo.toml | 6 ++++-- inquire/src/prompts/multiselect/mod.rs | 18 ++++++++++++++++-- inquire/src/prompts/select/mod.rs | 19 ++++++++++++++++--- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/inquire/Cargo.toml b/inquire/Cargo.toml index ef4b4538..57c969d4 100644 --- a/inquire/Cargo.toml +++ b/inquire/Cargo.toml @@ -20,11 +20,12 @@ path = "src/lib.rs" doctest = true [features] -default = ["macros", "crossterm", "one-liners"] +default = ["macros", "crossterm", "one-liners", "fuzzy"] macros = [] one-liners = [] date = ["chrono"] editor = ["tempfile"] +fuzzy = ["fuzzy-matcher"] [package.metadata.docs.rs] all-features = true @@ -41,13 +42,14 @@ chrono = { version = "0.4", optional = true } tempfile = { version = "3", optional = true } +fuzzy-matcher = { version = "0.3.7", default-features = false, optional = true } + bitflags = "2" dyn-clone = "1" newline-converter = "0.3" once_cell = "1.18.0" unicode-segmentation = "1" unicode-width = "0.1" -fuzzy-matcher = { version = "0.3.7", default-features = false } [[example]] name = "form" diff --git a/inquire/src/prompts/multiselect/mod.rs b/inquire/src/prompts/multiselect/mod.rs index 70781d59..fdac2917 100644 --- a/inquire/src/prompts/multiselect/mod.rs +++ b/inquire/src/prompts/multiselect/mod.rs @@ -6,8 +6,6 @@ mod prompt; mod test; pub use action::*; -use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; -use once_cell::sync::Lazy; use std::fmt::Display; @@ -25,6 +23,11 @@ use crate::{ use self::prompt::MultiSelectPrompt; +#[cfg(feature = "fuzzy")] +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; +#[cfg(feature = "fuzzy")] +use once_cell::sync::Lazy; +#[cfg(feature = "fuzzy")] static DEFAULT_MATCHER: Lazy = Lazy::new(|| SkimMatcherV2::default().ignore_case()); /// Prompt suitable for when you need the user to select many options (including none if applicable) among a list of them. /// @@ -161,11 +164,22 @@ where /// assert_eq!(None, scorer("sa", &"Jacksonville", "Jacksonville", 11)); /// assert_eq!(Some(49), scorer("sa", &"San Jose", "San Jose", 12)); /// ``` + #[cfg(feature = "fuzzy")] pub const DEFAULT_SCORER: Scorer<'a, T> = &|input, _option, string_value, _idx| -> Option { DEFAULT_MATCHER.fuzzy_match(string_value, input) }; + #[cfg(not(feature = "fuzzy"))] + pub const DEFAULT_SCORER: Scorer<'a, T> = + &|input, _option, string_value, _idx| -> Option { + let filter = input.to_lowercase(); + match string_value.to_lowercase().contains(&filter) { + true => Some(0), + false => None, + } + }; + /// Default page size, equal to the global default page size [config::DEFAULT_PAGE_SIZE] pub const DEFAULT_PAGE_SIZE: usize = crate::config::DEFAULT_PAGE_SIZE; diff --git a/inquire/src/prompts/select/mod.rs b/inquire/src/prompts/select/mod.rs index c883f318..a406182c 100644 --- a/inquire/src/prompts/select/mod.rs +++ b/inquire/src/prompts/select/mod.rs @@ -6,9 +6,6 @@ mod prompt; mod test; pub use action::*; -use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; -use once_cell::sync::Lazy; - use std::fmt::Display; use crate::{ @@ -24,6 +21,11 @@ use crate::{ use self::prompt::SelectPrompt; +#[cfg(feature = "fuzzy")] +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; +#[cfg(feature = "fuzzy")] +use once_cell::sync::Lazy; +#[cfg(feature = "fuzzy")] static DEFAULT_MATCHER: Lazy = Lazy::new(|| SkimMatcherV2::default().ignore_case()); /// Prompt suitable for when you need the user to select one option among many. /// @@ -147,11 +149,22 @@ where /// assert_eq!(None, scorer("sa", &"Jacksonville", "Jacksonville", 11)); /// assert_eq!(Some(49), scorer("sa", &"San Jose", "San Jose", 12)); /// ``` + #[cfg(feature = "fuzzy")] pub const DEFAULT_SCORER: Scorer<'a, T> = &|input, _option, string_value, _idx| -> Option { DEFAULT_MATCHER.fuzzy_match(string_value, input) }; + #[cfg(not(feature = "fuzzy"))] + pub const DEFAULT_SCORER: Scorer<'a, T> = + &|input, _option, string_value, _idx| -> Option { + let filter = input.to_lowercase(); + match string_value.to_lowercase().contains(&filter) { + true => Some(0), + false => None, + } + }; + /// Default page size. pub const DEFAULT_PAGE_SIZE: usize = crate::config::DEFAULT_PAGE_SIZE; From ef6349cefbc63d13ab644b2fdb9962ab63f86810 Mon Sep 17 00:00:00 2001 From: ste Date: Wed, 13 Sep 2023 23:12:56 +0200 Subject: [PATCH 07/11] Enable/Disable cursor resetting to first option --- inquire/src/prompts/multiselect/config.rs | 3 +++ inquire/src/prompts/multiselect/mod.rs | 17 +++++++++++++++++ inquire/src/prompts/multiselect/prompt.rs | 4 +++- inquire/src/prompts/select/config.rs | 3 +++ inquire/src/prompts/select/mod.rs | 17 +++++++++++++++++ inquire/src/prompts/select/prompt.rs | 4 +++- 6 files changed, 46 insertions(+), 2 deletions(-) diff --git a/inquire/src/prompts/multiselect/config.rs b/inquire/src/prompts/multiselect/config.rs index a5250b64..9601969f 100644 --- a/inquire/src/prompts/multiselect/config.rs +++ b/inquire/src/prompts/multiselect/config.rs @@ -9,6 +9,8 @@ pub struct MultiSelectConfig { pub page_size: usize, /// Whether to keep the filter text when an option is selected. pub keep_filter: bool, + /// Whether to reset the cursor to the first option on filter input change. + pub reset_cursor: bool, } impl From<&MultiSelect<'_, T>> for MultiSelectConfig { @@ -17,6 +19,7 @@ impl From<&MultiSelect<'_, T>> for MultiSelectConfig { vim_mode: value.vim_mode, page_size: value.page_size, keep_filter: value.keep_filter, + reset_cursor: value.reset_cursor, } } } diff --git a/inquire/src/prompts/multiselect/mod.rs b/inquire/src/prompts/multiselect/mod.rs index fdac2917..aaf37493 100644 --- a/inquire/src/prompts/multiselect/mod.rs +++ b/inquire/src/prompts/multiselect/mod.rs @@ -83,6 +83,10 @@ pub struct MultiSelect<'a, T> { /// Starting cursor index of the selection. pub starting_cursor: usize, + /// Reset cursor position to first option on filter input change. + /// Defaults to true when 'fuzzy' is enabled. + pub reset_cursor: bool, + /// Function called with the current user input to score the provided /// options. /// The list of options is sorted in descending order (highest score first) @@ -189,6 +193,10 @@ where /// Default starting cursor index. pub const DEFAULT_STARTING_CURSOR: usize = 0; + /// Default cursor behaviour on filter input change. + /// Defaults to true if 'fuzzy' is enabled. + pub const DEFAULT_RESET_CURSOR: bool = cfg!(feature = "fuzzy"); + /// Default behavior of keeping or cleaning the current filter value. pub const DEFAULT_KEEP_FILTER: bool = true; @@ -206,6 +214,7 @@ where page_size: Self::DEFAULT_PAGE_SIZE, vim_mode: Self::DEFAULT_VIM_MODE, starting_cursor: Self::DEFAULT_STARTING_CURSOR, + reset_cursor: Self::DEFAULT_RESET_CURSOR, keep_filter: Self::DEFAULT_KEEP_FILTER, scorer: Self::DEFAULT_SCORER, formatter: Self::DEFAULT_FORMATTER, @@ -293,6 +302,14 @@ where self } + /// Sets the reset_cursor behaviour. + /// Will reset cursor to first option on filter input change. + /// Defaults to true if 'fuzzy' is enabled. + pub fn with_reset_cursor(mut self, reset_cursor: bool) -> Self { + self.reset_cursor = reset_cursor; + self + } + /// Sets the provided color theme to this prompt. /// /// Note: The default render config considers if the NO_COLOR environment variable diff --git a/inquire/src/prompts/multiselect/prompt.rs b/inquire/src/prompts/multiselect/prompt.rs index 97858018..9a4b8e23 100644 --- a/inquire/src/prompts/multiselect/prompt.rs +++ b/inquire/src/prompts/multiselect/prompt.rs @@ -267,7 +267,9 @@ where options.sort_unstable_by_key(|(_idx, score)| Reverse(*score)); self.scored_options = options.into_iter().map(|(idx, _)| idx).collect(); - if self.scored_options.len() <= self.cursor_index { + if self.config.reset_cursor { + let _ = self.update_cursor_position(0); + } else if self.scored_options.len() <= self.cursor_index { let _ = self .update_cursor_position(self.scored_options.len().saturating_sub(1)); } diff --git a/inquire/src/prompts/select/config.rs b/inquire/src/prompts/select/config.rs index 12ccfbda..7e8ea697 100644 --- a/inquire/src/prompts/select/config.rs +++ b/inquire/src/prompts/select/config.rs @@ -7,6 +7,8 @@ pub struct SelectConfig { pub vim_mode: bool, /// Page size of the list of options. pub page_size: usize, + /// Whether to reset the cursor to the first option on filter input change. + pub reset_cursor: bool, } impl From<&Select<'_, T>> for SelectConfig { @@ -14,6 +16,7 @@ impl From<&Select<'_, T>> for SelectConfig { Self { vim_mode: value.vim_mode, page_size: value.page_size, + reset_cursor: value.reset_cursor, } } } diff --git a/inquire/src/prompts/select/mod.rs b/inquire/src/prompts/select/mod.rs index a406182c..c7e66755 100644 --- a/inquire/src/prompts/select/mod.rs +++ b/inquire/src/prompts/select/mod.rs @@ -89,6 +89,10 @@ pub struct Select<'a, T> { /// Starting cursor index of the selection. pub starting_cursor: usize, + /// Reset cursor position to first option on filter input change. + /// Defaults to true when 'fuzzy' is enabled. + pub reset_cursor: bool, + /// Function called with the current user input to score the provided /// options. pub scorer: Scorer<'a, T>, @@ -174,6 +178,10 @@ where /// Default starting cursor index. pub const DEFAULT_STARTING_CURSOR: usize = 0; + /// Default cursor behaviour on filter input change. + /// Defaults to true if 'fuzzy' is enabled. + pub const DEFAULT_RESET_CURSOR: bool = cfg!(feature = "fuzzy"); + /// Default help message. pub const DEFAULT_HELP_MESSAGE: Option<&'a str> = Some("↑↓ to move, enter to select, type to filter"); @@ -187,6 +195,7 @@ where page_size: Self::DEFAULT_PAGE_SIZE, vim_mode: Self::DEFAULT_VIM_MODE, starting_cursor: Self::DEFAULT_STARTING_CURSOR, + reset_cursor: Self::DEFAULT_RESET_CURSOR, scorer: Self::DEFAULT_SCORER, formatter: Self::DEFAULT_FORMATTER, render_config: get_configuration(), @@ -235,6 +244,14 @@ where self } + /// Sets the reset_cursor behaviour. + /// Will reset cursor to first option on filter input change. + /// Defaults to true if 'fuzzy' is enabled. + pub fn with_reset_cursor(mut self, reset_cursor: bool) -> Self { + self.reset_cursor = reset_cursor; + self + } + /// Sets the provided color theme to this prompt. /// /// Note: The default render config considers if the NO_COLOR environment variable diff --git a/inquire/src/prompts/select/prompt.rs b/inquire/src/prompts/select/prompt.rs index a9a61f10..c43e4e4d 100644 --- a/inquire/src/prompts/select/prompt.rs +++ b/inquire/src/prompts/select/prompt.rs @@ -178,7 +178,9 @@ where options.sort_unstable_by_key(|(_idx, score)| Reverse(*score)); self.scored_options = options.into_iter().map(|(idx, _)| idx).collect(); - if self.scored_options.len() <= self.cursor_index { + if self.config.reset_cursor { + let _ = self.update_cursor_position(0); + } else if self.scored_options.len() <= self.cursor_index { let _ = self .update_cursor_position(self.scored_options.len().saturating_sub(1)); } From c40cf3dbdb7b032c1a5fa9d21b63eec72d8c98ef Mon Sep 17 00:00:00 2001 From: ste Date: Sun, 17 Sep 2023 18:56:59 +0200 Subject: [PATCH 08/11] Undo rename of InputAction::FilterInput --- inquire/src/prompts/multiselect/action.rs | 4 ++-- inquire/src/prompts/multiselect/prompt.rs | 2 +- inquire/src/prompts/select/action.rs | 4 ++-- inquire/src/prompts/select/prompt.rs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/inquire/src/prompts/multiselect/action.rs b/inquire/src/prompts/multiselect/action.rs index 4869581c..87f15000 100644 --- a/inquire/src/prompts/multiselect/action.rs +++ b/inquire/src/prompts/multiselect/action.rs @@ -9,7 +9,7 @@ use super::config::MultiSelectConfig; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum MultiSelectPromptAction { /// Action on the value text input handler. - ScoreInput(InputAction), + FilterInput(InputAction), /// Moves the cursor to the option above. MoveUp, /// Moves the cursor to the option below. @@ -59,7 +59,7 @@ impl InnerAction for MultiSelectPromptAction { Key::Right(KeyModifiers::NONE) => Self::SelectAll, Key::Left(KeyModifiers::NONE) => Self::ClearSelections, key => match InputAction::from_key(key, &()) { - Some(action) => Self::ScoreInput(action), + Some(action) => Self::FilterInput(action), None => return None, }, }; diff --git a/inquire/src/prompts/multiselect/prompt.rs b/inquire/src/prompts/multiselect/prompt.rs index 9a4b8e23..a2699145 100644 --- a/inquire/src/prompts/multiselect/prompt.rs +++ b/inquire/src/prompts/multiselect/prompt.rs @@ -259,7 +259,7 @@ where ActionResult::NeedsRedraw } - MultiSelectPromptAction::ScoreInput(input_action) => { + MultiSelectPromptAction::FilterInput(input_action) => { let result = self.input.handle(input_action); if let InputActionResult::ContentChanged = result { diff --git a/inquire/src/prompts/select/action.rs b/inquire/src/prompts/select/action.rs index e8145e0c..41c777c4 100644 --- a/inquire/src/prompts/select/action.rs +++ b/inquire/src/prompts/select/action.rs @@ -9,7 +9,7 @@ use super::config::SelectConfig; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum SelectPromptAction { /// Action on the value text input handler. - ScoreInput(InputAction), + FilterInput(InputAction), /// Moves the cursor to the option above. MoveUp, /// Moves the cursor to the option below. @@ -50,7 +50,7 @@ impl InnerAction for SelectPromptAction { Key::End => Self::MoveToEnd, key => match InputAction::from_key(key, &()) { - Some(action) => Self::ScoreInput(action), + Some(action) => Self::FilterInput(action), None => return None, }, }; diff --git a/inquire/src/prompts/select/prompt.rs b/inquire/src/prompts/select/prompt.rs index c43e4e4d..fa8f53cf 100644 --- a/inquire/src/prompts/select/prompt.rs +++ b/inquire/src/prompts/select/prompt.rs @@ -170,7 +170,7 @@ where SelectPromptAction::PageDown => self.move_cursor_down(self.config.page_size, false), SelectPromptAction::MoveToStart => self.move_cursor_up(usize::MAX, false), SelectPromptAction::MoveToEnd => self.move_cursor_down(usize::MAX, false), - SelectPromptAction::ScoreInput(input_action) => { + SelectPromptAction::FilterInput(input_action) => { let result = self.input.handle(input_action); if let InputActionResult::ContentChanged = result { From 7d16745e4a3c631e91a112979aff047f40aa0569 Mon Sep 17 00:00:00 2001 From: ste Date: Sun, 17 Sep 2023 19:12:24 +0200 Subject: [PATCH 09/11] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a7f84bf..6b6dcc5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ - Add strict clippy lints to improve code consistency and readability. - Expand workflow clippy task to lint all-features in workspace. - Add docs badge to readme. +- **Breaking** The Select and Multiselect Filter now scores input and is now expected to return an `Option`, making it possible to order/rank the list of options. [#176](https://github.com/mikaelmello/inquire/pull/176) + `None`: Will not be displayed in the list of options. + `Some(score)`: score determines the order of options, higher score, higher on the list of options. +- Implement fuzzy search as default on Select and MultiSelect prompts. [#176](https://github.com/mikaelmello/inquire/pull/176) +- Add new option on Select/MultiSelect prompts to reset selection to the first item on filter input changes. [#176](https://github.com/mikaelmello/inquire/pull/176) ### Fixes @@ -26,6 +31,7 @@ - Raised MSRV to 1.63 due to requirements in downstream dependencies. - MSRV is now explicitly set in the package definition. - Replaced `lazy_static` with `once_cell` as `once_cell::sync::Lazy` is being standardized and `lazy_static` is not actively maintained anymore. +- Added `fuzzy-matcher` as an optional dependency for fuzzy filtering in Select and MultiSelect prompts [#176](https://github.com/mikaelmello/inquire/pull/176) ## [0.6.2] - 2023-05-07 From 2d1e0e34fe64ffb71262a1d4fb5963892a5f3269 Mon Sep 17 00:00:00 2001 From: ste Date: Sun, 1 Oct 2023 12:52:22 +0200 Subject: [PATCH 10/11] (M)Select: Enable selection reset by default in all cases. --- inquire/src/prompts/multiselect/mod.rs | 2 +- inquire/src/prompts/select/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/inquire/src/prompts/multiselect/mod.rs b/inquire/src/prompts/multiselect/mod.rs index aaf37493..f2e96459 100644 --- a/inquire/src/prompts/multiselect/mod.rs +++ b/inquire/src/prompts/multiselect/mod.rs @@ -195,7 +195,7 @@ where /// Default cursor behaviour on filter input change. /// Defaults to true if 'fuzzy' is enabled. - pub const DEFAULT_RESET_CURSOR: bool = cfg!(feature = "fuzzy"); + pub const DEFAULT_RESET_CURSOR: bool = true; /// Default behavior of keeping or cleaning the current filter value. pub const DEFAULT_KEEP_FILTER: bool = true; diff --git a/inquire/src/prompts/select/mod.rs b/inquire/src/prompts/select/mod.rs index c7e66755..6614859c 100644 --- a/inquire/src/prompts/select/mod.rs +++ b/inquire/src/prompts/select/mod.rs @@ -180,7 +180,7 @@ where /// Default cursor behaviour on filter input change. /// Defaults to true if 'fuzzy' is enabled. - pub const DEFAULT_RESET_CURSOR: bool = cfg!(feature = "fuzzy"); + pub const DEFAULT_RESET_CURSOR: bool = true; /// Default help message. pub const DEFAULT_HELP_MESSAGE: Option<&'a str> = From 0dfa9abebaf0b045857dbe34022a083d41ad0ae0 Mon Sep 17 00:00:00 2001 From: ste Date: Sun, 1 Oct 2023 14:44:06 +0200 Subject: [PATCH 11/11] Update RESET_CURSOR Doc comments as well --- inquire/src/prompts/multiselect/mod.rs | 6 +++--- inquire/src/prompts/select/mod.rs | 6 +++--- inquire/src/type_aliases.rs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/inquire/src/prompts/multiselect/mod.rs b/inquire/src/prompts/multiselect/mod.rs index f2e96459..e59c0a69 100644 --- a/inquire/src/prompts/multiselect/mod.rs +++ b/inquire/src/prompts/multiselect/mod.rs @@ -84,7 +84,7 @@ pub struct MultiSelect<'a, T> { pub starting_cursor: usize, /// Reset cursor position to first option on filter input change. - /// Defaults to true when 'fuzzy' is enabled. + /// Defaults to true. pub reset_cursor: bool, /// Function called with the current user input to score the provided @@ -194,7 +194,7 @@ where pub const DEFAULT_STARTING_CURSOR: usize = 0; /// Default cursor behaviour on filter input change. - /// Defaults to true if 'fuzzy' is enabled. + /// Defaults to true. pub const DEFAULT_RESET_CURSOR: bool = true; /// Default behavior of keeping or cleaning the current filter value. @@ -304,7 +304,7 @@ where /// Sets the reset_cursor behaviour. /// Will reset cursor to first option on filter input change. - /// Defaults to true if 'fuzzy' is enabled. + /// Defaults to true. pub fn with_reset_cursor(mut self, reset_cursor: bool) -> Self { self.reset_cursor = reset_cursor; self diff --git a/inquire/src/prompts/select/mod.rs b/inquire/src/prompts/select/mod.rs index 6614859c..43d0164f 100644 --- a/inquire/src/prompts/select/mod.rs +++ b/inquire/src/prompts/select/mod.rs @@ -90,7 +90,7 @@ pub struct Select<'a, T> { pub starting_cursor: usize, /// Reset cursor position to first option on filter input change. - /// Defaults to true when 'fuzzy' is enabled. + /// Defaults to true. pub reset_cursor: bool, /// Function called with the current user input to score the provided @@ -179,7 +179,7 @@ where pub const DEFAULT_STARTING_CURSOR: usize = 0; /// Default cursor behaviour on filter input change. - /// Defaults to true if 'fuzzy' is enabled. + /// Defaults to true. pub const DEFAULT_RESET_CURSOR: bool = true; /// Default help message. @@ -246,7 +246,7 @@ where /// Sets the reset_cursor behaviour. /// Will reset cursor to first option on filter input change. - /// Defaults to true if 'fuzzy' is enabled. + /// Defaults to true. pub fn with_reset_cursor(mut self, reset_cursor: bool) -> Self { self.reset_cursor = reset_cursor; self diff --git a/inquire/src/type_aliases.rs b/inquire/src/type_aliases.rs index 0469bcbe..bcc4e7e7 100644 --- a/inquire/src/type_aliases.rs +++ b/inquire/src/type_aliases.rs @@ -46,7 +46,7 @@ use crate::error::CustomUserError; /// /// /// -/// Default implementation for fuzzy search +/// Default implementation for fuzzy search (almost) ///``` /// use inquire::type_aliases::Scorer; /// use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};