use super::{
Counted, ModeSwitch, Motion, NormalCommand, NormalCommandContext, NormalState, OperationEffect,
OperationOutcome, PageDirection, SearchDirection, ViewportPosition, VimCommandState, VimCursor,
VimMode, VimOperation, VimSearchState, VimSelectionState, VimStatusLine, config::VimOptions,
motion,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum VimAction {
NormalCommand(NormalCommand),
RepeatSearch(SearchDirection),
ViewportPage(PageDirection),
ViewportPosition(ViewportPosition),
ExCommand(String),
NoOp,
}
impl From<NormalCommand> for VimAction {
fn from(command: NormalCommand) -> Self {
Self::NormalCommand(command)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct ActionDispatcher;
impl ActionDispatcher {
pub fn dispatch(action: &VimAction, context: ActionContext<'_>) {
let ActionContext {
text,
cursor,
mode,
selection_state,
search_state,
command_state,
status_line,
normal_state,
visible_line_count,
options,
} = context;
let operation = match action {
VimAction::NormalCommand(command) => VimOperation::from(*command),
VimAction::RepeatSearch(direction) => VimOperation::RepeatSearchInDirection(*direction),
VimAction::ViewportPage(direction) => {
VimOperation::MoveCursor(Counted::once(Motion::Page(*direction)))
}
VimAction::ViewportPosition(position) => VimOperation::ViewportPosition(*position),
VimAction::ExCommand(command) => VimOperation::PrefilledCommandLine(command.clone()),
VimAction::NoOp => VimOperation::NoOp,
};
let Ok(outcome) = operation.resolve_without_target() else {
return;
};
apply_outcome(
outcome,
DispatchState {
text,
cursor,
mode,
selection_state,
search_state,
command_state,
status_line,
normal_state,
visible_line_count,
options,
},
);
}
}
pub struct ActionContext<'state> {
pub text: &'state str,
pub cursor: &'state mut VimCursor,
pub mode: &'state mut VimMode,
pub selection_state: &'state mut VimSelectionState,
pub search_state: &'state mut VimSearchState,
pub command_state: &'state mut VimCommandState,
pub status_line: &'state mut VimStatusLine,
pub normal_state: &'state mut NormalState,
pub visible_line_count: usize,
pub options: &'state VimOptions,
}
struct DispatchState<'state> {
text: &'state str,
cursor: &'state mut VimCursor,
mode: &'state mut VimMode,
selection_state: &'state mut VimSelectionState,
search_state: &'state mut VimSearchState,
command_state: &'state mut VimCommandState,
status_line: &'state mut VimStatusLine,
normal_state: &'state mut NormalState,
visible_line_count: usize,
options: &'state VimOptions,
}
fn apply_outcome(outcome: OperationOutcome, state: DispatchState<'_>) {
let DispatchState {
text,
cursor,
mode,
selection_state,
search_state,
command_state,
status_line,
normal_state,
visible_line_count,
options,
} = state;
for effect in outcome.into_effects() {
match effect {
OperationEffect::MoveCursor(counted) => {
apply_motion(text, cursor, normal_state, counted, visible_line_count);
}
OperationEffect::SwitchMode(mode_switch) => match mode_switch {
ModeSwitch::VisualCharacterwise
| ModeSwitch::VisualLinewise
| ModeSwitch::VisualBlockwise => {
normal_state.apply_command(
NormalCommand::ModeSwitch(mode_switch),
NormalCommandContext {
text,
cursor,
mode,
selection_state,
search_state,
command_state,
status_line,
options,
},
);
}
},
OperationEffect::EnterInsert(entry) => {
normal_state.apply_command(
NormalCommand::Insert(entry),
NormalCommandContext {
text,
cursor,
mode,
selection_state,
search_state,
command_state,
status_line,
options,
},
);
}
OperationEffect::Paste { .. }
| OperationEffect::Undo
| OperationEffect::Redo
| OperationEffect::ApplyOperator { .. } => {}
OperationEffect::StartCommandLine => {
status_line.clear();
command_state.start();
}
OperationEffect::PrefilledCommandLine(command) => {
status_line.clear();
command_state.start_with(&command);
}
OperationEffect::StartSearch(direction) => {
status_line.clear();
search_state.start(direction);
}
OperationEffect::RepeatSearch(direction) => {
let outcome =
search_state.repeat_relative(text, cursor.byte_index(), direction, options);
super::apply_search_outcome(text, cursor, status_line, outcome);
}
OperationEffect::RepeatSearchInDirection(direction) => {
let outcome = search_state.repeat(text, cursor.byte_index(), direction, options);
super::apply_search_outcome(text, cursor, status_line, outcome);
}
OperationEffect::ViewportPosition(_) => {
status_line.clear();
}
}
}
}
fn apply_motion(
text: &str,
cursor: &mut VimCursor,
normal_state: &mut NormalState,
counted: Counted<Motion>,
visible_line_count: usize,
) {
match counted.item {
Motion::Page(direction) => {
for _step in 0..counted.count.get() {
let next = motion::apply_page_motion(
text,
cursor.byte_index(),
direction,
visible_line_count,
);
cursor.set_byte_index(text, next);
}
}
Motion::LineAddress(_)
| Motion::CharSearch(_)
| Motion::RepeatCharSearch
| Motion::RepeatCharSearchReversed
| Motion::Left
| Motion::Down
| Motion::Up
| Motion::Right
| Motion::WordForward(_)
| Motion::WordBackward(_)
| Motion::WordEnd(_)
| Motion::Column(_)
| Motion::Paragraph(_) => normal_state.apply_counted_motion(text, cursor, counted),
}
}
#[cfg(test)]
mod tests {
use super::{ActionContext, ActionDispatcher, VimAction};
use crate::vim::{
PageDirection, SearchDirection, SearchOutcome, VimCommandState, VimConfig, VimCursor,
VimMode, VimSearchState, VimSelectionState, VimStatusLine,
};
#[test]
fn viewport_page_action_uses_visible_line_count() {
let text = "00\n01\n02\n03\n04";
let mut cursor = VimCursor::new();
let mut mode = VimMode::Normal;
let mut selection_state = VimSelectionState::default();
let mut search_state = VimSearchState::default();
let mut command_state = VimCommandState::default();
let mut status_line = VimStatusLine::default();
let mut normal_state = crate::vim::NormalState::default();
let config = VimConfig::default();
ActionDispatcher::dispatch(
&VimAction::ViewportPage(PageDirection::Forward),
ActionContext {
text,
cursor: &mut cursor,
mode: &mut mode,
selection_state: &mut selection_state,
search_state: &mut search_state,
command_state: &mut command_state,
status_line: &mut status_line,
normal_state: &mut normal_state,
visible_line_count: 3,
options: &config.options,
},
);
assert_eq!(cursor.byte_index(), "00\n01\n02\n".len());
}
#[test]
fn repeat_search_action_uses_search_state() {
let text = "one two one two";
let mut cursor = VimCursor::new();
let mut mode = VimMode::Normal;
let mut selection_state = VimSelectionState::default();
let mut search_state = VimSearchState::default();
let mut command_state = VimCommandState::default();
let mut status_line = VimStatusLine::default();
let mut normal_state = crate::vim::NormalState::default();
let config = VimConfig::default();
search_state.start(SearchDirection::Forward);
search_state.push_text("two");
assert_eq!(
search_state.submit(text, 0, &config.options),
SearchOutcome::Match {
byte_index: "one ".len()
}
);
cursor.set_byte_index(text, "one ".len());
ActionDispatcher::dispatch(
&VimAction::RepeatSearch(SearchDirection::Forward),
ActionContext {
text,
cursor: &mut cursor,
mode: &mut mode,
selection_state: &mut selection_state,
search_state: &mut search_state,
command_state: &mut command_state,
status_line: &mut status_line,
normal_state: &mut normal_state,
visible_line_count: 20,
options: &config.options,
},
);
assert_eq!(cursor.byte_index(), "one two one ".len());
}
}