#![allow(clippy::missing_panics_doc)]
use std::sync::RwLock;
use {
reovim_driver_input::{
ExtensionMap, KeyCode, KeyEvent, KeySequence, ModeKeyResolver, ModeState, Modifiers,
ResolveContext, ResolveInput, ResolveResult, SessionApiDyn,
},
reovim_kernel::api::v1::ModeId,
};
use {
super::operator_common::{KeymapAction, apply_keymap_policy, is_count_digit, is_escape},
crate::modes::VimMode,
};
#[derive(Debug, Clone)]
pub struct VisualState {
pub motion_count: Option<usize>,
pub pending_keys: KeySequence,
pub initialized: bool,
}
impl VisualState {
const fn new() -> Self {
Self {
motion_count: None,
pending_keys: KeySequence::new(),
initialized: false,
}
}
pub const fn has_motion_count(&self) -> bool {
self.motion_count.is_some()
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn accumulate_motion_count(&mut self, key: &KeyEvent) {
if let KeyCode::Char(c @ '0'..='9') = key.code {
let digit = c.to_digit(10).expect("valid digit") as usize;
self.motion_count = Some(self.motion_count.unwrap_or(0) * 10 + digit);
}
}
#[allow(clippy::missing_const_for_fn)] fn take_motion_count(&mut self) -> Option<usize> {
self.motion_count.take()
}
const fn explicit_count(&self) -> Option<usize> {
self.motion_count
}
fn push_key(&mut self, key: KeyEvent) {
self.pending_keys.push(key);
}
fn keys(&self) -> KeySequence {
self.pending_keys.clone()
}
fn clear_keys(&mut self) {
self.pending_keys.clear();
}
fn reset(&mut self) {
self.motion_count = None;
self.pending_keys.clear();
self.initialized = false;
}
}
pub struct VimVisualResolver {
mode_id: ModeId,
parent_mode_id: ModeId,
state: RwLock<VisualState>,
}
impl VimVisualResolver {
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn new(mode_id: ModeId) -> Self {
Self {
mode_id,
parent_mode_id: VimMode::NORMAL_ID,
state: RwLock::new(VisualState::new()),
}
}
#[must_use]
pub fn character_wise() -> Self {
Self::new(VimMode::VISUAL_ID)
}
#[must_use]
pub fn line_wise() -> Self {
Self::new(VimMode::VISUAL_LINE_ID)
}
#[must_use]
pub fn block_wise() -> Self {
Self::new(VimMode::VISUAL_BLOCK_ID)
}
fn clear_state(&self) {
self.state.write().expect("lock poisoned").reset();
}
#[cfg(test)]
pub fn state(&self) -> VisualState {
self.state.read().expect("lock poisoned").clone()
}
}
impl ModeKeyResolver for VimVisualResolver {
#[cfg_attr(coverage_nightly, coverage(off))]
fn resolve_with_keymap(
&self,
key: &KeyEvent,
_state: &mut ModeState,
input: &ResolveInput<'_>,
) -> ResolveResult {
if is_escape(key)
|| (key.code == KeyCode::Char('c') && key.modifiers.contains(Modifiers::CTRL))
{
self.clear_state();
return ResolveResult::Execute(crate::ids::EXIT_VISUAL, ResolveContext::new());
}
let mut state = self.state.write().expect("lock poisoned");
if is_count_digit(key, state.has_motion_count()) {
state.accumulate_motion_count(key);
return ResolveResult::Pending;
}
state.push_key(*key);
let keys = state.keys();
let lookup_state = {
let visual_lookup = input.keymap.query(input.mode, &keys);
if matches!(visual_lookup, reovim_driver_input::KeyLookupState::NotFound) {
input.keymap.query(&self.parent_mode_id, &keys)
} else {
visual_lookup
}
};
match apply_keymap_policy(&lookup_state) {
KeymapAction::Execute(cmd) => {
let explicit_count = state.explicit_count();
let _motion_count = state.take_motion_count();
state.clear_keys();
drop(state);
let ctx = ResolveContext {
count: explicit_count,
register: None,
keys,
metadata: std::collections::HashMap::new(),
};
ResolveResult::Execute(cmd, ctx)
}
KeymapAction::Pending => {
drop(state);
ResolveResult::Pending
}
KeymapAction::Cancel => {
state.clear_keys();
drop(state);
ResolveResult::NotHandled
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn resolve_with_session(
&self,
key: &KeyEvent,
_mstate: &mut ModeState,
input: &ResolveInput<'_>,
_session: &mut dyn SessionApiDyn,
_shared_extensions: &mut ExtensionMap,
client_extensions: &mut ExtensionMap,
) -> ResolveResult {
tracing::debug!(key = ?key, mode = ?self.mode_id, "visual resolver: resolve_with_session");
if is_escape(key)
|| (key.code == KeyCode::Char('c') && key.modifiers.contains(Modifiers::CTRL))
{
tracing::debug!("visual resolver: escape/ctrl-c - executing EXIT_VISUAL command");
self.clear_state();
return ResolveResult::Execute(crate::ids::EXIT_VISUAL, ResolveContext::new());
}
let mut state = self.state.write().expect("lock poisoned");
if !state.initialized {
if let Some(vim) = client_extensions.get_mut::<crate::VimSessionState>() {
if let Some(count) = vim.pending_count.take() {
state.motion_count = Some(count);
tracing::debug!(count, "visual resolver: inherited count from normal mode");
}
}
state.initialized = true;
}
if is_count_digit(key, state.has_motion_count()) {
state.accumulate_motion_count(key);
tracing::debug!(count = ?state.motion_count, "visual resolver: count digit");
return ResolveResult::Pending;
}
state.push_key(*key);
let keys = state.keys();
let lookup_state = {
let visual_lookup = input.keymap.query(input.mode, &keys);
if matches!(visual_lookup, reovim_driver_input::KeyLookupState::NotFound) {
input.keymap.query(&self.parent_mode_id, &keys)
} else {
visual_lookup
}
};
tracing::debug!(?lookup_state, ?keys, "visual resolver: keymap lookup");
match apply_keymap_policy(&lookup_state) {
KeymapAction::Execute(cmd) => {
let explicit_count = state.explicit_count();
let _motion_count = state.take_motion_count();
state.clear_keys();
drop(state);
tracing::debug!(
cmd = %cmd,
explicit_count = ?explicit_count,
"visual resolver: executing command"
);
let ctx = ResolveContext {
count: explicit_count,
register: None,
keys,
metadata: std::collections::HashMap::new(),
};
ResolveResult::Execute(cmd, ctx)
}
KeymapAction::Pending => {
drop(state);
tracing::debug!("visual resolver: waiting for more keys");
ResolveResult::Pending
}
KeymapAction::Cancel => {
state.clear_keys();
drop(state);
tracing::debug!("visual resolver: key not found, returning NotHandled");
ResolveResult::NotHandled
}
}
}
fn mode_id(&self) -> &ModeId {
&self.mode_id
}
fn inherits_from(&self) -> Option<&ModeId> {
Some(&self.parent_mode_id)
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn pending_keys(&self) -> KeySequence {
self.state.read().expect("lock poisoned").keys()
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn reset(&mut self) {
self.state.write().expect("lock poisoned").reset();
}
}