reovim_driver_input/resolver.rs
1//! Mode key resolver trait and types.
2//!
3//! This module defines how modes resolve key events into commands or transitions.
4//! Resolvers provide the policy layer for key handling - the kernel/runner only
5//! provides the mechanism (dispatch, mode stack, command execution).
6//!
7//! # Architecture
8//!
9//! The resolver system separates mechanism from policy:
10//!
11//! | Layer | Responsibility |
12//! |-------|---------------|
13//! | Kernel | Mechanism: `ModeId`, `ModeStack`, `CommandId`, `Position` |
14//! | Input Driver (this) | Contract: `ModeKeyResolver` trait, result types |
15//! | Modules | Policy: `VimNormalResolver`, `VimInsertResolver`, etc. |
16//! | Runner | Dispatch: get resolver → `resolve()` → execute result |
17//!
18//! # Different Editing Styles
19//!
20//! The same kernel/runner supports different editing paradigms:
21//!
22//! | Style | Resolver Behavior |
23//! |-------|-------------------|
24//! | Vim | Counts, operators+motions, mode stacking |
25//! | Emacs | C-x prefix chains, M-x commands |
26//! | Kakoune | Object-verb (select then act) |
27//! | CUA | Ctrl+C/V/X, Shift+arrows |
28//!
29//! # Example Flow: `dw` (delete word)
30//!
31//! ```text
32//! 1. Normal mode receives 'd'
33//! └─ VimNormalResolver.resolve('d', state)
34//! └─ Returns: ModeTransition::Set { mode: "vim:delete" }
35//!
36//! 2. EventLoop sets delete mode
37//!
38//! 3. Delete mode receives 'w'
39//! └─ VimDeleteResolver.resolve('w', state)
40//! └─ Dispatches word motion, captures range
41//! └─ Returns: Delete text + ModeTransition::Set { mode: "vim:normal" }
42//!
43//! 4. EventLoop returns to normal mode
44//! ```
45
46use std::{any::TypeId, collections::HashMap};
47
48use {
49 reovim_driver_session::{ExtensionMap, SessionExtension, TextInputSink},
50 reovim_kernel::api::v1::{BufferId, CommandId, ModeId, ModeStack, Position},
51};
52
53use crate::{KeyEvent, KeySequence, KeymapQuery};
54
55// Re-export transition types and session API from session driver (canonical source)
56pub use reovim_driver_session::{PopResult, SessionApi, SessionApiDyn, TransitionContext};
57
58// ============================================================================
59// OperatorArgs - Shared count/register fields (#391)
60// ============================================================================
61
62/// Common arguments for operator operations.
63///
64/// Extracted to avoid field duplication across context types (DRY principle).
65/// Used by `TransitionContext` and `ResolveContext`.
66///
67/// # Example
68///
69/// ```
70/// use reovim_driver_input::OperatorArgs;
71///
72/// // Create with defaults
73/// let args = OperatorArgs::new();
74/// assert_eq!(args.effective_count(), 1); // Default count is 1
75///
76/// // Builder pattern
77/// let args = OperatorArgs::new().count(3).register('a');
78/// assert_eq!(args.count, Some(3));
79/// assert_eq!(args.register, Some('a'));
80///
81/// // From count shorthand
82/// let args = OperatorArgs::with_count(5);
83/// assert_eq!(args.effective_count(), 5);
84/// ```
85#[derive(Debug, Clone, Default, PartialEq, Eq)]
86pub struct OperatorArgs {
87 /// Count prefix (e.g., 3 in `3j` or `2d3w`).
88 ///
89 /// Counts can be combined across mode transitions: `2d3w` = 6 words.
90 pub count: Option<usize>,
91
92 /// Register for the operation (e.g., `a` in `"ayw`).
93 ///
94 /// In vim, registers store yanked/deleted text.
95 pub register: Option<char>,
96}
97
98impl OperatorArgs {
99 /// Create a new empty operator args.
100 #[must_use]
101 pub const fn new() -> Self {
102 Self {
103 count: None,
104 register: None,
105 }
106 }
107
108 /// Create operator args with a count.
109 #[must_use]
110 pub const fn with_count(count: usize) -> Self {
111 Self {
112 count: Some(count),
113 register: None,
114 }
115 }
116
117 /// Set the count (builder pattern).
118 #[must_use]
119 pub const fn count(mut self, count: usize) -> Self {
120 self.count = Some(count);
121 self
122 }
123
124 /// Set the register (builder pattern).
125 #[must_use]
126 pub const fn register(mut self, reg: char) -> Self {
127 self.register = Some(reg);
128 self
129 }
130
131 /// Get the effective count (default to 1 if not set).
132 ///
133 /// In vim, an unspecified count means "1", not "none".
134 #[must_use]
135 pub const fn effective_count(&self) -> usize {
136 match self.count {
137 Some(c) => c,
138 None => 1,
139 }
140 }
141
142 /// Check if any arguments are set.
143 #[must_use]
144 pub const fn is_empty(&self) -> bool {
145 self.count.is_none() && self.register.is_none()
146 }
147}
148
149// ============================================================================
150// ResolveInput - Input context for resolvers
151// ============================================================================
152
153/// Input context passed to resolvers for key resolution.
154///
155/// This struct provides resolvers with access to:
156/// - The current key sequence being resolved
157/// - The current mode
158/// - Access to keymap queries (via `KeymapQuery` trait)
159///
160/// # Mechanism vs Policy
161///
162/// `ResolveInput` enables the mechanism/policy separation:
163/// - **Mechanism**: The keymap registry reports FACTS via `keymap.query()`
164/// - **Policy**: The resolver decides what to DO with those facts
165///
166/// For example, given `KeyLookupState::ExactWithLonger`:
167/// - **Vim policy**: Wait for more keys (dd might follow d)
168/// - **Eager policy**: Execute immediately
169///
170/// # Example
171///
172/// ```ignore
173/// use reovim_driver_input::{ResolveInput, KeyLookupState, ResolveResult};
174///
175/// fn resolve(input: &ResolveInput<'_>) -> ResolveResult {
176/// // Query the keymap for facts
177/// let state = input.keymap.query(input.mode, input.keys);
178///
179/// // Apply policy to the facts
180/// match state {
181/// KeyLookupState::ExactWithLonger { exact } => {
182/// // Vim: wait for more keys
183/// ResolveResult::Pending
184/// }
185/// KeyLookupState::ExactOnly(cmd) => {
186/// // Execute the command
187/// ResolveResult::Execute(cmd, ResolveContext::new())
188/// }
189/// // ...
190/// }
191/// }
192/// ```
193pub struct ResolveInput<'a> {
194 /// The accumulated key sequence being resolved.
195 pub keys: &'a KeySequence,
196
197 /// The current mode.
198 pub mode: &'a ModeId,
199
200 /// Access to keymap queries.
201 ///
202 /// Use `keymap.query(mode, keys)` to get facts about what bindings exist.
203 pub keymap: &'a dyn KeymapQuery,
204
205 /// Optional access to register bank (Epic #465 Phase 8D - Macro Playback).
206 ///
207 /// Provides read/write access to registers for macro recording/playback.
208 /// This is optional because not all resolve contexts need register access.
209 pub registers: Option<
210 &'a std::sync::Arc<reovim_kernel::api::v1::RwLock<reovim_kernel::api::v1::RegisterBank>>,
211 >,
212}
213
214impl<'a> ResolveInput<'a> {
215 /// Create a new resolve input context.
216 #[must_use]
217 pub const fn new(keys: &'a KeySequence, mode: &'a ModeId, keymap: &'a dyn KeymapQuery) -> Self {
218 Self {
219 keys,
220 mode,
221 keymap,
222 registers: None,
223 }
224 }
225
226 /// Create a resolve input context with register access.
227 #[must_use]
228 pub const fn with_registers(
229 keys: &'a KeySequence,
230 mode: &'a ModeId,
231 keymap: &'a dyn KeymapQuery,
232 registers: &'a std::sync::Arc<
233 reovim_kernel::api::v1::RwLock<reovim_kernel::api::v1::RegisterBank>,
234 >,
235 ) -> Self {
236 Self {
237 keys,
238 mode,
239 keymap,
240 registers: Some(registers),
241 }
242 }
243}
244
245// ============================================================================
246// ModeKeyResolver Trait
247// ============================================================================
248
249/// Trait for resolving key events in a specific mode.
250///
251/// Resolvers have full control over how keys are interpreted:
252/// - Count prefixes (1-9 in Vim, none in Emacs)
253/// - Register selection (" in Vim)
254/// - Multi-key sequences (gg, <C-w>h)
255/// - Operator-pending state (d awaiting motion)
256///
257/// Each editing style implements different resolvers:
258/// - Vim: `VimNormalResolver`, `VimInsertResolver`, `VimDeleteResolver`, `VimYankResolver`, `VimChangeResolver`
259/// - Emacs: `EmacsResolver` (single mode with prefix handling)
260/// - etc.
261///
262/// # Object Safety
263///
264/// This trait is object-safe and requires `Send + Sync` for use in
265/// multi-threaded server contexts.
266///
267/// # Example
268///
269/// ```ignore
270/// use reovim_driver_input::{ModeKeyResolver, ResolveResult, KeyEvent, ModeState};
271/// use reovim_kernel::api::v1::ModeId;
272///
273/// struct MyResolver {
274/// mode: ModeId,
275/// }
276///
277/// impl ModeKeyResolver for MyResolver {
278/// fn resolve(&self, key: &KeyEvent, state: &mut ModeState) -> ResolveResult {
279/// // Handle key based on mode-specific logic
280/// ResolveResult::NotHandled
281/// }
282///
283/// fn mode_id(&self) -> &ModeId {
284/// &self.mode
285/// }
286/// }
287/// ```
288pub trait ModeKeyResolver: Send + Sync {
289 /// Process a key event in this mode's context (legacy API).
290 ///
291 /// **Deprecated**: Use `resolve_with_keymap` instead for access to keymap queries.
292 ///
293 /// The resolver has full control over interpretation:
294 /// - Accumulate counts, registers, pending sequences
295 /// - Execute commands
296 /// - Request mode transitions
297 ///
298 /// # Arguments
299 ///
300 /// * `key` - The key event to process
301 /// * `state` - Mutable access to shared mode state
302 ///
303 /// # Returns
304 ///
305 /// A `ResolveResult` indicating what action to take.
306 #[deprecated(since = "0.9.5", note = "Override resolve_with_keymap() instead")]
307 #[cfg_attr(coverage_nightly, coverage(off))]
308 fn resolve(&self, _key: &KeyEvent, _state: &mut ModeState) -> ResolveResult {
309 panic!("resolve() is removed - implement resolve_with_keymap() instead")
310 }
311
312 /// Process a key event with access to keymap queries.
313 ///
314 /// This is the preferred method for resolvers that need to query keybindings.
315 /// It enables mechanism/policy separation:
316 /// - Query `input.keymap.query()` to get FACTS about what bindings exist
317 /// - Apply your own POLICY to decide what to do
318 ///
319 /// # Arguments
320 ///
321 /// * `key` - The key event to process
322 /// * `state` - Mutable access to shared mode state
323 /// * `input` - Input context with keymap access
324 ///
325 /// # Returns
326 ///
327 /// A `ResolveResult` indicating what action to take.
328 ///
329 /// # Default Implementation
330 ///
331 /// Falls back to `resolve()` for backward compatibility with existing resolvers.
332 /// Override this method to use keymap queries.
333 ///
334 /// # Example
335 ///
336 /// ```ignore
337 /// fn resolve_with_keymap(
338 /// &self,
339 /// key: &KeyEvent,
340 /// state: &mut ModeState,
341 /// input: &ResolveInput<'_>,
342 /// ) -> ResolveResult {
343 /// // Get facts about what bindings exist
344 /// let lookup_state = input.keymap.query(input.mode, input.keys);
345 ///
346 /// // Apply Vim policy: wait for longer sequences
347 /// match lookup_state {
348 /// KeyLookupState::ExactWithLonger { .. } => ResolveResult::Pending,
349 /// KeyLookupState::ExactOnly(cmd) => {
350 /// ResolveResult::Execute(cmd, ResolveContext::new())
351 /// }
352 /// // ...
353 /// }
354 /// }
355 /// ```
356 #[allow(deprecated)]
357 fn resolve_with_keymap(
358 &self,
359 key: &KeyEvent,
360 state: &mut ModeState,
361 _input: &ResolveInput<'_>,
362 ) -> ResolveResult {
363 // Default: delegate to legacy resolve() for backward compatibility
364 // Note: Resolvers should override this method instead
365 self.resolve(key, state)
366 }
367
368 /// Process a key event with access to keymap queries AND session extensions.
369 ///
370 /// This is the preferred method for resolvers that need to access per-session
371 /// module state (e.g., `VimSessionState` for pending operators, find-char state).
372 ///
373 /// # Architecture (Epic #385)
374 ///
375 /// This method enables the proper mechanism/policy separation:
376 /// - **Mechanism**: Runner routes keys to resolvers, provides extension access
377 /// - **Policy**: Vim resolver stores/reads `VimSessionState` in extensions
378 ///
379 /// # Extension Maps
380 ///
381 /// Two extension maps are provided so modules can choose the correct scope:
382 /// - `shared_extensions` - Shared across all clients in a session (e.g., syntax state)
383 /// - `client_extensions` - Per-client isolated state (e.g., vim operator state, counts)
384 ///
385 /// # Arguments
386 ///
387 /// * `key` - The key event to process
388 /// * `state` - Mutable access to shared mode state
389 /// * `input` - Input context with keymap access
390 /// * `shared_extensions` - Session-wide extension storage (shared across clients)
391 /// * `client_extensions` - Per-client extension storage (isolated per connection)
392 ///
393 /// # Returns
394 ///
395 /// A `ResolveResult` indicating what action to take.
396 ///
397 /// # Default Implementation
398 ///
399 /// Falls back to `resolve_with_keymap()` for backward compatibility.
400 /// Override this method to use session extensions.
401 ///
402 /// # Example
403 ///
404 /// ```ignore
405 /// use reovim_module_vim::VimSessionState;
406 ///
407 /// fn resolve_with_extensions(
408 /// &self,
409 /// key: &KeyEvent,
410 /// state: &mut ModeState,
411 /// input: &ResolveInput<'_>,
412 /// shared_extensions: &mut ExtensionMap,
413 /// client_extensions: &mut ExtensionMap,
414 /// ) -> ResolveResult {
415 /// // Per-client vim state (pending operator, counts)
416 /// let vim = client_extensions.get_or_insert::<VimSessionState>();
417 ///
418 /// // Check for pending operator
419 /// if let Some(pending) = &vim.pending_operator {
420 /// // Handle operator-pending logic
421 /// }
422 ///
423 /// // ... rest of resolution logic
424 /// }
425 /// ```
426 fn resolve_with_extensions(
427 &self,
428 key: &KeyEvent,
429 state: &mut ModeState,
430 input: &ResolveInput<'_>,
431 _shared_extensions: &mut ExtensionMap,
432 _client_extensions: &mut ExtensionMap,
433 ) -> ResolveResult {
434 // Default: delegate to resolve_with_keymap for backward compatibility
435 self.resolve_with_keymap(key, state, input)
436 }
437
438 /// Process a key event with full session API access.
439 ///
440 /// This is the preferred method for resolvers that need direct state manipulation.
441 /// Instead of returning commands for the runner to execute, resolvers can perform
442 /// operations directly via the session API and return `ResolveResult::Completed`.
443 ///
444 /// # Architecture (Epic #393)
445 ///
446 /// This method completes the mechanism/policy separation:
447 /// - **Mechanism (runner)**: Creates `SessionRuntime`, routes keys to resolvers
448 /// - **Policy (resolvers)**: Perform actions directly via `session.*` methods
449 ///
450 /// # Extension Maps
451 ///
452 /// Two extension maps are provided so modules can choose the correct scope:
453 /// - `shared_extensions` - Shared across all clients in a session
454 /// - `client_extensions` - Per-client isolated state
455 ///
456 /// `ExtensionApi` has generic methods, making it incompatible with trait objects.
457 /// By passing extensions separately, we preserve dyn-compatibility for
458 /// `ModeKeyResolver` while still providing full access.
459 ///
460 /// # Arguments
461 ///
462 /// * `key` - The key event to process
463 /// * `state` - Mutable access to shared mode state
464 /// * `input` - Input context with keymap access
465 /// * `session` - Dyn-compatible session API (mode, buffer, window, command, changes)
466 /// * `shared_extensions` - Session-wide extension storage (shared across clients)
467 /// * `client_extensions` - Per-client extension storage (isolated per connection)
468 ///
469 /// # Returns
470 ///
471 /// A `ResolveResult` indicating what action to take:
472 /// - `Completed` - Resolver handled everything via `SessionApi`
473 /// - Other variants - Runner should handle as before
474 ///
475 /// # Default Implementation
476 ///
477 /// Falls back to `resolve_with_extensions()` for backward compatibility.
478 ///
479 /// # Example
480 ///
481 /// ```ignore
482 /// use reovim_driver_session::{SessionApiDyn, ModeApi, BufferApi, ExtensionMap};
483 ///
484 /// fn resolve_with_session(
485 /// &self,
486 /// key: &KeyEvent,
487 /// state: &mut ModeState,
488 /// input: &ResolveInput<'_>,
489 /// session: &mut dyn SessionApiDyn,
490 /// shared_extensions: &mut ExtensionMap,
491 /// client_extensions: &mut ExtensionMap,
492 /// ) -> ResolveResult {
493 /// // Directly manipulate state via session
494 /// session.push_mode(insert_mode, TransitionContext::new());
495 /// session.move_cursor(buffer, Position::new(0, 5));
496 ///
497 /// // Per-client module state
498 /// let vim = client_extensions.get_or_insert::<VimSessionState>();
499 /// vim.pending_count = None;
500 ///
501 /// // Tell runner: "I'm done, just broadcast the changes"
502 /// ResolveResult::Completed
503 /// }
504 /// ```
505 fn resolve_with_session(
506 &self,
507 key: &KeyEvent,
508 state: &mut ModeState,
509 input: &ResolveInput<'_>,
510 _session: &mut dyn SessionApiDyn,
511 shared_extensions: &mut ExtensionMap,
512 client_extensions: &mut ExtensionMap,
513 ) -> ResolveResult {
514 // Default: delegate to resolve_with_extensions for backward compatibility.
515 // Resolvers that need session access should override this method.
516 self.resolve_with_extensions(key, state, input, shared_extensions, client_extensions)
517 }
518
519 /// Hook called after a command executes successfully.
520 ///
521 /// Called by the runner after executing a command that returns `Success`.
522 /// This allows resolvers to complete pending operations.
523 ///
524 /// # Architecture (Issue #388, Epic #415)
525 ///
526 /// This method keeps vim-specific operator completion logic in the vim module
527 /// instead of the runner, maintaining the mechanism/policy separation:
528 /// - **Runner (mechanism)**: Executes commands, calls this hook after success
529 /// - **Resolver (policy)**: Checks its stored state, completes pending operations
530 ///
531 /// Per #388, `CommandResult` has no `Motion` variant. Instead, the resolver
532 /// stores motion type info in `VimSessionState` BEFORE dispatching the motion,
533 /// then uses this hook to complete the operator.
534 ///
535 /// # Example Use Case: `dw` (delete word)
536 ///
537 /// 1. `d` key → Normal resolver stores pending operator, pushes operator-pending mode
538 /// 2. `w` key → Operator-pending resolver:
539 /// - Stores start position in `VimSessionState`
540 /// - Stores motion type (characterwise for 'w') in `VimSessionState`
541 /// - Returns `Execute(word-forward)`
542 /// 3. Runner executes motion → `CommandResult::Success`
543 /// 4. Runner calls `on_command_complete(session, extensions)`
544 /// 5. Operator-pending resolver:
545 /// - Checks `VimSessionState` for pending motion
546 /// - Gets end position from session
547 /// - Builds `PopResult::ExecuteCommand` with operator and args
548 /// - Returns `Some(ModeTransition::Pop { result: ExecuteCommand })`
549 /// 6. Runner handles the mode transition (executes the operator command)
550 ///
551 /// # Arguments
552 ///
553 /// * `session` - Session API for cursor/buffer access
554 /// * `shared_extensions` - Session-wide extension storage (shared across clients)
555 /// * `client_extensions` - Per-client extension storage (isolated per connection)
556 ///
557 /// # Returns
558 ///
559 /// * `Some(ModeTransition)` - Resolver wants to complete a pending operation
560 /// * `None` - No pending operation to complete
561 ///
562 /// # Default Implementation
563 ///
564 /// Returns `None` - most modes don't have pending operations.
565 fn on_command_complete(
566 &self,
567 _session: &mut dyn SessionApiDyn,
568 _shared_extensions: &mut ExtensionMap,
569 _client_extensions: &mut ExtensionMap,
570 ) -> Option<ModeTransition> {
571 // Default: no pending operation to complete
572 None
573 }
574
575 /// Which mode this resolver handles.
576 fn mode_id(&self) -> &ModeId;
577
578 /// Optional parent mode to try if we return `NotHandled`.
579 ///
580 /// This enables mode inheritance. For example, operator-pending mode
581 /// might inherit from normal mode for motion keys.
582 fn inherits_from(&self) -> Option<&ModeId> {
583 None
584 }
585
586 /// Reset any internal state (counts, pending keys, etc.).
587 ///
588 /// Called when:
589 /// - Mode is exited
590 /// - Escape is pressed
591 /// - An error occurs
592 fn reset(&mut self) {}
593
594 /// Get the accumulated pending keys for this resolver.
595 ///
596 /// Used by the session layer to populate `PendingBindings` generically
597 /// after a `Pending` result, enabling bridges (e.g., which-key) to
598 /// display available continuations without knowing about specific resolvers.
599 ///
600 /// # Default
601 ///
602 /// Returns an empty `KeySequence`. Resolvers that track pending keys
603 /// (e.g., `VimNormalResolver`) should override this.
604 fn pending_keys(&self) -> KeySequence {
605 KeySequence::new()
606 }
607}
608
609// ============================================================================
610// InputTarget - Where to route character input (#482)
611// ============================================================================
612
613/// Target for character input routing.
614///
615/// When a resolver returns `InsertChar`, this enum specifies where the
616/// character should be inserted. This eliminates string-based mode detection
617/// (like `mode_name.contains("command")`) in favor of explicit, type-safe routing.
618///
619/// # Architecture (#482)
620///
621/// | Layer | Responsibility |
622/// |-------|----------------|
623/// | Resolver | Returns `ResolveResult::InsertChar { char, target }` |
624/// | Runner | Routes char to target (buffer or extension) |
625/// | Extension | Receives char via `TextInputSink::insert_char()` |
626///
627/// # Example
628///
629/// ```ignore
630/// use reovim_driver_input::{ResolveResult, InputTarget};
631///
632/// // Insert into buffer (default for insert mode)
633/// ResolveResult::insert_char('x');
634///
635/// // Insert into a session extension implementing TextInputSink
636/// ResolveResult::insert_char_to::<MyExtension>('x');
637/// ```
638#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
639pub enum InputTarget {
640 /// Insert into the active buffer at cursor position.
641 ///
642 /// This is the default target, used by insert mode, replace mode, etc.
643 #[default]
644 Buffer,
645
646 /// Insert into a session extension that implements `TextInputSink`.
647 ///
648 /// The `TypeId` identifies the extension type. The runner looks up
649 /// the extension in `ExtensionMap` and calls `insert_char()` on it.
650 ///
651 /// # Example
652 ///
653 /// ```ignore
654 /// // Route to a session extension implementing TextInputSink
655 /// InputTarget::extension::<MyExtension>()
656 /// ```
657 Extension(TypeId),
658}
659
660impl InputTarget {
661 /// Create an extension target for the given type.
662 ///
663 /// # Example
664 ///
665 /// ```ignore
666 /// let target = InputTarget::extension::<MyExtension>();
667 /// ```
668 #[must_use]
669 pub const fn extension<T: SessionExtension + TextInputSink>() -> Self {
670 Self::Extension(TypeId::of::<T>())
671 }
672}
673
674// ============================================================================
675// ResolveResult
676// ============================================================================
677
678/// Result of resolving a key event.
679///
680/// This enum represents all possible outcomes of key resolution:
681/// - Execute a command with context
682/// - Wait for more keys (pending sequence)
683/// - Pass to parent mode
684/// - Insert a character
685/// - Request a mode transition
686/// - Completed (resolver executed everything via `SessionApi`)
687#[derive(Debug, Clone)]
688pub enum ResolveResult {
689 /// Execute this command with the given context.
690 ///
691 /// The command will be looked up in the command registry and executed
692 /// with the provided context (count, register, etc.).
693 Execute(CommandId, ResolveContext),
694
695 /// Key is part of a pending sequence, wait for more.
696 ///
697 /// Used for multi-key commands like `gg`, `<C-w>h`, etc.
698 /// The key has been accumulated; more keys are needed.
699 Pending,
700
701 /// This mode doesn't handle the key, try parent mode.
702 ///
703 /// If `inherits_from()` returns a parent mode, the runner should
704 /// try that mode's resolver next.
705 NotHandled,
706
707 /// Insert this character into the specified target.
708 ///
709 /// For input-accepting modes (insert, command-line, search).
710 /// The `target` specifies where to insert:
711 /// - `InputTarget::Buffer` - Insert at cursor position in active buffer
712 /// - `InputTarget::Extension(type_id)` - Insert into session extension
713 ///
714 /// # Helpers
715 ///
716 /// Use the helper methods for ergonomic construction:
717 /// - `ResolveResult::insert_char(c)` - Insert into buffer (default)
718 /// - `ResolveResult::insert_char_to::<T>(c)` - Insert into extension
719 ///
720 /// # Example
721 ///
722 /// ```ignore
723 /// use reovim_driver_input::ResolveResult;
724 ///
725 /// // Insert mode → buffer
726 /// ResolveResult::insert_char('x');
727 ///
728 /// // Extension mode → TextInputSink extension
729 /// ResolveResult::insert_char_to::<MyExtension>(':');
730 /// ```
731 InsertChar {
732 /// The character to insert.
733 char: char,
734 /// Where to insert (buffer or extension).
735 target: InputTarget,
736 },
737
738 /// Request a mode transition.
739 ///
740 /// Push, pop, or replace the current mode on the mode stack.
741 ModeTransition(ModeTransition),
742
743 /// Resolver already executed everything via `SessionApi`.
744 ///
745 /// The resolver used `SessionRuntime` methods directly to perform
746 /// all necessary operations (mode changes, cursor moves, buffer edits, etc.).
747 /// Runner should just take accumulated changes via `take_changes()` and
748 /// broadcast notifications to clients.
749 ///
750 /// This variant is used with the new `resolve_with_session` pattern
751 /// where resolvers receive `&mut impl SessionApi` and can act directly.
752 ///
753 /// # Example
754 ///
755 /// ```ignore
756 /// fn resolve_with_session<S: ModeApi + BufferApi>(
757 /// &self,
758 /// key: &KeyEvent,
759 /// state: &mut ModeState,
760 /// session: &mut S,
761 /// ) -> ResolveResult {
762 /// // Directly modify state via SessionApi
763 /// session.push_mode(insert_mode, TransitionContext::new());
764 ///
765 /// // Tell runner: "I'm done, just broadcast the changes"
766 /// ResolveResult::Completed
767 /// }
768 /// ```
769 Completed,
770
771 /// Inject keys into the input queue (Epic #465 Phase 8D - Macro Playback).
772 ///
773 /// Used by macro playback to inject recorded key sequences. The runner
774 /// should process these keys as if they were typed by the user.
775 ///
776 /// # Fields
777 ///
778 /// - `keys`: The key sequence to inject
779 /// - `exit_macro_playback`: If true, call `VimSessionState::exit_macro_playback()`
780 /// after all injected keys are processed
781 ///
782 /// # Example
783 ///
784 /// ```ignore
785 /// // Play macro from register 'a'
786 /// ResolveResult::InjectKeys {
787 /// keys: vec![KeyEvent::new(KeyCode::Char('d')), KeyEvent::new(KeyCode::Char('w'))],
788 /// exit_macro_playback: true,
789 /// }
790 /// ```
791 InjectKeys {
792 /// Keys to inject into the input queue.
793 keys: Vec<crate::KeyEvent>,
794 /// Whether to call `exit_macro_playback()` after processing.
795 exit_macro_playback: bool,
796 },
797}
798
799impl ResolveResult {
800 /// Create an `InsertChar` result that inserts into the buffer (default).
801 ///
802 /// Use this for insert mode, replace mode, and other modes that edit
803 /// the active buffer.
804 ///
805 /// # Example
806 ///
807 /// ```ignore
808 /// use reovim_driver_input::ResolveResult;
809 ///
810 /// // In insert mode resolver
811 /// ResolveResult::insert_char('x')
812 /// ```
813 #[must_use]
814 pub const fn insert_char(c: char) -> Self {
815 Self::InsertChar {
816 char: c,
817 target: InputTarget::Buffer,
818 }
819 }
820
821 /// Create an `InsertChar` result that inserts into a session extension.
822 ///
823 /// Use this for modes that input into extension state, such as command-line
824 /// mode or search mode.
825 ///
826 /// The extension must implement both `SessionExtension` and `TextInputSink`.
827 ///
828 /// # Example
829 ///
830 /// ```ignore
831 /// use reovim_driver_input::ResolveResult;
832 ///
833 /// // In a resolver that inputs to an extension
834 /// ResolveResult::insert_char_to::<MyExtension>(':')
835 /// ```
836 #[must_use]
837 pub const fn insert_char_to<T: SessionExtension + TextInputSink>(c: char) -> Self {
838 Self::InsertChar {
839 char: c,
840 target: InputTarget::extension::<T>(),
841 }
842 }
843}
844
845// ============================================================================
846// ResolveContext
847// ============================================================================
848
849/// Context passed when executing a command.
850///
851/// Contains all the information gathered during key resolution:
852/// - Count prefix (e.g., 3 in `3j`)
853/// - Register (e.g., `a` in `"ayw`)
854/// - Key sequence that triggered the command
855/// - Arbitrary metadata for complex commands
856#[derive(Debug, Clone, Default)]
857pub struct ResolveContext {
858 /// Count prefix (e.g., 3 in `3j`).
859 pub count: Option<usize>,
860
861 /// Register for the operation (e.g., `a` in `"ayw`).
862 pub register: Option<char>,
863
864 /// The key sequence that triggered this command.
865 pub keys: KeySequence,
866
867 /// Arbitrary metadata for command-specific data.
868 ///
869 /// Used for passing operator-specific information, motion flags, etc.
870 pub metadata: HashMap<String, ArgValue>,
871}
872
873impl ResolveContext {
874 /// Create a new empty context.
875 #[must_use]
876 pub fn new() -> Self {
877 Self::default()
878 }
879
880 /// Create a context with just a count.
881 #[must_use]
882 pub fn with_count(count: usize) -> Self {
883 Self {
884 count: Some(count),
885 ..Default::default()
886 }
887 }
888
889 /// Set the count.
890 #[must_use]
891 pub const fn count(mut self, count: usize) -> Self {
892 self.count = Some(count);
893 self
894 }
895
896 /// Set the register.
897 #[must_use]
898 pub const fn register(mut self, register: char) -> Self {
899 self.register = Some(register);
900 self
901 }
902
903 /// Set the keys.
904 #[must_use]
905 pub fn keys(mut self, keys: KeySequence) -> Self {
906 self.keys = keys;
907 self
908 }
909
910 /// Add metadata.
911 #[must_use]
912 pub fn with_metadata(mut self, key: impl Into<String>, value: ArgValue) -> Self {
913 self.metadata.insert(key.into(), value);
914 self
915 }
916
917 /// Get the effective count (default to 1 if not set).
918 #[must_use]
919 pub fn effective_count(&self) -> usize {
920 self.count.unwrap_or(1)
921 }
922}
923
924// ============================================================================
925// ArgValue
926// ============================================================================
927
928/// Dynamic argument value for command metadata.
929///
930/// Supports common types used in command arguments.
931#[derive(Debug, Clone, PartialEq)]
932pub enum ArgValue {
933 /// Boolean flag.
934 Bool(bool),
935 /// Integer value.
936 Int(i64),
937 /// Unsigned integer.
938 Uint(u64),
939 /// Floating point.
940 Float(f64),
941 /// String value.
942 String(String),
943 /// Character value.
944 Char(char),
945 /// Position in buffer.
946 Position(Position),
947 /// Range (start, end, linewise).
948 Range {
949 start: Position,
950 end: Position,
951 linewise: bool,
952 },
953}
954
955impl From<bool> for ArgValue {
956 fn from(v: bool) -> Self {
957 Self::Bool(v)
958 }
959}
960
961impl From<i64> for ArgValue {
962 fn from(v: i64) -> Self {
963 Self::Int(v)
964 }
965}
966
967impl From<u64> for ArgValue {
968 fn from(v: u64) -> Self {
969 Self::Uint(v)
970 }
971}
972
973impl From<f64> for ArgValue {
974 fn from(v: f64) -> Self {
975 Self::Float(v)
976 }
977}
978
979impl From<String> for ArgValue {
980 fn from(v: String) -> Self {
981 Self::String(v)
982 }
983}
984
985impl From<&str> for ArgValue {
986 fn from(v: &str) -> Self {
987 Self::String(v.to_owned())
988 }
989}
990
991impl From<char> for ArgValue {
992 fn from(v: char) -> Self {
993 Self::Char(v)
994 }
995}
996
997impl From<Position> for ArgValue {
998 fn from(v: Position) -> Self {
999 Self::Position(v)
1000 }
1001}
1002
1003// ============================================================================
1004// ModeTransition
1005// ============================================================================
1006
1007/// Mode transition request.
1008///
1009/// Resolvers return this to request changes to the mode stack.
1010#[derive(Debug, Clone)]
1011pub enum ModeTransition {
1012 /// Push a mode onto the stack with context.
1013 ///
1014 /// The new mode becomes current. The previous mode remains on the stack.
1015 /// Used for operator-pending, command-line, search, etc.
1016 Push {
1017 /// The mode to push.
1018 mode: ModeId,
1019 /// Context to pass to the new mode.
1020 context: TransitionContext,
1021 },
1022
1023 /// Pop the current mode, passing result to parent.
1024 ///
1025 /// The current mode is removed from the stack. The parent mode
1026 /// receives the result (e.g., operator range, cancelled).
1027 Pop {
1028 /// Result to pass to the parent mode.
1029 result: Option<PopResult>,
1030 },
1031
1032 /// Replace the current mode (equivalent to pop + push, but atomic).
1033 ///
1034 /// Used for mode changes that don't stack (normal → insert).
1035 Set {
1036 /// The mode to switch to.
1037 mode: ModeId,
1038 /// Context for the new mode.
1039 context: TransitionContext,
1040 },
1041}
1042
1043// TransitionContext and PopResult are re-exported from session driver above.
1044
1045// ============================================================================
1046// ModeState
1047// ============================================================================
1048
1049/// Shared state passed to all resolvers.
1050///
1051/// This is the mechanism layer - it provides access to kernel state
1052/// without encoding any policy about how modes should behave.
1053///
1054/// # Note on Resolver-Owned State
1055///
1056/// Mode-specific state (counts, pending keys, pending operators) is
1057/// owned by each resolver, NOT by `ModeState`. This allows:
1058/// - Different editing styles to have different state needs
1059/// - Unit testing resolvers without full runner
1060/// - Clean hot-reload (replace resolver, state resets)
1061#[derive(Debug, Clone)]
1062pub struct ModeState {
1063 /// Keys accumulated for multi-key sequence matching.
1064 ///
1065 /// Cleared when a sequence completes or times out.
1066 pub pending_keys: KeySequence,
1067
1068 /// Current mode stack.
1069 ///
1070 /// Read-only access; transitions happen via `ResolveResult::ModeTransition`.
1071 pub mode_stack: ModeStack,
1072
1073 /// Currently active buffer (if any).
1074 pub active_buffer: Option<BufferId>,
1075
1076 /// Context from the last mode transition.
1077 ///
1078 /// Contains operator, count, register from the parent mode.
1079 pub transition_context: Option<TransitionContext>,
1080}
1081
1082impl ModeState {
1083 /// Create a new mode state with the given initial mode.
1084 #[must_use]
1085 pub fn new(initial_mode: ModeId) -> Self {
1086 Self {
1087 pending_keys: KeySequence::new(),
1088 mode_stack: ModeStack::new(initial_mode),
1089 active_buffer: None,
1090 transition_context: None,
1091 }
1092 }
1093
1094 /// Get the current mode.
1095 #[must_use]
1096 pub fn current_mode(&self) -> &ModeId {
1097 self.mode_stack.current()
1098 }
1099
1100 /// Check if keys are pending.
1101 #[must_use]
1102 pub const fn has_pending_keys(&self) -> bool {
1103 !self.pending_keys.is_empty()
1104 }
1105
1106 /// Clear pending keys.
1107 pub fn clear_pending_keys(&mut self) {
1108 self.pending_keys.clear();
1109 }
1110
1111 /// Add a key to the pending sequence.
1112 pub fn push_pending_key(&mut self, key: KeyEvent) {
1113 self.pending_keys.push(key);
1114 }
1115
1116 /// Take the transition context (clears it).
1117 pub const fn take_transition_context(&mut self) -> Option<TransitionContext> {
1118 self.transition_context.take()
1119 }
1120}
1121
1122// ============================================================================
1123// Tests
1124// ============================================================================