use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubMode {
Insert,
Normal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Outcome {
Noop,
Changed,
Submitted,
Cancelled,
}
#[derive(Debug, Clone)]
pub struct VimInput {
chars: Vec<char>,
cursor: usize,
pub submode: SubMode,
modal: bool,
replace_on_edit: bool,
pending_delete: bool,
}
impl Default for VimInput {
fn default() -> Self {
Self::new()
}
}
impl VimInput {
pub(crate) fn diagnostics(&self, full: bool) -> serde_json::Value {
serde_json::json!({
"characters":self.chars.len(), "cursor_character":self.cursor,
"mode":match self.submode { SubMode::Insert => "insert", SubMode::Normal => "normal" },
"modal":self.modal, "replace_on_edit":self.replace_on_edit,
"pending_delete":self.pending_delete,
"text":full.then(||self.value()),
})
}
pub fn new() -> Self {
Self {
chars: Vec::new(),
cursor: 0,
submode: SubMode::Insert,
modal: true,
replace_on_edit: false,
pending_delete: false,
}
}
pub fn transient() -> Self {
Self {
modal: false,
..Self::new()
}
}
pub fn value(&self) -> String {
self.chars.iter().collect()
}
pub fn set(&mut self, value: &str) {
self.chars = value.chars().collect();
self.cursor = self.chars.len();
self.replace_on_edit = false;
}
pub fn prefill(&mut self, value: &str) {
self.set(value);
self.replace_on_edit = true;
}
pub fn clear(&mut self) {
self.set("");
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn handle_key(&mut self, key: KeyEvent) -> Outcome {
match self.submode {
SubMode::Insert => self.insert_key(key),
SubMode::Normal => self.normal_key(key),
}
}
fn insert_key(&mut self, key: KeyEvent) -> Outcome {
match crate::keymap::input_control(key, false, self.modal) {
Some(crate::keymap::InputControl::Submit) => return Outcome::Submitted,
Some(crate::keymap::InputControl::Escape) => {
if !self.modal {
return Outcome::Cancelled;
}
self.submode = SubMode::Normal;
if self.cursor > 0 && self.cursor == self.chars.len() {
self.cursor -= 1;
}
return Outcome::Noop;
}
_ => {}
}
let is_edit = matches!(
(key.code, key.modifiers),
(KeyCode::Char(_), KeyModifiers::NONE | KeyModifiers::SHIFT)
| (KeyCode::Backspace, _)
| (KeyCode::Delete, _)
| (KeyCode::Char('w'), KeyModifiers::CONTROL)
| (KeyCode::Char('u'), KeyModifiers::CONTROL)
);
if self.replace_on_edit && is_edit {
self.chars.clear();
self.cursor = 0;
self.replace_on_edit = false;
}
match key.code {
KeyCode::Char('w') if key.modifiers == KeyModifiers::CONTROL => {
let from = word_start_back(&self.chars, self.cursor);
self.chars.drain(from..self.cursor);
self.cursor = from;
Outcome::Changed
}
KeyCode::Char('u') if key.modifiers == KeyModifiers::CONTROL => {
self.chars.drain(..self.cursor);
self.cursor = 0;
Outcome::Changed
}
KeyCode::Char(c)
if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT =>
{
self.chars.insert(self.cursor, c);
self.cursor += 1;
Outcome::Changed
}
KeyCode::Backspace => {
if self.cursor > 0 {
self.cursor -= 1;
self.chars.remove(self.cursor);
}
Outcome::Changed
}
KeyCode::Delete => {
if self.cursor < self.chars.len() {
self.chars.remove(self.cursor);
}
Outcome::Changed
}
KeyCode::Left => {
self.cursor = self.cursor.saturating_sub(1);
Outcome::Noop
}
KeyCode::Right => {
self.cursor = (self.cursor + 1).min(self.chars.len());
Outcome::Noop
}
KeyCode::Home => {
self.cursor = 0;
Outcome::Noop
}
KeyCode::End => {
self.cursor = self.chars.len();
Outcome::Noop
}
_ => Outcome::Noop,
}
}
fn normal_key(&mut self, key: KeyEvent) -> Outcome {
let len = self.chars.len();
let pending = std::mem::take(&mut self.pending_delete);
if pending && key.code == KeyCode::Char('d') {
self.chars.clear();
self.cursor = 0;
return Outcome::Changed;
}
if pending && !matches!(key.code, KeyCode::Char('w' | 'e' | 'b')) {
return Outcome::Noop;
}
match crate::keymap::input_control(key, true, self.modal) {
Some(crate::keymap::InputControl::Insert) => {
self.submode = SubMode::Insert;
return Outcome::Noop;
}
Some(crate::keymap::InputControl::Escape) => return Outcome::Cancelled,
_ => {}
}
match key.code {
KeyCode::Char('h') | KeyCode::Left => {
self.cursor = self.cursor.saturating_sub(1);
Outcome::Noop
}
KeyCode::Char('l') | KeyCode::Right => {
if len > 0 {
self.cursor = (self.cursor + 1).min(len - 1);
}
Outcome::Noop
}
KeyCode::Char('0') | KeyCode::Home => {
self.cursor = 0;
Outcome::Noop
}
KeyCode::Char('$') | KeyCode::End => {
if len > 0 {
self.cursor = len - 1;
}
Outcome::Noop
}
KeyCode::Char('d') => {
self.pending_delete = true; Outcome::Noop
}
KeyCode::Char('w') => {
if pending {
let to = word_start_fwd(&self.chars, self.cursor);
self.chars.drain(self.cursor..to);
self.clamp_cursor();
Outcome::Changed
} else {
let to = word_start_fwd(&self.chars, self.cursor);
if len > 0 {
self.cursor = to.min(len - 1);
}
Outcome::Noop
}
}
KeyCode::Char('e') => {
if pending {
let to = word_end_fwd(&self.chars, self.cursor);
self.chars
.drain(self.cursor..=(to.min(len.saturating_sub(1))));
self.clamp_cursor();
Outcome::Changed
} else {
self.cursor = word_end_fwd(&self.chars, self.cursor);
Outcome::Noop
}
}
KeyCode::Char('b') => {
if pending {
let from = word_start_back(&self.chars, self.cursor);
self.chars.drain(from..self.cursor);
self.cursor = from;
Outcome::Changed
} else {
self.cursor = word_start_back(&self.chars, self.cursor);
Outcome::Noop
}
}
KeyCode::Char('I') => {
self.cursor = 0;
self.submode = SubMode::Insert;
Outcome::Noop
}
KeyCode::Char('D') => {
self.chars.truncate(self.cursor);
Outcome::Changed
}
KeyCode::Char('x') => {
if self.cursor < len {
self.chars.remove(self.cursor);
if self.cursor >= self.chars.len() && self.cursor > 0 {
self.cursor -= 1;
}
}
Outcome::Changed
}
KeyCode::Char('a') => {
self.cursor = (self.cursor + 1).min(len);
self.submode = SubMode::Insert;
Outcome::Noop
}
KeyCode::Char('A') => {
self.cursor = len;
self.submode = SubMode::Insert;
Outcome::Noop
}
_ => Outcome::Noop,
}
}
}
impl VimInput {
fn clamp_cursor(&mut self) {
let len = self.chars.len();
if len == 0 {
self.cursor = 0;
} else if self.cursor >= len {
self.cursor = len - 1;
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Class {
Word,
Space,
Punct,
}
fn class(c: char) -> Class {
if c.is_alphanumeric() || c == '_' {
Class::Word
} else if c.is_whitespace() {
Class::Space
} else {
Class::Punct
}
}
fn word_start_fwd(chars: &[char], i: usize) -> usize {
let n = chars.len();
if i >= n {
return n;
}
let mut j = i;
if class(chars[j]) != Class::Space {
let c = class(chars[j]);
while j < n && class(chars[j]) == c {
j += 1;
}
}
while j < n && class(chars[j]) == Class::Space {
j += 1;
}
j
}
fn word_end_fwd(chars: &[char], i: usize) -> usize {
let n = chars.len();
if n == 0 {
return 0;
}
let mut j = i.min(n - 1);
if class(chars[j]) != Class::Space && (j + 1 == n || class(chars[j + 1]) != class(chars[j])) {
j += 1;
while j < n && class(chars[j]) == Class::Space {
j += 1;
}
if j >= n {
return n - 1;
}
}
while j < n && class(chars[j]) == Class::Space {
j += 1;
}
let c = class(chars[j]);
while j + 1 < n && class(chars[j + 1]) == c {
j += 1;
}
j
}
fn word_start_back(chars: &[char], i: usize) -> usize {
if i == 0 {
return 0;
}
let mut j = i - 1;
if class(chars[j]) == Class::Space {
while j > 0 && class(chars[j - 1]) == Class::Space {
j -= 1;
}
return j;
}
let c = class(chars[j]);
while j > 0 && class(chars[j - 1]) == c {
j -= 1;
}
j
}
#[cfg(test)]
mod tests;