enum Side {
Left,
Right,
}
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum InputRequest {
SetCursor(usize),
InsertChar(char),
GoToPrevChar,
GoToNextChar,
GoToPrevWord,
GoToNextWord,
GoToStart,
GoToEnd,
DeletePrevChar,
DeleteNextChar,
DeletePrevWord,
DeleteNextWord,
DeleteLine,
DeleteTillEnd,
Yank,
}
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct StateChanged {
pub value: bool,
pub cursor: bool,
}
pub type InputResponse = Option<StateChanged>;
#[derive(Default, Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Input {
value: String,
cursor: usize,
yank: String,
last_was_cut: bool,
}
impl Input {
pub fn new(value: String) -> Self {
let len = value.chars().count();
Self {
value,
cursor: len,
yank: String::new(),
last_was_cut: false,
}
}
pub fn with_value(mut self, value: String) -> Self {
self.cursor = value.chars().count();
self.value = value;
self
}
pub fn with_cursor(mut self, cursor: usize) -> Self {
self.cursor = cursor.min(self.value.chars().count());
self
}
pub fn reset(&mut self) {
self.cursor = Default::default();
self.value = Default::default();
}
pub fn value_and_reset(&mut self) -> String {
let val = self.value.clone();
self.reset();
val
}
fn add_to_yank(&mut self, deleted: String, side: Side) {
if self.last_was_cut {
match side {
Side::Left => self.yank.insert_str(0, &deleted),
Side::Right => self.yank.push_str(&deleted),
}
} else {
self.yank = deleted;
}
}
fn set_last_was_cut(&mut self, req: InputRequest) {
use InputRequest::*;
self.last_was_cut = matches!(
req,
DeleteLine | DeletePrevWord | DeleteNextWord | DeleteTillEnd
);
}
pub fn handle(&mut self, req: InputRequest) -> InputResponse {
use InputRequest::*;
let result = match req {
SetCursor(pos) => {
let pos = pos.min(self.value.chars().count());
if self.cursor == pos {
None
} else {
self.cursor = pos;
Some(StateChanged {
value: false,
cursor: true,
})
}
}
InsertChar(c) => {
if self.cursor == self.value.chars().count() {
self.value.push(c);
} else {
self.value = self
.value
.chars()
.take(self.cursor)
.chain(
std::iter::once(c)
.chain(self.value.chars().skip(self.cursor)),
)
.collect();
}
self.cursor += 1;
Some(StateChanged {
value: true,
cursor: true,
})
}
DeletePrevChar => {
if self.cursor == 0 {
None
} else {
self.cursor -= 1;
self.value = self
.value
.chars()
.enumerate()
.filter(|(i, _)| i != &self.cursor)
.map(|(_, c)| c)
.collect();
Some(StateChanged {
value: true,
cursor: true,
})
}
}
DeleteNextChar => {
if self.cursor == self.value.chars().count() {
None
} else {
self.value = self
.value
.chars()
.enumerate()
.filter(|(i, _)| i != &self.cursor)
.map(|(_, c)| c)
.collect();
Some(StateChanged {
value: true,
cursor: false,
})
}
}
GoToPrevChar => {
if self.cursor == 0 {
None
} else {
self.cursor -= 1;
Some(StateChanged {
value: false,
cursor: true,
})
}
}
GoToPrevWord => {
if self.cursor == 0 {
None
} else {
self.cursor = self
.value
.chars()
.rev()
.skip(self.value.chars().count().max(self.cursor) - self.cursor)
.skip_while(|c| !c.is_alphanumeric())
.skip_while(|c| c.is_alphanumeric())
.count();
Some(StateChanged {
value: false,
cursor: true,
})
}
}
GoToNextChar => {
if self.cursor == self.value.chars().count() {
None
} else {
self.cursor += 1;
Some(StateChanged {
value: false,
cursor: true,
})
}
}
GoToNextWord => {
if self.cursor == self.value.chars().count() {
None
} else {
self.cursor = self
.value
.chars()
.enumerate()
.skip(self.cursor)
.skip_while(|(_, c)| c.is_alphanumeric())
.find(|(_, c)| c.is_alphanumeric())
.map(|(i, _)| i)
.unwrap_or_else(|| self.value.chars().count());
Some(StateChanged {
value: false,
cursor: true,
})
}
}
DeleteLine => {
if self.value.is_empty() {
None
} else {
let side = if self.cursor == self.value.chars().count() {
Side::Left
} else {
Side::Right
};
self.add_to_yank(self.value.clone(), side);
self.value = "".into();
self.cursor = 0;
Some(StateChanged {
value: true,
cursor: true,
})
}
}
DeletePrevWord => {
if self.cursor == 0 {
None
} else {
let rev = self
.value
.chars()
.rev()
.skip(self.value.chars().count().max(self.cursor) - self.cursor)
.skip_while(|c| !c.is_alphanumeric())
.skip_while(|c| c.is_alphanumeric())
.collect::<Vec<char>>();
let rev_len = rev.len();
let deleted: String = self
.value
.chars()
.skip(rev_len)
.take(self.cursor - rev_len)
.collect();
self.add_to_yank(deleted, Side::Left);
let remaining = self.value.chars().skip(self.cursor);
self.value = rev.into_iter().rev().chain(remaining).collect();
self.cursor = rev_len;
Some(StateChanged {
value: true,
cursor: true,
})
}
}
DeleteNextWord => {
if self.cursor == self.value.chars().count() {
None
} else {
let after: Vec<_> = self
.value
.chars()
.skip(self.cursor)
.skip_while(|c| c.is_alphanumeric())
.skip_while(|c| !c.is_alphanumeric())
.collect();
let deleted_count =
self.value.chars().count() - self.cursor - after.len();
let deleted: String = self
.value
.chars()
.skip(self.cursor)
.take(deleted_count)
.collect();
self.add_to_yank(deleted, Side::Right);
self.value =
self.value.chars().take(self.cursor).chain(after).collect();
Some(StateChanged {
value: true,
cursor: false,
})
}
}
GoToStart => {
if self.cursor == 0 {
None
} else {
self.cursor = 0;
Some(StateChanged {
value: false,
cursor: true,
})
}
}
GoToEnd => {
let count = self.value.chars().count();
if self.cursor == count {
None
} else {
self.cursor = count;
Some(StateChanged {
value: false,
cursor: true,
})
}
}
DeleteTillEnd => {
let deleted: String = self.value.chars().skip(self.cursor).collect();
self.add_to_yank(deleted, Side::Right);
self.value = self.value.chars().take(self.cursor).collect();
Some(StateChanged {
value: true,
cursor: false,
})
}
Yank => {
if self.yank.is_empty() {
None
} else if self.cursor == self.value.chars().count() {
self.value.push_str(&self.yank);
self.cursor += self.yank.chars().count();
Some(StateChanged {
value: true,
cursor: true,
})
} else {
self.value = self
.value
.chars()
.take(self.cursor)
.chain(self.yank.chars())
.chain(self.value.chars().skip(self.cursor))
.collect();
self.cursor += self.yank.chars().count();
Some(StateChanged {
value: true,
cursor: true,
})
}
}
};
self.set_last_was_cut(req);
result
}
pub fn value(&self) -> &str {
self.value.as_str()
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn visual_cursor(&self) -> usize {
if self.cursor == 0 {
return 0;
}
unicode_width::UnicodeWidthStr::width(unsafe {
self.value.get_unchecked(
0..self
.value
.char_indices()
.nth(self.cursor)
.map_or_else(|| self.value.len(), |(index, _)| index),
)
})
}
pub fn visual_scroll(&self, width: usize) -> usize {
let scroll = (self.visual_cursor()).max(width) - width;
let mut uscroll = 0;
let mut chars = self.value().chars();
while uscroll < scroll {
match chars.next() {
Some(c) => {
uscroll += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
}
None => break,
}
}
uscroll
}
}
impl From<Input> for String {
fn from(input: Input) -> Self {
input.value
}
}
impl From<String> for Input {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl From<&str> for Input {
fn from(value: &str) -> Self {
Self::new(value.into())
}
}
impl std::fmt::Display for Input {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.value.fmt(f)
}
}
#[cfg(test)]
mod tests {
const TEXT: &str = "first second, third.";
use super::*;
#[test]
fn format() {
let input: Input = TEXT.into();
println!("{}", input);
println!("{}", input);
}
#[test]
fn set_cursor() {
let mut input: Input = TEXT.into();
let req = InputRequest::SetCursor(3);
let resp = input.handle(req);
assert_eq!(
resp,
Some(StateChanged {
value: false,
cursor: true,
})
);
assert_eq!(input.value(), "first second, third.");
assert_eq!(input.cursor(), 3);
let req = InputRequest::SetCursor(30);
let resp = input.handle(req);
assert_eq!(input.cursor(), TEXT.chars().count());
assert_eq!(
resp,
Some(StateChanged {
value: false,
cursor: true,
})
);
let req = InputRequest::SetCursor(TEXT.chars().count());
let resp = input.handle(req);
assert_eq!(input.cursor(), TEXT.chars().count());
assert_eq!(resp, None);
}
#[test]
fn insert_char() {
let mut input: Input = TEXT.into();
let req = InputRequest::InsertChar('x');
let resp = input.handle(req);
assert_eq!(
resp,
Some(StateChanged {
value: true,
cursor: true,
})
);
assert_eq!(input.value(), "first second, third.x");
assert_eq!(input.cursor(), TEXT.chars().count() + 1);
input.handle(req);
assert_eq!(input.value(), "first second, third.xx");
assert_eq!(input.cursor(), TEXT.chars().count() + 2);
let mut input = input.with_cursor(3);
input.handle(req);
assert_eq!(input.value(), "firxst second, third.xx");
assert_eq!(input.cursor(), 4);
input.handle(req);
assert_eq!(input.value(), "firxxst second, third.xx");
assert_eq!(input.cursor(), 5);
}
#[test]
fn go_to_prev_char() {
let mut input: Input = TEXT.into();
let req = InputRequest::GoToPrevChar;
let resp = input.handle(req);
assert_eq!(
resp,
Some(StateChanged {
value: false,
cursor: true,
})
);
assert_eq!(input.value(), "first second, third.");
assert_eq!(input.cursor(), TEXT.chars().count() - 1);
let mut input = input.with_cursor(3);
input.handle(req);
assert_eq!(input.value(), "first second, third.");
assert_eq!(input.cursor(), 2);
input.handle(req);
assert_eq!(input.value(), "first second, third.");
assert_eq!(input.cursor(), 1);
}
#[test]
fn remove_unicode_chars() {
let mut input: Input = "¡test¡".into();
let req = InputRequest::DeletePrevChar;
let resp = input.handle(req);
assert_eq!(
resp,
Some(StateChanged {
value: true,
cursor: true,
})
);
assert_eq!(input.value(), "¡test");
assert_eq!(input.cursor(), 5);
input.handle(InputRequest::GoToStart);
let req = InputRequest::DeleteNextChar;
let resp = input.handle(req);
assert_eq!(
resp,
Some(StateChanged {
value: true,
cursor: false,
})
);
assert_eq!(input.value(), "test");
assert_eq!(input.cursor(), 0);
}
#[test]
fn insert_unicode_chars() {
let mut input = Input::from("¡test¡").with_cursor(5);
let req = InputRequest::InsertChar('☆');
let resp = input.handle(req);
assert_eq!(
resp,
Some(StateChanged {
value: true,
cursor: true,
})
);
assert_eq!(input.value(), "¡test☆¡");
assert_eq!(input.cursor(), 6);
input.handle(InputRequest::GoToStart);
input.handle(InputRequest::GoToNextChar);
let req = InputRequest::InsertChar('☆');
let resp = input.handle(req);
assert_eq!(
resp,
Some(StateChanged {
value: true,
cursor: true,
})
);
assert_eq!(input.value(), "¡☆test☆¡");
assert_eq!(input.cursor(), 2);
}
#[test]
fn multispace_characters() {
let input: Input = "Hello, world!".into();
assert_eq!(input.cursor(), 13);
assert_eq!(input.visual_cursor(), 23);
assert_eq!(input.visual_scroll(6), 18);
}
#[test]
fn yank_delete_line() {
let mut input: Input = TEXT.into();
input.handle(InputRequest::DeleteLine);
assert_eq!(input.value(), "");
assert_eq!(input.cursor(), 0);
assert_eq!(input.yank, TEXT);
input.handle(InputRequest::Yank);
assert_eq!(input.value(), TEXT);
assert_eq!(input.cursor(), TEXT.chars().count());
assert_eq!(input.yank, TEXT);
}
#[test]
fn yank_delete_till_end() {
let mut input = Input::from(TEXT).with_cursor(6);
input.handle(InputRequest::DeleteTillEnd);
assert_eq!(input.value(), "first ");
assert_eq!(input.cursor(), 6);
assert_eq!(input.yank, "second, third.");
input.handle(InputRequest::Yank);
assert_eq!(input.value(), "first second, third.");
assert_eq!(input.cursor(), TEXT.chars().count());
assert_eq!(input.yank, "second, third.");
}
#[test]
fn yank_delete_prev_word() {
let mut input = Input::from(TEXT).with_cursor(12);
input.handle(InputRequest::DeletePrevWord);
assert_eq!(input.value(), "first , third.");
assert_eq!(input.yank, "second");
input.handle(InputRequest::Yank);
assert_eq!(input.value(), "first second, third.");
assert_eq!(input.yank, "second");
}
#[test]
fn yank_delete_next_word() {
let mut input = Input::from(TEXT).with_cursor(6);
input.handle(InputRequest::DeleteNextWord);
assert_eq!(input.value(), "first third.");
assert_eq!(input.yank, "second, ");
input.handle(InputRequest::Yank);
assert_eq!(input.value(), "first second, third.");
assert_eq!(input.yank, "second, ");
}
#[test]
fn yank_empty() {
let mut input: Input = TEXT.into();
let result = input.handle(InputRequest::Yank);
assert_eq!(result, None);
assert_eq!(input.value(), TEXT);
assert_eq!(input.yank, "");
}
#[test]
fn yank_at_middle() {
let mut input = Input::from(TEXT).with_cursor(6);
input.handle(InputRequest::DeleteTillEnd);
assert_eq!(input.value(), "first ");
assert_eq!(input.yank, "second, third.");
input.handle(InputRequest::GoToStart);
input.handle(InputRequest::Yank);
assert_eq!(input.value(), "second, third.first ");
assert_eq!(input.cursor(), 14);
assert_eq!(input.yank, "second, third.");
}
#[test]
fn yank_consecutive_delete_prev_word() {
let mut input = Input::from(TEXT).with_cursor(TEXT.chars().count());
input.handle(InputRequest::DeletePrevWord);
assert_eq!(input.value(), "first second, ");
assert_eq!(input.yank, "third.");
input.handle(InputRequest::DeletePrevWord);
assert_eq!(input.value(), "first ");
assert_eq!(input.yank, "second, third.");
input.handle(InputRequest::Yank);
assert_eq!(input.value(), "first second, third.");
}
#[test]
fn yank_consecutive_delete_next_word() {
let mut input = Input::from(TEXT).with_cursor(0);
input.handle(InputRequest::DeleteNextWord);
assert_eq!(input.value(), "second, third.");
assert_eq!(input.yank, "first ");
input.handle(InputRequest::DeleteNextWord);
assert_eq!(input.value(), "third.");
assert_eq!(input.yank, "first second, ");
input.handle(InputRequest::Yank);
assert_eq!(input.value(), "first second, third.");
}
#[test]
fn yank_insert_breaks_cut_sequence() {
let mut input = Input::from(TEXT).with_cursor(TEXT.chars().count());
input.handle(InputRequest::DeletePrevWord);
assert_eq!(input.yank, "third.");
input.handle(InputRequest::InsertChar('x'));
input.handle(InputRequest::DeletePrevChar);
input.handle(InputRequest::DeletePrevWord);
assert_eq!(input.yank, "second, ");
}
#[test]
fn yank_mixed_delete_word_and_line() {
let mut input = Input::from(TEXT).with_cursor(6);
input.handle(InputRequest::DeletePrevWord);
assert_eq!(input.value(), "second, third.");
assert_eq!(input.yank, "first ");
input.handle(InputRequest::DeleteLine);
assert_eq!(input.value(), "");
assert_eq!(input.yank, "first second, third.");
input.handle(InputRequest::Yank);
assert_eq!(input.value(), "first second, third.");
}
#[test]
fn yank_mixed_delete_word_and_line_from_end() {
let mut input = Input::from(TEXT).with_cursor(TEXT.chars().count());
input.handle(InputRequest::DeletePrevWord);
assert_eq!(input.value(), "first second, ");
assert_eq!(input.yank, "third.");
input.handle(InputRequest::DeleteLine);
assert_eq!(input.value(), "");
assert_eq!(input.yank, "first second, third.");
input.handle(InputRequest::Yank);
assert_eq!(input.value(), "first second, third.");
}
}