Skip to content

Commit

Permalink
FIX: use graphemes instead of stering for readline to render emoji an…
Browse files Browse the repository at this point in the history
…d special chars
  • Loading branch information
ynqa committed Feb 26, 2024
1 parent c402f78 commit 794bb1d
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 44 deletions.
2 changes: 1 addition & 1 deletion src/core/checkbox/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ impl Renderable for Renderer {
} else {
format!(
"[{}] {}",
" ".repeat(Grapheme::new(self.mark).width()),
" ".repeat(Grapheme::from(self.mark).width()),
item
)
}
Expand Down
54 changes: 29 additions & 25 deletions src/core/text_editor.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::core::cursor::Cursor;
use crate::{
core::cursor::Cursor,
grapheme::{Grapheme, Graphemes},
};

mod history;
pub use history::History;
Expand All @@ -13,27 +16,27 @@ pub use mode::Mode;
/// such as insert, delete, and overwrite.
/// It utilizes a cursor to navigate and manipulate the text.
#[derive(Clone)]
pub struct TextEditor(Cursor<String>);
pub struct TextEditor(Cursor<Graphemes>);

impl Default for TextEditor {
fn default() -> Self {
Self(Cursor::new(
// Set cursor
String::from(" "),
Graphemes::from(" "),
))
}
}

