Skip to content
This repository has been archived by the owner on Jul 24, 2024. It is now read-only.

Commit

Permalink
fix: Fix prompt for multi-byte characters.
Browse files Browse the repository at this point in the history
Before this contribution, entering characters that consists of multiple
bytes lead to undesired behaviour such as panics.
This results from the position previous position handling inside the
prompt.
The String::len() returns the size of the string in bytes. Since a
single unicode scalar value may consist of multiple bytes, this is not
useful to determine the position inside the prompt.
The panic occured in the State::push() method and resulted from the
attempt to insert at a byte index that does not represent a character
boundary.
See:
https://doc.rust-lang.org/stable/std/primitive.str.html#method.len
https://doc.rust-lang.org/stable/std/string/struct.String.html#method.insert
  • Loading branch information
frederictobiasc committed Apr 3, 2024
1 parent 589e848 commit 46e8849
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 10 deletions.
20 changes: 14 additions & 6 deletions src/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,23 +114,23 @@ pub trait State {

fn delete(&mut self) {
let position = self.position();
if position == self.value().len() {
if position == self.value().chars().count() {
return;
}
self.value_mut().remove(position);
}

fn backspace(&mut self) {
let position = self.position().saturating_sub(1);
if position == self.value().len() {
if position == self.value().chars().count() {
return;
}
*self.position_mut() = position;
self.value_mut().remove(position);
}

fn move_right(&mut self) {
if self.position() == self.value().len() {
if self.position() == self.value().chars().count() {
return;
}
*self.position_mut() = self.position().saturating_add(1);
Expand All @@ -141,7 +141,7 @@ pub trait State {
}

fn move_end(&mut self) {
*self.position_mut() = self.value().len();
*self.position_mut() = self.value().chars().count();
}

fn move_start(&mut self) {
Expand All @@ -159,8 +159,16 @@ pub trait State {
}

fn push(&mut self, c: char) {
let position = self.position();
self.value_mut().insert(position, c);
if self.position() == self.value().chars().count() {
self.value_mut().push(c);
} else {
*self.value_mut() = self
.value()
.chars()
.take(self.position())
.chain(std::iter::once(c).chain(self.value().chars().skip(self.position())))
.collect();
}
*self.position_mut() = self.position().saturating_add(1);
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/text_prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ impl TextRenderStyle {
pub fn render(&self, state: &TextState) -> String {
match self {
Self::Default => state.value().to_string(),
Self::Password => "*".repeat(state.value().len()),
Self::Password => "*".repeat(state.value().chars().count()),
Self::Invisible => String::new(),
}
}
Expand Down Expand Up @@ -91,7 +91,7 @@ impl<'a> StatefulWidget for TextPrompt<'a> {
let width = area.width as usize;
let height = area.height as usize;
let value = self.render_style.render(state);
let value_length = value.len();
let value_length = value.chars().count();

let line = Line::from(vec![
state.status().symbol(),
Expand All @@ -100,7 +100,7 @@ impl<'a> StatefulWidget for TextPrompt<'a> {
" › ".cyan().dim(),
Span::raw(value),
]);
let prompt_length = line.width() - value_length;
let prompt_length = line.to_string().chars().count() - value_length;
let lines = wrap(line, width).take(height).collect_vec();

// constrain the position to the area
Expand Down
11 changes: 10 additions & 1 deletion src/text_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,13 @@ impl State for TextState<'_> {
}

#[cfg(test)]
mod tests {}
mod tests {
use crate::{State, TextState};

#[test]
fn multi_byte_characters() {
let mut test = TextState::new();
test.push('Ö');
test.push('a');
}
}

0 comments on commit 46e8849

Please sign in to comment.