Alexandre Pasmantier e36f0d450f refactoring
2024-10-16 00:29:30 +02:00

590 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

pub mod actions;
pub mod backend;
/// Input requests are used to change the input state.
///
/// Different backends can be used to convert events into requests.
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
pub(crate) enum InputRequest {
SetCursor(usize),
InsertChar(char),
GoToPrevChar,
GoToNextChar,
GoToPrevWord,
GoToNextWord,
GoToStart,
GoToEnd,
DeletePrevChar,
DeleteNextChar,
DeletePrevWord,
DeleteNextWord,
DeleteLine,
DeleteTillEnd,
}
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
pub(crate) struct StateChanged {
pub value: bool,
pub cursor: bool,
}
#[allow(clippy::module_name_repetitions)]
pub type InputResponse = Option<StateChanged>;
/// The input buffer with cursor support.
///
/// Example:
///
/// ```
/// use tui_input::Input;
///
/// let input: Input = "Hello World".into();
///
/// assert_eq!(input.cursor(), 11);
/// assert_eq!(input.to_string(), "Hello World");
/// ```
#[derive(Default, Debug, Clone)]
pub(crate) struct Input {
value: String,
cursor: usize,
}
impl Input {
/// Initialize a new instance with a given value
/// Cursor will be set to the given value's length.
pub(crate) fn new(value: String) -> Self {
let len = value.chars().count();
Self { value, cursor: len }
}
/// Set the value manually.
/// Cursor will be set to the given value's length.
pub(crate) fn with_value(mut self, value: String) -> Self {
self.cursor = value.chars().count();
self.value = value;
self
}
/// Set the cursor manually.
/// If the input is larger than the value length, it'll be auto adjusted.
pub(crate) fn with_cursor(mut self, cursor: usize) -> Self {
self.cursor = cursor.min(self.value.chars().count());
self
}
// Reset the cursor and value to default
pub(crate) fn reset(&mut self) {
self.cursor = Default::default();
self.value = String::default();
}
/// Handle request and emit response.
#[allow(clippy::too_many_lines)]
pub(crate) fn handle(&mut self, req: InputRequest) -> InputResponse {
use InputRequest::{
DeleteLine, DeleteNextChar, DeleteNextWord, DeletePrevChar,
DeletePrevWord, DeleteTillEnd, GoToEnd, GoToNextChar,
GoToNextWord, GoToPrevChar, GoToPrevWord, GoToStart, InsertChar,
SetCursor,
};
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 cursor = self.cursor;
self.value = "".into();
self.cursor = 0;
Some(StateChanged {
value: true,
cursor: self.cursor == cursor,
})
}
}
DeletePrevWord => {
if self.cursor == 0 {
None
} else {
let remaining = self.value.chars().skip(self.cursor);
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();
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 {
self.value = self
.value
.chars()
.take(self.cursor)
.chain(
self.value
.chars()
.skip(self.cursor)
.skip_while(|c| c.is_alphanumeric())
.skip_while(|c| !c.is_alphanumeric()),
)
.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 => {
self.value = self.value.chars().take(self.cursor).collect();
Some(StateChanged {
value: true,
cursor: false,
})
}
}
}
/// Get a reference to the current value.
pub(crate) fn value(&self) -> &str {
self.value.as_str()
}
/// Get the currect cursor placement.
pub(crate) fn cursor(&self) -> usize {
self.cursor
}
/// Get the current cursor position with account for multispace characters.
pub(crate) fn visual_cursor(&self) -> usize {
if self.cursor == 0 {
return 0;
}
// Safe, because the end index will always be within bounds
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),
)
})
}
/// Get the scroll position with account for multispace characters.
pub(crate) 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 = ", !".into();
assert_eq!(input.cursor(), 13);
assert_eq!(input.visual_cursor(), 23);
assert_eq!(input.visual_scroll(6), 18);
}
}