alma 0.1.1

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! Dot-repeat state.
//!
//! Repeat stores semantic Vim operations, not input tokens. Replayed work must
//! still resolve targets and register text against the current editor state.

use std::fmt::{self, Debug, Formatter};

use super::{Operator, VimOperation};

/// Last repeatable normal-mode change.
#[derive(Clone, Default, Eq, PartialEq)]
pub struct RepeatState {
    /// Stored operation.
    last_change: Option<RepeatableOperation>,
}

impl RepeatState {
    /// Returns the last repeatable operation.
    #[must_use]
    pub fn last_change(&self) -> Option<RepeatableOperation> {
        self.last_change.clone()
    }

    /// Records a repeatable operation.
    pub fn record(&mut self, operation: RepeatableOperation) {
        self.last_change = Some(operation);
    }
}

impl Debug for RepeatState {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("RepeatState")
            .field("shape", &RepeatStateShape::from_repeat_state(self))
            .finish()
    }
}

/// Redacted repeat-state diagnostic shape.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RepeatStateShape {
    /// No repeatable change has been recorded.
    Empty,
    /// A repeatable change exists.
    Present(RepeatableOperationShape),
}

impl RepeatStateShape {
    /// Builds a redacted shape from repeat state.
    #[must_use]
    pub fn from_repeat_state(state: &RepeatState) -> Self {
        state
            .last_change
            .as_ref()
            .map_or(Self::Empty, |operation| Self::Present(operation.shape()))
    }
}

/// Operation accepted for `.` replay.
#[derive(Clone, Eq, PartialEq)]
pub struct RepeatableOperation {
    /// Stored semantic operation.
    operation: VimOperation,
}

impl RepeatableOperation {
    /// Returns a repeatable operation when the operation is a normal-mode
    /// buffer-changing command.
    #[must_use]
    pub fn from_operation(operation: VimOperation) -> Option<Self> {
        match operation {
            VimOperation::ApplyOperator {
                operator: Operator::Delete | Operator::Change,
                ..
            }
            | VimOperation::Paste { .. } => Some(Self { operation }),
            _ => None,
        }
    }

    /// Returns the stored operation.
    #[must_use]
    pub fn operation(&self) -> VimOperation {
        self.operation.clone()
    }

    /// Returns a redacted diagnostic shape.
    #[must_use]
    pub const fn shape(&self) -> RepeatableOperationShape {
        match &self.operation {
            VimOperation::ApplyOperator { operator, .. } => {
                RepeatableOperationShape::Operator(*operator)
            }
            VimOperation::Paste { .. } => RepeatableOperationShape::Paste,
            _ => RepeatableOperationShape::Unsupported,
        }
    }
}

impl Debug for RepeatableOperation {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("RepeatableOperation")
            .field("shape", &self.shape())
            .finish()
    }
}

/// Redacted repeatable-operation diagnostic shape.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RepeatableOperationShape {
    /// Delete or change through an operator target producer.
    Operator(Operator),
    /// Paste from the unnamed register.
    Paste,
    /// Defensive shape for invalid internal construction.
    Unsupported,
}

#[cfg(test)]
mod tests {
    use crate::vim::{
        Count, Counted, Motion, NormalOperatorTarget, Operator, RepeatState, RepeatStateShape,
        RepeatableOperation, VimOperation,
    };

    #[test]
    fn repeat_accepts_buffer_changing_normal_operations() {
        let operation = VimOperation::ApplyOperator {
            count: Count::default(),
            operator: Operator::Delete,
            target: NormalOperatorTarget::Motion(Counted::once(Motion::Right)).into(),
        };

        assert_eq!(
            RepeatableOperation::from_operation(operation.clone())
                .map(|operation| operation.operation()),
            Some(operation)
        );
    }

    #[test]
    fn repeat_rejects_yank_and_motion() {
        let yank = VimOperation::ApplyOperator {
            count: Count::default(),
            operator: Operator::Yank,
            target: NormalOperatorTarget::CurrentLine.into(),
        };

        assert!(RepeatableOperation::from_operation(yank).is_none());
        assert!(
            RepeatableOperation::from_operation(VimOperation::MoveCursor(Counted::once(
                Motion::Right
            )))
            .is_none()
        );
    }

    #[test]
    fn repeat_debug_output_is_shape_only() {
        let mut state = RepeatState::default();
        state.record(
            RepeatableOperation::from_operation(VimOperation::PrefilledCommandLine(String::from(
                "secret",
            )))
            .unwrap_or_else(|| {
                RepeatableOperation::from_operation(VimOperation::Paste {
                    count: Count::default(),
                    placement: crate::vim::PastePlacement::After,
                })
                .unwrap()
            }),
        );

        let debug = format!("{state:?}");

        assert!(debug.contains("RepeatState"));
        assert!(!debug.contains("secret"));
        assert!(matches!(
            RepeatStateShape::from_repeat_state(&state),
            RepeatStateShape::Present(_)
        ));
    }
}