alma 0.1.0

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! Typed Vim configuration defaults.

use super::{KeyToken, LeaderConfig, NormalCommand, motion::Motion};
use bevy::prelude::Resource;

/// Vim configuration resource.
#[derive(Clone, Debug, Default, Resource)]
pub struct VimConfig {
    /// Mode-specific key mappings.
    pub keymaps: KeymapSet,
    /// Editor options used by Vim subsystems.
    pub options: VimOptions,
    /// Motion behavior options.
    pub motions: MotionConfig,
    /// Leader-key behavior.
    pub leader: LeaderConfig,
}

/// User-visible Vim options with typed defaults.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VimOptions {
    /// Ignore case while searching.
    pub ignore_case: bool,
    /// Re-enable case-sensitive search when the pattern contains uppercase characters.
    pub smart_case: bool,
    /// Wrap searches around file edges.
    pub wrap_scan: bool,
    /// Mapping timeout in milliseconds.
    pub timeout_len_ms: u64,
}

impl Default for VimOptions {
    fn default() -> Self {
        Self {
            ignore_case: true,
            smart_case: true,
            wrap_scan: true,
            timeout_len_ms: 1_000,
        }
    }
}

/// Motion behavior configuration.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct MotionConfig {}

/// A collection of keymaps.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct KeymapSet {
    /// Stored mappings in lookup order.
    mappings: Vec<Keymap>,
}

impl KeymapSet {
    /// Returns configured mappings.
    #[must_use]
    pub fn mappings(&self) -> &[Keymap] {
        &self.mappings
    }
}

impl Default for KeymapSet {
    fn default() -> Self {
        Self {
            mappings: default_normal_keymaps(),
        }
    }
}

/// One typed key mapping.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Keymap {
    /// Modes where this map applies.
    pub modes: ModeSet,
    /// Left-hand key sequence.
    pub lhs: KeySequence,
    /// Right-hand action.
    pub rhs: KeyAction,
    /// Whether rhs may be remapped.
    pub remap: bool,
}

/// Modes where a keymap applies.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ModeSet {
    /// Applies to normal mode.
    normal: bool,
    /// Applies to visual modes.
    visual: bool,
}

impl ModeSet {
    /// Normal mode only.
    #[must_use]
    pub const fn normal() -> Self {
        Self {
            normal: true,
            visual: false,
        }
    }
}

/// A normalized key sequence.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct KeySequence(Vec<KeyToken>);

impl KeySequence {
    /// Creates a key sequence.
    #[must_use]
    pub fn new(tokens: impl Into<Vec<KeyToken>>) -> Self {
        Self(tokens.into())
    }

    /// Reads the normalized tokens in this sequence.
    #[must_use]
    pub fn as_slice(&self) -> &[KeyToken] {
        &self.0
    }
}

/// A typed key action.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum KeyAction {
    /// Builtin editor action.
    Builtin(BuiltinAction),
    /// Parsed normal-mode command.
    Command(NormalCommand),
    /// Ex command text.
    Ex(String),
}

/// Builtin actions that require application state beyond pure command execution.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BuiltinAction {
    /// Repeat the most recent `/` or `?` search forward.
    RepeatSearchForward,
    /// Repeat the most recent `/` or `?` search backward.
    RepeatSearchBackward,
    /// Repeat the last character search.
    RepeatCharSearch,
    /// Repeat the last character search in the opposite direction.
    RepeatCharSearchReversed,
    /// Apply a motion.
    Motion(Motion),
}

/// Builds the default normal-mode keymaps.
fn default_normal_keymaps() -> Vec<Keymap> {
    [
        ('h', Motion::Left),
        ('j', Motion::Down),
        ('k', Motion::Up),
        ('l', Motion::Right),
    ]
    .into_iter()
    .map(|(key, motion)| Keymap {
        modes: ModeSet::normal(),
        lhs: KeySequence::new([KeyToken::Char(key)]),
        rhs: KeyAction::Builtin(BuiltinAction::Motion(motion)),
        remap: false,
    })
    .collect()
}