Skip to main content

reovim_module_vim/
session_state.rs

1//! Vim-specific per-session state.
2//!
3//! This module provides [`VimSessionState`], which stores all vim policy state
4//! per client session. This state was previously scattered in the runner, but
5//! now lives in the vim module where it belongs.
6
7#![allow(clippy::missing_const_for_fn)] // Methods may be extended later
8//!
9//! # Design
10//!
11//! The vim module owns all vim-specific state:
12//! - Pending motion info (for operator completion)
13//! - Pending text object range (for operator completion)
14//! - Pending character operations (f, F, t, T, r)
15//! - Last find for ; and , repeat
16//! - Numeric count prefix
17//! - Register selection
18//! - Repeat state for . command
19//! - Macro recording state (Epic #465 Phase 8D)
20//!
21//! # Note on Operators (Epic #415)
22//!
23//! Operator state (d, y, c) is owned by dedicated mode resolvers
24//! (`VimDeleteResolver`, `VimYankResolver`, `VimChangeResolver`), not by
25//! this session state. Each resolver maintains its own `OperatorState`.
26//!
27//! # Text Object Range (Epic #465)
28//!
29//! Text objects (like `iw`, `aw`, `i"`) calculate a range directly instead of
30//! moving the cursor. When executed, they store the range in `pending_textobj_range`.
31//! The operator resolver's `on_command_complete` consumes this range via `take()`.
32//!
33//! # Example
34//!
35//! ```ignore
36//! use reovim_module_vim::VimSessionState;
37//! use reovim_driver_session::SessionRuntime;
38//!
39//! fn check_pending_char(runtime: &SessionRuntime) -> bool {
40//!     ctx.ext::<VimSessionState>()
41//!         .map(|vim| vim.pending_char.is_some())
42//!         .unwrap_or(false)
43//! }
44//! ```
45
46use {
47    reovim_driver_input::KeyEvent, reovim_driver_session::SessionExtension,
48    reovim_kernel::api::v1::Position,
49};
50
51// Re-export TextObjRange from session driver for convenience
52pub use reovim_driver_session::TextObjRange;
53
54// =============================================================================
55// Replace Mode Types (#666)
56// =============================================================================
57
58/// Entry in the replace mode restore stack.
59///
60/// Records the position and original character that was overwritten.
61/// Used by backspace to restore the original.
62#[derive(Debug, Clone, Copy)]
63pub struct ReplaceRestoreEntry {
64    /// The position where the replacement occurred.
65    pub position: Position,
66    /// The original character (`None` if cursor was at EOL — char was inserted).
67    pub original: Option<char>,
68}
69
70/// Vim-specific per-session state.
71///
72/// Stored via [`SessionExtension`], accessed by vim resolvers and commands.
73/// Each client session has its own independent vim state.
74///
75/// # Note on Operators (Epic #415)
76///
77/// Operator state (d, y, c) is owned by dedicated mode resolvers, not here.
78/// See `VimDeleteResolver`, `VimYankResolver`, `VimChangeResolver`.
79#[derive(Debug, Default)]
80pub struct VimSessionState {
81    /// Pending motion info (Epic #415, Issue #388).
82    ///
83    /// When the operator resolver dispatches a motion, it stores the motion type
84    /// here. After the motion executes, `on_command_complete` uses this to
85    /// complete the operator with the correct linewise flag.
86    ///
87    /// This replaces `CommandResult::Motion` - motion type is resolver policy,
88    /// not command mechanism.
89    pub pending_motion: Option<PendingMotion>,
90
91    /// Pending text object range (Epic #465).
92    ///
93    /// When a text object command executes during operator-pending mode, it
94    /// stores the calculated range here. The operator resolver's
95    /// `on_command_complete` consumes this range via `take()`.
96    ///
97    /// Text object range takes priority over motion-based range calculation.
98    pub pending_textobj_range: Option<TextObjRange>,
99
100    /// Pending character operation (f, F, t, T, r).
101    ///
102    /// When a user presses a character-wait command, the operation type
103    /// is stored here until the next character is typed.
104    pub pending_char: Option<PendingCharOp>,
105
106    /// Numeric prefix accumulator.
107    ///
108    /// When digits are pressed before a command (e.g., `5j`), the count
109    /// is accumulated here.
110    pub pending_count: Option<usize>,
111
112    /// Register selection (e.g., `"a` sets register to 'a').
113    ///
114    /// When a register prefix is typed, the register is stored here
115    /// until the next operator/command uses it.
116    pub pending_register: Option<char>,
117
118    /// Last change for dot repeat (Epic #465).
119    ///
120    /// Set by operator resolvers after completing an operation, and by
121    /// insert mode exit. Used by the `.` command to replay the last change.
122    pub last_change: Option<LastChange>,
123
124    /// Insert mode text accumulator.
125    ///
126    /// Tracks characters inserted during insert mode. Cleared on insert mode
127    /// entry, recorded as `LastChange::Insert` on insert mode exit.
128    pub insert_buffer: String,
129
130    // =========================================================================
131    // Macro Recording State (Epic #465 Phase 8D)
132    // =========================================================================
133    /// Currently recording macro to this register (None = not recording).
134    ///
135    /// When set, all keys processed by the normal resolver are accumulated
136    /// in `recording_keys`. Set by `q{a-z}`, cleared by `q` (stop recording).
137    pub recording_register: Option<char>,
138
139    /// Accumulated keys during macro recording.
140    ///
141    /// Cleared when recording starts, stored to register when recording stops.
142    /// The final `q` key that stops recording is NOT included.
143    pub recording_keys: Vec<KeyEvent>,
144
145    /// Last played macro register (for `@@` repeat).
146    ///
147    /// Updated each time a macro is played with `@{a-z}`.
148    pub last_macro_register: Option<char>,
149
150    /// Current macro playback depth (for recursion detection).
151    ///
152    /// Incremented when a macro starts playing, decremented when it finishes.
153    /// Playback is blocked when depth reaches `MAX_MACRO_DEPTH` (16).
154    pub macro_playback_depth: usize,
155
156    // =========================================================================
157    // Dot Repeat Key Recording (#577)
158    // =========================================================================
159    /// Whether we are currently recording keys for dot repeat.
160    ///
161    /// Set when an operator or insert-entry command starts, cleared when
162    /// the change completes. Independent from macro recording — both can
163    /// be active simultaneously.
164    pub recording_repeat: bool,
165
166    /// Accumulated key sequence during the current change.
167    ///
168    /// Cleared when recording starts, saved into `last_change.keys` when
169    /// recording finishes. Used by `.` to replay the exact key sequence.
170    pub repeat_keys: Vec<KeyEvent>,
171
172    // =========================================================================
173    // Replace Mode State (#666)
174    // =========================================================================
175    /// Restore stack for replace mode backspace.
176    ///
177    /// Each entry records the position and original character that was
178    /// overwritten. Backspace pops from this stack to restore the original.
179    /// Cleared on replace mode entry.
180    pub replace_restore_stack: Vec<ReplaceRestoreEntry>,
181}
182
183impl SessionExtension for VimSessionState {
184    fn create() -> Self {
185        Self::default()
186    }
187}
188
189/// Maximum macro recursion depth to prevent infinite loops.
190///
191/// Vim uses 1000 by default, but we use 16 for safety. If a macro
192/// calls itself (or creates a cycle), this prevents stack overflow.
193pub const MAX_MACRO_DEPTH: usize = 16;
194
195impl VimSessionState {
196    /// Check if there is any pending state.
197    ///
198    /// Returns `true` if any vim operation is waiting for input.
199    ///
200    /// # Note (Epic #415)
201    ///
202    /// This no longer checks `pending_operator` - the dedicated operator
203    /// resolvers (DELETE, YANK, CHANGE) own their state directly.
204    #[must_use]
205    pub fn is_pending(&self) -> bool {
206        self.pending_char.is_some()
207            || self.pending_count.is_some()
208            || self.pending_register.is_some()
209    }
210
211    /// Check if currently recording a macro.
212    #[must_use]
213    pub const fn is_recording(&self) -> bool {
214        self.recording_register.is_some()
215    }
216
217    /// Check if macro playback would exceed depth limit.
218    #[must_use]
219    pub const fn is_macro_depth_exceeded(&self) -> bool {
220        self.macro_playback_depth >= MAX_MACRO_DEPTH
221    }
222
223    /// Clear all pending state.
224    ///
225    /// Called when an operation is cancelled (e.g., pressing Escape).
226    ///
227    /// # Note (Epic #415)
228    ///
229    /// Operator state (d, y, c) is managed by dedicated mode resolvers
230    /// (`VimDeleteResolver`, `VimYankResolver`, `VimChangeResolver`),
231    /// not by this method.
232    ///
233    /// # Note (Epic #465)
234    ///
235    /// `last_change` is NOT cleared here - it persists until the next
236    /// change operation so dot repeat can replay it.
237    pub fn clear_pending(&mut self) {
238        self.pending_motion = None;
239        self.pending_textobj_range = None;
240        self.pending_char = None;
241        self.pending_count = None;
242        self.pending_register = None;
243        // Note: last_change is NOT cleared - persists for dot repeat
244        // Note: insert_buffer is cleared on insert mode entry, not here
245    }
246
247    /// Get the effective count, defaulting to 1 if not set.
248    ///
249    /// Takes the pending count, clearing it in the process.
250    pub fn take_count(&mut self) -> usize {
251        self.pending_count.take().unwrap_or(1)
252    }
253
254    /// Get the register, clearing it in the process.
255    ///
256    /// Returns `None` if no register was selected (use default register).
257    pub fn take_register(&mut self) -> Option<char> {
258        self.pending_register.take()
259    }
260
261    // =========================================================================
262    // Macro Recording Methods (Epic #465 Phase 8D)
263    // =========================================================================
264
265    /// Start recording a macro to the given register.
266    ///
267    /// Clears any previously recorded keys and sets the recording register.
268    /// Returns `false` if the register name is invalid (not a-z).
269    pub fn start_recording(&mut self, register: char) -> bool {
270        if !register.is_ascii_lowercase() {
271            return false;
272        }
273        self.recording_register = Some(register);
274        self.recording_keys.clear();
275        true
276    }
277
278    /// Stop recording and return the recorded keys.
279    ///
280    /// Returns `None` if not currently recording.
281    /// The caller is responsible for storing the keys in the register.
282    pub fn stop_recording(&mut self) -> Option<(char, Vec<KeyEvent>)> {
283        let register = self.recording_register.take()?;
284        let keys = std::mem::take(&mut self.recording_keys);
285        Some((register, keys))
286    }
287
288    /// Record a key during macro recording.
289    ///
290    /// Does nothing if not currently recording.
291    pub fn record_key(&mut self, key: KeyEvent) {
292        if self.is_recording() {
293            self.recording_keys.push(key);
294        }
295    }
296
297    // =========================================================================
298    // Dot Repeat Key Recording (#577)
299    // =========================================================================
300
301    /// Start recording keys for dot repeat.
302    ///
303    /// Called when an operator or insert-entry command begins a change.
304    /// Clears any previously accumulated keys and sets the recording flag.
305    pub fn start_repeat_recording(&mut self) {
306        self.recording_repeat = true;
307        self.repeat_keys.clear();
308    }
309
310    /// Record a key for dot repeat.
311    ///
312    /// Does nothing if not currently recording for repeat.
313    pub fn record_repeat_key(&mut self, key: KeyEvent) {
314        if self.recording_repeat {
315            self.repeat_keys.push(key);
316        }
317    }
318
319    /// Finish recording and save keys into `last_change`.
320    ///
321    /// Copies the accumulated `repeat_keys` into `last_change.keys` and
322    /// clears the recording flag. If `last_change` is `None`, does nothing
323    /// (the caller should have set `last_change` before calling this).
324    pub fn finish_repeat_recording(&mut self) {
325        self.recording_repeat = false;
326        if let Some(ref mut lc) = self.last_change {
327            lc.keys.clone_from(&self.repeat_keys);
328        }
329        self.repeat_keys.clear();
330    }
331
332    /// Enter macro playback (increment depth counter).
333    ///
334    /// Returns `false` if depth limit would be exceeded.
335    pub fn enter_macro_playback(&mut self) -> bool {
336        if self.is_macro_depth_exceeded() {
337            return false;
338        }
339        self.macro_playback_depth += 1;
340        true
341    }
342
343    /// Exit macro playback (decrement depth counter).
344    pub fn exit_macro_playback(&mut self) {
345        self.macro_playback_depth = self.macro_playback_depth.saturating_sub(1);
346    }
347}
348
349/// Pending motion info for operator completion (Epic #415, Issue #388).
350///
351/// When the operator-pending resolver dispatches a motion, it stores
352/// info here. After the motion executes, `on_command_complete` uses
353/// this to complete the operator.
354///
355/// This replaces `CommandResult::Motion` - motion type classification
356/// is resolver policy knowledge, not command mechanism.
357///
358/// # Motion Semantics
359///
360/// - **Linewise**: The motion affects entire lines (j, k, gg, G)
361/// - **Inclusive**: The cursor lands ON the last character of the range ($, e, f, t)
362/// - **Exclusive**: The cursor lands at the START of the next unit (w, b, h, l)
363/// - **Word Forward**: The motion is word-forward (w, W) - needs special handling for `c`
364///
365/// For operators, exclusive motions work directly with `Range.end` (which is exclusive),
366/// but inclusive motions need +1 adjustment to include the character under the cursor.
367#[derive(Debug, Clone, Copy)]
368pub struct PendingMotion {
369    /// Whether the motion is linewise (j, k, gg, G) or characterwise (w, b, h, l, $).
370    pub linewise: bool,
371    /// Whether the motion is inclusive (cursor lands ON last char).
372    ///
373    /// Inclusive motions: $, e, E, f, F, t, T, G, gg, %
374    /// Exclusive motions: w, W, b, B, h, l, 0, ^
375    ///
376    /// For characterwise inclusive motions, the end position must be
377    /// adjusted by +1 to convert to exclusive range semantics.
378    pub inclusive: bool,
379    /// Whether this is a word-forward motion (w, W).
380    ///
381    /// This is used by the change operator for the `cw` special case:
382    /// `cw` behaves like `ce` (change to end of word, not to start of next word).
383    /// See `:help cw` in Vim for documentation of this behavior.
384    pub word_forward: bool,
385}
386
387impl PendingMotion {
388    /// Create a new pending motion with explicit flags.
389    #[must_use]
390    pub const fn new(linewise: bool, inclusive: bool, word_forward: bool) -> Self {
391        Self {
392            linewise,
393            inclusive,
394            word_forward,
395        }
396    }
397
398    /// Create a characterwise exclusive pending motion (w, b, h, l, etc.).
399    #[must_use]
400    pub const fn characterwise() -> Self {
401        Self {
402            linewise: false,
403            inclusive: false,
404            word_forward: false,
405        }
406    }
407
408    /// Create a characterwise inclusive pending motion ($, e, f, t, etc.).
409    #[must_use]
410    pub const fn characterwise_inclusive() -> Self {
411        Self {
412            linewise: false,
413            inclusive: true,
414            word_forward: false,
415        }
416    }
417
418    /// Create a linewise pending motion (j, k, gg, G, etc.).
419    #[must_use]
420    pub const fn linewise() -> Self {
421        Self {
422            linewise: true,
423            inclusive: false,
424            word_forward: false,
425        }
426    }
427
428    /// Create a word-forward motion (w, W) for special cw handling.
429    #[must_use]
430    pub const fn word_forward() -> Self {
431        Self {
432            linewise: false,
433            inclusive: false,
434            word_forward: true,
435        }
436    }
437}
438
439/// Pending character operation (f, F, t, T, r).
440///
441/// These commands wait for a single character input to complete.
442#[derive(Debug, Clone, Copy, PartialEq, Eq)]
443pub enum PendingCharOp {
444    /// Find character forward (f).
445    FindForward,
446    /// Find character backward (F).
447    FindBackward,
448    /// Till character forward (t).
449    TillForward,
450    /// Till character backward (T).
451    TillBackward,
452    /// Replace character (r).
453    Replace,
454    /// Set mark (m).
455    SetMark,
456    /// Go to mark line (').
457    GotoMarkLine,
458    /// Go to mark exact position (`` ` ``).
459    GotoMarkExact,
460}
461
462impl PendingCharOp {
463    /// Check if this is a find operation (f or F).
464    #[must_use]
465    pub const fn is_find(&self) -> bool {
466        matches!(self, Self::FindForward | Self::FindBackward)
467    }
468
469    /// Check if this is a till operation (t or T).
470    #[must_use]
471    pub const fn is_till(&self) -> bool {
472        matches!(self, Self::TillForward | Self::TillBackward)
473    }
474
475    /// Check if this is a motion (f, F, t, T) vs replace (r).
476    #[must_use]
477    pub const fn is_motion(&self) -> bool {
478        !matches!(self, Self::Replace | Self::SetMark | Self::GotoMarkLine | Self::GotoMarkExact)
479    }
480
481    /// Check if searching forward.
482    #[must_use]
483    pub const fn is_forward(&self) -> bool {
484        matches!(self, Self::FindForward | Self::TillForward)
485    }
486}
487
488// =============================================================================
489// Dot Repeat Support (Epic #465)
490// =============================================================================
491
492/// Type of change recorded for dot repeat.
493#[derive(Debug, Clone)]
494pub enum ChangeType {
495    /// Operator with motion (e.g., `dw`, `c$`).
496    OperatorMotion {
497        /// The operator type (Delete, Yank, Change).
498        operator: OperatorType,
499        /// Whether the operation was linewise.
500        linewise: bool,
501    },
502    /// Operator with text object (e.g., `diw`, `ci"`).
503    OperatorTextObject {
504        /// The operator type.
505        operator: OperatorType,
506        /// Whether the operation was linewise.
507        linewise: bool,
508    },
509    /// Insert mode text (e.g., `ihello<Esc>`).
510    Insert {
511        /// The inserted text content.
512        text: String,
513    },
514}
515
516/// Operator type for dot repeat tracking.
517#[derive(Debug, Clone, Copy, PartialEq, Eq)]
518pub enum OperatorType {
519    /// Delete operator (d).
520    Delete,
521    /// Yank operator (y).
522    Yank,
523    /// Change operator (c).
524    Change,
525}
526
527/// Record of the last change for dot repeat (.).
528///
529/// Stores information needed to replay the last edit operation.
530/// This is set by operator resolvers and insert mode exit.
531#[derive(Debug, Clone)]
532pub struct LastChange {
533    /// Type of change that was made.
534    pub change_type: ChangeType,
535    /// Count used with the original command (None = no explicit count).
536    pub count: Option<usize>,
537    /// Register used with the original command.
538    pub register: Option<char>,
539    /// Recorded key sequence for replay via `InjectKeys` (#577).
540    ///
541    /// When non-empty, `.` replays these keys instead of using the
542    /// metadata-based approach. This handles all cases including
543    /// operator+motion, operator+textobj, and change+insert.
544    pub keys: Vec<KeyEvent>,
545}