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}