use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
pub fn left(buf: &str, cur: usize) -> usize {
if cur > 0 {
buf[..cur]
.char_indices()
.next_back()
.map(|(i, _)| i)
.unwrap_or(0)
} else {
0
}
}
pub fn right(buf: &str, cur: usize) -> usize {
if cur < buf.len() {
buf[cur..]
.char_indices()
.nth(1)
.map(|(i, _)| cur + i)
.unwrap_or(buf.len())
} else {
buf.len()
}
}
pub fn delete_word(buf: &mut String, cur: usize) -> usize {
let s = &buf[..cur];
let trimmed = s.trim_end();
let word_start = match trimmed
.char_indices()
.rev()
.find(|(_, c)| c.is_whitespace())
{
Some((i, c)) => i + c.len_utf8(),
None => 0,
};
buf.drain(word_start..cur);
word_start
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Line {
pub buf: String,
pub cur: usize,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Edit {
Took,
Commit,
Cancel,
Pass,
}
impl Line {
pub fn at_end(text: &str) -> Self {
Line {
buf: text.to_string(),
cur: text.len(),
}
}
pub fn on_key(&mut self, key: KeyEvent) -> Edit {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
self.cur = self.cur.min(self.buf.len());
match key.code {
KeyCode::Esc => return Edit::Cancel,
KeyCode::Enter => return Edit::Commit,
KeyCode::Left => self.cur = left(&self.buf, self.cur),
KeyCode::Right => self.cur = right(&self.buf, self.cur),
KeyCode::Home => self.cur = 0,
KeyCode::End => self.cur = self.buf.len(),
KeyCode::Char('a') if ctrl => self.cur = 0,
KeyCode::Char('e') if ctrl => self.cur = self.buf.len(),
KeyCode::Char('b') if ctrl => self.cur = left(&self.buf, self.cur),
KeyCode::Char('f') if ctrl => self.cur = right(&self.buf, self.cur),
KeyCode::Char('w') if ctrl => self.cur = delete_word(&mut self.buf, self.cur),
KeyCode::Char('u') if ctrl => {
self.buf.drain(..self.cur);
self.cur = 0;
}
KeyCode::Char('k') if ctrl => self.buf.truncate(self.cur),
KeyCode::Backspace => {
if self.cur > 0 {
let p = left(&self.buf, self.cur);
self.buf.drain(p..self.cur);
self.cur = p;
}
}
KeyCode::Delete => {
if self.cur < self.buf.len() {
let n = right(&self.buf, self.cur);
self.buf.drain(self.cur..n);
}
}
KeyCode::Char(c) => {
self.buf.insert(self.cur, c);
self.cur += c.len_utf8();
}
_ => return Edit::Pass,
}
Edit::Took
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key(c: char) -> KeyEvent {
KeyEvent::new(KeyCode::Char(c), KeyModifiers::empty())
}
fn ctrl(c: char) -> KeyEvent {
KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL)
}
#[test]
fn typing_and_killing_agree_with_the_prompt_motions() {
let mut l = Line::default();
for c in "one two".chars() {
assert_eq!(l.on_key(key(c)), Edit::Took);
}
assert_eq!(l.buf, "one two");
assert_eq!(l.on_key(ctrl('w')), Edit::Took);
assert_eq!(l.buf, "one ");
assert_eq!(l.on_key(ctrl('u')), Edit::Took);
assert_eq!(l.buf, "");
}
#[test]
fn the_cursor_steps_by_characters_not_bytes() {
let mut l = Line::at_end("aé");
assert_eq!(l.cur, 3);
l.on_key(KeyEvent::new(KeyCode::Left, KeyModifiers::empty()));
assert_eq!(l.cur, 1, "one char back over a two-byte char");
l.on_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty()));
assert_eq!(l.buf, "é");
}
#[test]
fn enter_and_esc_are_reported_rather_than_swallowed() {
let mut l = Line::at_end("x");
assert_eq!(
l.on_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())),
Edit::Commit
);
assert_eq!(
l.on_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::empty())),
Edit::Cancel
);
assert_eq!(
l.on_key(KeyEvent::new(KeyCode::F(1), KeyModifiers::empty())),
Edit::Pass
);
}
#[test]
fn ctrl_w_eats_one_word_and_the_space_before_the_cursor() {
let mut line = Line::at_end("select name from t");
assert_eq!(line.on_key(ctrl('w')), Edit::Took);
assert_eq!(line.buf, "select name from ");
assert_eq!(line.cur, line.buf.len());
let mut line = Line::at_end("select name ");
line.on_key(ctrl('w'));
assert_eq!(line.buf, "select ");
let mut line = Line {
buf: "alpha beta gamma".into(),
cur: "alpha beta".len(),
};
line.on_key(ctrl('w'));
assert_eq!(line.buf, "alpha gamma");
assert_eq!(line.cur, "alpha ".len());
let mut line = Line {
buf: "word".into(),
cur: 0,
};
line.on_key(ctrl('w'));
assert_eq!(line.buf, "word");
assert_eq!(line.cur, 0);
let mut line = Line::at_end(" ");
line.on_key(ctrl('w'));
assert_eq!(line.buf, "");
}
#[test]
fn the_line_kills_in_both_directions_from_where_the_cursor_is() {
let mut line = Line {
buf: "keep this half".into(),
cur: "keep ".len(),
};
line.on_key(ctrl('u'));
assert_eq!((line.buf.as_str(), line.cur), ("this half", 0));
let mut line = Line {
buf: "keep this half".into(),
cur: "keep ".len(),
};
line.on_key(ctrl('k'));
assert_eq!((line.buf.as_str(), line.cur), ("keep ", 5));
let mut line = Line::at_end("abc");
line.on_key(ctrl('a'));
assert_eq!(line.cur, 0);
line.on_key(ctrl('e'));
assert_eq!(line.cur, 3);
line.on_key(KeyEvent::from(KeyCode::Home));
assert_eq!(line.cur, 0);
line.on_key(KeyEvent::from(KeyCode::End));
assert_eq!(line.cur, 3);
}
#[test]
fn delete_and_backspace_stop_at_the_edges() {
let mut line = Line {
buf: "abc".into(),
cur: 1,
};
line.on_key(KeyEvent::from(KeyCode::Delete));
assert_eq!((line.buf.as_str(), line.cur), ("ac", 1), "forward");
line.on_key(KeyEvent::from(KeyCode::Backspace));
assert_eq!((line.buf.as_str(), line.cur), ("c", 0), "backward");
line.cur = 0;
line.on_key(KeyEvent::from(KeyCode::Backspace));
assert_eq!(line.buf, "c");
line.cur = line.buf.len();
line.on_key(KeyEvent::from(KeyCode::Delete));
assert_eq!(line.buf, "c");
}
#[test]
fn multi_byte_text_is_never_split_by_a_motion() {
let mut line = Line::at_end("héllo→");
line.on_key(KeyEvent::from(KeyCode::Left));
assert_eq!(line.cur, "héllo".len());
line.on_key(KeyEvent::from(KeyCode::Left));
assert_eq!(line.cur, "héll".len());
line.on_key(key('x'));
assert_eq!(line.buf, "héllxo→");
let mut line = Line::at_end("é");
line.on_key(KeyEvent::from(KeyCode::Backspace));
assert_eq!(line.buf, "");
assert_eq!(line.cur, 0);
let mut line = Line {
buf: "ab".into(),
cur: 99,
};
assert_eq!(line.on_key(key('c')), Edit::Took);
assert_eq!(line.buf, "abc");
}
#[test]
fn an_unhandled_key_is_passed_back_to_the_caller() {
let mut line = Line::at_end("x");
assert_eq!(line.on_key(KeyEvent::from(KeyCode::Tab)), Edit::Pass);
assert_eq!(line.on_key(KeyEvent::from(KeyCode::F(5))), Edit::Pass);
assert_eq!(line.on_key(KeyEvent::from(KeyCode::Up)), Edit::Pass);
assert_eq!(line.buf, "x", "and the line is untouched");
}
}