Skip to main content

reovim_module_vim/resolvers/
case.rs

1// Methods made `pub` for test access from `resolvers::tests::case`.
2// This module is private, so `pub` is effectively crate-internal.
3#![allow(clippy::missing_panics_doc)]
4
5//! Case operator mode key resolver.
6//!
7//! This resolver handles the `vim:lowercase`, `vim:uppercase`, and
8//! `vim:toggle-case` modes. These are entered via `gu`, `gU`, `g~` in
9//! normal mode. The resolver waits for a motion or text object to define
10//! the range, then dispatches the case transformation operator.
11//!
12//! A single `VimCaseResolver` struct is parameterized by `OperatorType`
13//! to avoid duplicating the entire resolver for each case variant.
14
15use std::sync::RwLock;
16
17use {
18    reovim_driver_input::{
19        ExtensionMap, KeyEvent, KeySequence, ModeKeyResolver, ModeState, ModeTransition,
20        ResolveContext, ResolveInput, ResolveResult, SessionApiDyn,
21    },
22    reovim_driver_session::OperatorPendingState,
23    reovim_kernel::api::v1::{ModeId, Position},
24};
25
26use {
27    super::operator_common::{
28        KeymapAction, OperatorState, OperatorType, apply_keymap_policy, build_cancelled,
29        build_operator_execute, is_count_digit, is_escape, is_inclusive_motion,
30        is_line_operator_key, is_linewise_motion,
31    },
32    crate::{modes::VimMode, session_state::PendingMotion},
33};
34
35/// Parameterized case operator resolver.
36///
37/// Handles `gu{motion}`, `gU{motion}`, `g~{motion}` and their
38/// linewise forms `guu`, `gUU`, `g~~`.
39pub struct VimCaseResolver {
40    /// The specific case operator type.
41    operator_type: OperatorType,
42    /// Mode ID for this operator mode.
43    mode_id: ModeId,
44    /// Parent mode ID (normal mode) for inheritance.
45    parent_mode_id: ModeId,
46    /// Operator state owned by this resolver.
47    pub state: RwLock<OperatorState>,
48}
49
50impl VimCaseResolver {
51    /// Create a new case resolver for the given operator type.
52    #[must_use]
53    pub fn new(operator_type: OperatorType) -> Self {
54        let mode_id = operator_type.mode_id();
55        Self {
56            operator_type,
57            mode_id,
58            parent_mode_id: VimMode::NORMAL_ID,
59            state: RwLock::new(OperatorState::new(operator_type)),
60        }
61    }
62
63    /// Get a clone of the current state (for testing).
64    #[cfg(test)]
65    pub fn state(&self) -> OperatorState {
66        self.state.read().expect("lock poisoned").clone()
67    }
68
69    /// Clear all internal state.
70    fn clear_state(&self) {
71        self.state.write().expect("lock poisoned").reset();
72    }
73}
74
75#[cfg_attr(coverage_nightly, coverage(off))]
76impl ModeKeyResolver for VimCaseResolver {
77    fn resolve_with_keymap(
78        &self,
79        key: &KeyEvent,
80        _state: &mut ModeState,
81        input: &ResolveInput<'_>,
82    ) -> ResolveResult {
83        if is_escape(key) {
84            self.clear_state();
85            return ResolveResult::ModeTransition(build_cancelled());
86        }
87
88        let mut state = self.state.write().expect("lock poisoned");
89
90        if is_count_digit(key, state.has_motion_count()) {
91            state.accumulate_motion_count(key);
92            return ResolveResult::Pending;
93        }
94
95        // Check for line operator (guu, gUU, g~~)
96        if is_line_operator_key(key, self.operator_type) {
97            let count = state.operator_count;
98            let motion_count = state.take_motion_count().unwrap_or(1);
99            let register = state.register;
100            state.clear_keys();
101            drop(state);
102
103            return ResolveResult::ModeTransition(ModeTransition::Pop {
104                result: Some(build_operator_execute(
105                    self.operator_type,
106                    Position::new(0, 0),
107                    Position::new(0, 0),
108                    true,
109                    Some(count.unwrap_or(1) * motion_count),
110                    register,
111                )),
112            });
113        }
114
115        state.push_key(*key);
116        let keys = state.keys();
117
118        let lookup_state = {
119            let state = input.keymap.query(input.mode, &keys);
120            if matches!(state, reovim_driver_input::KeyLookupState::NotFound) {
121                input.keymap.query(&self.parent_mode_id, &keys)
122            } else {
123                state
124            }
125        };
126
127        match apply_keymap_policy(&lookup_state) {
128            KeymapAction::Execute(cmd) => {
129                let explicit_count = state.explicit_count();
130                let _motion_count = state.take_motion_count();
131                state.clear_keys();
132                drop(state);
133
134                let ctx = ResolveContext {
135                    count: explicit_count,
136                    register: None,
137                    keys,
138                    metadata: std::collections::HashMap::new(),
139                };
140
141                ResolveResult::Execute(cmd, ctx)
142            }
143            KeymapAction::Pending => {
144                drop(state);
145                ResolveResult::Pending
146            }
147            KeymapAction::Cancel => {
148                state.clear_keys();
149                drop(state);
150                self.clear_state();
151                ResolveResult::ModeTransition(build_cancelled())
152            }
153        }
154    }
155
156    #[allow(clippy::too_many_lines)]
157    #[cfg_attr(coverage_nightly, coverage(off))]
158    fn resolve_with_session(
159        &self,
160        key: &KeyEvent,
161        _mstate: &mut ModeState,
162        input: &ResolveInput<'_>,
163        session: &mut dyn SessionApiDyn,
164        _shared_extensions: &mut ExtensionMap,
165        client_extensions: &mut ExtensionMap,
166    ) -> ResolveResult {
167        tracing::debug!(key = ?key, operator = ?self.operator_type, "case resolver: resolve_with_session");
168
169        if is_escape(key) {
170            self.clear_state();
171            return ResolveResult::ModeTransition(build_cancelled());
172        }
173
174        let mut state = self.state.write().expect("lock poisoned");
175
176        // Initialize from VimSessionState on first key
177        if !state.initialized {
178            if let Some(vim) = client_extensions.get_mut::<crate::VimSessionState>() {
179                state.operator_count = vim.pending_count.take();
180                state.register = vim.pending_register.take();
181            }
182            state.initialized = true;
183        }
184
185        if is_count_digit(key, state.has_motion_count()) {
186            state.accumulate_motion_count(key);
187            return ResolveResult::Pending;
188        }
189
190        // Check for line operator (guu, gUU, g~~)
191        if is_line_operator_key(key, self.operator_type) {
192            let count = state.operator_count;
193            let motion_count = state.take_motion_count().unwrap_or(1);
194            let register = state.register;
195            state.clear_keys();
196            drop(state);
197
198            let (start, end) = session.cursor_position().map_or_else(
199                || (Position::new(0, 0), Position::new(0, 0)),
200                |cursor_pos| {
201                    let start = Position::new(cursor_pos.line, 0);
202                    let total_count = count.unwrap_or(1) * motion_count;
203                    let end_line = cursor_pos.line + total_count - 1;
204                    let end = Position::new(end_line, 0);
205                    (start, end)
206                },
207            );
208
209            return ResolveResult::ModeTransition(ModeTransition::Pop {
210                result: Some(build_operator_execute(
211                    self.operator_type,
212                    start,
213                    end,
214                    true,
215                    Some(count.unwrap_or(1) * motion_count),
216                    register,
217                )),
218            });
219        }
220
221        state.push_key(*key);
222        let keys = state.keys();
223
224        let lookup_state = {
225            let state = input.keymap.query(input.mode, &keys);
226            if matches!(state, reovim_driver_input::KeyLookupState::NotFound) {
227                input.keymap.query(&self.parent_mode_id, &keys)
228            } else {
229                state
230            }
231        };
232
233        match apply_keymap_policy(&lookup_state) {
234            KeymapAction::Execute(cmd) => {
235                let linewise = is_linewise_motion(&cmd);
236                let explicit_count = state.explicit_count();
237                let _motion_count = state.take_motion_count();
238                state.clear_keys();
239
240                if let Some(start_pos) = session.cursor_position() {
241                    state.set_start_position(start_pos);
242                }
243
244                let inclusive = !linewise && is_inclusive_motion(&cmd);
245                if let Some(vim) = client_extensions.get_mut::<crate::VimSessionState>() {
246                    vim.pending_motion = Some(PendingMotion::new(linewise, inclusive, false));
247                }
248
249                drop(state);
250
251                let ctx = ResolveContext {
252                    count: explicit_count,
253                    register: None,
254                    keys,
255                    metadata: std::collections::HashMap::new(),
256                };
257
258                ResolveResult::Execute(cmd, ctx)
259            }
260            KeymapAction::Pending => {
261                drop(state);
262                ResolveResult::Pending
263            }
264            KeymapAction::Cancel => {
265                state.clear_keys();
266                drop(state);
267                self.clear_state();
268                ResolveResult::ModeTransition(build_cancelled())
269            }
270        }
271    }
272
273    #[cfg_attr(coverage_nightly, coverage(off))]
274    fn on_command_complete(
275        &self,
276        session: &mut dyn SessionApiDyn,
277        _shared_extensions: &mut ExtensionMap,
278        client_extensions: &mut ExtensionMap,
279    ) -> Option<ModeTransition> {
280        let state = self.state.read().expect("lock poisoned");
281        let count = state.operator_count;
282        let register = state.register;
283        drop(state);
284
285        // Check for text object range first
286        if let Some(op_state) = client_extensions.get_mut::<OperatorPendingState>()
287            && let Some(textobj_range) = op_state.take_textobj_range()
288        {
289            self.clear_state();
290            return Some(ModeTransition::Pop {
291                result: Some(build_operator_execute(
292                    self.operator_type,
293                    textobj_range.start,
294                    textobj_range.end,
295                    textobj_range.is_linewise,
296                    count,
297                    register,
298                )),
299            });
300        }
301
302        // Motion-based range
303        let vim = client_extensions.get_mut::<crate::VimSessionState>()?;
304
305        // Peek first: multi-step motions (jump search) push a mode for
306        // label selection before the cursor moves.  Defer completion.
307        let _ = vim.pending_motion.as_ref()?;
308
309        let state = self.state.read().expect("lock poisoned");
310        let start_pos = state.start_position?;
311        drop(state);
312
313        let end_pos = session.cursor_position()?;
314
315        if start_pos == end_pos {
316            return None;
317        }
318
319        let motion = vim.pending_motion.take()?;
320
321        let (range_start, range_end) = {
322            let (start, end) = if start_pos <= end_pos {
323                (start_pos, end_pos)
324            } else {
325                (end_pos, start_pos)
326            };
327
328            if !motion.linewise && motion.inclusive {
329                (start, Position::new(end.line, end.column + 1))
330            } else {
331                (start, end)
332            }
333        };
334
335        self.clear_state();
336
337        Some(ModeTransition::Pop {
338            result: Some(build_operator_execute(
339                self.operator_type,
340                range_start,
341                range_end,
342                motion.linewise,
343                count,
344                register,
345            )),
346        })
347    }
348
349    fn mode_id(&self) -> &ModeId {
350        &self.mode_id
351    }
352
353    fn inherits_from(&self) -> Option<&ModeId> {
354        Some(&self.parent_mode_id)
355    }
356
357    fn pending_keys(&self) -> KeySequence {
358        self.state.read().expect("lock poisoned").keys()
359    }
360
361    fn reset(&mut self) {
362        self.state.write().expect("lock poisoned").reset();
363    }
364}