use crate::editor::vim_ops::{FindKind, VisualKind};
pub const COUNT_CAP: u32 = 9999;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VimSubMode {
#[default]
Normal,
OperatorPending,
Insert,
Visual,
VisualLine,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PendingOp {
Delete,
Change,
Yank,
IndentRight,
IndentLeft,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct VimRegister {
pub text: String,
pub linewise: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CmdLineKind {
Ex,
SearchForward,
SearchBackward,
}
impl CmdLineKind {
pub fn prefix(self) -> char {
match self {
CmdLineKind::Ex => ':',
CmdLineKind::SearchForward => '/',
CmdLineKind::SearchBackward => '?',
}
}
pub fn is_search(self) -> bool {
matches!(
self,
CmdLineKind::SearchForward | CmdLineKind::SearchBackward
)
}
}
pub const HISTORY_CAP: usize = 100;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CmdLineState {
pub kind: CmdLineKind,
pub input: String,
pub cursor: usize,
pub history_idx: Option<usize>,
pub draft: String,
}
impl CmdLineState {
pub fn new(kind: CmdLineKind) -> Self {
Self {
kind,
input: String::new(),
cursor: 0,
history_idx: None,
draft: String::new(),
}
}
pub fn with_input(kind: CmdLineKind, input: String) -> Self {
let cursor = input.chars().count();
Self {
kind,
input,
cursor,
history_idx: None,
draft: String::new(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct VimState {
pub sub_mode: VimSubMode,
pub count: Option<u32>,
pub pending_op: Option<PendingOp>,
pub motion_count: Option<u32>,
pub pending_g: bool,
pub pending_replace: bool,
pub pending_find: Option<FindKind>,
pub pending_text_object: Option<bool>,
pub last_find: Option<(FindKind, char)>,
pub visual_anchor: Option<usize>,
pub last_visual_range: Option<(usize, usize)>,
pub register: VimRegister,
pub cmdline: Option<CmdLineState>,
pub incsearch: Option<crate::editor::vim_ops::IncsearchSession>,
pub ex_history: Vec<String>,
pub search_history: Vec<String>,
}
impl VimState {
pub fn reset_pending(&mut self) {
self.count = None;
self.pending_op = None;
self.motion_count = None;
self.pending_g = false;
self.pending_replace = false;
self.pending_find = None;
self.pending_text_object = None;
}
fn history_for(&mut self, kind: CmdLineKind) -> &mut Vec<String> {
match kind {
CmdLineKind::Ex => &mut self.ex_history,
CmdLineKind::SearchForward | CmdLineKind::SearchBackward => &mut self.search_history,
}
}
pub fn record_command(&mut self, kind: CmdLineKind, cmd: &str) {
let history = self.history_for(kind);
if let Some(pos) = history.iter().position(|e| e == cmd) {
history.remove(pos);
}
history.push(cmd.to_owned());
let overflow = history.len().saturating_sub(HISTORY_CAP);
if overflow > 0 {
history.drain(0..overflow);
}
}
pub fn is_visual_line(&self) -> bool {
self.sub_mode == VimSubMode::VisualLine
}
pub fn visual_kind(&self) -> Option<VisualKind> {
match self.sub_mode {
VimSubMode::Visual => Some(VisualKind::Char),
VimSubMode::VisualLine => Some(VisualKind::Line),
_ => None,
}
}
pub fn mode_label(&self) -> &'static str {
match self.sub_mode {
VimSubMode::Normal | VimSubMode::OperatorPending => "NORMAL",
VimSubMode::Insert => "INSERT",
VimSubMode::Visual => "VISUAL",
VimSubMode::VisualLine => "V-LINE",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mode_label_maps_each_sub_mode() {
let mut v = VimState::default();
assert_eq!(v.mode_label(), "NORMAL");
v.sub_mode = VimSubMode::OperatorPending;
assert_eq!(v.mode_label(), "NORMAL");
v.sub_mode = VimSubMode::Insert;
assert_eq!(v.mode_label(), "INSERT");
v.sub_mode = VimSubMode::Visual;
assert_eq!(v.mode_label(), "VISUAL");
v.sub_mode = VimSubMode::VisualLine;
assert_eq!(v.mode_label(), "V-LINE");
}
#[test]
fn reset_pending_clears_parse_but_keeps_mode_and_register() {
let mut v = VimState {
sub_mode: VimSubMode::Insert,
count: Some(3),
pending_op: Some(PendingOp::Delete),
motion_count: Some(2),
pending_g: true,
pending_replace: true,
pending_find: Some(FindKind::Forward),
pending_text_object: Some(true),
register: VimRegister {
text: "x".into(),
linewise: true,
},
..Default::default()
};
v.reset_pending();
assert_eq!(v.count, None);
assert_eq!(v.pending_op, None);
assert_eq!(v.motion_count, None);
assert!(!v.pending_g);
assert!(!v.pending_replace);
assert_eq!(v.pending_find, None);
assert_eq!(v.pending_text_object, None);
assert_eq!(v.sub_mode, VimSubMode::Insert);
assert_eq!(v.register.text, "x");
}
#[test]
fn record_command_dedups_to_end_and_keeps_search_separate() {
let mut v = VimState::default();
v.record_command(CmdLineKind::Ex, "w");
v.record_command(CmdLineKind::Ex, "q");
v.record_command(CmdLineKind::Ex, "w");
assert_eq!(v.ex_history, vec!["q".to_owned(), "w".to_owned()]);
v.record_command(CmdLineKind::SearchForward, "foo");
v.record_command(CmdLineKind::SearchBackward, "bar");
assert_eq!(v.search_history, vec!["foo".to_owned(), "bar".to_owned()]);
assert_eq!(v.ex_history.len(), 2);
}
#[test]
fn record_command_caps_history_dropping_oldest() {
let mut v = VimState::default();
for i in 0..HISTORY_CAP + 5 {
v.record_command(CmdLineKind::Ex, &format!("cmd{i}"));
}
assert_eq!(v.ex_history.len(), HISTORY_CAP);
assert_eq!(v.ex_history[0], "cmd5");
assert_eq!(
v.ex_history[HISTORY_CAP - 1],
format!("cmd{}", HISTORY_CAP + 4)
);
}
}