use unicode_segmentation::UnicodeSegmentation;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
pub fn normalize_query_string(s: &mut String) {
*s = s
.chars()
.filter_map(normalize_char)
.map(|(ch, _)| ch)
.collect();
}
#[inline]
fn normalize_char(ch: char) -> Option<(char, usize)> {
match ch {
'\n' | '\t' => Some((' ', 1)),
ch => ch.width().map(|w| (ch, w)),
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum Edit {
Insert(char),
Backspace,
Delete,
Paste(String),
Left,
WordLeft,
Right,
WordRight,
ToStart,
ToEnd,
ClearBefore,
ClearAfter,
}
#[derive(Debug, PartialEq, Eq)]
enum CursorMovement {
Left,
WordLeft,
Right,
WordRight,
ToStart,
ToEnd,
}
#[derive(Debug)]
pub struct EditableString {
contents: String,
offset: usize,
screen_offset: u16,
width: u16,
left_padding: u16,
right_padding: u16,
}
impl EditableString {
pub fn new(width: u16, padding: u16) -> Self {
let prompt_padding = padding.min(width.saturating_sub(1) / 2);
Self {
contents: String::new(),
offset: 0,
screen_offset: 0,
width,
left_padding: prompt_padding,
right_padding: prompt_padding,
}
}
pub fn view(&self) -> (&str, u16) {
let mut left_indices = self.contents[..self.offset].grapheme_indices(true).rev();
let mut total_left_width = 0;
let (left_offset, extra) = loop {
match left_indices.next() {
Some((offset, grapheme)) => {
total_left_width += grapheme.width();
if total_left_width >= self.screen_offset.into() {
let extra = (total_left_width - self.screen_offset as usize) as u16;
break (
offset
+ if total_left_width == self.screen_offset.into() {
0
} else {
grapheme.len()
},
extra,
);
}
}
None => break (0, 0),
}
};
let mut right_indices = self.contents[self.offset..].grapheme_indices(true);
let mut total_right_width = 0;
let max_right_width = self.width - self.screen_offset;
let right_offset = loop {
match right_indices.next() {
Some((offset, grapheme)) => {
total_right_width += grapheme.width();
if total_right_width > max_right_width as usize {
break self.offset + offset;
}
}
None => break self.contents.len(),
}
};
(&self.contents[left_offset..right_offset], extra)
}
pub fn resize(&mut self, width: u16, padding: u16) {
let prompt_padding = padding.min(width.saturating_sub(1) / 2);
self.left_padding = prompt_padding;
self.right_padding = prompt_padding;
self.width = width;
self.screen_offset = self.screen_offset.min(width - prompt_padding);
}
pub fn screen_offset(&self) -> u16 {
self.screen_offset
}
pub fn contents(&self) -> &str {
&self.contents
}
pub fn set_prompt<Q: Into<String>>(&mut self, prompt: Q) {
self.contents = prompt.into();
self.offset = self.contents.len();
}
fn increase_by_width(&mut self, width: usize) {
self.screen_offset = self
.screen_offset
.saturating_add(width.try_into().unwrap_or(u16::MAX))
.min(self.width - self.right_padding);
}
fn insert_char(&mut self, ch: char, w: usize) -> bool {
self.contents.insert(self.offset, ch);
self.increase_by_width(w);
self.offset += ch.len_utf8();
true
}
fn insert(&mut self, string: &str) -> bool {
self.contents.insert_str(self.offset, string);
self.increase_by_width(string.width());
self.offset += string.len();
true
}
#[inline]
fn move_left(&self, width: usize) -> u16 {
let mut total_left_width = 0;
let mut graphemes = self.contents[..self.offset].graphemes(true).rev();
let left_padding = loop {
match graphemes.next() {
Some(g) => {
total_left_width += g.width();
if total_left_width >= self.left_padding as usize {
break self.left_padding;
}
}
None => {
break total_left_width as u16;
}
}
};
self.screen_offset
.saturating_sub(width.try_into().unwrap_or(u16::MAX))
.max(left_padding)
}
#[inline]
#[allow(clippy::needless_pass_by_value)]
fn move_cursor(&mut self, cm: CursorMovement) -> bool {
match cm {
CursorMovement::Left => {
match self.contents[..self.offset]
.grapheme_indices(true)
.next_back()
{
Some((new_offset, gp)) => {
self.offset = new_offset;
self.screen_offset = self.move_left(gp.width());
true
}
None => false,
}
}
CursorMovement::WordLeft => {
match self.contents[..self.offset]
.unicode_word_indices()
.next_back()
{
Some((new_offset, _)) => {
let step_width = self.contents[new_offset..self.offset].width();
self.offset = new_offset;
self.screen_offset = self.move_left(step_width);
true
}
None => false,
}
}
CursorMovement::Right => match self.contents[self.offset..].graphemes(true).next() {
Some(gp) => {
self.offset += gp.len();
self.increase_by_width(gp.width());
true
}
None => false,
},
CursorMovement::WordRight => {
let mut word_indices = self.contents[self.offset..].unicode_word_indices();
if word_indices.next().is_some() {
let next_offset = word_indices
.next()
.map(|(s, _)| self.offset + s)
.unwrap_or(self.contents.len());
let step_width = self.contents[self.offset..next_offset].width();
self.offset = next_offset;
self.increase_by_width(step_width);
true
} else {
false
}
}
CursorMovement::ToStart => {
if self.offset == 0 {
false
} else {
self.offset = 0;
self.screen_offset = 0;
true
}
}
CursorMovement::ToEnd => {
if self.offset == self.contents.len() {
false
} else {
let max_offset = self.width - self.right_padding;
for gp in self.contents[self.offset..].graphemes(true) {
self.screen_offset = self
.screen_offset
.saturating_add(gp.width().try_into().unwrap_or(u16::MAX));
if self.screen_offset >= max_offset {
self.screen_offset = max_offset;
break;
}
}
self.offset = self.contents.len();
true
}
}
}
}
pub fn edit(&mut self, e: Edit) -> bool {
match e {
Edit::Left => self.move_cursor(CursorMovement::Left),
Edit::WordLeft => self.move_cursor(CursorMovement::WordLeft),
Edit::Right => self.move_cursor(CursorMovement::Right),
Edit::WordRight => self.move_cursor(CursorMovement::WordRight),
Edit::ToStart => self.move_cursor(CursorMovement::ToStart),
Edit::ToEnd => self.move_cursor(CursorMovement::ToEnd),
Edit::Insert(ch) => {
if let Some((ch, w)) = normalize_char(ch) {
self.insert_char(ch, w)
} else {
false
}
}
Edit::Paste(mut s) => {
normalize_query_string(&mut s);
self.insert(&s)
}
Edit::Backspace => {
let delete_until = self.offset;
if self.move_cursor(CursorMovement::Left) {
self.contents.replace_range(self.offset..delete_until, "");
true
} else {
false
}
}
Edit::ClearBefore => {
if self.offset == 0 {
false
} else {
self.contents.replace_range(..self.offset, "");
self.offset = 0;
self.screen_offset = 0;
true
}
}
Edit::Delete => match self.contents[self.offset..].graphemes(true).next() {
Some(next) => {
self.contents
.replace_range(self.offset..self.offset + next.len(), "");
true
}
None => false,
},
Edit::ClearAfter => {
if self.offset == self.contents.len() {
false
} else {
self.contents.truncate(self.offset);
true
}
}
}
}
fn no_trailing_escape(&self) -> bool {
(self
.contents
.bytes()
.rev()
.take_while(|ch| *ch == b'\\')
.count()
% 2)
== 0
}
pub fn is_appending(&self) -> bool {
self.offset == self.contents.len() && self.no_trailing_escape()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_layout() {
let mut editable = EditableString::new(6, 2);
editable.edit(Edit::Insert('a'));
assert_eq!(editable.screen_offset, 1);
editable.edit(Edit::Insert('A'));
assert_eq!(editable.screen_offset, 3);
editable.edit(Edit::Insert('B'));
assert_eq!(editable.screen_offset, 4);
let mut editable = EditableString::new(6, 2);
editable.edit(Edit::Paste("AaA".to_owned()));
assert_eq!(editable.screen_offset, 4);
let mut editable = EditableString::new(6, 2);
editable.edit(Edit::Paste("abc".to_owned()));
assert_eq!(editable.screen_offset, 3);
editable.edit(Edit::Paste("ab".to_owned()));
assert_eq!(editable.screen_offset, 4);
editable.edit(Edit::Left);
assert_eq!(editable.screen_offset, 3);
editable.edit(Edit::Left);
assert_eq!(editable.screen_offset, 2);
editable.edit(Edit::Left);
assert_eq!(editable.screen_offset, 2);
editable.edit(Edit::Left);
assert_eq!(editable.screen_offset, 1);
editable.edit(Edit::Left);
assert_eq!(editable.screen_offset, 0);
let mut editable = EditableString::new(7, 2);
editable.edit(Edit::Paste("AAAAA".to_owned()));
editable.edit(Edit::ToStart);
assert_eq!(editable.screen_offset, 0);
editable.edit(Edit::Right);
assert_eq!(editable.screen_offset, 2);
editable.edit(Edit::Right);
assert_eq!(editable.screen_offset, 4);
editable.edit(Edit::Right);
assert_eq!(editable.screen_offset, 5);
editable.edit(Edit::Right);
assert_eq!(editable.screen_offset, 5);
editable.edit(Edit::Left);
assert_eq!(editable.screen_offset, 3);
editable.edit(Edit::Left);
assert_eq!(editable.screen_offset, 2);
editable.edit(Edit::Left);
assert_eq!(editable.screen_offset, 2);
editable.edit(Edit::Left);
assert_eq!(editable.screen_offset, 0);
let mut editable = EditableString::new(7, 2);
editable.edit(Edit::Paste("abc".to_owned()));
editable.edit(Edit::ToStart);
editable.edit(Edit::ToEnd);
assert_eq!(editable.screen_offset, 3);
editable.edit(Edit::Paste("defghi".to_owned()));
editable.edit(Edit::ToStart);
editable.edit(Edit::ToEnd);
assert_eq!(editable.screen_offset, 5);
}
#[test]
fn test_view() {
let mut editable = EditableString::new(7, 2);
editable.edit(Edit::Paste("abc".to_owned()));
assert_eq!(editable.view(), ("abc", 0));
let mut editable = EditableString::new(6, 1);
editable.edit(Edit::Paste("AAAAAA".to_owned()));
assert_eq!(editable.view(), ("AA", 1));
let mut editable = EditableString::new(7, 2);
editable.edit(Edit::Paste("AAAA".to_owned()));
assert_eq!(editable.view(), ("AA", 1));
editable.edit(Edit::Left);
assert_eq!(editable.view(), ("AA", 1));
editable.edit(Edit::Left);
assert_eq!(editable.view(), ("AAA", 0));
let mut editable = EditableString::new(7, 2);
editable.edit(Edit::Paste("012345678".to_owned()));
editable.edit(Edit::ToStart);
assert_eq!(editable.view(), ("0123456", 0));
let mut editable = EditableString::new(7, 2);
editable.edit(Edit::Paste("012345A".to_owned()));
editable.edit(Edit::ToStart);
assert_eq!(editable.view(), ("012345", 0));
let mut editable = EditableString::new(4, 1);
editable.edit(Edit::Paste("01234567".to_owned()));
assert_eq!(editable.view(), ("567", 0));
editable.edit(Edit::Left);
assert_eq!(editable.view(), ("567", 0));
editable.edit(Edit::Left);
assert_eq!(editable.view(), ("567", 0));
editable.edit(Edit::Left);
assert_eq!(editable.view(), ("4567", 0));
editable.edit(Edit::Left);
assert_eq!(editable.view(), ("3456", 0));
editable.edit(Edit::Left);
assert_eq!(editable.view(), ("2345", 0));
editable.edit(Edit::Right);
assert_eq!(editable.view(), ("2345", 0));
editable.edit(Edit::Right);
assert_eq!(editable.view(), ("2345", 0));
editable.edit(Edit::Right);
assert_eq!(editable.view(), ("3456", 0));
}
#[test]
fn test_word_movement() {
let mut editable = EditableString::new(100, 2);
editable.edit(Edit::Paste("one two".to_owned()));
editable.edit(Edit::WordLeft);
editable.edit(Edit::WordLeft);
assert_eq!(editable.screen_offset, 0);
editable.edit(Edit::WordRight);
assert_eq!(editable.screen_offset, 4);
editable.edit(Edit::WordRight);
assert_eq!(editable.screen_offset, 7);
editable.edit(Edit::WordRight);
assert_eq!(editable.screen_offset, 7);
}
#[test]
fn test_clear() {
let mut editable = EditableString::new(7, 2);
editable.edit(Edit::Paste("Abcde".to_owned()));
editable.edit(Edit::ToStart);
editable.edit(Edit::Right);
editable.edit(Edit::Right);
editable.edit(Edit::ClearAfter);
assert_eq!(editable.contents, "Ab");
editable.edit(Edit::Insert('c'));
editable.edit(Edit::Left);
editable.edit(Edit::ClearBefore);
assert_eq!(editable.contents, "c");
}
#[test]
fn test_delete() {
let mut editable = EditableString::new(7, 2);
editable.edit(Edit::Paste("Ab".to_owned()));
editable.edit(Edit::Backspace);
assert_eq!(editable.contents, "A");
assert_eq!(editable.screen_offset, 2);
editable.edit(Edit::Backspace);
assert_eq!(editable.contents, "");
assert_eq!(editable.screen_offset, 0);
}
#[test]
fn test_normalize_query() {
let mut s = "a\nb".to_owned();
normalize_query_string(&mut s);
assert_eq!(s, "a b");
let mut s = "o\no".to_owned();
normalize_query_string(&mut s);
assert_eq!(s, "o o");
let mut s = "a\n\u{07}o".to_owned();
normalize_query_string(&mut s);
assert_eq!(s, "a o");
}
#[test]
fn test_editable() {
let mut editable = EditableString::new(3, 1);
for e in [
Edit::Insert('a'),
Edit::Left,
Edit::Insert('b'),
Edit::ToEnd,
Edit::Insert('c'),
Edit::ToStart,
Edit::Insert('d'),
Edit::Left,
Edit::Left,
Edit::Right,
Edit::Insert('e'),
] {
editable.edit(e);
}
assert_eq!(editable.contents, "debac");
let mut editable = EditableString::new(3, 1);
for e in [
Edit::Insert('a'),
Edit::Insert('b'),
Edit::Insert('c'),
Edit::Insert('d'),
Edit::Left,
Edit::Insert('1'),
Edit::Insert('2'),
Edit::Insert('3'),
Edit::ToStart,
Edit::Backspace,
Edit::Insert('4'),
Edit::ToEnd,
Edit::Backspace,
Edit::Left,
Edit::Delete,
] {
editable.edit(e);
}
assert_eq!(editable.contents, "4abc12");
}
#[test]
fn test_editable_unicode() {
let mut editable = EditableString::new(3, 1);
for e in [
Edit::Paste("दे".to_owned()),
Edit::Left,
Edit::Insert('a'),
Edit::ToEnd,
Edit::Insert('A'),
] {
editable.edit(e);
}
assert_eq!(editable.contents, "aदेA");
for e in [
Edit::ToStart,
Edit::Right,
Edit::ToEnd,
Edit::Left,
Edit::Backspace,
] {
editable.edit(e);
}
assert_eq!(editable.contents, "aA");
}
}