impl TextEditor {
/// Returns the current text including the cursor.
pub fn text(&self) -> String {
pub fn text(&self) -> Graphemes {
self.0.contents().clone()
}

/// Returns the text without the cursor.
pub fn text_without_cursor(&self) -> String {
pub fn text_without_cursor(&self) -> Graphemes {
let mut ret = self.text();
ret.pop();
ret.pop_back();
ret
}

Expand All @@ -43,26 +46,27 @@ impl TextEditor {
}

/// Masks all characters except the cursor with the specified mask character.
pub fn masking(&self, mask: char) -> String {
pub fn masking(&self, mask: char) -> Graphemes {
self.text()
.chars()
.into_iter()
.enumerate()
.map(|(i, c)| if i == self.text().len() - 1 { c } else { mask })
.collect::<String>()
.map(|(i, c)| Grapheme::from(if i == self.text().len() - 1 { c } else { mask }))
.collect::<Graphemes>()
}

/// Replaces the current text with new text and positions the cursor at the end.
pub fn replace(&mut self, new: &str) {
let mut buf = new.to_owned();
buf.push(' ');
let pos = buf.len() - 1;
*self = Self(Cursor::new_with_position(buf, pos));
*self = Self(Cursor::new_with_position(Graphemes::from(buf), pos));
}

/// Inserts a character at the current cursor position.
pub fn insert(&mut self, ch: char) {
let pos = self.position();
self.0.contents_mut().insert(pos, ch);
self.0.contents_mut().insert(pos, Grapheme::from(ch));
self.forward();
}

Expand Down Expand Up @@ -116,21 +120,21 @@ impl TextEditor {

#[cfg(test)]
mod test {
use crate::core::cursor::Cursor;
use crate::{core::cursor::Cursor, grapheme::Graphemes};

use super::TextEditor;

fn new_with_position(s: String, p: usize) -> TextEditor {
TextEditor(Cursor::new_with_position(s, p))
TextEditor(Cursor::new_with_position(Graphemes::from(s), p))
}

mod masking {
use crate::text_editor::test::new_with_position;
use crate::{grapheme::Graphemes, text_editor::test::new_with_position};

#[test]
fn test() {
let txt = new_with_position(String::from("abcde "), 0);
assert_eq!("***** ", txt.masking('*'))
assert_eq!(Graphemes::from("***** "), txt.masking('*'))
}
}

Expand All @@ -142,7 +146,7 @@ mod test {
#[test]
fn test_for_empty() {
let txt = TextEditor::default();
assert_eq!(String::from(" "), txt.text());
assert_eq!(Graphemes::from(" "), txt.text());
assert_eq!(0, txt.position());
}

Expand Down Expand Up @@ -182,7 +186,7 @@ mod test {
String::from("abc "),
0, // indicate `a`.
);
assert_eq!(String::from("abc "), txt.text());
assert_eq!(Graphemes::from("abc "), txt.text());
assert_eq!(0, txt.position());
}
}
Expand Down Expand Up @@ -322,7 +326,7 @@ mod test {
fn test_for_empty() {
let mut txt = TextEditor::default();
txt.backward();
assert_eq!(String::from(" "), txt.text());
assert_eq!(Graphemes::from(" "), txt.text());
assert_eq!(0, txt.position());
}

Expand Down Expand Up @@ -363,7 +367,7 @@ mod test {
0, // indicate `a`.
);
txt.backward();
assert_eq!(String::from("abc "), txt.text());
assert_eq!(Graphemes::from("abc "), txt.text());
assert_eq!(0, txt.position());
}
}
Expand All @@ -377,7 +381,7 @@ mod test {
fn test_for_empty() {
let mut txt = TextEditor::default();
txt.forward();
assert_eq!(String::from(" "), txt.text());
assert_eq!(Graphemes::from(" "), txt.text());
assert_eq!(0, txt.position());
}

Expand All @@ -403,7 +407,7 @@ mod test {
3, // indicate tail.
);
txt.forward();
assert_eq!(String::from("abc "), txt.text());
assert_eq!(Graphemes::from("abc "), txt.text());
assert_eq!(3, txt.position());
}

Expand Down Expand Up @@ -432,7 +436,7 @@ mod test {
fn test_for_empty() {
let mut txt = TextEditor::default();
txt.move_to_head();
assert_eq!(String::from(" "), txt.text());
assert_eq!(Graphemes::from(" "), txt.text());
assert_eq!(0, txt.position());
}

Expand Down Expand Up @@ -473,7 +477,7 @@ mod test {
0, // indicate `a`.
);
txt.move_to_head();
assert_eq!(String::from("abc "), txt.text());
assert_eq!(Graphemes::from("abc "), txt.text());
assert_eq!(0, txt.position());
}
}
Expand All @@ -487,7 +491,7 @@ mod test {
fn test_for_empty() {
let mut txt = TextEditor::default();
txt.move_to_tail();
assert_eq!(String::from(" "), txt.text());
assert_eq!(Graphemes::from(" "), txt.text());
assert_eq!(0, txt.position());
}

Expand All @@ -513,7 +517,7 @@ mod test {
3, // indicate tail.
);
txt.move_to_tail();
assert_eq!(String::from("abc "), txt.text());
assert_eq!(Graphemes::from("abc "), txt.text());
assert_eq!(3, txt.position());
}

Expand Down
9 changes: 6 additions & 3 deletions src/core/text_editor/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ impl Renderable for Renderer {
None => self.texteditor.text(),
};

let mut styled = StyledGraphemes::from_str(text, self.inactive_char_style)
let mut styled = StyledGraphemes::from_graphemes(text, self.inactive_char_style)
.apply_style_at(self.texteditor.position(), self.active_char_style);

buf.append(&mut styled);
Expand Down Expand Up @@ -164,7 +164,10 @@ impl Renderable for Renderer {
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}) => {
if let Some(new) = self.suggest.search(self.texteditor.text_without_cursor()) {
if let Some(new) = self
.suggest
.search(self.texteditor.text_without_cursor().to_string())
{
self.texteditor.replace(new)
}
}
Expand Down Expand Up @@ -192,7 +195,7 @@ impl Renderable for Renderer {

fn postrun(&mut self) {
if let Some(ref mut history) = &mut self.history {
history.insert(self.texteditor.text_without_cursor());
history.insert(self.texteditor.text_without_cursor().to_string());
}
self.texteditor = TextEditor::default();
}
Expand Down
55 changes: 45 additions & 10 deletions src/grapheme.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{
collections::VecDeque,
fmt::Debug,
fmt,
iter::FromIterator,
ops::{Deref, DerefMut},
};
Expand All @@ -12,6 +12,14 @@ use crate::Len;
mod styled;
pub use styled::{matrixify, trim, StyledGraphemes};

impl From<char> for Grapheme {
fn from(ch: char) -> Self {
Self {
ch,
width: UnicodeWidthChar::width(ch).unwrap_or(0),
}
}
}
/// Represents a single grapheme with its character and display width.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Grapheme {
Expand All @@ -20,14 +28,6 @@ pub struct Grapheme {
}

impl Grapheme {
/// Creates a new `Grapheme` from a character, calculating its display width.
pub fn new(ch: char) -> Self {
Self {
ch,
width: UnicodeWidthChar::width(ch).unwrap_or(0),
}
}

/// Returns the display width of the grapheme.
pub fn width(&self) -> usize {
self.width
Expand Down Expand Up @@ -85,7 +85,7 @@ impl Iterator for Graphemes {
impl<S: AsRef<str>> From<S> for Graphemes {
/// Creates a `Graphemes` instance from a string slice, converting each char to a `Grapheme`.
fn from(string: S) -> Self {
string.as_ref().chars().map(Grapheme::new).collect()
string.as_ref().chars().map(Grapheme::from).collect()
}
}

Expand All @@ -101,9 +101,44 @@ impl Len for Graphemes {
}
}

impl fmt::Display for Graphemes {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let string: String = self.chars().iter().collect();
write!(f, "{}", string)
}
}

impl fmt::Debug for Graphemes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for grapheme in self.iter() {
write!(f, "{}", grapheme.ch)?;
}
Ok(())
}
}

impl Graphemes {
/// Calculates the total display width of all `Grapheme` instances in the collection.
pub fn widths(&self) -> usize {
self.0.iter().map(|grapheme| grapheme.width).sum()
}

/// Returns a `Vec<char>` containing the characters of all `Grapheme` instances in the collection.
pub fn chars(&self) -> Vec<char> {
self.0.iter().map(|grapheme| grapheme.ch).collect()
}

/// Replaces the specified range with the given string.
pub fn replace_range<S: AsRef<str>>(&mut self, range: std::ops::Range<usize>, replacement: S) {
// Remove the specified range.
for _ in range.clone() {
self.0.remove(range.start);
}

// Insert the replacement at the start of the range.
let replacement_graphemes: Graphemes = replacement.as_ref().into();
for grapheme in replacement_graphemes.0.iter().rev() {
self.0.insert(range.start, grapheme.clone());
}
}
}
4 changes: 2 additions & 2 deletions src/grapheme/styled.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{
collections::VecDeque,
fmt::{self, Debug},
fmt,
ops::{Deref, DerefMut},
};

Expand Down Expand Up @@ -84,7 +84,7 @@ impl<S: AsRef<str>> From<S> for StyledGraphemes {
}
}

impl Debug for StyledGraphemes {
impl fmt::Debug for StyledGraphemes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for styled_grapheme in self.iter() {
write!(f, "{}", styled_grapheme.ch)?;
Expand Down
3 changes: 2 additions & 1 deletion src/preset/qs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@ impl QuerySelector {
.after
.borrow()
.texteditor
.text_without_cursor();
.text_without_cursor()
.to_string();

let list = filter(&query, select_state.init.listbox.items());
select_state.after.borrow_mut().listbox = Listbox::from_iter(list);
Expand Down
6 changes: 4 additions & 2 deletions src/preset/readline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ impl Readline {
.after
.borrow()
.texteditor
.text_without_cursor();
.text_without_cursor()
.to_string();

let error_message_state = renderables[2]
.as_any()
Expand Down Expand Up @@ -204,7 +205,8 @@ impl Readline {
.after
.borrow()
.texteditor
.text_without_cursor())
.text_without_cursor()
.to_string())
},
)
}
Expand Down

0 comments on commit 794bb1d

Please sign in to comment.