use crate::vim::{
LeaderState, NormalState, RegisterBank, RegisterBankShape, RepeatState, RepeatStateShape,
VimCommandState, VimMode, VimSearchState, VimSelectionState, VimStatusLine, VisualState,
};
use bevy::prelude::Component;
use std::fmt::{Debug, Formatter};
#[derive(Clone, Component, Default)]
pub struct VimModalState {
pub grammar: VimGrammarState,
pub editor: VimEditorState,
}
#[derive(Clone, Debug, Default)]
pub struct VimGrammarState {
pub normal: NormalState,
pub visual: VisualState,
}
impl VimGrammarState {
pub const fn reset(&mut self) {
self.normal.reset_grammar();
self.visual.reset_grammar();
}
}
#[derive(Clone, Default)]
pub struct VimEditorState {
pub mode: VimMode,
pub selection: VimSelectionState,
pub search: VimSearchState,
pub command: VimCommandState,
pub leader: LeaderState,
pub status: VimStatusLine,
pub registers: RegisterBank,
pub repeat: RepeatState,
}
impl Debug for VimEditorState {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("VimEditorState")
.field("shape", &VimEditorStateShape::from(self))
.finish()
}
}
impl Debug for VimModalState {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("VimModalState")
.field("shape", &VimModalStateShape::from(self))
.finish()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct VimEditorStateShape {
mode: VimMode,
selection: SelectionShape,
search: SearchShape,
command: PromptShape,
leader: LeaderShape,
status: StatusShape,
registers: RegisterBankShape,
repeat: RepeatStateShape,
}
impl VimEditorStateShape {
#[must_use]
pub fn from_editor_state(state: &VimEditorState) -> Self {
Self::from(state)
}
}
impl From<&VimEditorState> for VimEditorStateShape {
fn from(state: &VimEditorState) -> Self {
Self {
mode: state.mode,
selection: SelectionShape::from_active(state.selection.selection().is_some()),
search: SearchShape {
prompt: PromptShape::from_active(state.search.is_active()),
previous: PreviousSearchShape::from_present(state.search.last_query().is_some()),
},
command: PromptShape::from_active(state.command.is_active()),
leader: LeaderShape::from_state(&state.leader),
status: StatusShape::from_present(state.status.message().is_some()),
registers: RegisterBankShape::from_register_bank(&state.registers),
repeat: RepeatStateShape::from_repeat_state(&state.repeat),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct VimModalStateShape {
mode: VimMode,
selection: SelectionShape,
search: SearchShape,
command: PromptShape,
leader: LeaderShape,
status: StatusShape,
registers: RegisterBankShape,
repeat: RepeatStateShape,
}
impl VimModalStateShape {
#[must_use]
pub fn from_modal_state(state: &VimModalState) -> Self {
Self::from(state)
}
}
impl From<&VimModalState> for VimModalStateShape {
fn from(state: &VimModalState) -> Self {
Self::from(&state.editor)
}
}
impl From<&VimEditorState> for VimModalStateShape {
fn from(state: &VimEditorState) -> Self {
let shape = VimEditorStateShape::from(state);
Self {
mode: shape.mode,
selection: shape.selection,
search: shape.search,
command: shape.command,
leader: shape.leader,
status: shape.status,
registers: shape.registers,
repeat: shape.repeat,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum SelectionShape {
Inactive,
Active,
}
impl SelectionShape {
const fn from_active(active: bool) -> Self {
if active { Self::Active } else { Self::Inactive }
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum PromptShape {
Inactive,
Active,
}
impl PromptShape {
const fn from_active(active: bool) -> Self {
if active { Self::Active } else { Self::Inactive }
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct SearchShape {
prompt: PromptShape,
previous: PreviousSearchShape,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum PreviousSearchShape {
Empty,
Present,
}
impl PreviousSearchShape {
const fn from_present(present: bool) -> Self {
if present { Self::Present } else { Self::Empty }
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum LeaderShape {
Idle,
Pending,
MenuVisible,
}
impl LeaderShape {
const fn from_state(state: &LeaderState) -> Self {
if state.is_menu_visible() {
Self::MenuVisible
} else if state.is_pending() {
Self::Pending
} else {
Self::Idle
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum StatusShape {
Clear,
Present,
}
impl StatusShape {
const fn from_present(present: bool) -> Self {
if present { Self::Present } else { Self::Clear }
}
}
#[cfg(test)]
mod tests {
use super::VimModalState;
use crate::text_stream::TextByteStream;
use crate::vim::{OperatorTarget, RegisterName, RegisterText, SearchDirection};
#[test]
fn modal_state_debug_output_redacts_prompt_text() {
let mut state = VimModalState::default();
state.editor.command.start_with("secret-write-command");
state.editor.search.start(SearchDirection::Forward);
state.editor.search.push_text("secret-search-query");
let debug = format!("{state:?}");
assert!(debug.contains("VimModalState"));
assert!(debug.contains("command"));
assert!(debug.contains("search"));
assert!(!debug.contains("secret-write-command"));
assert!(!debug.contains("secret-search-query"));
}
#[test]
fn modal_state_debug_output_redacts_register_text() {
let text = "secret-register-text";
let stream = TextByteStream::new(text);
let target = OperatorTarget::characterwise(&stream, 0..text.len()).unwrap();
let mut state = VimModalState::default();
state.editor.registers.write_yank(&stream, target).unwrap();
let debug = format!("{state:?}");
assert!(debug.contains("registers"));
assert!(debug.contains("byte_len"));
assert!(!debug.contains("secret-register-text"));
}
#[test]
fn modal_state_registers_are_view_local() {
let first_text = "first-view-secret";
let second_text = "second-view-secret";
let first_stream = TextByteStream::new(first_text);
let second_stream = TextByteStream::new(second_text);
let first_target =
OperatorTarget::characterwise(&first_stream, 0..first_text.len()).unwrap();
let second_target =
OperatorTarget::characterwise(&second_stream, 0..second_text.len()).unwrap();
let mut first = VimModalState::default();
let mut second = VimModalState::default();
first
.editor
.registers
.write_yank(&first_stream, first_target)
.unwrap();
second
.editor
.registers
.write_yank(&second_stream, second_target)
.unwrap();
assert_eq!(
first
.editor
.registers
.get(RegisterName::Unnamed)
.map(RegisterText::as_str),
Some(first_text)
);
assert_eq!(
second
.editor
.registers
.get(RegisterName::Unnamed)
.map(RegisterText::as_str),
Some(second_text)
);
}
}