reovim_module_vim/resolvers/
visual.rs1#![allow(clippy::missing_panics_doc)]
4
5use std::sync::RwLock;
34
35use {
36 reovim_driver_input::{
37 ExtensionMap, KeyCode, KeyEvent, KeySequence, ModeKeyResolver, ModeState, Modifiers,
38 ResolveContext, ResolveInput, ResolveResult, SessionApiDyn,
39 },
40 reovim_kernel::api::v1::ModeId,
41};
42
43use {
44 super::operator_common::{KeymapAction, apply_keymap_policy, is_count_digit, is_escape},
45 crate::modes::VimMode,
46};
47
48#[derive(Debug, Clone)]
55pub struct VisualState {
56 pub motion_count: Option<usize>,
58 pub pending_keys: KeySequence,
60 pub initialized: bool,
62}
63
64impl VisualState {
65 const fn new() -> Self {
67 Self {
68 motion_count: None,
69 pending_keys: KeySequence::new(),
70 initialized: false,
71 }
72 }
73
74 pub const fn has_motion_count(&self) -> bool {
76 self.motion_count.is_some()
77 }
78
79 #[cfg_attr(coverage_nightly, coverage(off))]
81 fn accumulate_motion_count(&mut self, key: &KeyEvent) {
82 if let KeyCode::Char(c @ '0'..='9') = key.code {
83 let digit = c.to_digit(10).expect("valid digit") as usize;
84 self.motion_count = Some(self.motion_count.unwrap_or(0) * 10 + digit);
85 }
86 }
87
88 #[allow(clippy::missing_const_for_fn)] fn take_motion_count(&mut self) -> Option<usize> {
91 self.motion_count.take()
92 }
93
94 const fn explicit_count(&self) -> Option<usize> {
96 self.motion_count
97 }
98
99 fn push_key(&mut self, key: KeyEvent) {
101 self.pending_keys.push(key);
102 }
103
104 fn keys(&self) -> KeySequence {
106 self.pending_keys.clone()
107 }
108
109 fn clear_keys(&mut self) {
111 self.pending_keys.clear();
112 }
113
114 fn reset(&mut self) {
116 self.motion_count = None;
117 self.pending_keys.clear();
118 self.initialized = false;
119 }
120}
121
122pub struct VimVisualResolver {
136 mode_id: ModeId,
138 parent_mode_id: ModeId,
140 state: RwLock<VisualState>,
142}
143
144impl VimVisualResolver {
145 #[must_use]
151 #[allow(clippy::missing_const_for_fn)] pub fn new(mode_id: ModeId) -> Self {
153 Self {
154 mode_id,
155 parent_mode_id: VimMode::NORMAL_ID,
156 state: RwLock::new(VisualState::new()),
157 }
158 }
159
160 #[must_use]
162 pub fn character_wise() -> Self {
163 Self::new(VimMode::VISUAL_ID)
164 }
165
166 #[must_use]
168 pub fn line_wise() -> Self {
169 Self::new(VimMode::VISUAL_LINE_ID)
170 }
171
172 #[must_use]
174 pub fn block_wise() -> Self {
175 Self::new(VimMode::VISUAL_BLOCK_ID)
176 }
177
178 fn clear_state(&self) {
180 self.state.write().expect("lock poisoned").reset();
181 }
182
183 #[cfg(test)]
185 pub fn state(&self) -> VisualState {
186 self.state.read().expect("lock poisoned").clone()
187 }
188}
189
190impl ModeKeyResolver for VimVisualResolver {
191 #[cfg_attr(coverage_nightly, coverage(off))]
192 fn resolve_with_keymap(
193 &self,
194 key: &KeyEvent,
195 _state: &mut ModeState,
196 input: &ResolveInput<'_>,
197 ) -> ResolveResult {
198 if is_escape(key)
201 || (key.code == KeyCode::Char('c') && key.modifiers.contains(Modifiers::CTRL))
202 {
203 self.clear_state();
204 return ResolveResult::Execute(crate::ids::EXIT_VISUAL, ResolveContext::new());
205 }
206
207 let mut state = self.state.write().expect("lock poisoned");
208
209 if is_count_digit(key, state.has_motion_count()) {
211 state.accumulate_motion_count(key);
212 return ResolveResult::Pending;
213 }
214
215 state.push_key(*key);
217 let keys = state.keys();
218
219 let lookup_state = {
221 let visual_lookup = input.keymap.query(input.mode, &keys);
222 if matches!(visual_lookup, reovim_driver_input::KeyLookupState::NotFound) {
223 input.keymap.query(&self.parent_mode_id, &keys)
225 } else {
226 visual_lookup
227 }
228 };
229
230 match apply_keymap_policy(&lookup_state) {
231 KeymapAction::Execute(cmd) => {
232 let explicit_count = state.explicit_count();
233 let _motion_count = state.take_motion_count();
234 state.clear_keys();
235 drop(state);
236
237 let ctx = ResolveContext {
239 count: explicit_count,
240 register: None,
241 keys,
242 metadata: std::collections::HashMap::new(),
243 };
244
245 ResolveResult::Execute(cmd, ctx)
246 }
247 KeymapAction::Pending => {
248 drop(state);
249 ResolveResult::Pending
250 }
251 KeymapAction::Cancel => {
252 state.clear_keys();
253 drop(state);
254 ResolveResult::NotHandled
256 }
257 }
258 }
259
260 #[cfg_attr(coverage_nightly, coverage(off))]
261 fn resolve_with_session(
262 &self,
263 key: &KeyEvent,
264 _mstate: &mut ModeState,
265 input: &ResolveInput<'_>,
266 _session: &mut dyn SessionApiDyn,
267 _shared_extensions: &mut ExtensionMap,
268 client_extensions: &mut ExtensionMap,
269 ) -> ResolveResult {
270 tracing::debug!(key = ?key, mode = ?self.mode_id, "visual resolver: resolve_with_session");
271
272 if is_escape(key)
275 || (key.code == KeyCode::Char('c') && key.modifiers.contains(Modifiers::CTRL))
276 {
277 tracing::debug!("visual resolver: escape/ctrl-c - executing EXIT_VISUAL command");
278 self.clear_state();
279 return ResolveResult::Execute(crate::ids::EXIT_VISUAL, ResolveContext::new());
280 }
281
282 let mut state = self.state.write().expect("lock poisoned");
283
284 if !state.initialized {
286 if let Some(vim) = client_extensions.get_mut::<crate::VimSessionState>() {
287 if let Some(count) = vim.pending_count.take() {
289 state.motion_count = Some(count);
290 tracing::debug!(count, "visual resolver: inherited count from normal mode");
291 }
292 }
293 state.initialized = true;
294 }
295
296 if is_count_digit(key, state.has_motion_count()) {
298 state.accumulate_motion_count(key);
299 tracing::debug!(count = ?state.motion_count, "visual resolver: count digit");
300 return ResolveResult::Pending;
301 }
302
303 state.push_key(*key);
305 let keys = state.keys();
306
307 let lookup_state = {
309 let visual_lookup = input.keymap.query(input.mode, &keys);
310 if matches!(visual_lookup, reovim_driver_input::KeyLookupState::NotFound) {
311 input.keymap.query(&self.parent_mode_id, &keys)
313 } else {
314 visual_lookup
315 }
316 };
317
318 tracing::debug!(?lookup_state, ?keys, "visual resolver: keymap lookup");
319
320 match apply_keymap_policy(&lookup_state) {
321 KeymapAction::Execute(cmd) => {
322 let explicit_count = state.explicit_count();
323 let _motion_count = state.take_motion_count();
324 state.clear_keys();
325 drop(state);
326
327 tracing::debug!(
328 cmd = %cmd,
329 explicit_count = ?explicit_count,
330 "visual resolver: executing command"
331 );
332
333 let ctx = ResolveContext {
335 count: explicit_count,
336 register: None,
337 keys,
338 metadata: std::collections::HashMap::new(),
339 };
340
341 ResolveResult::Execute(cmd, ctx)
342 }
343 KeymapAction::Pending => {
344 drop(state);
345 tracing::debug!("visual resolver: waiting for more keys");
346 ResolveResult::Pending
347 }
348 KeymapAction::Cancel => {
349 state.clear_keys();
350 drop(state);
351 tracing::debug!("visual resolver: key not found, returning NotHandled");
352 ResolveResult::NotHandled
355 }
356 }
357 }
358
359 fn mode_id(&self) -> &ModeId {
360 &self.mode_id
361 }
362
363 fn inherits_from(&self) -> Option<&ModeId> {
364 Some(&self.parent_mode_id)
365 }
366
367 #[cfg_attr(coverage_nightly, coverage(off))]
368 fn pending_keys(&self) -> KeySequence {
369 self.state.read().expect("lock poisoned").keys()
370 }
371
372 #[cfg_attr(coverage_nightly, coverage(off))]
373 fn reset(&mut self) {
374 self.state.write().expect("lock poisoned").reset();
375 }
376}