use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::state::CmdLineState;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CmdLineStep {
Editing,
Submit(String),
Cancel,
}
pub fn feed_key(cl: &mut CmdLineState, key: KeyEvent) -> CmdLineStep {
match key.code {
KeyCode::Enter => CmdLineStep::Submit(cl.input.clone()),
KeyCode::Esc => CmdLineStep::Cancel,
KeyCode::Backspace => {
if cl.cursor == 0 {
if cl.input.is_empty() {
return CmdLineStep::Cancel;
}
return CmdLineStep::Editing;
}
let idx = byte_index(&cl.input, cl.cursor - 1);
cl.input.remove(idx);
cl.cursor -= 1;
CmdLineStep::Editing
}
KeyCode::Left => {
cl.cursor = cl.cursor.saturating_sub(1);
CmdLineStep::Editing
}
KeyCode::Right => {
cl.cursor = (cl.cursor + 1).min(cl.input.chars().count());
CmdLineStep::Editing
}
KeyCode::Home => {
cl.cursor = 0;
CmdLineStep::Editing
}
KeyCode::End => {
cl.cursor = cl.input.chars().count();
CmdLineStep::Editing
}
KeyCode::Char(c)
if !key
.modifiers
.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SUPER) =>
{
let idx = byte_index(&cl.input, cl.cursor);
cl.input.insert(idx, c);
cl.cursor += 1;
CmdLineStep::Editing
}
_ => CmdLineStep::Editing,
}
}
pub fn history_prev(cl: &mut CmdLineState, history: &[String]) {
if history.is_empty() {
return;
}
let idx = match cl.history_idx {
None => {
cl.draft = cl.input.clone();
history.len() - 1
}
Some(0) => return, Some(i) => i.saturating_sub(1),
};
cl.history_idx = Some(idx);
set_input(cl, history[idx].clone());
}
pub fn history_next(cl: &mut CmdLineState, history: &[String]) {
let Some(idx) = cl.history_idx else {
return;
};
if idx + 1 < history.len() {
cl.history_idx = Some(idx + 1);
set_input(cl, history[idx + 1].clone());
} else {
cl.history_idx = None;
let draft = std::mem::take(&mut cl.draft);
set_input(cl, draft);
}
}
fn set_input(cl: &mut CmdLineState, text: String) {
cl.cursor = text.chars().count();
cl.input = text;
}
pub fn paste_str(cl: &mut CmdLineState, text: &str) {
let escaped;
let text = if cl.kind.is_search() {
escaped = crate::search::escape::escape(text);
escaped.as_str()
} else {
text
};
for c in text.chars().filter(|c| *c != '\n' && *c != '\r') {
let idx = byte_index(&cl.input, cl.cursor);
cl.input.insert(idx, c);
cl.cursor += 1;
}
}
fn byte_index(s: &str, char_idx: usize) -> usize {
s.char_indices().nth(char_idx).map_or(s.len(), |(i, _)| i)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::input::vim::state::CmdLineKind;
fn cl() -> CmdLineState {
CmdLineState::new(CmdLineKind::SearchForward)
}
fn ch(c: char) -> KeyEvent {
KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)
}
#[test]
fn typing_appends_and_advances_cursor() {
let mut s = cl();
assert_eq!(feed_key(&mut s, ch('f')), CmdLineStep::Editing);
feed_key(&mut s, ch('o'));
feed_key(&mut s, ch('o'));
assert_eq!(s.input, "foo");
assert_eq!(s.cursor, 3);
}
#[test]
fn enter_submits_and_esc_cancels() {
let mut s = cl();
feed_key(&mut s, ch('x'));
assert_eq!(
feed_key(&mut s, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)),
CmdLineStep::Submit("x".to_owned())
);
assert_eq!(
feed_key(&mut s, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)),
CmdLineStep::Cancel
);
}
#[test]
fn backspace_deletes_then_cancels_on_empty() {
let mut s = cl();
feed_key(&mut s, ch('a'));
let bs = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
assert_eq!(feed_key(&mut s, bs), CmdLineStep::Editing);
assert_eq!(s.input, "");
assert_eq!(feed_key(&mut s, bs), CmdLineStep::Cancel);
}
#[test]
fn cursor_moves_let_insertion_happen_mid_string() {
let mut s = cl();
for c in "fo".chars() {
feed_key(&mut s, ch(c));
}
feed_key(&mut s, KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
feed_key(&mut s, ch('X'));
assert_eq!(s.input, "fXo");
assert_eq!(s.cursor, 2);
}
#[test]
fn history_up_walks_older_then_down_restores_draft() {
let history = vec!["w".to_owned(), "q".to_owned(), "wq".to_owned()];
let mut s = cl();
feed_key(&mut s, ch('a'));
history_prev(&mut s, &history);
assert_eq!(s.draft, "a");
assert_eq!(s.input, "wq");
assert_eq!(s.cursor, 2);
assert_eq!(s.history_idx, Some(2));
history_prev(&mut s, &history);
history_prev(&mut s, &history);
assert_eq!(s.input, "w");
assert_eq!(s.history_idx, Some(0));
history_prev(&mut s, &history);
assert_eq!(s.input, "w");
history_next(&mut s, &history);
assert_eq!(s.input, "q");
history_next(&mut s, &history);
assert_eq!(s.input, "wq");
history_next(&mut s, &history);
assert_eq!(s.input, "a");
assert_eq!(s.cursor, 1);
assert_eq!(s.history_idx, None);
history_next(&mut s, &history);
assert_eq!(s.input, "a");
}
#[test]
fn history_up_on_empty_history_is_a_noop() {
let mut s = cl();
feed_key(&mut s, ch('x'));
history_prev(&mut s, &[]);
assert_eq!(s.input, "x");
assert_eq!(s.history_idx, None);
}
#[test]
fn ctrl_chord_is_ignored_not_inserted() {
let mut s = cl();
let ctrl_c = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
assert_eq!(feed_key(&mut s, ctrl_c), CmdLineStep::Editing);
assert_eq!(s.input, "");
}
}