alma 0.1.0

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! Visual-mode grammar.

use super::{
    Counted, KeyToken, Motion, NormalCommand, NormalGrammar, NormalGrammarOutput, NormalState,
    VimCursor, VimMode, VimSelectionState, VisualMode,
};

/// Visual command.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum VisualCommand {
    /// Motion.
    Motion(Counted<Motion>),
    /// Switch or toggle mode.
    ModeSwitch(VisualMode),
    /// Swap cursor and anchor.
    SwapEndpoint(VisualEndpoint),
    /// Exit visual mode.
    Exit,
}

/// Visual endpoint.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum VisualEndpoint {
    /// `o`
    Anchor,
    /// `O`
    OtherCorner,
}

/// Visual grammar output.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum VisualGrammarOutput {
    /// Incomplete.
    Pending,
    /// Complete command.
    Command(VisualCommand),
    /// Unsupported token.
    Unmatched,
}

/// Visual grammar.
#[derive(Clone, Debug, Default)]
pub struct VisualGrammar {
    /// Normal grammar reused for motion-shaped commands.
    motions: NormalGrammar,
}

impl VisualGrammar {
    /// Clears pending prefixes.
    pub const fn reset(&mut self) {
        self.motions.reset();
    }

    /// Feeds one token.
    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
                }
            },
        }
    }
}

/// Visual-mode controller.
#[derive(Clone, Debug, Default)]
pub struct VisualState {
    /// Grammar.
    grammar: VisualGrammar,
}

impl VisualState {
    /// Clears pending grammar.
    pub const fn reset_grammar(&mut self) {
        self.grammar.reset();
    }

    /// Feeds and applies one token.
    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,
        }
    }

    /// Feeds one token without applying it.
    pub fn feed_command(&mut self, token: KeyToken) -> VisualGrammarOutput {
        self.grammar.feed(token)
    }

    /// Applies one command.
    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();
            }
        }
    }
}

/// Visual-command context.
pub struct VisualCommandContext<'state> {
    /// Text.
    pub text: &'state str,
    /// Cursor state.
    pub cursor: &'state mut VimCursor,
    /// Mode.
    pub mode: &'state mut VimMode,
    /// Mode before dispatch.
    pub active_visual_mode: VisualMode,
    /// Selection.
    pub selection_state: &'state mut VimSelectionState,
    /// Normal controller for motion-shaped commands.
    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)
            )))
        );
    }
}