Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix panic in fuzzy-select when using non-ASCII characters #245

Merged
merged 3 commits into from Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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

Expand All @@ -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

Expand Down
33 changes: 22 additions & 11 deletions src/prompts/fuzzy_select.rs
Expand Up @@ -195,7 +195,7 @@ impl FuzzySelect<'_> {

fn _interact_on(self, term: &Term, allow_quit: bool) -> Result<Option<usize>> {
// 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);
Expand Down Expand Up @@ -224,8 +224,15 @@ impl FuzzySelect<'_> {
let mut vim_mode = false;

loop {
let mut byte_indices = search_term
.char_indices()
.map(|(index, _)| index)
.collect::<Vec<_>>();

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
Expand Down Expand Up @@ -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 =>
stormshield-kg marked this conversation as resolved.
Show resolved Hide resolved
{
position += 1;
cursor += 1;
term.flush()?;
}
(Key::Enter, Some(sel), _) if !filtered_list.is_empty() => {
Expand All @@ -331,14 +338,18 @@ 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::Del, _, _) if cursor < byte_indices.len() - 1 => {
stormshield-kg marked this conversation as resolved.
Show resolved Hide resolved
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;
Expand Down
27 changes: 10 additions & 17 deletions src/theme/colorful.rs
Expand Up @@ -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}",)
}
}
15 changes: 4 additions & 11 deletions src/theme/mod.rs
Expand Up @@ -253,20 +253,13 @@ pub trait Theme {
f: &mut dyn fmt::Write,
prompt: &str,
search_term: &str,
cursor_pos: usize,
bytes_pos: usize,
stormshield-kg marked this conversation as resolved.
Show resolved Hide resolved
) -> 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}")
}
}