From dc230eb9b1a8db9045b114a12d750efd8eec7a74 Mon Sep 17 00:00:00 2001 From: Kevin GRONDIN Date: Wed, 6 Sep 2023 11:44:42 +0200 Subject: [PATCH 1/3] Fix panic in fuzzy-select when using non-ASCII characters --- src/prompts/fuzzy_select.rs | 29 ++++++++++++++++++----------- src/theme/colorful.rs | 27 ++++++++++----------------- src/theme/mod.rs | 15 ++++----------- 3 files changed, 32 insertions(+), 39 deletions(-) diff --git a/src/prompts/fuzzy_select.rs b/src/prompts/fuzzy_select.rs index 28dc68c2..ea2f7bcf 100644 --- a/src/prompts/fuzzy_select.rs +++ b/src/prompts/fuzzy_select.rs @@ -195,7 +195,7 @@ impl FuzzySelect<'_> { fn _interact_on(self, term: &Term, allow_quit: bool) -> Result> { // Place cursor at the end of the search term - let mut position = self.initial_text.len(); + let mut cursor = self.initial_text.chars().count(); let mut search_term = self.initial_text.to_owned(); let mut render = TermThemeRenderer::new(term, self.theme); @@ -224,8 +224,15 @@ impl FuzzySelect<'_> { let mut vim_mode = false; loop { + let mut byte_indices = search_term + .char_indices() + .map(|(index, _)| index) + .collect::>(); + + byte_indices.push(search_term.len()); + render.clear()?; - render.fuzzy_select_prompt(self.prompt.as_str(), &search_term, position)?; + render.fuzzy_select_prompt(self.prompt.as_str(), &search_term, byte_indices[cursor])?; // Maps all items to a tuple of item and its match score. let mut filtered_list = self @@ -304,14 +311,14 @@ impl FuzzySelect<'_> { } term.flush()?; } - (Key::ArrowLeft, _, _) | (Key::Char('h'), _, true) if position > 0 => { - position -= 1; + (Key::ArrowLeft, _, _) | (Key::Char('h'), _, true) if cursor > 0 => { + cursor -= 1; term.flush()?; } (Key::ArrowRight, _, _) | (Key::Char('l'), _, true) - if position < search_term.len() => + if cursor < byte_indices.len() - 1 => { - position += 1; + cursor += 1; term.flush()?; } (Key::Enter, Some(sel), _) if !filtered_list.is_empty() => { @@ -331,14 +338,14 @@ impl FuzzySelect<'_> { term.show_cursor()?; return Ok(sel_string_pos_in_items); } - (Key::Backspace, _, _) if position > 0 => { - position -= 1; - search_term.remove(position); + (Key::Backspace, _, _) if cursor > 0 => { + cursor -= 1; + search_term.remove(byte_indices[cursor]); term.flush()?; } (Key::Char(chr), _, _) if !chr.is_ascii_control() => { - search_term.insert(position, chr); - position += 1; + search_term.insert(byte_indices[cursor], chr); + cursor += 1; term.flush()?; sel = Some(0); starting_row = 0; diff --git a/src/theme/colorful.rs b/src/theme/colorful.rs index 1537a22f..0f915364 100644 --- a/src/theme/colorful.rs +++ b/src/theme/colorful.rs @@ -408,31 +408,24 @@ impl Theme for ColorfulTheme { f: &mut dyn fmt::Write, prompt: &str, search_term: &str, - cursor_pos: usize, + bytes_pos: usize, ) -> fmt::Result { if !prompt.is_empty() { write!( f, "{} {} ", - &self.prompt_prefix, + self.prompt_prefix, self.prompt_style.apply_to(prompt) )?; } - if cursor_pos < search_term.len() { - let st_head = search_term[0..cursor_pos].to_string(); - let st_tail = search_term[cursor_pos + 1..search_term.len()].to_string(); - let st_cursor = self - .fuzzy_cursor_style - .apply_to(search_term.to_string().chars().nth(cursor_pos).unwrap()); - write!( - f, - "{} {}{}{}", - &self.prompt_suffix, st_head, st_cursor, st_tail - ) - } else { - let cursor = self.fuzzy_cursor_style.apply_to(" "); - write!(f, "{} {}{}", &self.prompt_suffix, search_term, cursor) - } + let (st_head, remaining) = search_term.split_at(bytes_pos); + let mut chars = remaining.chars(); + let chr = chars.next().unwrap_or(' '); + let st_cursor = self.fuzzy_cursor_style.apply_to(chr); + let st_tail = chars.as_str(); + + let prompt_suffix = &self.prompt_suffix; + write!(f, "{prompt_suffix} {st_head}{st_cursor}{st_tail}",) } } diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 2d661f0d..d22001cf 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -253,20 +253,13 @@ pub trait Theme { f: &mut dyn fmt::Write, prompt: &str, search_term: &str, - cursor_pos: usize, + bytes_pos: usize, ) -> fmt::Result { if !prompt.is_empty() { - write!(f, "{} ", prompt)?; + write!(f, "{prompt} ")?; } - if cursor_pos < search_term.len() { - let st_head = search_term[0..cursor_pos].to_string(); - let st_tail = search_term[cursor_pos..search_term.len()].to_string(); - let st_cursor = "|".to_string(); - write!(f, "{}{}{}", st_head, st_cursor, st_tail) - } else { - let cursor = "|".to_string(); - write!(f, "{}{}", search_term, cursor) - } + let (st_head, st_tail) = search_term.split_at(bytes_pos); + write!(f, "{st_head}|{st_tail}") } } From 134d4c9127b6d412120d345e6d112733549b4fae Mon Sep 17 00:00:00 2001 From: Kevin GRONDIN Date: Mon, 11 Sep 2023 15:55:10 +0200 Subject: [PATCH 2/3] Add handling of Delete key for fuzzy-select --- src/prompts/fuzzy_select.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/prompts/fuzzy_select.rs b/src/prompts/fuzzy_select.rs index ea2f7bcf..706cd5ef 100644 --- a/src/prompts/fuzzy_select.rs +++ b/src/prompts/fuzzy_select.rs @@ -343,6 +343,10 @@ impl FuzzySelect<'_> { search_term.remove(byte_indices[cursor]); term.flush()?; } + (Key::Del, _, _) if cursor < byte_indices.len() - 1 => { + search_term.remove(byte_indices[cursor]); + term.flush()?; + } (Key::Char(chr), _, _) if !chr.is_ascii_control() => { search_term.insert(byte_indices[cursor], chr); cursor += 1; From c1db6ff3a981a461420812e051ae1bfa75fb3e9b Mon Sep 17 00:00:00 2001 From: Kevin GRONDIN Date: Mon, 11 Sep 2023 15:55:44 +0200 Subject: [PATCH 3/3] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4c4ef54..39dc1638 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ * `Input` values that are invalid are now also stored in `History` * Resolve some issues with cursor positioning in `Input` when using `utf-8` characters * Correct page is shown when default selected option is not on the first page for `Select` +* Fix panic in `FuzzySelect` when using non-ASCII characters +* Add handling of `Delete` key for `FuzzySelect` ### Breaking @@ -26,6 +28,7 @@ * Prompt interaction functions now take `self` instead of `&self` * Prompt interaction functions and other operations now return `dialouger::Result` instead of `std::io::Result` * Rename `Validator` to `InputValidator` +* The trait method `Theme::format_fuzzy_select_prompt()` now takes a byte position instead of a cursor position in order to support UTF-8. ## 0.10.4