Skip to main content

reovim_module_vim/commands/
mode.rs

1//! Mode switching commands.
2//!
3//! Provides commands for switching between Vim modes:
4//! - Enter insert mode (i, a)
5//! - Exit to normal mode (Escape)
6//! - Enter window mode (Ctrl-W)
7//! - Enter/exit command-line mode
8//!
9//! # Epic #372 - Mode Ownership
10//!
11//! These commands use `VimMode::*_ID` constants directly, which is why they
12//! belong in the vim module rather than the generic editor module.
13//!
14//! # `SessionApi` Migration (Epic #394)
15//!
16//! These commands use the `ModeApi` trait to switch modes, which updates the
17//! session's mode stack directly. Changes are synced back to `AppState` after
18//! command execution.
19
20use {
21    reovim_driver_command::{Command, CommandContext, CommandHandler, CommandResult},
22    reovim_driver_search::Direction,
23    reovim_driver_session::{
24        BufferApi, SessionRuntime, TransitionContext,
25        api::{ChangeTracker, ExtensionApi, ModeApi, SearchState},
26    },
27    reovim_driver_undo::{UndoKey, UndoProviderRegistry},
28    reovim_kernel::api::v1::{CommandId, Position},
29    reovim_module_cmdline::{CmdlineMessage, CmdlinePrompt, CmdlineState},
30    std::sync::Arc,
31};
32
33/// Helper to get cursor position from the active window.
34fn get_cursor_position(runtime: &SessionRuntime<'_>) -> Option<Position> {
35    let window = runtime.windows().active()?;
36    Some(Position::new(window.cursor.line, window.cursor.column))
37}
38
39/// Helper to set cursor position on the active window.
40#[cfg_attr(coverage_nightly, coverage(off))]
41fn set_cursor_position(runtime: &mut SessionRuntime<'_>, pos: Position) {
42    if let Some(window) = runtime.windows_mut().active_mut() {
43        window.cursor = pos.into();
44    }
45}
46
47use crate::{ids, modes::VimMode};
48
49/// Start undo batching for insert mode.
50///
51/// All edits until `end_insert_batch` are grouped as a single undo entry.
52#[cfg_attr(coverage_nightly, coverage(off))]
53fn begin_insert_batch(runtime: &SessionRuntime<'_>, buffer_id: reovim_kernel::api::v1::BufferId) {
54    if let Some(pos) = get_cursor_position(runtime)
55        && let Some(undo_registry) = runtime.kernel().services.get::<UndoProviderRegistry>()
56        && let Some(undo_provider) = undo_registry.get(&UndoKey::Buffer)
57    {
58        undo_provider.begin_batch(buffer_id, pos);
59    }
60}
61
62/// End undo batching for insert mode.
63///
64/// Commits all accumulated edits as a single undo entry.
65#[cfg_attr(coverage_nightly, coverage(off))]
66fn end_insert_batch(runtime: &SessionRuntime<'_>, buffer_id: reovim_kernel::api::v1::BufferId) {
67    if let Some(pos) = get_cursor_position(runtime)
68        && let Some(undo_registry) = runtime.kernel().services.get::<UndoProviderRegistry>()
69        && let Some(undo_provider) = undo_registry.get(&UndoKey::Buffer)
70    {
71        undo_provider.end_batch(buffer_id, pos);
72    }
73}
74
75/// Enter insert mode (before cursor).
76#[derive(Debug, Clone, Copy, Default)]
77pub struct EnterInsertMode;
78
79impl Command for EnterInsertMode {
80    fn id(&self) -> CommandId {
81        ids::ENTER_INSERT
82    }
83
84    fn description(&self) -> &'static str {
85        "Enter insert mode"
86    }
87}
88
89impl CommandHandler for EnterInsertMode {
90    #[cfg_attr(coverage_nightly, coverage(off))]
91    fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
92        // Start undo batching for insert mode
93        if let Some(buffer_id) = args.buffer_id() {
94            begin_insert_batch(runtime, buffer_id);
95        }
96        // Clear insert buffer for dot repeat tracking (Epic #465)
97        if let Some(vim) = runtime.ext_mut::<crate::VimSessionState>().into() {
98            vim.insert_buffer.clear();
99        }
100        runtime.set_mode(VimMode::INSERT_ID, TransitionContext::new());
101        CommandResult::Success
102    }
103}
104
105/// Enter insert mode after cursor (a).
106#[derive(Debug, Clone, Copy, Default)]
107pub struct EnterInsertModeAppend;
108
109impl Command for EnterInsertModeAppend {
110    fn id(&self) -> CommandId {
111        ids::ENTER_INSERT_AFTER
112    }
113
114    fn description(&self) -> &'static str {
115        "Enter insert mode after cursor (append)"
116    }
117}
118
119impl CommandHandler for EnterInsertModeAppend {
120    #[cfg_attr(coverage_nightly, coverage(off))]
121    fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
122        // Move cursor right first, then enter insert mode
123        if let Some(buffer_id) = args.buffer_id() {
124            if let Some(pos) = get_cursor_position(runtime)
125                && let Some(line_len) = runtime.buffer_line_len(buffer_id, pos.line)
126            {
127                // Move right only if not at end of line
128                if pos.column < line_len {
129                    set_cursor_position(runtime, Position::new(pos.line, pos.column + 1));
130                }
131            }
132            // Start undo batching for insert mode
133            begin_insert_batch(runtime, buffer_id);
134        }
135
136        runtime.set_mode(VimMode::INSERT_ID, TransitionContext::new());
137        CommandResult::Success
138    }
139}
140
141/// Exit to normal mode (Escape from insert mode).
142#[derive(Debug, Clone, Copy, Default)]
143pub struct ExitToNormal;
144
145impl Command for ExitToNormal {
146    fn id(&self) -> CommandId {
147        ids::EXIT_INSERT
148    }
149
150    fn description(&self) -> &'static str {
151        "Exit insert mode and return to normal mode"
152    }
153}
154
155impl CommandHandler for ExitToNormal {
156    #[cfg_attr(coverage_nightly, coverage(off))]
157    fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
158        if let Some(buffer_id) = args.buffer_id() {
159            // End undo batching - commits all insert edits as one undo entry
160            end_insert_batch(runtime, buffer_id);
161
162            // Move cursor left one position when exiting insert mode (Vim behavior)
163            if let Some(pos) = get_cursor_position(runtime)
164                && pos.column > 0
165            {
166                set_cursor_position(runtime, Position::new(pos.line, pos.column - 1));
167            }
168        }
169
170        // Record insert mode text for dot repeat (Epic #465, #577)
171        if let Some(vim) = runtime.ext_mut::<crate::VimSessionState>().into() {
172            let insert_text = std::mem::take(&mut vim.insert_buffer);
173            if vim.recording_repeat {
174                // #577: Operator-initiated insert (e.g., cwbar<Esc>) —
175                // don't overwrite last_change (operator set it), just finish recording
176                vim.finish_repeat_recording();
177            } else if !insert_text.is_empty() {
178                // Standalone insert (e.g., ihello<Esc>) — record as Insert change
179                vim.last_change = Some(crate::session_state::LastChange {
180                    change_type: crate::session_state::ChangeType::Insert { text: insert_text },
181                    count: None,
182                    register: None,
183                    keys: Vec::new(),
184                });
185            }
186        }
187
188        runtime.set_mode(VimMode::NORMAL_ID, TransitionContext::new());
189        CommandResult::Success
190    }
191}
192
193/// Enter replace mode (R in normal mode).
194///
195/// In replace mode, typed characters overwrite existing text at the cursor.
196/// Backspace restores original characters. Escape returns to normal mode.
197#[derive(Debug, Clone, Copy, Default)]
198pub struct EnterReplaceMode;
199
200impl Command for EnterReplaceMode {
201    fn id(&self) -> CommandId {
202        ids::ENTER_REPLACE_MODE
203    }
204
205    fn description(&self) -> &'static str {
206        "Enter replace mode"
207    }
208}
209
210impl CommandHandler for EnterReplaceMode {
211    #[cfg_attr(coverage_nightly, coverage(off))]
212    fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
213        // Start undo batching for replace mode (like insert mode entry)
214        if let Some(buffer_id) = args.buffer_id() {
215            begin_insert_batch(runtime, buffer_id);
216        }
217        // Clear insert buffer and replace restore stack
218        if let Some(vim) = runtime.ext_mut::<crate::VimSessionState>().into() {
219            vim.insert_buffer.clear();
220            vim.replace_restore_stack.clear();
221        }
222        runtime.set_mode(VimMode::REPLACE_ID, TransitionContext::new());
223        CommandResult::Success
224    }
225}
226
227/// Backspace in replace mode.
228///
229/// Restores the original character that was overwritten. Pops from
230/// the replace restore stack in `VimSessionState`.
231#[derive(Debug, Clone, Copy, Default)]
232pub struct ReplaceBackspace;
233
234impl Command for ReplaceBackspace {
235    fn id(&self) -> CommandId {
236        ids::REPLACE_BACKSPACE
237    }
238
239    fn description(&self) -> &'static str {
240        "Restore original character in replace mode"
241    }
242}
243
244impl CommandHandler for ReplaceBackspace {
245    #[cfg_attr(coverage_nightly, coverage(off))]
246    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
247        let entry = runtime
248            .ext_mut::<crate::VimSessionState>()
249            .replace_restore_stack
250            .pop();
251
252        let Some(entry) = entry else {
253            // Nothing to restore
254            return CommandResult::Success;
255        };
256
257        let Some(window) = runtime.windows().active() else {
258            return CommandResult::Success;
259        };
260        let Some(buffer_id) = window.buffer_id else {
261            return CommandResult::Success;
262        };
263        let cursor = Position::new(window.cursor.line, window.cursor.column);
264
265        if cursor.column > 0 {
266            let delete_col = cursor.column - 1;
267            // Delete the replacement character
268            runtime.delete_range(buffer_id, Position::new(cursor.line, delete_col), cursor);
269
270            // Restore original character if there was one
271            if let Some(original) = entry.original {
272                let restore_str = String::from(original);
273                runtime.insert_text(
274                    buffer_id,
275                    Position::new(cursor.line, delete_col),
276                    &restore_str,
277                );
278            }
279
280            // Move cursor back
281            if let Some(w) = runtime.windows_mut().active_mut() {
282                w.cursor.column = delete_col;
283            }
284        } else if cursor.line > 0 {
285            // At start of line — join with previous line
286            let prev_line_len = runtime
287                .buffer_line_len(buffer_id, cursor.line - 1)
288                .unwrap_or(0);
289
290            // Delete the newline
291            runtime.delete_range(
292                buffer_id,
293                Position::new(cursor.line - 1, prev_line_len),
294                Position::new(cursor.line, 0),
295            );
296
297            // Restore original char if there was one
298            if let Some(original) = entry.original {
299                let restore_str = String::from(original);
300                runtime.insert_text(
301                    buffer_id,
302                    Position::new(cursor.line - 1, prev_line_len),
303                    &restore_str,
304                );
305            }
306
307            // Move cursor to join point
308            if let Some(w) = runtime.windows_mut().active_mut() {
309                w.cursor.line -= 1;
310                w.cursor.column = prev_line_len;
311            }
312        }
313
314        // Pop from insert buffer for dot repeat
315        runtime
316            .ext_mut::<crate::VimSessionState>()
317            .insert_buffer
318            .pop();
319
320        CommandResult::Success
321    }
322}
323
324/// Enter window management mode (Ctrl-W in normal mode).
325///
326/// This pushes "window" mode onto the mode stack. In window mode,
327/// subsequent keys (h/j/k/l for navigation, s/v for splits, etc.)
328/// are handled by the layout module's keybindings.
329#[derive(Debug, Clone, Copy, Default)]
330pub struct EnterWindowMode;
331
332impl Command for EnterWindowMode {
333    fn id(&self) -> CommandId {
334        ids::ENTER_WINDOW_MODE
335    }
336
337    fn description(&self) -> &'static str {
338        "Enter window management mode"
339    }
340}
341
342impl CommandHandler for EnterWindowMode {
343    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
344        // Window mode is pushed onto the stack (can be exited to return to normal)
345        runtime.push_mode(VimMode::WINDOW_ID, TransitionContext::new());
346        CommandResult::Success
347    }
348}
349
350/// Cancel and return to normal mode (no cursor adjustment).
351///
352/// Used by operator modes (delete, yank, change) when the user presses Escape.
353/// Unlike `ExitToNormal`, this does not adjust the cursor position.
354#[derive(Debug, Clone, Copy, Default)]
355pub struct CancelToNormal;
356
357impl Command for CancelToNormal {
358    fn id(&self) -> CommandId {
359        ids::CANCEL_TO_NORMAL
360    }
361
362    fn description(&self) -> &'static str {
363        "Cancel and return to normal mode"
364    }
365}
366
367impl CommandHandler for CancelToNormal {
368    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
369        runtime.set_mode(VimMode::NORMAL_ID, TransitionContext::new());
370        CommandResult::Success
371    }
372}
373
374/// Enter command-line mode (`:` in normal mode).
375///
376/// This pushes "commandline" mode onto the mode stack. In command-line mode,
377/// the user can type Ex commands like `:w`, `:q`, `:set`, etc.
378#[derive(Debug, Clone, Copy, Default)]
379pub struct EnterCommandLineMode;
380
381impl Command for EnterCommandLineMode {
382    fn id(&self) -> CommandId {
383        ids::ENTER_COMMANDLINE
384    }
385
386    fn description(&self) -> &'static str {
387        "Enter command-line mode"
388    }
389}
390
391impl CommandHandler for EnterCommandLineMode {
392    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
393        // Activate cmdline with command prompt
394        runtime
395            .ext_mut::<CmdlineState>()
396            .enter(CmdlinePrompt::Command);
397        runtime.set_mode(VimMode::COMMANDLINE_ID, TransitionContext::new());
398        CommandResult::Success
399    }
400}
401
402/// Exit command-line mode (Escape or Enter).
403///
404/// Returns to normal mode from command-line mode.
405/// If a search was pending, executes the search with the entered pattern.
406#[derive(Debug, Clone, Copy, Default)]
407pub struct ExitCommandLineMode;
408
409impl Command for ExitCommandLineMode {
410    fn id(&self) -> CommandId {
411        ids::EXIT_COMMANDLINE
412    }
413
414    fn description(&self) -> &'static str {
415        "Exit command-line mode and return to normal mode"
416    }
417}
418
419impl CommandHandler for ExitCommandLineMode {
420    fn execute(&self, runtime: &mut SessionRuntime<'_>, args: &CommandContext) -> CommandResult {
421        // Get the prompt type to determine how to handle the input
422        let prompt = runtime.ext_mut::<CmdlineState>().prompt();
423
424        // Push input to history before taking it (#451)
425        runtime.ext_mut::<CmdlineState>().push_to_history();
426
427        // Get the cmdline input
428        let cmdline = runtime.ext_mut::<CmdlineState>().take_cmdline_input();
429
430        // Exit cmdline and return to normal BEFORE dispatching the action.
431        // This ensures any mode transition made by the action (e.g., push_mode)
432        // stacks on top of NORMAL rather than being clobbered by set_mode below.
433        runtime.ext_mut::<CmdlineState>().exit();
434        runtime.set_mode(VimMode::NORMAL_ID, TransitionContext::new());
435
436        match prompt {
437            CmdlinePrompt::SearchForward | CmdlinePrompt::SearchBackward => {
438                // Handle search
439                let pending = {
440                    let search_state = runtime.ext_mut::<SearchState>();
441                    search_state.take_pending_search()
442                };
443
444                if let Some(direction) = pending
445                    && !cmdline.is_empty()
446                {
447                    // Store pattern and direction for n/N repeat
448                    runtime
449                        .ext_mut::<SearchState>()
450                        .set(cmdline.clone(), direction);
451
452                    // Execute the search
453                    execute_search(runtime, args, &cmdline, direction);
454                }
455            }
456            CmdlinePrompt::Command => {
457                // Handle ex-command (e.g., :w, :q, :e)
458                if !cmdline.is_empty() {
459                    execute_ex_command(runtime, args, &cmdline);
460                }
461            }
462        }
463
464        CommandResult::Success
465    }
466}
467
468/// Strip range prefix from a command line string.
469///
470/// Detects and removes range prefixes like `%`, returning the range
471/// as `(start_line, end_line)` and the remaining command string.
472///
473/// Supported prefixes:
474/// - `%` — entire buffer (0, `last_line`)
475/// - No prefix — returns `None` (command handles default)
476#[cfg_attr(coverage_nightly, coverage(off))]
477fn strip_range_prefix<'a>(
478    runtime: &SessionRuntime<'_>,
479    args: &CommandContext,
480    cmdline: &'a str,
481) -> (Option<(usize, usize)>, &'a str) {
482    let trimmed = cmdline.trim_start();
483
484    // % prefix means entire buffer
485    if let Some(rest) = trimmed.strip_prefix('%') {
486        let line_count = args
487            .buffer_id()
488            .and_then(|bid| runtime.buffer_line_count(bid))
489            .unwrap_or(1);
490        let last_line = line_count.saturating_sub(1);
491        return (Some((0, last_line)), rest);
492    }
493
494    (None, cmdline)
495}
496
497/// Execute an ex-command via `CommandNameIndex` and `runtime.execute_command()`.
498///
499/// Parses the command line, resolves the command name via `CommandNameIndex`,
500/// binds arguments to the command's `ArgSpec` declarations via `bind_args()`,
501/// and dispatches through the unified command system.
502#[cfg_attr(coverage_nightly, coverage(off))]
503fn execute_ex_command(runtime: &mut SessionRuntime<'_>, args: &CommandContext, cmdline: &str) {
504    use {
505        reovim_driver_command::{CommandNameIndex, bind_args, parse_cmdline},
506        reovim_driver_session::CommandApi,
507    };
508
509    // Strip range prefix (%, line numbers) from cmdline (#666)
510    let (range, effective_cmdline) = strip_range_prefix(runtime, args, cmdline);
511
512    let Some(parsed) = parse_cmdline(effective_cmdline) else {
513        return;
514    };
515
516    let Some(name_index) = runtime.kernel().services.get::<CommandNameIndex>() else {
517        tracing::warn!("CommandNameIndex not registered - ex-commands not available");
518        return;
519    };
520
521    let (cmd_id, specs) = match name_index.resolve_prefix(&parsed.name) {
522        Ok(Some((id, cmd))) => (id.clone(), cmd.args()),
523        Ok(None) => {
524            let msg = format!("E492: Not an editor command: {}", parsed.name);
525            runtime
526                .ext_mut::<CmdlineState>()
527                .set_message(CmdlineMessage::Error(msg));
528            return;
529        }
530        Err(ambiguous) => {
531            runtime
532                .ext_mut::<CmdlineState>()
533                .set_message(CmdlineMessage::Error(ambiguous.to_string()));
534            return;
535        }
536    };
537    // Drop the borrow on name_index before calling execute_command
538    drop(name_index);
539
540    let bound = match bind_args(&specs, &parsed.raw_args, parsed.bang) {
541        Ok(map) => map,
542        Err(e) => {
543            runtime
544                .ext_mut::<CmdlineState>()
545                .set_message(CmdlineMessage::Error(e.to_string()));
546            return;
547        }
548    };
549
550    // Build command context from bound arguments
551    let mut ctx = CommandContext::new();
552    for (name, value) in bound {
553        ctx.set(&name, value);
554    }
555    // Propagate buffer_id and VFS from outer args
556    if let Some(bid) = args.buffer_id() {
557        ctx.set_buffer_id(bid);
558    }
559    if let Some(vfs) = args.vfs() {
560        ctx.set_vfs(Arc::clone(vfs));
561    }
562    // Propagate range if detected (#666)
563    if let Some((start, end)) = range {
564        ctx.set("range", reovim_driver_command_types::ArgValue::Range(start, end));
565    }
566
567    let result = runtime.execute_command(cmd_id, ctx);
568    if let CommandResult::Error(msg) = result {
569        runtime
570            .ext_mut::<CmdlineState>()
571            .set_message(CmdlineMessage::Error(msg));
572    }
573}
574
575/// Execute a search and move cursor to the first match.
576fn execute_search(
577    runtime: &mut SessionRuntime<'_>,
578    args: &CommandContext,
579    pattern: &str,
580    direction: Direction,
581) {
582    use reovim_driver_search::{SearchKey, SearchProviderRegistry};
583
584    let Some(buffer_id) = args.buffer_id() else {
585        return;
586    };
587
588    let Some(cursor) = get_cursor_position(runtime) else {
589        return;
590    };
591
592    let Some(search_registry) = runtime.kernel().services.get::<SearchProviderRegistry>() else {
593        tracing::warn!("Search provider not available");
594        return;
595    };
596
597    let Some(search_provider) = search_registry.get(&SearchKey::Regex) else {
598        tracing::warn!("Regex search engine not registered");
599        return;
600    };
601
602    // Search for pattern
603    let search_result = runtime.with_buffer_read(buffer_id, |buffer| {
604        search_provider.find_next(buffer, cursor, pattern, direction, true)
605    });
606
607    match search_result {
608        Some(Ok(Some(m))) => {
609            // Move cursor to match start
610            set_cursor_position(runtime, m.start);
611            runtime.record_cursor_move(buffer_id);
612            tracing::debug!(
613                pattern,
614                ?direction,
615                match_start = ?m.start,
616                "Search found match"
617            );
618        }
619        Some(Ok(None)) => {
620            tracing::debug!(pattern, "Search: no match found");
621        }
622        Some(Err(e)) => {
623            tracing::debug!(pattern, ?e, "Search: invalid pattern");
624        }
625        None => {
626            tracing::warn!("Buffer not found for search");
627        }
628    }
629}
630
631/// Cancel command-line mode without executing (Escape).
632///
633/// Unlike `ExitCommandLineMode`, this clears the pending search and cmdline
634/// input without executing any action.
635#[derive(Debug, Clone, Copy, Default)]
636pub struct CancelCommandLineMode;
637
638impl Command for CancelCommandLineMode {
639    fn id(&self) -> CommandId {
640        ids::CANCEL_COMMANDLINE
641    }
642
643    fn description(&self) -> &'static str {
644        "Cancel command-line mode without executing"
645    }
646}
647
648impl CommandHandler for CancelCommandLineMode {
649    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
650        // Clear any pending search - we're canceling, not executing
651        runtime.ext_mut::<SearchState>().clear_pending_search();
652
653        // Cancel cmdline (signals to runner: don't execute)
654        runtime.ext_mut::<CmdlineState>().cancel();
655        runtime.set_mode(VimMode::NORMAL_ID, TransitionContext::new());
656        CommandResult::Success
657    }
658}
659
660// =============================================================================
661// Command-line editing commands (#451)
662// =============================================================================
663
664/// Move cursor left in command-line.
665#[derive(Debug, Clone, Copy, Default)]
666pub struct CmdlineCursorLeft;
667
668impl Command for CmdlineCursorLeft {
669    fn id(&self) -> CommandId {
670        ids::CMDLINE_CURSOR_LEFT
671    }
672    fn description(&self) -> &'static str {
673        "Move cursor left in command-line"
674    }
675}
676
677impl CommandHandler for CmdlineCursorLeft {
678    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
679        runtime.ext_mut::<CmdlineState>().move_cursor_left();
680        CommandResult::Success
681    }
682}
683
684/// Move cursor right in command-line.
685#[derive(Debug, Clone, Copy, Default)]
686pub struct CmdlineCursorRight;
687
688impl Command for CmdlineCursorRight {
689    fn id(&self) -> CommandId {
690        ids::CMDLINE_CURSOR_RIGHT
691    }
692    fn description(&self) -> &'static str {
693        "Move cursor right in command-line"
694    }
695}
696
697impl CommandHandler for CmdlineCursorRight {
698    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
699        runtime.ext_mut::<CmdlineState>().move_cursor_right();
700        CommandResult::Success
701    }
702}
703
704/// Move cursor to start of command-line.
705#[derive(Debug, Clone, Copy, Default)]
706pub struct CmdlineCursorHome;
707
708impl Command for CmdlineCursorHome {
709    fn id(&self) -> CommandId {
710        ids::CMDLINE_CURSOR_HOME
711    }
712    fn description(&self) -> &'static str {
713        "Move cursor to start of command-line"
714    }
715}
716
717impl CommandHandler for CmdlineCursorHome {
718    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
719        runtime.ext_mut::<CmdlineState>().move_to_start();
720        CommandResult::Success
721    }
722}
723
724/// Move cursor to end of command-line.
725#[derive(Debug, Clone, Copy, Default)]
726pub struct CmdlineCursorEnd;
727
728impl Command for CmdlineCursorEnd {
729    fn id(&self) -> CommandId {
730        ids::CMDLINE_CURSOR_END
731    }
732    fn description(&self) -> &'static str {
733        "Move cursor to end of command-line"
734    }
735}
736
737impl CommandHandler for CmdlineCursorEnd {
738    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
739        runtime.ext_mut::<CmdlineState>().move_to_end();
740        CommandResult::Success
741    }
742}
743
744/// Delete character at cursor in command-line (Del key).
745#[derive(Debug, Clone, Copy, Default)]
746pub struct CmdlineDeleteChar;
747
748impl Command for CmdlineDeleteChar {
749    fn id(&self) -> CommandId {
750        ids::CMDLINE_DELETE_CHAR
751    }
752    fn description(&self) -> &'static str {
753        "Delete character at cursor in command-line"
754    }
755}
756
757impl CommandHandler for CmdlineDeleteChar {
758    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
759        runtime.ext_mut::<CmdlineState>().delete_at_cursor();
760        CommandResult::Success
761    }
762}
763
764/// Delete character before cursor in command-line (Backspace).
765#[derive(Debug, Clone, Copy, Default)]
766pub struct CmdlineBackspace;
767
768impl Command for CmdlineBackspace {
769    fn id(&self) -> CommandId {
770        ids::CMDLINE_BACKSPACE
771    }
772    fn description(&self) -> &'static str {
773        "Delete character before cursor in command-line"
774    }
775}
776
777impl CommandHandler for CmdlineBackspace {
778    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
779        runtime.ext_mut::<CmdlineState>().backspace();
780        CommandResult::Success
781    }
782}
783
784/// Delete word before cursor in command-line (Ctrl-W).
785#[derive(Debug, Clone, Copy, Default)]
786pub struct CmdlineDeleteWord;
787
788impl Command for CmdlineDeleteWord {
789    fn id(&self) -> CommandId {
790        ids::CMDLINE_DELETE_WORD
791    }
792    fn description(&self) -> &'static str {
793        "Delete word before cursor in command-line"
794    }
795}
796
797impl CommandHandler for CmdlineDeleteWord {
798    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
799        runtime.ext_mut::<CmdlineState>().delete_word_back();
800        CommandResult::Success
801    }
802}
803
804/// Delete to start of command-line (Ctrl-U).
805#[derive(Debug, Clone, Copy, Default)]
806pub struct CmdlineDeleteToStart;
807
808impl Command for CmdlineDeleteToStart {
809    fn id(&self) -> CommandId {
810        ids::CMDLINE_DELETE_TO_START
811    }
812    fn description(&self) -> &'static str {
813        "Delete to start of command-line"
814    }
815}
816
817impl CommandHandler for CmdlineDeleteToStart {
818    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
819        runtime.ext_mut::<CmdlineState>().delete_to_start();
820        CommandResult::Success
821    }
822}
823
824// =============================================================================
825// Command-line history commands (#451)
826// =============================================================================
827
828/// Navigate to older history entry (Up / Ctrl-P).
829#[derive(Debug, Clone, Copy, Default)]
830pub struct CmdlineHistoryUp;
831
832impl Command for CmdlineHistoryUp {
833    fn id(&self) -> CommandId {
834        ids::CMDLINE_HISTORY_UP
835    }
836    fn description(&self) -> &'static str {
837        "Navigate to older history entry"
838    }
839}
840
841impl CommandHandler for CmdlineHistoryUp {
842    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
843        runtime.ext_mut::<CmdlineState>().history_up();
844        CommandResult::Success
845    }
846}
847
848/// Navigate to newer history entry (Down / Ctrl-N).
849#[derive(Debug, Clone, Copy, Default)]
850pub struct CmdlineHistoryDown;
851
852impl Command for CmdlineHistoryDown {
853    fn id(&self) -> CommandId {
854        ids::CMDLINE_HISTORY_DOWN
855    }
856    fn description(&self) -> &'static str {
857        "Navigate to newer history entry"
858    }
859}
860
861impl CommandHandler for CmdlineHistoryDown {
862    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
863        runtime.ext_mut::<CmdlineState>().history_down();
864        CommandResult::Success
865    }
866}
867
868// =============================================================================
869// Command-line completion commands (#451)
870// =============================================================================
871
872/// Cycle to next completion (Tab).
873///
874/// On first press, queries `CommandNameIndex` for candidates matching
875/// the current input prefix. On subsequent presses, cycles forward.
876#[derive(Debug, Clone, Copy, Default)]
877pub struct CmdlineCompleteNext;
878
879impl Command for CmdlineCompleteNext {
880    fn id(&self) -> CommandId {
881        ids::CMDLINE_COMPLETE_NEXT
882    }
883    fn description(&self) -> &'static str {
884        "Cycle to next command-line completion"
885    }
886}
887
888impl CommandHandler for CmdlineCompleteNext {
889    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
890        populate_completions_if_needed(runtime);
891        runtime.ext_mut::<CmdlineState>().complete_next();
892        CommandResult::Success
893    }
894}
895
896/// Cycle to previous completion (Shift-Tab).
897#[derive(Debug, Clone, Copy, Default)]
898pub struct CmdlineCompletePrev;
899
900impl Command for CmdlineCompletePrev {
901    fn id(&self) -> CommandId {
902        ids::CMDLINE_COMPLETE_PREV
903    }
904    fn description(&self) -> &'static str {
905        "Cycle to previous command-line completion"
906    }
907}
908
909impl CommandHandler for CmdlineCompletePrev {
910    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
911        populate_completions_if_needed(runtime);
912        runtime.ext_mut::<CmdlineState>().complete_prev();
913        CommandResult::Success
914    }
915}
916
917/// Populate completions from `CommandNameIndex` if not already populated.
918fn populate_completions_if_needed(runtime: &mut SessionRuntime<'_>) {
919    // Only populate when completions list is empty
920    if !runtime.ext_mut::<CmdlineState>().completions().is_empty() {
921        return;
922    }
923
924    let prefix = runtime.ext_mut::<CmdlineState>().input().to_string();
925    if prefix.is_empty() {
926        return;
927    }
928
929    // Query CommandNameIndex for matching commands (#547)
930    let candidates = {
931        use reovim_driver_command::CommandNameIndex;
932        runtime
933            .kernel()
934            .services
935            .get::<CommandNameIndex>()
936            .map_or_else(Vec::new, |index| {
937                index
938                    .search_by_prefix(&prefix)
939                    .into_iter()
940                    .flat_map(|(_, cmd)| cmd.names().iter().copied().map(String::from))
941                    .filter(|name| name.starts_with(&prefix))
942                    .collect()
943            })
944    };
945
946    if !candidates.is_empty() {
947        runtime
948            .ext_mut::<CmdlineState>()
949            .set_completions(prefix, candidates);
950    }
951}
952
953// =============================================================================
954// Search Mode Entry Commands (#435)
955// =============================================================================
956
957/// Enter search forward mode (`/`).
958///
959/// Sets pending search direction to Forward and enters command-line mode.
960#[derive(Debug, Clone, Copy, Default)]
961pub struct EnterSearchForward;
962
963impl Command for EnterSearchForward {
964    fn id(&self) -> CommandId {
965        ids::ENTER_SEARCH_FORWARD
966    }
967
968    fn description(&self) -> &'static str {
969        "Enter search forward mode (/)"
970    }
971}
972
973impl CommandHandler for EnterSearchForward {
974    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
975        // Set pending search direction
976        runtime
977            .ext_mut::<SearchState>()
978            .start_pending_search(Direction::Forward);
979        // Activate cmdline with search forward prompt
980        runtime
981            .ext_mut::<CmdlineState>()
982            .enter(CmdlinePrompt::SearchForward);
983        // Enter command-line mode
984        runtime.set_mode(VimMode::COMMANDLINE_ID, TransitionContext::new());
985        CommandResult::Success
986    }
987}
988
989/// Enter search backward mode (`?`).
990///
991/// Sets pending search direction to Backward and enters command-line mode.
992#[derive(Debug, Clone, Copy, Default)]
993pub struct EnterSearchBackward;
994
995impl Command for EnterSearchBackward {
996    fn id(&self) -> CommandId {
997        ids::ENTER_SEARCH_BACKWARD
998    }
999
1000    fn description(&self) -> &'static str {
1001        "Enter search backward mode (?)"
1002    }
1003}
1004
1005impl CommandHandler for EnterSearchBackward {
1006    fn execute(&self, runtime: &mut SessionRuntime<'_>, _args: &CommandContext) -> CommandResult {
1007        // Set pending search direction
1008        runtime
1009            .ext_mut::<SearchState>()
1010            .start_pending_search(Direction::Backward);
1011        // Activate cmdline with search backward prompt
1012        runtime
1013            .ext_mut::<CmdlineState>()
1014            .enter(CmdlinePrompt::SearchBackward);
1015        // Enter command-line mode
1016        runtime.set_mode(VimMode::COMMANDLINE_ID, TransitionContext::new());
1017        CommandResult::Success
1018    }
1019}