use super::{
Count, Counted, InsertEntry, ModeSwitch, Motion, NormalCommand, NormalOperatorTarget, Operator,
PastePlacement, SearchDirection, TextObject, ViewportPosition,
operator::{ResolvedTarget, TargetResolutionError},
search::SearchRepeatDirection,
};
use crate::text_stream::TextByteStream;
use std::fmt::{Display, Formatter};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum VimOperation {
MoveCursor(Counted<Motion>),
ApplyOperator {
count: Count,
operator: Operator,
target: OperatorTargetSource,
},
SwitchMode(ModeSwitch),
EnterInsert(InsertEntry),
Paste {
count: Count,
placement: PastePlacement,
},
Undo,
Redo,
StartCommandLine,
StartSearch(SearchDirection),
RepeatSearch(SearchRepeatDirection),
RepeatSearchInDirection(SearchDirection),
ViewportPosition(ViewportPosition),
PrefilledCommandLine(String),
NoOp,
}
impl From<NormalCommand> for VimOperation {
fn from(command: NormalCommand) -> Self {
match command {
NormalCommand::Motion(motion) => Self::MoveCursor(motion),
NormalCommand::Operator {
count,
operator,
target,
} => Self::ApplyOperator {
count,
operator,
target: OperatorTargetSource::from(target),
},
NormalCommand::ModeSwitch(mode_switch) => Self::SwitchMode(mode_switch),
NormalCommand::Insert(entry) => Self::EnterInsert(entry),
NormalCommand::Paste { count, placement } => Self::Paste { count, placement },
NormalCommand::Undo => Self::Undo,
NormalCommand::Redo => Self::Redo,
NormalCommand::RepeatLastChange => Self::NoOp,
NormalCommand::ExCommandStart => Self::StartCommandLine,
NormalCommand::SearchStart(direction) => Self::StartSearch(direction),
NormalCommand::SearchRepeat(direction) => Self::RepeatSearch(direction),
NormalCommand::ViewportPosition(position) => Self::ViewportPosition(position),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OperatorTargetSource {
Motion(Counted<Motion>),
CurrentLine {
count: Count,
},
TextObject(Counted<TextObject>),
VisualSelection {
selection: super::VimSelection,
mode: super::VisualMode,
},
}
impl From<NormalOperatorTarget> for OperatorTargetSource {
fn from(target: NormalOperatorTarget) -> Self {
match target {
NormalOperatorTarget::Motion(motion) => Self::Motion(motion),
NormalOperatorTarget::CurrentLine => Self::CurrentLine {
count: Count::default(),
},
NormalOperatorTarget::TextObject(object) => Self::TextObject(object),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum OperationEffect {
MoveCursor(Counted<Motion>),
SwitchMode(ModeSwitch),
EnterInsert(InsertEntry),
Paste {
count: Count,
placement: PastePlacement,
},
Undo,
Redo,
StartCommandLine,
PrefilledCommandLine(String),
StartSearch(SearchDirection),
RepeatSearch(SearchRepeatDirection),
RepeatSearchInDirection(SearchDirection),
ViewportPosition(ViewportPosition),
ApplyOperator {
operator: Operator,
target: ResolvedTarget,
},
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct OperationOutcome {
effects: Vec<OperationEffect>,
}
impl OperationOutcome {
#[must_use]
pub const fn new(effects: Vec<OperationEffect>) -> Self {
Self { effects }
}
#[must_use]
pub fn effects(&self) -> &[OperationEffect] {
&self.effects
}
#[must_use]
pub fn into_effects(self) -> Vec<OperationEffect> {
self.effects
}
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum OperationError {
TargetResolution(#[source] TargetResolutionError),
}
impl Display for OperationError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::TargetResolution(_) => formatter.write_str("operation target resolution failed"),
}
}
}
impl VimOperation {
pub fn resolve(
self,
stream: &TextByteStream,
cursor_byte_index: usize,
) -> Result<OperationOutcome, OperationError> {
match self {
Self::ApplyOperator {
count,
operator,
target,
} => {
let target = target.with_operator_count(count);
let target = super::OperatorTarget::from_source(stream, cursor_byte_index, target)
.map_err(OperationError::TargetResolution)?;
Ok(OperationOutcome::new(vec![
OperationEffect::ApplyOperator { operator, target },
]))
}
operation => operation.resolve_without_target(),
}
}
pub fn resolve_without_target(self) -> Result<OperationOutcome, OperationError> {
let effect = match self {
Self::MoveCursor(motion) => OperationEffect::MoveCursor(motion),
Self::SwitchMode(mode_switch) => OperationEffect::SwitchMode(mode_switch),
Self::EnterInsert(entry) => OperationEffect::EnterInsert(entry),
Self::Paste { count, placement } => OperationEffect::Paste { count, placement },
Self::Undo => OperationEffect::Undo,
Self::Redo => OperationEffect::Redo,
Self::StartCommandLine => OperationEffect::StartCommandLine,
Self::StartSearch(direction) => OperationEffect::StartSearch(direction),
Self::RepeatSearch(direction) => OperationEffect::RepeatSearch(direction),
Self::RepeatSearchInDirection(direction) => {
OperationEffect::RepeatSearchInDirection(direction)
}
Self::ViewportPosition(position) => OperationEffect::ViewportPosition(position),
Self::PrefilledCommandLine(command) => OperationEffect::PrefilledCommandLine(command),
Self::NoOp => return Ok(OperationOutcome::default()),
Self::ApplyOperator { .. } => {
return Err(OperationError::TargetResolution(
TargetResolutionError::UnsupportedTarget,
));
}
};
Ok(OperationOutcome::new(vec![effect]))
}
}
impl OperatorTargetSource {
#[must_use]
fn with_operator_count(self, count: Count) -> Self {
match self {
Self::Motion(mut counted) => {
counted.count = multiply_counts(count, counted.count);
Self::Motion(counted)
}
Self::CurrentLine { .. } => Self::CurrentLine { count },
Self::TextObject(mut counted) => {
counted.count = multiply_counts(count, counted.count);
Self::TextObject(counted)
}
Self::VisualSelection { .. } => self,
}
}
}
fn multiply_counts(left: Count, right: Count) -> Count {
std::num::NonZeroUsize::new(left.get().saturating_mul(right.get()))
.map_or_else(Count::default, Count::new)
}
#[cfg(test)]
mod tests {
use super::{OperationEffect, OperationError, VimOperation};
use crate::text_stream::TextByteStream;
use crate::vim::{
Count, Counted, InsertEntry, ModeSwitch, Motion, NormalCommand, NormalOperatorTarget,
Operator, OperatorTargetSource, PastePlacement, TargetKind, TargetResolutionError,
TextObject, TextObjectKind, TextObjectScope, ViewportPosition,
};
fn stream(text: &str) -> TextByteStream {
TextByteStream::new(text)
}
#[test]
fn normal_motion_converts_to_operation_effect() {
let operation = VimOperation::from(NormalCommand::Motion(Counted::once(Motion::Right)));
assert_eq!(
operation.resolve_without_target().unwrap().effects(),
&[OperationEffect::MoveCursor(Counted::once(Motion::Right))]
);
}
#[test]
fn normal_operator_preserves_operator_and_motion_target() {
let command = NormalCommand::Operator {
count: Count::default(),
operator: Operator::Delete,
target: NormalOperatorTarget::Motion(Counted::once(Motion::WordForward(
crate::vim::WordKind::Normal,
))),
};
assert_eq!(
VimOperation::from(command),
VimOperation::ApplyOperator {
count: Count::default(),
operator: Operator::Delete,
target: OperatorTargetSource::Motion(Counted::once(Motion::WordForward(
crate::vim::WordKind::Normal
))),
}
);
}
#[test]
fn unresolved_operator_is_not_silently_ignored() {
let operation = VimOperation::ApplyOperator {
count: Count::default(),
operator: Operator::Yank,
target: OperatorTargetSource::Motion(Counted::once(Motion::Right)),
};
assert_eq!(
operation.resolve_without_target(),
Err(OperationError::TargetResolution(
TargetResolutionError::UnsupportedTarget
))
);
}
#[test]
fn operator_operation_resolves_target_before_effect() {
let stream = stream("one two");
let operation = VimOperation::ApplyOperator {
count: Count::default(),
operator: Operator::Delete,
target: OperatorTargetSource::Motion(Counted::once(Motion::WordForward(
crate::vim::WordKind::Normal,
))),
};
assert_eq!(
operation.resolve(&stream, 0).unwrap().effects(),
&[OperationEffect::ApplyOperator {
operator: Operator::Delete,
target: crate::vim::OperatorTarget::characterwise(&stream, 0..4).unwrap(),
}]
);
}
#[test]
fn operator_operation_multiplies_operator_and_motion_counts() {
let operation = VimOperation::ApplyOperator {
count: std::num::NonZeroUsize::new(2).unwrap().into(),
operator: Operator::Delete,
target: OperatorTargetSource::Motion(Counted {
count: std::num::NonZeroUsize::new(2).unwrap().into(),
item: Motion::WordForward(crate::vim::WordKind::Normal),
}),
};
let stream = stream("one two three four five");
let target = match operation.resolve(&stream, 0).unwrap().effects() {
[OperationEffect::ApplyOperator { target, .. }] => *target,
_ => panic!("expected one operator effect"),
};
assert_eq!(target.kind(), TargetKind::Characterwise);
assert_eq!(target.range().as_range(), 0.."one two three four ".len());
}
#[test]
fn operator_operation_resolves_text_object_targets() {
let stream = stream("one two");
let operation = VimOperation::ApplyOperator {
count: Count::default(),
operator: Operator::Yank,
target: OperatorTargetSource::TextObject(Counted::once(TextObject::new(
TextObjectScope::Inner,
TextObjectKind::Word,
))),
};
assert_eq!(
operation.resolve(&stream, "one ".len()).unwrap().effects(),
&[OperationEffect::ApplyOperator {
operator: Operator::Yank,
target: crate::vim::OperatorTarget::characterwise(
&stream,
"one ".len().."one two".len()
)
.unwrap(),
}]
);
}
#[test]
fn command_line_and_viewport_operations_are_first_class() {
assert_eq!(
VimOperation::from(NormalCommand::ModeSwitch(ModeSwitch::VisualCharacterwise))
.resolve_without_target()
.unwrap()
.effects(),
&[OperationEffect::SwitchMode(ModeSwitch::VisualCharacterwise)]
);
assert_eq!(
VimOperation::from(NormalCommand::ViewportPosition(ViewportPosition::Center))
.resolve_without_target()
.unwrap()
.effects(),
&[OperationEffect::ViewportPosition(ViewportPosition::Center)]
);
}
#[test]
fn insert_entry_converts_to_operation_effect() {
assert_eq!(
VimOperation::from(NormalCommand::Insert(InsertEntry::AfterCursor))
.resolve_without_target()
.unwrap()
.effects(),
&[OperationEffect::EnterInsert(InsertEntry::AfterCursor)]
);
}
#[test]
fn paste_command_converts_to_operation_effect() {
let count = std::num::NonZeroUsize::new(2).unwrap().into();
assert_eq!(
VimOperation::from(NormalCommand::Paste {
count,
placement: PastePlacement::After,
})
.resolve_without_target()
.unwrap()
.effects(),
&[OperationEffect::Paste {
count,
placement: PastePlacement::After,
}]
);
}
}