use super::{
Counted, KeyToken, Motion, NormalCommand, NormalGrammar, NormalGrammarOutput, NormalState,
Operator, OperatorTargetSource, TextObject, TextObjectKind, TextObjectScope, VimCursor,
VimMode, VimOperation, VimSelectionState, VisualMode, resolve_text_object_range,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum VisualCommand {
Motion(Counted<Motion>),
ModeSwitch(VisualMode),
SwapEndpoint(VisualEndpoint),
Operator(Operator),
TextObject(TextObject),
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, Copy, Debug, Eq, PartialEq)]
enum VisualPendingPrefix {
InnerTextObject,
AroundTextObject,
}
#[derive(Clone, Debug, Default)]
pub struct VisualGrammar {
motions: NormalGrammar,
pending: Option<VisualPendingPrefix>,
}
impl VisualGrammar {
pub const fn reset(&mut self) {
self.motions.reset();
self.pending = None;
}
pub fn feed(&mut self, token: KeyToken) -> VisualGrammarOutput {
if let Some(pending) = self.pending.take() {
return self.finish_pending(pending, token);
}
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::Ctrl('v' | 'V') => {
self.reset();
VisualGrammarOutput::Command(VisualCommand::ModeSwitch(VisualMode::Blockwise))
}
KeyToken::Char('o') => {
self.reset();
VisualGrammarOutput::Command(VisualCommand::SwapEndpoint(VisualEndpoint::Anchor))
}
KeyToken::Char('O') => {
self.reset();
VisualGrammarOutput::Command(VisualCommand::SwapEndpoint(
VisualEndpoint::OtherCorner,
))
}
KeyToken::Char('d') => {
self.reset();
VisualGrammarOutput::Command(VisualCommand::Operator(Operator::Delete))
}
KeyToken::Char('y') => {
self.reset();
VisualGrammarOutput::Command(VisualCommand::Operator(Operator::Yank))
}
KeyToken::Char('c') => {
self.reset();
VisualGrammarOutput::Command(VisualCommand::Operator(Operator::Change))
}
KeyToken::Char('i') => {
self.pending = Some(VisualPendingPrefix::InnerTextObject);
VisualGrammarOutput::Pending
}
KeyToken::Char('a') => {
self.pending = Some(VisualPendingPrefix::AroundTextObject);
VisualGrammarOutput::Pending
}
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
}
},
}
}
const fn finish_pending(
&mut self,
pending: VisualPendingPrefix,
token: KeyToken,
) -> VisualGrammarOutput {
let scope = match pending {
VisualPendingPrefix::InnerTextObject => TextObjectScope::Inner,
VisualPendingPrefix::AroundTextObject => TextObjectScope::Around,
};
let kind = match token {
KeyToken::Char('w') => TextObjectKind::Word,
KeyToken::Char('p') => TextObjectKind::Paragraph,
_ => {
self.reset();
return VisualGrammarOutput::Unmatched;
}
};
self.reset();
VisualGrammarOutput::Command(VisualCommand::TextObject(TextObject::new(scope, kind)))
}
}
#[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::Operator(_) => {}
VisualCommand::TextObject(object) => {
if let Ok(range) = resolve_text_object_range(text, cursor.byte_index(), object, 1) {
selection_state.start(text, range.start);
cursor.set_byte_index(text, last_cursor_cell(text, range.end));
*mode = VimMode::Visual(VisualMode::Characterwise);
}
}
VisualCommand::Exit => {
*mode = VimMode::Normal;
selection_state.clear();
}
}
}
}
fn last_cursor_cell(text: &str, range_end: usize) -> usize {
text[..range_end]
.char_indices()
.next_back()
.map_or(range_end, |(byte_index, _character)| byte_index)
}
impl VisualCommand {
#[must_use]
pub fn operator_operation(
self,
selection_state: &VimSelectionState,
active_visual_mode: VisualMode,
) -> Option<VimOperation> {
let Self::Operator(operator) = self else {
return None;
};
let selection = selection_state.selection()?;
Some(VimOperation::ApplyOperator {
count: super::Count::default(),
operator,
target: OperatorTargetSource::VisualSelection {
selection,
mode: active_visual_mode,
},
})
}
}
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, OperationEffect, Operator, TargetKind, TextObject,
TextObjectKind, TextObjectScope, VimCursor, VimMode, VimSelectionState, 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))
);
assert_eq!(
grammar.feed(KeyToken::Ctrl('v')),
VisualGrammarOutput::Command(VisualCommand::ModeSwitch(VisualMode::Blockwise))
);
}
#[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 parses_visual_operator_commands() {
let mut grammar = VisualGrammar::default();
assert_eq!(
grammar.feed(KeyToken::Char('d')),
VisualGrammarOutput::Command(VisualCommand::Operator(Operator::Delete))
);
assert_eq!(
grammar.feed(KeyToken::Char('y')),
VisualGrammarOutput::Command(VisualCommand::Operator(Operator::Yank))
);
assert_eq!(
grammar.feed(KeyToken::Char('c')),
VisualGrammarOutput::Command(VisualCommand::Operator(Operator::Change))
);
}
#[test]
fn parses_visual_text_object_commands() {
let mut grammar = VisualGrammar::default();
assert_eq!(
grammar.feed(KeyToken::Char('i')),
VisualGrammarOutput::Pending
);
assert_eq!(
grammar.feed(KeyToken::Char('w')),
VisualGrammarOutput::Command(VisualCommand::TextObject(TextObject::new(
TextObjectScope::Inner,
TextObjectKind::Word,
)))
);
assert_eq!(
grammar.feed(KeyToken::Char('a')),
VisualGrammarOutput::Pending
);
assert_eq!(
grammar.feed(KeyToken::Char('p')),
VisualGrammarOutput::Command(VisualCommand::TextObject(TextObject::new(
TextObjectScope::Around,
TextObjectKind::Paragraph,
)))
);
}
#[test]
fn visual_text_object_selects_resolved_range() {
let text = "one two";
let mut cursor = VimCursor::new();
cursor.set_byte_index(text, "one ".len());
let mut mode = VimMode::Visual(VisualMode::Characterwise);
let mut selection_state = VimSelectionState::default();
selection_state.start(text, 0);
let mut normal_state = crate::vim::NormalState::default();
crate::vim::VisualState::apply_command(
VisualCommand::TextObject(TextObject::new(
TextObjectScope::Inner,
TextObjectKind::Word,
)),
crate::vim::VisualCommandContext {
text,
cursor: &mut cursor,
mode: &mut mode,
active_visual_mode: VisualMode::Characterwise,
selection_state: &mut selection_state,
normal_state: &mut normal_state,
},
);
assert_eq!(mode, VimMode::Visual(VisualMode::Characterwise));
assert_eq!(
selection_state
.selection()
.unwrap()
.characterwise_byte_range(text, cursor.byte_index()),
"one ".len().."one two".len()
);
}
#[test]
fn visual_operator_resolves_selection_through_operation_api() {
let text = "one two";
let stream = crate::text_stream::TextByteStream::new(text);
let mut selection_state = VimSelectionState::default();
selection_state.start(text, 0);
let mut cursor = VimCursor::new();
cursor.set_byte_index(text, "one".len());
let operation = VisualCommand::Operator(Operator::Delete)
.operator_operation(&selection_state, VisualMode::Characterwise)
.unwrap();
let outcome = operation.resolve(&stream, cursor.byte_index()).unwrap();
assert_eq!(
outcome.effects(),
&[OperationEffect::ApplyOperator {
operator: Operator::Delete,
target: crate::vim::OperatorTarget::characterwise(&stream, 0.."one ".len())
.unwrap(),
}]
);
}
#[test]
fn visual_linewise_operator_resolves_linewise_selection() {
let text = "one\ntwo\nthree";
let stream = crate::text_stream::TextByteStream::new(text);
let mut selection_state = VimSelectionState::default();
selection_state.start(text, "one\nt".len());
let cursor = "one\ntwo".len();
let operation = VisualCommand::Operator(Operator::Yank)
.operator_operation(&selection_state, VisualMode::Linewise)
.unwrap();
let target = match operation.resolve(&stream, cursor).unwrap().effects() {
[OperationEffect::ApplyOperator { target, .. }] => *target,
_ => panic!("expected one operator effect"),
};
assert_eq!(target.kind(), TargetKind::Linewise);
assert_eq!(target.range().as_range(), 4..8);
}
#[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)
)))
);
}
}