use super::{
Counted, KeyToken, Motion, NormalCommand, NormalGrammar, NormalGrammarOutput, NormalState,
VimCursor, VimMode, VimSelectionState, VisualMode,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum VisualCommand {
Motion(Counted<Motion>),
ModeSwitch(VisualMode),
SwapEndpoint(VisualEndpoint),
Exit,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum VisualEndpoint {
Anchor,
OtherCorner,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum VisualGrammarOutput {
Pending,
Command(VisualCommand),
Unmatched,
}
#[derive(Clone, Debug, Default)]
pub struct VisualGrammar {
motions: NormalGrammar,
}
impl VisualGrammar {
pub const fn reset(&mut self) {
self.motions.reset();
}
pub fn feed(&mut self, token: KeyToken) -> VisualGrammarOutput {
match token {
KeyToken::Escape => {
self.reset();
VisualGrammarOutput::Command(VisualCommand::Exit)
}
KeyToken::Char('v') => {
self.reset();
VisualGrammarOutput::Command(VisualCommand::ModeSwitch(VisualMode::Characterwise))
}
KeyToken::Char('V') => {
self.reset();
VisualGrammarOutput::Command(VisualCommand::ModeSwitch(VisualMode::Linewise))
}
KeyToken::Char('o') => {
self.reset();
VisualGrammarOutput::Command(VisualCommand::SwapEndpoint(VisualEndpoint::Anchor))
}
KeyToken::Char('O') => {
self.reset();
VisualGrammarOutput::Command(VisualCommand::SwapEndpoint(
VisualEndpoint::OtherCorner,
))
}
token => match self.motions.feed(token) {
NormalGrammarOutput::Command(NormalCommand::Motion(motion)) => {
VisualGrammarOutput::Command(VisualCommand::Motion(motion))
}
NormalGrammarOutput::Pending => VisualGrammarOutput::Pending,
NormalGrammarOutput::Command(_) | NormalGrammarOutput::Unmatched => {
self.reset();
VisualGrammarOutput::Unmatched
}
},
}
}
}
#[derive(Clone, Debug, Default)]
pub struct VisualState {
grammar: VisualGrammar,
}
impl VisualState {
pub const fn reset_grammar(&mut self) {
self.grammar.reset();
}
pub fn feed(
&mut self,
token: KeyToken,
context: VisualCommandContext<'_>,
) -> VisualGrammarOutput {
match self.feed_command(token) {
VisualGrammarOutput::Command(command) => {
Self::apply_command(command, context);
VisualGrammarOutput::Command(command)
}
output @ (VisualGrammarOutput::Pending | VisualGrammarOutput::Unmatched) => output,
}
}
pub fn feed_command(&mut self, token: KeyToken) -> VisualGrammarOutput {
self.grammar.feed(token)
}
pub fn apply_command(command: VisualCommand, context: VisualCommandContext<'_>) {
let VisualCommandContext {
text,
cursor,
mode,
active_visual_mode,
selection_state,
normal_state,
} = context;
match command {
VisualCommand::Motion(motion) => {
normal_state.apply_counted_motion(text, cursor, motion);
}
VisualCommand::ModeSwitch(visual_mode) if visual_mode == active_visual_mode => {
*mode = VimMode::Normal;
selection_state.clear();
}
VisualCommand::ModeSwitch(visual_mode) => {
*mode = VimMode::Visual(visual_mode);
selection_state.start(text, cursor.byte_index());
}
VisualCommand::SwapEndpoint(VisualEndpoint::Anchor | VisualEndpoint::OtherCorner) => {
if let Some(selection) = selection_state.selection() {
let anchor = selection.anchor_byte_index();
selection_state.set_anchor(text, cursor.byte_index());
cursor.set_byte_index(text, anchor);
}
}
VisualCommand::Exit => {
*mode = VimMode::Normal;
selection_state.clear();
}
}
}
}
pub struct VisualCommandContext<'state> {
pub text: &'state str,
pub cursor: &'state mut VimCursor,
pub mode: &'state mut VimMode,
pub active_visual_mode: VisualMode,
pub selection_state: &'state mut VimSelectionState,
pub normal_state: &'state mut NormalState,
}
#[cfg(test)]
mod tests {
use super::{VisualCommand, VisualEndpoint, VisualGrammar, VisualGrammarOutput};
use crate::vim::{Counted, KeyToken, Motion, VisualMode};
#[test]
fn parses_visual_mode_toggles_without_normal_commands() {
let mut grammar = VisualGrammar::default();
assert_eq!(
grammar.feed(KeyToken::Char('v')),
VisualGrammarOutput::Command(VisualCommand::ModeSwitch(VisualMode::Characterwise))
);
assert_eq!(
grammar.feed(KeyToken::Char('V')),
VisualGrammarOutput::Command(VisualCommand::ModeSwitch(VisualMode::Linewise))
);
}
#[test]
fn parses_visual_specific_endpoint_swap() {
let mut grammar = VisualGrammar::default();
assert_eq!(
grammar.feed(KeyToken::Char('o')),
VisualGrammarOutput::Command(VisualCommand::SwapEndpoint(VisualEndpoint::Anchor))
);
}
#[test]
fn normal_motion_prefixes_stay_pending_until_complete() {
let mut grammar = VisualGrammar::default();
assert_eq!(
grammar.feed(KeyToken::Char('g')),
VisualGrammarOutput::Pending
);
assert_eq!(
grammar.feed(KeyToken::Char('g')),
VisualGrammarOutput::Command(VisualCommand::Motion(Counted::once(
Motion::LineAddress(crate::vim::LineAddress::FirstNonBlank)
)))
);
}
}