alma 0.1.0

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! Vim command-line errors and bottom-status message state.

use bevy::prelude::Resource;
use std::fmt::{Display, Formatter};

/// Vim-compatible errors shown on the command/status line.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum VimError {
    /// A write command has no file name to write to.
    NoFileName,
    /// No previous search pattern exists for a repeat or empty search.
    NoPreviousRegularExpression,
    /// The buffer has unsaved changes and quit was not forced.
    NoWriteSinceLastChange,
    /// A write command could not open the file for writing.
    CantOpenFileForWriting,
    /// The submitted search pattern was not found.
    PatternNotFound,
    /// The submitted `:` command is not supported.
    NotAnEditorCommand,
}

impl VimError {
    /// Returns the canonical Vim error code.
    #[must_use]
    pub const fn code(self) -> &'static str {
        match self {
            Self::NoFileName => "E32",
            Self::NoPreviousRegularExpression => "E35",
            Self::NoWriteSinceLastChange => "E37",
            Self::CantOpenFileForWriting => "E212",
            Self::PatternNotFound => "E486",
            Self::NotAnEditorCommand => "E492",
        }
    }

    /// Returns the canonical short Vim error text.
    #[must_use]
    pub const fn message(self) -> &'static str {
        match self {
            Self::NoFileName => "No file name",
            Self::NoPreviousRegularExpression => "No previous regular expression",
            Self::NoWriteSinceLastChange => "No write since last change",
            Self::CantOpenFileForWriting => "Can't open file for writing",
            Self::PatternNotFound => "Pattern not found",
            Self::NotAnEditorCommand => "Not an editor command",
        }
    }

    /// Renders the error for the Vim command/status line.
    #[must_use]
    pub fn status_text(self) -> String {
        format!("{}: {}", self.code(), self.message())
    }
}

impl Display for VimError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        formatter.write_str(&self.status_text())
    }
}

/// A bottom-line Vim status message.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum VimStatusMessage {
    /// Error message rendered with Vim's error highlighting.
    Error(VimError),
    /// Informational message rendered with normal status highlighting.
    Info(String),
}

impl VimStatusMessage {
    /// Reads the message text.
    #[must_use]
    pub fn text(self) -> String {
        match self {
            Self::Error(error) => error.status_text(),
            Self::Info(text) => text,
        }
    }
}

/// Bottom status line state shared by Vim command handlers and the renderer.
#[derive(Clone, Debug, Default, Eq, PartialEq, Resource)]
pub struct VimStatusLine {
    /// The current terminal status message, if any.
    message: Option<VimStatusMessage>,
}

impl VimStatusLine {
    /// Clears the current bottom-line message.
    pub fn clear(&mut self) {
        self.message = None;
    }

    /// Stores a Vim error for bottom-line rendering.
    pub fn set_error(&mut self, error: VimError) {
        self.message = Some(VimStatusMessage::Error(error));
    }

    /// Stores informational bottom-line text.
    pub fn set_info(&mut self, text: impl Into<String>) {
        self.message = Some(VimStatusMessage::Info(text.into()));
    }

    /// Reads the current bottom-line message.
    #[must_use]
    pub const fn message(&self) -> Option<&VimStatusMessage> {
        self.message.as_ref()
    }
}

#[cfg(test)]
mod tests {
    use super::{VimError, VimStatusLine, VimStatusMessage};

    #[test]
    fn vim_errors_render_with_codes() {
        assert_eq!(
            VimError::PatternNotFound.status_text(),
            "E486: Pattern not found"
        );
        assert_eq!(VimError::NoFileName.status_text(), "E32: No file name");
        assert_eq!(
            VimError::NoPreviousRegularExpression.status_text(),
            "E35: No previous regular expression"
        );
    }

    #[test]
    fn status_line_tracks_errors_and_clear() {
        let mut status_line = VimStatusLine::default();

        status_line.set_error(VimError::PatternNotFound);
        assert_eq!(
            status_line.message(),
            Some(&VimStatusMessage::Error(VimError::PatternNotFound))
        );

        status_line.clear();
        assert_eq!(status_line.message(), None);

        status_line.set_info("written");
        assert_eq!(
            status_line.message(),
            Some(&VimStatusMessage::Info(String::from("written")))
        );
    }
}