use super::{Cursor, View};
pub fn normalize_query_string(s: &mut String) {
*s = s.chars().filter_map(normalize_char).collect();
}
fn normalize_char(ch: char) -> Option<char> {
match ch {
'\n' | '\t' => Some(' '),
ch if ch.is_ascii_control() => None,
ch => Some(ch),
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum Edit {
Insert(char),
Delete,
Paste(String),
Left,
Right,
ToStart,
ToEnd,
}
#[derive(Debug, PartialEq, Eq)]
enum Jump {
Left(usize),
Right(usize),
ToStart,
ToEnd,
}
#[derive(Debug)]
#[allow(clippy::module_name_repetitions)]
pub struct EditableString {
contents: Vec<char>,
cursor: Cursor,
scratch: Vec<char>,
}
impl EditableString {
pub fn full_contents(&self) -> String {
self.contents.iter().collect()
}
#[allow(unused)]
#[inline]
pub fn view(&self) -> View<'_, char> {
self.cursor.view(&self.contents)
}
pub fn set_prompt(&mut self, new: &str) {
self.contents = new.chars().collect();
self.jump(Jump::ToEnd);
}
pub fn view_padded(&self, left: usize, right: usize) -> View<'_, char> {
self.cursor.view_padded(left, right, &self.contents)
}
pub fn new(width: usize) -> Self {
Self {
contents: Vec::default(),
cursor: Cursor::new(width),
scratch: Vec::new(),
}
}
pub fn resize(&mut self, width: usize) {
self.cursor.set_width(width);
}
fn no_trailing_escape(&self) -> bool {
(self
.contents
.iter()
.rev()
.take_while(|ch| **ch == '\\')
.count()
% 2)
== 0
}
pub fn is_appending(&self) -> bool {
self.cursor.index() == self.contents.len() && self.no_trailing_escape()
}
pub fn is_empty(&self) -> bool {
self.contents.is_empty()
}
#[inline]
fn shift_to(&mut self, pos: usize) -> bool {
if pos <= self.contents.len() && self.cursor.index() != pos {
self.cursor.set_index(pos);
true
} else {
false
}
}
#[inline]
#[allow(clippy::needless_pass_by_value)]
fn jump(&mut self, jm: Jump) -> bool {
match jm {
Jump::Left(dist) => self.shift_to(self.cursor.index().saturating_sub(dist)),
Jump::Right(dist) => self.shift_to(self.cursor.index().saturating_add(dist)),
Jump::ToStart => self.shift_to(0),
Jump::ToEnd => self.shift_to(self.contents.len()),
}
}
pub fn edit(&mut self, e: Edit) -> bool {
match e {
Edit::Left => self.jump(Jump::Left(1)),
Edit::Right => self.jump(Jump::Right(1)),
Edit::ToStart => self.jump(Jump::ToStart),
Edit::ToEnd => self.jump(Jump::ToEnd),
Edit::Insert(ch) => {
if let Some(ch) = normalize_char(ch) {
self.contents.insert(self.cursor.index(), ch);
self.jump(Jump::Right(1))
} else {
false
}
}
Edit::Paste(mut s) => {
if s.is_empty() {
false
} else {
normalize_query_string(&mut s);
if self.is_appending() {
self.contents.extend(s.chars());
self.jump(Jump::ToEnd);
} else {
self.scratch.clear();
self.scratch
.extend(self.contents.drain(self.cursor.index()..));
self.contents.extend(s.chars());
self.jump(Jump::Right(s.len()));
self.contents.append(&mut self.scratch);
}
true
}
}
Edit::Delete => {
let changed = self.jump(Jump::Left(1));
if changed {
self.contents.remove(self.cursor.index());
}
changed
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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 edit() {
let mut editable = EditableString::new(usize::MAX);
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::Delete,
Edit::Insert('4'),
Edit::ToEnd,
Edit::Delete,
] {
editable.edit(e);
}
assert_eq!(&editable.view().to_string(), "4abc123");
}
#[test]
fn window() {
let mut editable = EditableString::new(2);
for e in [
Edit::Insert('1'),
Edit::Insert('2'),
Edit::Insert('3'),
Edit::Insert('4'),
Edit::Left,
] {
editable.edit(e);
}
assert_eq!(editable.view().index(), 1);
editable.edit(Edit::Left);
editable.edit(Edit::Left);
assert_eq!(editable.view().index(), 0);
editable.edit(Edit::Insert('a'));
editable.edit(Edit::ToEnd);
assert_eq!(editable.view().index(), 2);
}
}