use crate::{
ecs::{
components::{
buffer::{EditorView, FocusedEditorView, ViewEntity},
vim::VimModalState,
},
events::input::{EditorInputEvent, KeyInputEvent, TextInputEvent},
},
vim::KeyToken,
};
use bevy::{
input::{ButtonState, keyboard::KeyboardInput},
prelude::{
ButtonInput, KeyCode, MessageReader, MessageWriter, Query, Res, ResMut, Resource, Time,
With,
},
};
pub(super) const KEY_REPEAT_INITIAL_DELAY_SECONDS: f32 = 0.34;
pub(super) const KEY_REPEAT_INTERVAL_SECONDS: f32 = 0.045;
#[derive(Clone, Debug, Default, Resource)]
pub struct VimInputState {
pub(super) repeat: KeyRepeatState,
pub(super) focused_view: Option<ViewEntity>,
}
#[derive(Clone, Debug, Default)]
pub(super) struct KeyRepeatState {
command: Option<RepeatableNormalCommand>,
seconds_until_repeat: f32,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum RepeatableNormalCommand {
Left,
Down,
Up,
Right,
NextWord,
PreviousWord,
RepeatSearchForward,
RepeatSearchBackward,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(super) enum InputModeDispatch {
#[default]
Normal,
Insert,
Prompt,
}
pub fn emit_editor_input_events(
mut keyboard_events: MessageReader<KeyboardInput>,
keys: Res<ButtonInput<KeyCode>>,
time: Res<Time>,
mut input_events: MessageWriter<EditorInputEvent>,
mut input_state: ResMut<VimInputState>,
focused_modes: Query<&VimModalState, (With<EditorView>, With<FocusedEditorView>)>,
) {
let dispatch = InputModeDispatch::from_focused(focused_modes.iter().next());
let time = time.into_inner();
let typed_text: Vec<String> = keyboard_events
.read()
.filter(|event| event.state == ButtonState::Pressed)
.filter_map(|event| dispatch.text_from_event(event))
.collect();
let keys = keys.into_inner();
for token in special_key_tokens(keys).chain(control_key_tokens(keys)) {
let _sent = input_events.write(EditorInputEvent::Key(KeyInputEvent {
token,
repeated: false,
}));
}
for text in &typed_text {
let _sent = input_events.write(EditorInputEvent::Text(TextInputEvent {
text: text.clone(),
}));
}
if dispatch.is_normal_repeat_enabled()
&& typed_text.is_empty()
&& let Some(command) = repeatable_normal_command(keys)
&& input_state
.repeat
.tick(command, key_just_pressed(keys, command), time.delta_secs())
&& !key_just_pressed(keys, command)
{
let _sent = input_events.write(EditorInputEvent::Key(KeyInputEvent {
token: repeatable_command_token(command),
repeated: true,
}));
} else if typed_text.is_empty() || !dispatch.is_normal_repeat_enabled() {
input_state.repeat.cancel();
}
}
impl InputModeDispatch {
const fn from_focused(modal_state: Option<&VimModalState>) -> Self {
let Some(modal_state) = modal_state else {
return Self::Normal;
};
if modal_state.editor.command.is_active() || modal_state.editor.search.is_active() {
Self::Prompt
} else if modal_state.editor.mode.is_insert() {
Self::Insert
} else {
Self::Normal
}
}
fn text_from_event(self, event: &KeyboardInput) -> Option<String> {
let text = event.text.as_ref()?;
let accepted = match self {
Self::Normal | Self::Prompt => text.chars().all(is_prompt_text_character),
Self::Insert => text.chars().all(is_insert_text_character),
};
accepted.then(|| text.to_string())
}
const fn is_normal_repeat_enabled(self) -> bool {
matches!(self, Self::Normal)
}
}
fn repeatable_normal_command(keys: &ButtonInput<KeyCode>) -> Option<RepeatableNormalCommand> {
if keys.pressed(KeyCode::KeyH) {
Some(RepeatableNormalCommand::Left)
} else if keys.pressed(KeyCode::KeyJ) {
Some(RepeatableNormalCommand::Down)
} else if keys.pressed(KeyCode::KeyK) {
Some(RepeatableNormalCommand::Up)
} else if keys.pressed(KeyCode::KeyL) {
Some(RepeatableNormalCommand::Right)
} else if keys.pressed(KeyCode::KeyW) {
Some(RepeatableNormalCommand::NextWord)
} else if keys.pressed(KeyCode::KeyB) {
Some(RepeatableNormalCommand::PreviousWord)
} else if keys.pressed(KeyCode::KeyN) && shift_pressed(keys) {
Some(RepeatableNormalCommand::RepeatSearchBackward)
} else if keys.pressed(KeyCode::KeyN) {
Some(RepeatableNormalCommand::RepeatSearchForward)
} else {
None
}
}
fn key_just_pressed(keys: &ButtonInput<KeyCode>, command: RepeatableNormalCommand) -> bool {
match command {
RepeatableNormalCommand::Left => keys.just_pressed(KeyCode::KeyH),
RepeatableNormalCommand::Down => keys.just_pressed(KeyCode::KeyJ),
RepeatableNormalCommand::Up => keys.just_pressed(KeyCode::KeyK),
RepeatableNormalCommand::Right => keys.just_pressed(KeyCode::KeyL),
RepeatableNormalCommand::NextWord => keys.just_pressed(KeyCode::KeyW),
RepeatableNormalCommand::PreviousWord => keys.just_pressed(KeyCode::KeyB),
RepeatableNormalCommand::RepeatSearchForward
| RepeatableNormalCommand::RepeatSearchBackward => keys.just_pressed(KeyCode::KeyN),
}
}
impl KeyRepeatState {
pub(super) const fn cancel(&mut self) {
self.command = None;
self.seconds_until_repeat = 0.0;
}
pub(super) fn tick(
&mut self,
command: RepeatableNormalCommand,
just_pressed: bool,
delta_seconds: f32,
) -> bool {
if just_pressed || self.command != Some(command) {
self.command = Some(command);
self.seconds_until_repeat = KEY_REPEAT_INITIAL_DELAY_SECONDS;
return just_pressed;
}
self.seconds_until_repeat -= delta_seconds;
if self.seconds_until_repeat <= 0.0 {
self.seconds_until_repeat += KEY_REPEAT_INTERVAL_SECONDS;
true
} else {
false
}
}
}
fn control_key_tokens(keys: &ButtonInput<KeyCode>) -> impl Iterator<Item = KeyToken> {
let control_pressed = keys.pressed(KeyCode::ControlLeft) || keys.pressed(KeyCode::ControlRight);
[
(KeyCode::KeyF, KeyToken::Ctrl('f')),
(KeyCode::KeyB, KeyToken::Ctrl('b')),
(KeyCode::KeyR, KeyToken::Ctrl('r')),
]
.into_iter()
.filter_map(move |(key_code, token)| {
(control_pressed && keys.just_pressed(key_code)).then_some(token)
})
}
fn special_key_tokens(keys: &ButtonInput<KeyCode>) -> impl Iterator<Item = KeyToken> + '_ {
[
(KeyCode::Escape, KeyToken::Escape),
(KeyCode::Enter, KeyToken::Enter),
(KeyCode::Backspace, KeyToken::Backspace),
]
.into_iter()
.filter_map(|(key_code, token)| keys.just_pressed(key_code).then_some(token))
}
#[cfg(test)]
pub(super) fn normalized_key_tokens(
keys: &ButtonInput<KeyCode>,
typed_text: &[String],
) -> impl Iterator<Item = KeyToken> {
control_key_tokens(keys).chain(
typed_text
.iter()
.flat_map(|text| text.chars())
.map(KeyToken::Char),
)
}
pub(super) fn normalized_editor_tokens(
input_events: &[EditorInputEvent],
) -> impl Iterator<Item = KeyToken> + '_ {
input_events.iter().flat_map(|event| match event {
EditorInputEvent::Key(key) => vec![key.token],
EditorInputEvent::Text(text) => text.text.chars().map(KeyToken::Char).collect(),
})
}
const fn repeatable_command_token(command: RepeatableNormalCommand) -> KeyToken {
match command {
RepeatableNormalCommand::Left => KeyToken::Char('h'),
RepeatableNormalCommand::Down => KeyToken::Char('j'),
RepeatableNormalCommand::Up => KeyToken::Char('k'),
RepeatableNormalCommand::Right => KeyToken::Char('l'),
RepeatableNormalCommand::NextWord => KeyToken::Char('w'),
RepeatableNormalCommand::PreviousWord => KeyToken::Char('b'),
RepeatableNormalCommand::RepeatSearchForward => KeyToken::Char('n'),
RepeatableNormalCommand::RepeatSearchBackward => KeyToken::Char('N'),
}
}
fn shift_pressed(keys: &ButtonInput<KeyCode>) -> bool {
keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight)
}
pub(super) fn is_prompt_text_character(character: char) -> bool {
!character.is_control()
}
fn is_insert_text_character(character: char) -> bool {
!character.is_control() || matches!(character, '\n' | '\t')
}