use bevy::prelude::Resource;
use super::error::VimError;
use crate::{StatusInfoText, presentation::TransparencyMode};
const MAX_COMMAND_CHARS: usize = 256;
#[derive(Clone, Debug, Default, Eq, PartialEq, Resource)]
pub struct VimCommandState {
active_command: Option<VimCommandText>,
}
impl VimCommandState {
pub fn start(&mut self) {
self.active_command = Some(VimCommandText::new());
}
pub fn start_with(&mut self, text: &str) {
let mut command = VimCommandText::new();
command.push_str(text);
self.active_command = Some(command);
}
pub fn cancel(&mut self) {
self.active_command = None;
}
#[must_use]
pub const fn is_active(&self) -> bool {
self.active_command.is_some()
}
#[must_use]
pub const fn active_command(&self) -> Option<&VimCommandText> {
self.active_command.as_ref()
}
pub fn push_text(&mut self, text: &str) {
if let Some(command) = &mut self.active_command {
command.push_str(text);
}
}
pub fn backspace(&mut self) {
if let Some(command) = &mut self.active_command {
command.pop_char();
}
}
pub fn submit(&mut self) -> Result<VimCommand, VimError> {
let Some(command_text) = self.active_command.take() else {
return Err(VimError::NotAnEditorCommand);
};
command_text.parse()
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct VimCommandText {
text: String,
}
impl VimCommandText {
#[must_use]
pub const fn new() -> Self {
Self {
text: String::new(),
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.text
}
#[must_use]
pub fn status_text(&self) -> StatusInfoText {
StatusInfoText::escaped_display(&self.text)
}
pub fn push_str(&mut self, text: &str) {
let remaining = MAX_COMMAND_CHARS.saturating_sub(self.text.chars().count());
self.text.extend(text.chars().take(remaining));
}
pub fn pop_char(&mut self) {
let _removed = self.text.pop();
}
fn parse(self) -> Result<VimCommand, VimError> {
parse_command(self.text.trim())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum VimCommand {
Write,
Quit(QuitPolicy),
WriteQuit(WriteQuitPolicy),
SetTransparency(TransparencyMode),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QuitPolicy {
PreserveChanges,
Force,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WriteQuitPolicy {
AlwaysWrite,
WriteIfModified,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CommandName {
Write,
Quit,
WriteQuit,
Exit,
Set,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CommandArgSchema {
None,
SetTransparency,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CommandAuthority {
BufferWrite,
Lifecycle,
BufferWriteAndLifecycle,
Presentation,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CommandSpec {
name: CommandName,
aliases: &'static [&'static str],
args: CommandArgSchema,
authority: CommandAuthority,
}
impl CommandSpec {
#[must_use]
pub const fn name(self) -> CommandName {
self.name
}
#[must_use]
pub const fn aliases(self) -> &'static [&'static str] {
self.aliases
}
#[must_use]
pub const fn args(self) -> CommandArgSchema {
self.args
}
#[must_use]
pub const fn authority(self) -> CommandAuthority {
self.authority
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct CommandCatalog;
impl CommandCatalog {
#[must_use]
pub const fn specs(&self) -> &'static [CommandSpec] {
COMMAND_SPECS
}
pub fn parse(&self, text: &str) -> Result<VimCommand, VimError> {
let text = text.trim();
parse_force_variant(text)
.or_else(|| parse_catalog_entry(text))
.ok_or(VimError::NotAnEditorCommand)
}
}
const COMMAND_SPECS: &[CommandSpec] = &[
CommandSpec {
name: CommandName::Write,
aliases: &["w", "write"],
args: CommandArgSchema::None,
authority: CommandAuthority::BufferWrite,
},
CommandSpec {
name: CommandName::Quit,
aliases: &["q", "quit"],
args: CommandArgSchema::None,
authority: CommandAuthority::Lifecycle,
},
CommandSpec {
name: CommandName::WriteQuit,
aliases: &["wq"],
args: CommandArgSchema::None,
authority: CommandAuthority::BufferWriteAndLifecycle,
},
CommandSpec {
name: CommandName::Exit,
aliases: &["x", "xit", "exit"],
args: CommandArgSchema::None,
authority: CommandAuthority::BufferWriteAndLifecycle,
},
CommandSpec {
name: CommandName::Set,
aliases: &["set"],
args: CommandArgSchema::SetTransparency,
authority: CommandAuthority::Presentation,
},
];
fn parse_command(text: &str) -> Result<VimCommand, VimError> {
CommandCatalog.parse(text)
}
fn parse_force_variant(text: &str) -> Option<VimCommand> {
match text {
"q!" | "quit!" => Some(VimCommand::Quit(QuitPolicy::Force)),
"wq!" => Some(VimCommand::WriteQuit(WriteQuitPolicy::AlwaysWrite)),
_ => None,
}
}
fn parse_catalog_entry(text: &str) -> Option<VimCommand> {
let (name, argument) = split_command_name(text);
let spec = COMMAND_SPECS
.iter()
.copied()
.find(|spec| spec.aliases.contains(&name))?;
match spec.name {
CommandName::Write if argument.is_empty() => Some(VimCommand::Write),
CommandName::Quit if argument.is_empty() => {
Some(VimCommand::Quit(QuitPolicy::PreserveChanges))
}
CommandName::WriteQuit if argument.is_empty() => {
Some(VimCommand::WriteQuit(WriteQuitPolicy::AlwaysWrite))
}
CommandName::Exit if argument.is_empty() => {
Some(VimCommand::WriteQuit(WriteQuitPolicy::WriteIfModified))
}
CommandName::Set => parse_set_argument(argument),
_ => None,
}
}
fn split_command_name(text: &str) -> (&str, &str) {
text.split_once(char::is_whitespace)
.map_or((text, ""), |(name, argument)| (name, argument.trim()))
}
fn parse_set_argument(text: &str) -> Option<VimCommand> {
parse_transparency_mode(text).map(VimCommand::SetTransparency)
}
const fn parse_transparency_mode(text: &str) -> Option<TransparencyMode> {
match text.as_bytes() {
b"opaque" => Some(TransparencyMode::Opaque),
b"transparent" => Some(TransparencyMode::Transparent),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{
CommandArgSchema, CommandAuthority, CommandCatalog, CommandName, QuitPolicy, VimCommand,
VimCommandState, VimCommandText, WriteQuitPolicy,
};
use crate::presentation::TransparencyMode;
use crate::vim::VimError;
#[test]
fn parses_common_write_and_quit_commands() {
assert_eq!(
VimCommandText::new().parse(),
Err(VimError::NotAnEditorCommand)
);
let mut command = VimCommandText::new();
command.push_str("wq");
assert_eq!(
command.parse(),
Ok(VimCommand::WriteQuit(WriteQuitPolicy::AlwaysWrite))
);
let mut command = VimCommandText::new();
command.push_str("q!");
assert_eq!(command.parse(), Ok(VimCommand::Quit(QuitPolicy::Force)));
}
#[test]
fn command_state_submits_and_clears_active_command() {
let mut state = VimCommandState::default();
state.start();
state.push_text("write");
assert_eq!(state.submit(), Ok(VimCommand::Write));
assert!(!state.is_active());
}
#[test]
fn parses_transparency_commands() {
let mut command = VimCommandText::new();
command.push_str("set transparent");
assert_eq!(
command.parse(),
Ok(VimCommand::SetTransparency(TransparencyMode::Transparent))
);
let mut command = VimCommandText::new();
command.push_str("set opaque");
assert_eq!(
command.parse(),
Ok(VimCommand::SetTransparency(TransparencyMode::Opaque))
);
let mut command = VimCommandText::new();
command.push_str("set glass");
assert_eq!(command.parse(), Err(VimError::NotAnEditorCommand));
}
#[test]
fn command_catalog_exposes_typed_specs_and_authority() {
let catalog = CommandCatalog;
let write = catalog
.specs()
.iter()
.find(|spec| spec.name() == CommandName::Write)
.unwrap();
assert_eq!(write.aliases(), &["w", "write"]);
assert_eq!(write.args(), CommandArgSchema::None);
assert_eq!(write.authority(), CommandAuthority::BufferWrite);
let set = catalog
.specs()
.iter()
.find(|spec| spec.name() == CommandName::Set)
.unwrap();
assert_eq!(set.args(), CommandArgSchema::SetTransparency);
assert_eq!(set.authority(), CommandAuthority::Presentation);
}
}