alma 0.1.0

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! Bottom status-line rendering for Vim command/search state.

use crate::vim::{
    LeaderState, SearchDirection, VimCommandState, VimConfig, VimSearchState, VimStatusLine,
    VimStatusMessage,
};

/// Renders the Vim search prompt/status.
pub(super) fn render_search_status(
    search_state: &VimSearchState,
    command_state: &VimCommandState,
    leader_state: &LeaderState,
    vim_config: &VimConfig,
    status_line: &VimStatusLine,
) -> RenderedStatusLine {
    if let Some(command) = command_state.active_command() {
        return RenderedStatusLine::prompt(format!(":{}", command.as_str()));
    }

    if let Some(query) = search_state.active_query() {
        let prompt = match search_state.active_direction() {
            Some(SearchDirection::Backward) => '?',
            Some(SearchDirection::Forward) | None => '/',
        };
        return RenderedStatusLine::prompt(format!("{prompt}{}", query.as_str()));
    }

    if leader_state.is_menu_visible() {
        let labels = vim_config
            .leader
            .bindings
            .iter()
            .map(|binding| binding.label.as_str())
            .collect::<Vec<_>>()
            .join("  ");
        return RenderedStatusLine::prompt(format!("<leader> {labels}"));
    }

    match status_line.message() {
        Some(VimStatusMessage::Error(error)) => RenderedStatusLine::error(error.status_text()),
        Some(VimStatusMessage::Info(message)) => RenderedStatusLine::prompt(message.clone()),
        None => RenderedStatusLine::prompt(String::new()),
    }
}

/// Rendered bottom status-line text and presentation role.
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct RenderedStatusLine {
    /// Text shown on the bottom status line.
    pub(super) text: String,
    /// Presentation role for the status line.
    pub(super) kind: StatusLineKind,
}

impl RenderedStatusLine {
    /// Creates a prompt/status line.
    const fn prompt(text: String) -> Self {
        Self {
            text,
            kind: StatusLineKind::Prompt,
        }
    }

    /// Creates an error line.
    const fn error(text: String) -> Self {
        Self {
            text,
            kind: StatusLineKind::Error,
        }
    }
}

/// Presentation role for bottom status-line text.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum StatusLineKind {
    /// Active command prompt or neutral status.
    Prompt,
    /// Vim error status.
    Error,
}

#[cfg(test)]
mod tests {
    use super::{RenderedStatusLine, render_search_status};
    use crate::vim::{
        LeaderState, SearchDirection, VimCommandState, VimConfig, VimError, VimSearchState,
        VimStatusLine,
    };

    #[test]
    fn search_status_renders_active_prompt_and_terminal_outcomes() {
        let mut search_state = VimSearchState::default();
        let command_state = VimCommandState::default();
        let leader_state = LeaderState::default();
        let vim_config = VimConfig::default();
        let mut status_line = VimStatusLine::default();

        assert_eq!(
            render_search_status(
                &search_state,
                &command_state,
                &leader_state,
                &vim_config,
                &status_line
            ),
            RenderedStatusLine::prompt(String::new())
        );

        search_state.start(SearchDirection::Forward);
        search_state.push_text("λ");
        assert_eq!(
            render_search_status(
                &search_state,
                &command_state,
                &leader_state,
                &vim_config,
                &status_line
            ),
            RenderedStatusLine::prompt(String::from(""))
        );

        let _outcome = search_state.submit("abc", 0);
        status_line.set_error(VimError::PatternNotFound);
        assert_eq!(
            render_search_status(
                &search_state,
                &command_state,
                &leader_state,
                &vim_config,
                &status_line
            ),
            RenderedStatusLine::error(String::from("E486: Pattern not found"))
        );
    }

    #[test]
    fn command_status_takes_precedence() {
        let search_state = VimSearchState::default();
        let mut command_state = VimCommandState::default();
        let leader_state = LeaderState::default();
        let vim_config = VimConfig::default();
        let status_line = VimStatusLine::default();

        command_state.start();
        command_state.push_text("wq");

        assert_eq!(
            render_search_status(
                &search_state,
                &command_state,
                &leader_state,
                &vim_config,
                &status_line
            ),
            RenderedStatusLine::prompt(String::from(":wq"))
        );
    }

    #[test]
    fn leader_menu_renders_from_configured_bindings() {
        let search_state = VimSearchState::default();
        let command_state = VimCommandState::default();
        let mut leader_state = LeaderState::default();
        let vim_config = VimConfig::default();
        let status_line = VimStatusLine::default();

        leader_state.start();
        leader_state.tick(vim_config.leader.delay_ms, vim_config.leader.delay_ms);

        assert_eq!(
            render_search_status(
                &search_state,
                &command_state,
                &leader_state,
                &vim_config,
                &status_line
            ),
            RenderedStatusLine::prompt(String::from(
                "<leader> w write  q quit  x write-quit  n next search  N previous search  f page down  b page up  h help"
            ))
        );
    }
}