Skip to main content

fresh/app/
input.rs

1use super::*;
2use crate::model::event::LeafId;
3use crate::services::plugins::hooks::HookArgs;
4use anyhow::Result as AnyhowResult;
5use rust_i18n::t;
6impl Editor {
7    /// Determine the current keybinding context based on UI state
8    pub fn get_key_context(&self) -> crate::input::keybindings::KeyContext {
9        use crate::input::keybindings::KeyContext;
10
11        // Priority order: Settings > Menu > Prompt > Popup > CompositeBuffer > Current context (FileExplorer or Normal)
12        if self.settings_state.as_ref().is_some_and(|s| s.visible) {
13            KeyContext::Settings
14        } else if self.menu_state.active_menu.is_some() {
15            KeyContext::Menu
16        } else if self.is_prompting() {
17            KeyContext::Prompt
18        } else if self.active_state().popups.is_visible() {
19            KeyContext::Popup
20        } else if self.is_composite_buffer(self.active_buffer()) {
21            KeyContext::CompositeBuffer
22        } else {
23            // Use the current context (can be FileExplorer or Normal)
24            self.key_context.clone()
25        }
26    }
27
28    /// Handle a key event and return whether it was handled
29    /// This is the central key handling logic used by both main.rs and tests
30    pub fn handle_key(
31        &mut self,
32        code: crossterm::event::KeyCode,
33        modifiers: crossterm::event::KeyModifiers,
34    ) -> AnyhowResult<()> {
35        use crate::input::keybindings::Action;
36
37        let _t_total = std::time::Instant::now();
38
39        tracing::trace!(
40            "Editor.handle_key: code={:?}, modifiers={:?}",
41            code,
42            modifiers
43        );
44
45        // Create key event for dispatch methods
46        let key_event = crossterm::event::KeyEvent::new(code, modifiers);
47
48        // Event debug dialog intercepts ALL key events before any other processing.
49        // This must be checked here (not just in main.rs/gui) so it works in
50        // client/server mode where handle_key is called directly.
51        if self.is_event_debug_active() {
52            self.handle_event_debug_input(&key_event);
53            return Ok(());
54        }
55
56        // Try terminal input dispatch first (handles terminal mode and re-entry)
57        if self.dispatch_terminal_input(&key_event).is_some() {
58            return Ok(());
59        }
60
61        // Clear skip_ensure_visible flag so cursor becomes visible after key press
62        // (scroll actions will set it again if needed). Use the *effective*
63        // active split so this clears the flag on a focused buffer-group
64        // panel's own view state, not the group host's — without this, a
65        // scroll action in the panel (mouse scrollbar click, plugin
66        // scrollBufferToLine, etc.) sets `skip_ensure_visible` on the panel
67        // and subsequent key presses never clear it, so cursor motion stops
68        // scrolling the viewport.
69        let active_split = self.effective_active_split();
70        if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
71            view_state.viewport.clear_skip_ensure_visible();
72        }
73
74        // Dismiss theme info popup on any key press
75        if self.theme_info_popup.is_some() {
76            self.theme_info_popup = None;
77        }
78
79        // Determine the current context first
80        let mut context = self.get_key_context();
81
82        // Special case: Hover and Signature Help popups should be dismissed on any key press
83        // EXCEPT for Ctrl+C when the popup has a text selection (allow copy first)
84        if matches!(context, crate::input::keybindings::KeyContext::Popup) {
85            // Check if the current popup is transient (hover, signature help)
86            let (is_transient_popup, has_selection) = {
87                let popup = self.active_state().popups.top();
88                (
89                    popup.is_some_and(|p| p.transient),
90                    popup.is_some_and(|p| p.has_selection()),
91                )
92            };
93
94            // Don't dismiss if popup has selection and user is pressing Ctrl+C (let them copy first)
95            let is_copy_key = key_event.code == crossterm::event::KeyCode::Char('c')
96                && key_event
97                    .modifiers
98                    .contains(crossterm::event::KeyModifiers::CONTROL);
99
100            if is_transient_popup && !(has_selection && is_copy_key) {
101                // Dismiss the popup on any key press (except Ctrl+C with selection)
102                self.hide_popup();
103                tracing::debug!("Dismissed transient popup on key press");
104                // Recalculate context now that popup is gone
105                context = self.get_key_context();
106            }
107        }
108
109        // Try hierarchical modal input dispatch first (Settings, Menu, Prompt, Popup)
110        if self.dispatch_modal_input(&key_event).is_some() {
111            return Ok(());
112        }
113
114        // If a modal was dismissed (e.g., completion popup closed and returned Ignored),
115        // recalculate the context so the key is processed in the correct context.
116        if context != self.get_key_context() {
117            context = self.get_key_context();
118        }
119
120        // Only check buffer mode keybindings when the editor buffer has focus.
121        // FileExplorer, Menu, Prompt, Popup contexts should not trigger mode bindings
122        // (e.g. markdown-source's Enter handler should not fire while the explorer is focused).
123        let should_check_mode_bindings =
124            matches!(context, crate::input::keybindings::KeyContext::Normal);
125
126        if should_check_mode_bindings {
127            // effective_mode() returns buffer-local mode if present, else global mode.
128            // This ensures virtual buffer modes aren't hijacked by global modes.
129            let effective_mode = self.effective_mode().map(|s| s.to_owned());
130
131            if let Some(ref mode_name) = effective_mode {
132                let mode_ctx = crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
133                let key_event = crossterm::event::KeyEvent::new(code, modifiers);
134
135                // Mode chord resolution (via KeybindingResolver)
136                let (chord_result, resolved_action) = {
137                    let keybindings = self.keybindings.read().unwrap();
138                    let chord_result =
139                        keybindings.resolve_chord(&self.chord_state, &key_event, mode_ctx.clone());
140                    let resolved = keybindings.resolve(&key_event, mode_ctx);
141                    (chord_result, resolved)
142                };
143                match chord_result {
144                    crate::input::keybindings::ChordResolution::Complete(action) => {
145                        tracing::debug!("Mode chord resolved to action: {:?}", action);
146                        self.chord_state.clear();
147                        return self.handle_action(action);
148                    }
149                    crate::input::keybindings::ChordResolution::Partial => {
150                        tracing::debug!("Potential chord prefix in mode '{}'", mode_name);
151                        self.chord_state.push((code, modifiers));
152                        return Ok(());
153                    }
154                    crate::input::keybindings::ChordResolution::NoMatch => {
155                        if !self.chord_state.is_empty() {
156                            tracing::debug!("Chord sequence abandoned in mode, clearing state");
157                            self.chord_state.clear();
158                        }
159                    }
160                }
161
162                // Mode single-key resolution (custom > keymap > plugin defaults)
163                if resolved_action != Action::None {
164                    return self.handle_action(resolved_action);
165                }
166            }
167
168            // Handle unbound keys for modes that want to capture input.
169            //
170            // Buffer-local modes with allow_text_input (e.g. search-replace-list)
171            // capture character keys and block other unbound keys.
172            //
173            // Buffer-local modes WITHOUT allow_text_input (e.g. diff-view) let
174            // unbound keys fall through to normal keybinding handling so that
175            // Ctrl+C, arrows, etc. still work.
176            //
177            // Global editor modes (e.g. vi-normal) block all unbound keys when
178            // read-only.
179            if let Some(ref mode_name) = effective_mode {
180                if self.mode_registry.allows_text_input(mode_name) {
181                    if let KeyCode::Char(c) = code {
182                        let ch = if modifiers.contains(KeyModifiers::SHIFT) {
183                            c.to_uppercase().next().unwrap_or(c)
184                        } else {
185                            c
186                        };
187                        if !modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) {
188                            let action_name = format!("mode_text_input:{}", ch);
189                            return self.handle_action(Action::PluginAction(action_name));
190                        }
191                    }
192                    tracing::debug!("Blocking unbound key in text-input mode '{}'", mode_name);
193                    return Ok(());
194                }
195            }
196            if let Some(ref mode_name) = self.editor_mode {
197                if self.mode_registry.is_read_only(mode_name) {
198                    tracing::debug!("Ignoring unbound key in read-only mode '{}'", mode_name);
199                    return Ok(());
200                }
201                tracing::debug!(
202                    "Mode '{}' is not read-only, allowing key through",
203                    mode_name
204                );
205            }
206        }
207
208        // --- Composite buffer input routing ---
209        // If the active buffer is a composite buffer (side-by-side diff),
210        // route remaining composite-specific keys (scroll, pane switch, close)
211        // through CompositeInputRouter before falling through to regular
212        // keybinding resolution. Hunk navigation (n/p/]/[) is handled by the
213        // Action system via CompositeBuffer context bindings.
214        {
215            let active_buf = self.active_buffer();
216            let active_split = self.effective_active_split();
217            if self.is_composite_buffer(active_buf) {
218                if let Some(handled) =
219                    self.try_route_composite_key(active_split, active_buf, &key_event)
220                {
221                    return handled;
222                }
223            }
224        }
225
226        // Check for chord sequence matches first
227        let key_event = crossterm::event::KeyEvent::new(code, modifiers);
228        let (chord_result, action) = {
229            let keybindings = self.keybindings.read().unwrap();
230            let chord_result =
231                keybindings.resolve_chord(&self.chord_state, &key_event, context.clone());
232            let action = keybindings.resolve(&key_event, context.clone());
233            (chord_result, action)
234        };
235
236        match chord_result {
237            crate::input::keybindings::ChordResolution::Complete(action) => {
238                // Complete chord match - execute action and clear chord state
239                tracing::debug!("Complete chord match -> Action: {:?}", action);
240                self.chord_state.clear();
241                return self.handle_action(action);
242            }
243            crate::input::keybindings::ChordResolution::Partial => {
244                // Partial match - add to chord state and wait for more keys
245                tracing::debug!("Partial chord match - waiting for next key");
246                self.chord_state.push((code, modifiers));
247                return Ok(());
248            }
249            crate::input::keybindings::ChordResolution::NoMatch => {
250                // No chord match - clear state and try regular resolution
251                if !self.chord_state.is_empty() {
252                    tracing::debug!("Chord sequence abandoned, clearing state");
253                    self.chord_state.clear();
254                }
255            }
256        }
257
258        // Regular single-key resolution (already resolved above)
259        tracing::trace!("Context: {:?} -> Action: {:?}", context, action);
260
261        // Cancel pending LSP requests on user actions (except LSP actions themselves)
262        // This ensures stale completions don't show up after the user has moved on
263        match action {
264            Action::LspCompletion
265            | Action::LspGotoDefinition
266            | Action::LspReferences
267            | Action::LspHover
268            | Action::None => {
269                // Don't cancel for LSP actions or no-op
270            }
271            _ => {
272                // Cancel any pending LSP requests
273                self.cancel_pending_lsp_requests();
274            }
275        }
276
277        // Note: Modal components (Settings, Menu, Prompt, Popup, File Browser) are now
278        // handled by dispatch_modal_input using the InputHandler system.
279        // All remaining actions delegate to handle_action.
280        self.handle_action(action)
281    }
282
283    /// Handle an action (for normal mode and command execution).
284    /// Used by the app module internally and by the GUI module for native menu dispatch.
285    pub(crate) fn handle_action(&mut self, action: Action) -> AnyhowResult<()> {
286        use crate::input::keybindings::Action;
287
288        // Record action to macro if recording
289        self.record_macro_action(&action);
290
291        // Reset dabbrev cycling session on any non-dabbrev action.
292        if !matches!(action, Action::DabbrevExpand) {
293            self.reset_dabbrev_state();
294        }
295
296        match action {
297            Action::Quit => self.quit(),
298            Action::ForceQuit => {
299                self.should_quit = true;
300            }
301            Action::Detach => {
302                self.should_detach = true;
303            }
304            Action::Save => {
305                // Check if buffer has a file path - if not, redirect to SaveAs
306                if self.active_state().buffer.file_path().is_none() {
307                    self.start_prompt_with_initial_text(
308                        t!("file.save_as_prompt").to_string(),
309                        PromptType::SaveFileAs,
310                        String::new(),
311                    );
312                    self.init_file_open_state();
313                } else if self.check_save_conflict().is_some() {
314                    // Check if file was modified externally since we opened/saved it
315                    self.start_prompt(
316                        t!("file.file_changed_prompt").to_string(),
317                        PromptType::ConfirmSaveConflict,
318                    );
319                } else if let Err(e) = self.save() {
320                    let msg = format!("{}", e);
321                    self.status_message = Some(t!("file.save_failed", error = &msg).to_string());
322                }
323            }
324            Action::SaveAs => {
325                // Get current filename as default suggestion
326                let current_path = self
327                    .active_state()
328                    .buffer
329                    .file_path()
330                    .map(|p| {
331                        // Make path relative to working_dir if possible
332                        p.strip_prefix(&self.working_dir)
333                            .unwrap_or(p)
334                            .to_string_lossy()
335                            .to_string()
336                    })
337                    .unwrap_or_default();
338                self.start_prompt_with_initial_text(
339                    t!("file.save_as_prompt").to_string(),
340                    PromptType::SaveFileAs,
341                    current_path,
342                );
343                self.init_file_open_state();
344            }
345            Action::Open => {
346                self.start_prompt(t!("file.open_prompt").to_string(), PromptType::OpenFile);
347                self.prefill_open_file_prompt();
348                self.init_file_open_state();
349            }
350            Action::SwitchProject => {
351                self.start_prompt(
352                    t!("file.switch_project_prompt").to_string(),
353                    PromptType::SwitchProject,
354                );
355                self.init_folder_open_state();
356            }
357            Action::GotoLine => {
358                let has_line_index = self
359                    .buffers
360                    .get(&self.active_buffer())
361                    .is_none_or(|s| s.buffer.line_count().is_some());
362                if has_line_index {
363                    self.start_prompt(
364                        t!("file.goto_line_prompt").to_string(),
365                        PromptType::GotoLine,
366                    );
367                } else {
368                    self.start_prompt(
369                        t!("goto.scan_confirm_prompt", yes = "y", no = "N").to_string(),
370                        PromptType::GotoLineScanConfirm,
371                    );
372                }
373            }
374            Action::ScanLineIndex => {
375                self.start_incremental_line_scan(false);
376            }
377            Action::New => {
378                self.new_buffer();
379            }
380            Action::Close | Action::CloseTab => {
381                // Both Close and CloseTab use close_tab() which handles:
382                // - Closing the split if this is the last buffer and there are other splits
383                // - Prompting for unsaved changes
384                // - Properly closing the buffer
385                self.close_tab();
386            }
387            Action::Revert => {
388                // Check if buffer has unsaved changes - prompt for confirmation
389                if self.active_state().buffer.is_modified() {
390                    let revert_key = t!("prompt.key.revert").to_string();
391                    let cancel_key = t!("prompt.key.cancel").to_string();
392                    self.start_prompt(
393                        t!(
394                            "prompt.revert_confirm",
395                            revert_key = revert_key,
396                            cancel_key = cancel_key
397                        )
398                        .to_string(),
399                        PromptType::ConfirmRevert,
400                    );
401                } else {
402                    // No local changes, just revert
403                    if let Err(e) = self.revert_file() {
404                        self.set_status_message(
405                            t!("error.failed_to_revert", error = e.to_string()).to_string(),
406                        );
407                    }
408                }
409            }
410            Action::ToggleAutoRevert => {
411                self.toggle_auto_revert();
412            }
413            Action::FormatBuffer => {
414                if let Err(e) = self.format_buffer() {
415                    self.set_status_message(
416                        t!("error.format_failed", error = e.to_string()).to_string(),
417                    );
418                }
419            }
420            Action::TrimTrailingWhitespace => match self.trim_trailing_whitespace() {
421                Ok(true) => {
422                    self.set_status_message(t!("whitespace.trimmed").to_string());
423                }
424                Ok(false) => {
425                    self.set_status_message(t!("whitespace.no_trailing").to_string());
426                }
427                Err(e) => {
428                    self.set_status_message(
429                        t!("error.trim_whitespace_failed", error = e).to_string(),
430                    );
431                }
432            },
433            Action::EnsureFinalNewline => match self.ensure_final_newline() {
434                Ok(true) => {
435                    self.set_status_message(t!("whitespace.newline_added").to_string());
436                }
437                Ok(false) => {
438                    self.set_status_message(t!("whitespace.already_has_newline").to_string());
439                }
440                Err(e) => {
441                    self.set_status_message(
442                        t!("error.ensure_newline_failed", error = e).to_string(),
443                    );
444                }
445            },
446            Action::Copy => {
447                // Check if there's an active popup with text selection
448                let state = self.active_state();
449                if let Some(popup) = state.popups.top() {
450                    if popup.has_selection() {
451                        if let Some(text) = popup.get_selected_text() {
452                            self.clipboard.copy(text);
453                            self.set_status_message(t!("clipboard.copied").to_string());
454                            return Ok(());
455                        }
456                    }
457                }
458                // Check if active buffer is a composite buffer
459                let buffer_id = self.active_buffer();
460                if self.is_composite_buffer(buffer_id) {
461                    if let Some(_handled) = self.handle_composite_action(buffer_id, &Action::Copy) {
462                        return Ok(());
463                    }
464                }
465                self.copy_selection()
466            }
467            Action::CopyWithTheme(theme) => self.copy_selection_with_theme(&theme),
468            Action::Cut => {
469                if self.is_editing_disabled() {
470                    self.set_status_message(t!("buffer.editing_disabled").to_string());
471                    return Ok(());
472                }
473                self.cut_selection()
474            }
475            Action::Paste => {
476                if self.is_editing_disabled() {
477                    self.set_status_message(t!("buffer.editing_disabled").to_string());
478                    return Ok(());
479                }
480                self.paste()
481            }
482            Action::YankWordForward => self.yank_word_forward(),
483            Action::YankWordBackward => self.yank_word_backward(),
484            Action::YankToLineEnd => self.yank_to_line_end(),
485            Action::YankToLineStart => self.yank_to_line_start(),
486            Action::YankViWordEnd => self.yank_vi_word_end(),
487            Action::Undo => {
488                self.handle_undo();
489            }
490            Action::Redo => {
491                self.handle_redo();
492            }
493            Action::ShowHelp => {
494                self.open_help_manual();
495            }
496            Action::ShowKeyboardShortcuts => {
497                self.open_keyboard_shortcuts();
498            }
499            Action::ShowWarnings => {
500                self.show_warnings_popup();
501            }
502            Action::ShowStatusLog => {
503                self.open_status_log();
504            }
505            Action::ShowLspStatus => {
506                self.show_lsp_status_popup();
507            }
508            Action::ClearWarnings => {
509                self.clear_warnings();
510            }
511            Action::CommandPalette => {
512                // CommandPalette now delegates to QuickOpen (which starts with ">" prefix
513                // for command mode). Toggle if already open.
514                if let Some(prompt) = &self.prompt {
515                    if prompt.prompt_type == PromptType::QuickOpen {
516                        self.cancel_prompt();
517                        return Ok(());
518                    }
519                }
520                self.start_quick_open();
521            }
522            Action::QuickOpen => {
523                // Toggle Quick Open: close if already open, otherwise open it
524                if let Some(prompt) = &self.prompt {
525                    if prompt.prompt_type == PromptType::QuickOpen {
526                        self.cancel_prompt();
527                        return Ok(());
528                    }
529                }
530
531                // Start Quick Open with file suggestions (default mode)
532                self.start_quick_open();
533            }
534            Action::ToggleLineWrap => {
535                self.config.editor.line_wrap = !self.config.editor.line_wrap;
536
537                // Update all viewports to reflect the new line wrap setting,
538                // respecting per-language overrides
539                let leaf_ids: Vec<_> = self.split_view_states.keys().copied().collect();
540                for leaf_id in leaf_ids {
541                    let buffer_id = self
542                        .split_manager
543                        .get_buffer_id(leaf_id.into())
544                        .unwrap_or(BufferId(0));
545                    let effective_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
546                    let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
547                    if let Some(view_state) = self.split_view_states.get_mut(&leaf_id) {
548                        view_state.viewport.line_wrap_enabled = effective_wrap;
549                        view_state.viewport.wrap_indent = self.config.editor.wrap_indent;
550                        view_state.viewport.wrap_column = wrap_column;
551                    }
552                }
553
554                let state = if self.config.editor.line_wrap {
555                    t!("view.state_enabled").to_string()
556                } else {
557                    t!("view.state_disabled").to_string()
558                };
559                self.set_status_message(t!("view.line_wrap_state", state = state).to_string());
560            }
561            Action::ToggleCurrentLineHighlight => {
562                self.config.editor.highlight_current_line =
563                    !self.config.editor.highlight_current_line;
564
565                // Update all splits
566                let leaf_ids: Vec<_> = self.split_view_states.keys().copied().collect();
567                for leaf_id in leaf_ids {
568                    if let Some(view_state) = self.split_view_states.get_mut(&leaf_id) {
569                        view_state.highlight_current_line =
570                            self.config.editor.highlight_current_line;
571                    }
572                }
573
574                let state = if self.config.editor.highlight_current_line {
575                    t!("view.state_enabled").to_string()
576                } else {
577                    t!("view.state_disabled").to_string()
578                };
579                self.set_status_message(
580                    t!("view.current_line_highlight_state", state = state).to_string(),
581                );
582            }
583            Action::ToggleReadOnly => {
584                let buffer_id = self.active_buffer();
585                let is_now_read_only = self
586                    .buffer_metadata
587                    .get(&buffer_id)
588                    .map(|m| !m.read_only)
589                    .unwrap_or(false);
590                self.mark_buffer_read_only(buffer_id, is_now_read_only);
591
592                let state_str = if is_now_read_only {
593                    t!("view.state_enabled").to_string()
594                } else {
595                    t!("view.state_disabled").to_string()
596                };
597                self.set_status_message(t!("view.read_only_state", state = state_str).to_string());
598            }
599            Action::TogglePageView => {
600                self.handle_toggle_page_view();
601            }
602            Action::SetPageWidth => {
603                let active_split = self.split_manager.active_split();
604                let current = self
605                    .split_view_states
606                    .get(&active_split)
607                    .and_then(|v| v.compose_width.map(|w| w.to_string()))
608                    .unwrap_or_default();
609                self.start_prompt_with_initial_text(
610                    "Page width (empty = viewport): ".to_string(),
611                    PromptType::SetPageWidth,
612                    current,
613                );
614            }
615            Action::SetBackground => {
616                let default_path = self
617                    .ansi_background_path
618                    .as_ref()
619                    .and_then(|p| {
620                        p.strip_prefix(&self.working_dir)
621                            .ok()
622                            .map(|rel| rel.to_string_lossy().to_string())
623                    })
624                    .unwrap_or_else(|| DEFAULT_BACKGROUND_FILE.to_string());
625
626                self.start_prompt_with_initial_text(
627                    "Background file: ".to_string(),
628                    PromptType::SetBackgroundFile,
629                    default_path,
630                );
631            }
632            Action::SetBackgroundBlend => {
633                let default_amount = format!("{:.2}", self.background_fade);
634                self.start_prompt_with_initial_text(
635                    "Background blend (0-1): ".to_string(),
636                    PromptType::SetBackgroundBlend,
637                    default_amount,
638                );
639            }
640            Action::LspCompletion => {
641                self.request_completion();
642            }
643            Action::DabbrevExpand => {
644                self.dabbrev_expand();
645            }
646            Action::LspGotoDefinition => {
647                self.request_goto_definition()?;
648            }
649            Action::LspRename => {
650                self.start_rename()?;
651            }
652            Action::LspHover => {
653                self.request_hover()?;
654            }
655            Action::LspReferences => {
656                self.request_references()?;
657            }
658            Action::LspSignatureHelp => {
659                self.request_signature_help();
660            }
661            Action::LspCodeActions => {
662                self.request_code_actions()?;
663            }
664            Action::LspRestart => {
665                self.handle_lsp_restart();
666            }
667            Action::LspStop => {
668                self.handle_lsp_stop();
669            }
670            Action::LspToggleForBuffer => {
671                self.handle_lsp_toggle_for_buffer();
672            }
673            Action::ToggleInlayHints => {
674                self.toggle_inlay_hints();
675            }
676            Action::DumpConfig => {
677                self.dump_config();
678            }
679            Action::SelectTheme => {
680                self.start_select_theme_prompt();
681            }
682            Action::InspectThemeAtCursor => {
683                self.inspect_theme_at_cursor();
684            }
685            Action::SelectKeybindingMap => {
686                self.start_select_keybinding_map_prompt();
687            }
688            Action::SelectCursorStyle => {
689                self.start_select_cursor_style_prompt();
690            }
691            Action::SelectLocale => {
692                self.start_select_locale_prompt();
693            }
694            Action::Search => {
695                // If already in a search-related prompt, Ctrl+F acts like Enter (confirm search)
696                let is_search_prompt = self.prompt.as_ref().is_some_and(|p| {
697                    matches!(
698                        p.prompt_type,
699                        PromptType::Search
700                            | PromptType::ReplaceSearch
701                            | PromptType::QueryReplaceSearch
702                    )
703                });
704
705                if is_search_prompt {
706                    self.confirm_prompt();
707                } else {
708                    self.start_search_prompt(
709                        t!("file.search_prompt").to_string(),
710                        PromptType::Search,
711                        false,
712                    );
713                }
714            }
715            Action::Replace => {
716                // Use same flow as query-replace, just with confirm_each defaulting to false
717                self.start_search_prompt(
718                    t!("file.replace_prompt").to_string(),
719                    PromptType::ReplaceSearch,
720                    false,
721                );
722            }
723            Action::QueryReplace => {
724                // Enable confirm mode by default for query-replace
725                self.search_confirm_each = true;
726                self.start_search_prompt(
727                    "Query replace: ".to_string(),
728                    PromptType::QueryReplaceSearch,
729                    false,
730                );
731            }
732            Action::FindInSelection => {
733                self.start_search_prompt(
734                    t!("file.search_prompt").to_string(),
735                    PromptType::Search,
736                    true,
737                );
738            }
739            Action::FindNext => {
740                self.find_next();
741            }
742            Action::FindPrevious => {
743                self.find_previous();
744            }
745            Action::FindSelectionNext => {
746                self.find_selection_next();
747            }
748            Action::FindSelectionPrevious => {
749                self.find_selection_previous();
750            }
751            Action::AddCursorNextMatch => self.add_cursor_at_next_match(),
752            Action::AddCursorAbove => self.add_cursor_above(),
753            Action::AddCursorBelow => self.add_cursor_below(),
754            Action::NextBuffer => self.next_buffer(),
755            Action::PrevBuffer => self.prev_buffer(),
756            Action::SwitchToPreviousTab => self.switch_to_previous_tab(),
757            Action::SwitchToTabByName => self.start_switch_to_tab_prompt(),
758
759            // Tab scrolling (manual scroll - don't auto-adjust)
760            Action::ScrollTabsLeft => {
761                let active_split_id = self.split_manager.active_split();
762                if let Some(view_state) = self.split_view_states.get_mut(&active_split_id) {
763                    view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_sub(5);
764                    self.set_status_message(t!("status.scrolled_tabs_left").to_string());
765                }
766            }
767            Action::ScrollTabsRight => {
768                let active_split_id = self.split_manager.active_split();
769                if let Some(view_state) = self.split_view_states.get_mut(&active_split_id) {
770                    view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_add(5);
771                    self.set_status_message(t!("status.scrolled_tabs_right").to_string());
772                }
773            }
774            Action::NavigateBack => self.navigate_back(),
775            Action::NavigateForward => self.navigate_forward(),
776            Action::SplitHorizontal => self.split_pane_horizontal(),
777            Action::SplitVertical => self.split_pane_vertical(),
778            Action::CloseSplit => self.close_active_split(),
779            Action::NextSplit => self.next_split(),
780            Action::PrevSplit => self.prev_split(),
781            Action::IncreaseSplitSize => self.adjust_split_size(0.05),
782            Action::DecreaseSplitSize => self.adjust_split_size(-0.05),
783            Action::ToggleMaximizeSplit => self.toggle_maximize_split(),
784            Action::ToggleFileExplorer => self.toggle_file_explorer(),
785            Action::ToggleMenuBar => self.toggle_menu_bar(),
786            Action::ToggleTabBar => self.toggle_tab_bar(),
787            Action::ToggleStatusBar => self.toggle_status_bar(),
788            Action::TogglePromptLine => self.toggle_prompt_line(),
789            Action::ToggleVerticalScrollbar => self.toggle_vertical_scrollbar(),
790            Action::ToggleHorizontalScrollbar => self.toggle_horizontal_scrollbar(),
791            Action::ToggleLineNumbers => self.toggle_line_numbers(),
792            Action::ToggleScrollSync => self.toggle_scroll_sync(),
793            Action::ToggleMouseCapture => self.toggle_mouse_capture(),
794            Action::ToggleMouseHover => self.toggle_mouse_hover(),
795            Action::ToggleDebugHighlights => self.toggle_debug_highlights(),
796            // Rulers
797            Action::AddRuler => {
798                self.start_prompt(t!("rulers.add_prompt").to_string(), PromptType::AddRuler);
799            }
800            Action::RemoveRuler => {
801                self.start_remove_ruler_prompt();
802            }
803            // Buffer settings
804            Action::SetTabSize => {
805                let current = self
806                    .buffers
807                    .get(&self.active_buffer())
808                    .map(|s| s.buffer_settings.tab_size.to_string())
809                    .unwrap_or_else(|| "4".to_string());
810                self.start_prompt_with_initial_text(
811                    "Tab size: ".to_string(),
812                    PromptType::SetTabSize,
813                    current,
814                );
815            }
816            Action::SetLineEnding => {
817                self.start_set_line_ending_prompt();
818            }
819            Action::SetEncoding => {
820                self.start_set_encoding_prompt();
821            }
822            Action::ReloadWithEncoding => {
823                self.start_reload_with_encoding_prompt();
824            }
825            Action::SetLanguage => {
826                self.start_set_language_prompt();
827            }
828            Action::ToggleIndentationStyle => {
829                if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
830                    state.buffer_settings.use_tabs = !state.buffer_settings.use_tabs;
831                    let status = if state.buffer_settings.use_tabs {
832                        "Indentation: Tabs"
833                    } else {
834                        "Indentation: Spaces"
835                    };
836                    self.set_status_message(status.to_string());
837                }
838            }
839            Action::ToggleTabIndicators | Action::ToggleWhitespaceIndicators => {
840                if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
841                    state.buffer_settings.whitespace.toggle_all();
842                    let status = if state.buffer_settings.whitespace.any_visible() {
843                        t!("toggle.whitespace_indicators_shown")
844                    } else {
845                        t!("toggle.whitespace_indicators_hidden")
846                    };
847                    self.set_status_message(status.to_string());
848                }
849            }
850            Action::ResetBufferSettings => self.reset_buffer_settings(),
851            Action::FocusFileExplorer => self.focus_file_explorer(),
852            Action::FocusEditor => self.focus_editor(),
853            Action::FileExplorerUp => self.file_explorer_navigate_up(),
854            Action::FileExplorerDown => self.file_explorer_navigate_down(),
855            Action::FileExplorerPageUp => self.file_explorer_page_up(),
856            Action::FileExplorerPageDown => self.file_explorer_page_down(),
857            Action::FileExplorerExpand => self.file_explorer_toggle_expand(),
858            Action::FileExplorerCollapse => self.file_explorer_collapse(),
859            Action::FileExplorerOpen => self.file_explorer_open_file()?,
860            Action::FileExplorerRefresh => self.file_explorer_refresh(),
861            Action::FileExplorerNewFile => self.file_explorer_new_file(),
862            Action::FileExplorerNewDirectory => self.file_explorer_new_directory(),
863            Action::FileExplorerDelete => self.file_explorer_delete(),
864            Action::FileExplorerRename => self.file_explorer_rename(),
865            Action::FileExplorerToggleHidden => self.file_explorer_toggle_hidden(),
866            Action::FileExplorerToggleGitignored => self.file_explorer_toggle_gitignored(),
867            Action::FileExplorerSearchClear => self.file_explorer_search_clear(),
868            Action::FileExplorerSearchBackspace => self.file_explorer_search_pop_char(),
869            Action::RemoveSecondaryCursors => {
870                // Convert action to events and apply them
871                if let Some(events) = self.action_to_events(Action::RemoveSecondaryCursors) {
872                    // Wrap in batch for atomic undo
873                    let batch = Event::Batch {
874                        events: events.clone(),
875                        description: "Remove secondary cursors".to_string(),
876                    };
877                    self.active_event_log_mut().append(batch.clone());
878                    self.apply_event_to_active_buffer(&batch);
879
880                    // Ensure the primary cursor is visible after removing secondary cursors
881                    let active_split = self.split_manager.active_split();
882                    let active_buffer = self.active_buffer();
883                    if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
884                        let state = self.buffers.get_mut(&active_buffer).unwrap();
885                        view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
886                    }
887                }
888            }
889
890            // Menu navigation actions
891            Action::MenuActivate => {
892                self.handle_menu_activate();
893            }
894            Action::MenuClose => {
895                self.handle_menu_close();
896            }
897            Action::MenuLeft => {
898                self.handle_menu_left();
899            }
900            Action::MenuRight => {
901                self.handle_menu_right();
902            }
903            Action::MenuUp => {
904                self.handle_menu_up();
905            }
906            Action::MenuDown => {
907                self.handle_menu_down();
908            }
909            Action::MenuExecute => {
910                if let Some(action) = self.handle_menu_execute() {
911                    return self.handle_action(action);
912                }
913            }
914            Action::MenuOpen(menu_name) => {
915                if self.config.editor.menu_bar_mnemonics {
916                    self.handle_menu_open(&menu_name);
917                }
918            }
919
920            Action::SwitchKeybindingMap(map_name) => {
921                // Check if the map exists (either built-in or user-defined)
922                let is_builtin =
923                    matches!(map_name.as_str(), "default" | "emacs" | "vscode" | "macos");
924                let is_user_defined = self.config.keybinding_maps.contains_key(&map_name);
925
926                if is_builtin || is_user_defined {
927                    // Update the active keybinding map in config
928                    self.config.active_keybinding_map = map_name.clone().into();
929
930                    // Reload the keybinding resolver with the new map
931                    *self.keybindings.write().unwrap() =
932                        crate::input::keybindings::KeybindingResolver::new(&self.config);
933
934                    self.set_status_message(
935                        t!("view.keybindings_switched", map = map_name).to_string(),
936                    );
937                } else {
938                    self.set_status_message(
939                        t!("view.keybindings_unknown", map = map_name).to_string(),
940                    );
941                }
942            }
943
944            Action::SmartHome => {
945                // In composite (diff) views, use LineStart movement
946                let buffer_id = self.active_buffer();
947                if self.is_composite_buffer(buffer_id) {
948                    if let Some(_handled) =
949                        self.handle_composite_action(buffer_id, &Action::SmartHome)
950                    {
951                        return Ok(());
952                    }
953                }
954                self.smart_home();
955            }
956            Action::ToggleComment => {
957                self.toggle_comment();
958            }
959            Action::ToggleFold => {
960                self.toggle_fold_at_cursor();
961            }
962            Action::GoToMatchingBracket => {
963                self.goto_matching_bracket();
964            }
965            Action::JumpToNextError => {
966                self.jump_to_next_error();
967            }
968            Action::JumpToPreviousError => {
969                self.jump_to_previous_error();
970            }
971            Action::SetBookmark(key) => {
972                self.set_bookmark(key);
973            }
974            Action::JumpToBookmark(key) => {
975                self.jump_to_bookmark(key);
976            }
977            Action::ClearBookmark(key) => {
978                self.clear_bookmark(key);
979            }
980            Action::ListBookmarks => {
981                self.list_bookmarks();
982            }
983            Action::ToggleSearchCaseSensitive => {
984                self.search_case_sensitive = !self.search_case_sensitive;
985                let state = if self.search_case_sensitive {
986                    "enabled"
987                } else {
988                    "disabled"
989                };
990                self.set_status_message(
991                    t!("search.case_sensitive_state", state = state).to_string(),
992                );
993                // Update incremental highlights if in search prompt, otherwise re-run completed search
994                // Check prompt FIRST since we want to use current prompt input, not stale search_state
995                if let Some(prompt) = &self.prompt {
996                    if matches!(
997                        prompt.prompt_type,
998                        PromptType::Search
999                            | PromptType::ReplaceSearch
1000                            | PromptType::QueryReplaceSearch
1001                    ) {
1002                        let query = prompt.input.clone();
1003                        self.update_search_highlights(&query);
1004                    }
1005                } else if let Some(search_state) = &self.search_state {
1006                    let query = search_state.query.clone();
1007                    self.perform_search(&query);
1008                }
1009            }
1010            Action::ToggleSearchWholeWord => {
1011                self.search_whole_word = !self.search_whole_word;
1012                let state = if self.search_whole_word {
1013                    "enabled"
1014                } else {
1015                    "disabled"
1016                };
1017                self.set_status_message(t!("search.whole_word_state", state = state).to_string());
1018                // Update incremental highlights if in search prompt, otherwise re-run completed search
1019                // Check prompt FIRST since we want to use current prompt input, not stale search_state
1020                if let Some(prompt) = &self.prompt {
1021                    if matches!(
1022                        prompt.prompt_type,
1023                        PromptType::Search
1024                            | PromptType::ReplaceSearch
1025                            | PromptType::QueryReplaceSearch
1026                    ) {
1027                        let query = prompt.input.clone();
1028                        self.update_search_highlights(&query);
1029                    }
1030                } else if let Some(search_state) = &self.search_state {
1031                    let query = search_state.query.clone();
1032                    self.perform_search(&query);
1033                }
1034            }
1035            Action::ToggleSearchRegex => {
1036                self.search_use_regex = !self.search_use_regex;
1037                let state = if self.search_use_regex {
1038                    "enabled"
1039                } else {
1040                    "disabled"
1041                };
1042                self.set_status_message(t!("search.regex_state", state = state).to_string());
1043                // Update incremental highlights if in search prompt, otherwise re-run completed search
1044                // Check prompt FIRST since we want to use current prompt input, not stale search_state
1045                if let Some(prompt) = &self.prompt {
1046                    if matches!(
1047                        prompt.prompt_type,
1048                        PromptType::Search
1049                            | PromptType::ReplaceSearch
1050                            | PromptType::QueryReplaceSearch
1051                    ) {
1052                        let query = prompt.input.clone();
1053                        self.update_search_highlights(&query);
1054                    }
1055                } else if let Some(search_state) = &self.search_state {
1056                    let query = search_state.query.clone();
1057                    self.perform_search(&query);
1058                }
1059            }
1060            Action::ToggleSearchConfirmEach => {
1061                self.search_confirm_each = !self.search_confirm_each;
1062                let state = if self.search_confirm_each {
1063                    "enabled"
1064                } else {
1065                    "disabled"
1066                };
1067                self.set_status_message(t!("search.confirm_each_state", state = state).to_string());
1068            }
1069            Action::FileBrowserToggleHidden => {
1070                // Toggle hidden files in file browser (handled via file_open_toggle_hidden)
1071                self.file_open_toggle_hidden();
1072            }
1073            Action::StartMacroRecording => {
1074                // This is a no-op; use ToggleMacroRecording instead
1075                self.set_status_message(
1076                    "Use Ctrl+Shift+R to start recording (will prompt for register)".to_string(),
1077                );
1078            }
1079            Action::StopMacroRecording => {
1080                self.stop_macro_recording();
1081            }
1082            Action::PlayMacro(key) => {
1083                self.play_macro(key);
1084            }
1085            Action::ToggleMacroRecording(key) => {
1086                self.toggle_macro_recording(key);
1087            }
1088            Action::ShowMacro(key) => {
1089                self.show_macro_in_buffer(key);
1090            }
1091            Action::ListMacros => {
1092                self.list_macros_in_buffer();
1093            }
1094            Action::PromptRecordMacro => {
1095                self.start_prompt("Record macro (0-9): ".to_string(), PromptType::RecordMacro);
1096            }
1097            Action::PromptPlayMacro => {
1098                self.start_prompt("Play macro (0-9): ".to_string(), PromptType::PlayMacro);
1099            }
1100            Action::PlayLastMacro => {
1101                if let Some(key) = self.macros.last_register() {
1102                    self.play_macro(key);
1103                } else {
1104                    self.set_status_message(t!("status.no_macro_recorded").to_string());
1105                }
1106            }
1107            Action::PromptSetBookmark => {
1108                self.start_prompt("Set bookmark (0-9): ".to_string(), PromptType::SetBookmark);
1109            }
1110            Action::PromptJumpToBookmark => {
1111                self.start_prompt(
1112                    "Jump to bookmark (0-9): ".to_string(),
1113                    PromptType::JumpToBookmark,
1114                );
1115            }
1116            Action::CompositeNextHunk => {
1117                let buf = self.active_buffer();
1118                self.composite_next_hunk_active(buf);
1119            }
1120            Action::CompositePrevHunk => {
1121                let buf = self.active_buffer();
1122                self.composite_prev_hunk_active(buf);
1123            }
1124            Action::None => {}
1125            Action::DeleteBackward => {
1126                if self.is_editing_disabled() {
1127                    self.set_status_message(t!("buffer.editing_disabled").to_string());
1128                    return Ok(());
1129                }
1130                // Normal backspace handling
1131                if let Some(events) = self.action_to_events(Action::DeleteBackward) {
1132                    if events.len() > 1 {
1133                        // Multi-cursor: use optimized bulk edit (O(n) instead of O(n²))
1134                        let description = "Delete backward".to_string();
1135                        if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description)
1136                        {
1137                            self.active_event_log_mut().append(bulk_edit);
1138                        }
1139                    } else {
1140                        for event in events {
1141                            self.active_event_log_mut().append(event.clone());
1142                            self.apply_event_to_active_buffer(&event);
1143                        }
1144                    }
1145                }
1146            }
1147            Action::PluginAction(action_name) => {
1148                tracing::debug!("handle_action: PluginAction('{}')", action_name);
1149                // Execute the plugin callback via TypeScript plugin thread
1150                // Use non-blocking version to avoid deadlock with async plugin ops
1151                #[cfg(feature = "plugins")]
1152                if let Some(result) = self.plugin_manager.execute_action_async(&action_name) {
1153                    match result {
1154                        Ok(receiver) => {
1155                            // Store pending action for processing in main loop
1156                            self.pending_plugin_actions
1157                                .push((action_name.clone(), receiver));
1158                        }
1159                        Err(e) => {
1160                            self.set_status_message(
1161                                t!("view.plugin_error", error = e.to_string()).to_string(),
1162                            );
1163                            tracing::error!("Plugin action error: {}", e);
1164                        }
1165                    }
1166                } else {
1167                    self.set_status_message(t!("status.plugin_manager_unavailable").to_string());
1168                }
1169                #[cfg(not(feature = "plugins"))]
1170                {
1171                    let _ = action_name;
1172                    self.set_status_message(
1173                        "Plugins not available (compiled without plugin support)".to_string(),
1174                    );
1175                }
1176            }
1177            Action::LoadPluginFromBuffer => {
1178                #[cfg(feature = "plugins")]
1179                {
1180                    let buffer_id = self.active_buffer();
1181                    let state = self.active_state();
1182                    let buffer = &state.buffer;
1183                    let total = buffer.total_bytes();
1184                    let content =
1185                        String::from_utf8_lossy(&buffer.slice_bytes(0..total)).to_string();
1186
1187                    // Determine if TypeScript from file extension, default to TS
1188                    let is_ts = buffer
1189                        .file_path()
1190                        .and_then(|p| p.extension())
1191                        .and_then(|e| e.to_str())
1192                        .map(|e| e == "ts" || e == "tsx")
1193                        .unwrap_or(true);
1194
1195                    // Derive plugin name from buffer filename
1196                    let name = buffer
1197                        .file_path()
1198                        .and_then(|p| p.file_name())
1199                        .and_then(|s| s.to_str())
1200                        .map(|s| s.to_string())
1201                        .unwrap_or_else(|| "buffer-plugin".to_string());
1202
1203                    match self
1204                        .plugin_manager
1205                        .load_plugin_from_source(&content, &name, is_ts)
1206                    {
1207                        Ok(()) => {
1208                            self.set_status_message(format!(
1209                                "Plugin '{}' loaded from buffer",
1210                                name
1211                            ));
1212                        }
1213                        Err(e) => {
1214                            self.set_status_message(format!("Failed to load plugin: {}", e));
1215                            tracing::error!("LoadPluginFromBuffer error: {}", e);
1216                        }
1217                    }
1218
1219                    // Set up plugin dev workspace for LSP support
1220                    self.setup_plugin_dev_lsp(buffer_id, &content);
1221                }
1222                #[cfg(not(feature = "plugins"))]
1223                {
1224                    self.set_status_message(
1225                        "Plugins not available (compiled without plugin support)".to_string(),
1226                    );
1227                }
1228            }
1229            Action::OpenTerminal => {
1230                self.open_terminal();
1231            }
1232            Action::CloseTerminal => {
1233                self.close_terminal();
1234            }
1235            Action::FocusTerminal => {
1236                // If viewing a terminal buffer, switch to terminal mode
1237                if self.is_terminal_buffer(self.active_buffer()) {
1238                    self.terminal_mode = true;
1239                    self.key_context = KeyContext::Terminal;
1240                    self.set_status_message(t!("status.terminal_mode_enabled").to_string());
1241                }
1242            }
1243            Action::TerminalEscape => {
1244                // Exit terminal mode back to editor
1245                if self.terminal_mode {
1246                    self.terminal_mode = false;
1247                    self.key_context = KeyContext::Normal;
1248                    self.set_status_message(t!("status.terminal_mode_disabled").to_string());
1249                }
1250            }
1251            Action::ToggleKeyboardCapture => {
1252                // Toggle keyboard capture mode in terminal
1253                if self.terminal_mode {
1254                    self.keyboard_capture = !self.keyboard_capture;
1255                    if self.keyboard_capture {
1256                        self.set_status_message(
1257                            "Keyboard capture ON - all keys go to terminal (F9 to toggle)"
1258                                .to_string(),
1259                        );
1260                    } else {
1261                        self.set_status_message(
1262                            "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
1263                        );
1264                    }
1265                }
1266            }
1267            Action::TerminalPaste => {
1268                // Paste clipboard contents into terminal as a single batch
1269                if self.terminal_mode {
1270                    if let Some(text) = self.clipboard.paste() {
1271                        self.send_terminal_input(text.as_bytes());
1272                    }
1273                }
1274            }
1275            Action::ShellCommand => {
1276                // Run shell command on buffer/selection, output to new buffer
1277                self.start_shell_command_prompt(false);
1278            }
1279            Action::ShellCommandReplace => {
1280                // Run shell command on buffer/selection, replace content
1281                self.start_shell_command_prompt(true);
1282            }
1283            Action::OpenSettings => {
1284                self.open_settings();
1285            }
1286            Action::CloseSettings => {
1287                // Check if there are unsaved changes
1288                let has_changes = self
1289                    .settings_state
1290                    .as_ref()
1291                    .is_some_and(|s| s.has_changes());
1292                if has_changes {
1293                    // Show confirmation dialog
1294                    if let Some(ref mut state) = self.settings_state {
1295                        state.show_confirm_dialog();
1296                    }
1297                } else {
1298                    self.close_settings(false);
1299                }
1300            }
1301            Action::SettingsSave => {
1302                self.save_settings();
1303            }
1304            Action::SettingsReset => {
1305                if let Some(ref mut state) = self.settings_state {
1306                    state.reset_current_to_default();
1307                }
1308            }
1309            Action::SettingsInherit => {
1310                if let Some(ref mut state) = self.settings_state {
1311                    state.set_current_to_null();
1312                }
1313            }
1314            Action::SettingsToggleFocus => {
1315                if let Some(ref mut state) = self.settings_state {
1316                    state.toggle_focus();
1317                }
1318            }
1319            Action::SettingsActivate => {
1320                self.settings_activate_current();
1321            }
1322            Action::SettingsSearch => {
1323                if let Some(ref mut state) = self.settings_state {
1324                    state.start_search();
1325                }
1326            }
1327            Action::SettingsHelp => {
1328                if let Some(ref mut state) = self.settings_state {
1329                    state.toggle_help();
1330                }
1331            }
1332            Action::SettingsIncrement => {
1333                self.settings_increment_current();
1334            }
1335            Action::SettingsDecrement => {
1336                self.settings_decrement_current();
1337            }
1338            Action::CalibrateInput => {
1339                self.open_calibration_wizard();
1340            }
1341            Action::EventDebug => {
1342                self.open_event_debug();
1343            }
1344            Action::OpenKeybindingEditor => {
1345                self.open_keybinding_editor();
1346            }
1347            Action::PromptConfirm => {
1348                if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
1349                    use super::prompt_actions::PromptResult;
1350                    match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
1351                        PromptResult::ExecuteAction(action) => {
1352                            return self.handle_action(action);
1353                        }
1354                        PromptResult::EarlyReturn => {
1355                            return Ok(());
1356                        }
1357                        PromptResult::Done => {}
1358                    }
1359                }
1360            }
1361            Action::PromptConfirmWithText(ref text) => {
1362                // For macro playback: set the prompt text before confirming
1363                if let Some(ref mut prompt) = self.prompt {
1364                    prompt.set_input(text.clone());
1365                    self.update_prompt_suggestions();
1366                }
1367                if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
1368                    use super::prompt_actions::PromptResult;
1369                    match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
1370                        PromptResult::ExecuteAction(action) => {
1371                            return self.handle_action(action);
1372                        }
1373                        PromptResult::EarlyReturn => {
1374                            return Ok(());
1375                        }
1376                        PromptResult::Done => {}
1377                    }
1378                }
1379            }
1380            Action::PopupConfirm => {
1381                use super::popup_actions::PopupConfirmResult;
1382                if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
1383                    return Ok(());
1384                }
1385            }
1386            Action::PopupCancel => {
1387                self.handle_popup_cancel();
1388            }
1389            Action::InsertChar(c) => {
1390                if self.is_prompting() {
1391                    return self.handle_insert_char_prompt(c);
1392                } else if self.key_context == KeyContext::FileExplorer {
1393                    self.file_explorer_search_push_char(c);
1394                } else {
1395                    self.handle_insert_char_editor(c)?;
1396                }
1397            }
1398            // Prompt clipboard actions
1399            Action::PromptCopy => {
1400                if let Some(prompt) = &self.prompt {
1401                    let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
1402                    if !text.is_empty() {
1403                        self.clipboard.copy(text);
1404                        self.set_status_message(t!("clipboard.copied").to_string());
1405                    }
1406                }
1407            }
1408            Action::PromptCut => {
1409                if let Some(prompt) = &self.prompt {
1410                    let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
1411                    if !text.is_empty() {
1412                        self.clipboard.copy(text);
1413                    }
1414                }
1415                if let Some(prompt) = self.prompt.as_mut() {
1416                    if prompt.has_selection() {
1417                        prompt.delete_selection();
1418                    } else {
1419                        prompt.clear();
1420                    }
1421                }
1422                self.set_status_message(t!("clipboard.cut").to_string());
1423                self.update_prompt_suggestions();
1424            }
1425            Action::PromptPaste => {
1426                if let Some(text) = self.clipboard.paste() {
1427                    if let Some(prompt) = self.prompt.as_mut() {
1428                        prompt.insert_str(&text);
1429                    }
1430                    self.update_prompt_suggestions();
1431                }
1432            }
1433            _ => {
1434                // TODO: Why do we have this catch-all? It seems like actions should either:
1435                // 1. Be handled explicitly above (like InsertChar, PopupConfirm, etc.)
1436                // 2. Or be converted to events consistently
1437                // This catch-all makes it unclear which actions go through event conversion
1438                // vs. direct handling. Consider making this explicit or removing the pattern.
1439                self.apply_action_as_events(action)?;
1440            }
1441        }
1442
1443        Ok(())
1444    }
1445}