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 pub fn get_key_context(&self) -> crate::input::keybindings::KeyContext {
9 use crate::input::keybindings::KeyContext;
10
11 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 self.key_context.clone()
25 }
26 }
27
28 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 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
47
48 if self.is_event_debug_active() {
52 self.handle_event_debug_input(&key_event);
53 return Ok(());
54 }
55
56 if self.dispatch_terminal_input(&key_event).is_some() {
58 return Ok(());
59 }
60
61 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 if self.theme_info_popup.is_some() {
76 self.theme_info_popup = None;
77 }
78
79 let mut context = self.get_key_context();
81
82 if matches!(context, crate::input::keybindings::KeyContext::Popup) {
85 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 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 self.hide_popup();
103 tracing::debug!("Dismissed transient popup on key press");
104 context = self.get_key_context();
106 }
107 }
108
109 if self.dispatch_modal_input(&key_event).is_some() {
111 return Ok(());
112 }
113
114 if context != self.get_key_context() {
117 context = self.get_key_context();
118 }
119
120 let should_check_mode_bindings =
124 matches!(context, crate::input::keybindings::KeyContext::Normal);
125
126 if should_check_mode_bindings {
127 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 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 if resolved_action != Action::None {
164 return self.handle_action(resolved_action);
165 }
166 }
167
168 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 {
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 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 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 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 if !self.chord_state.is_empty() {
252 tracing::debug!("Chord sequence abandoned, clearing state");
253 self.chord_state.clear();
254 }
255 }
256 }
257
258 tracing::trace!("Context: {:?} -> Action: {:?}", context, action);
260
261 match action {
264 Action::LspCompletion
265 | Action::LspGotoDefinition
266 | Action::LspReferences
267 | Action::LspHover
268 | Action::None => {
269 }
271 _ => {
272 self.cancel_pending_lsp_requests();
274 }
275 }
276
277 self.handle_action(action)
281 }
282
283 pub(crate) fn handle_action(&mut self, action: Action) -> AnyhowResult<()> {
286 use crate::input::keybindings::Action;
287
288 self.record_macro_action(&action);
290
291 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 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 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 let current_path = self
327 .active_state()
328 .buffer
329 .file_path()
330 .map(|p| {
331 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 self.close_tab();
386 }
387 Action::Revert => {
388 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 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 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 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 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 if let Some(prompt) = &self.prompt {
525 if prompt.prompt_type == PromptType::QuickOpen {
526 self.cancel_prompt();
527 return Ok(());
528 }
529 }
530
531 self.start_quick_open();
533 }
534 Action::ToggleLineWrap => {
535 self.config.editor.line_wrap = !self.config.editor.line_wrap;
536
537 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 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 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 self.start_search_prompt(
718 t!("file.replace_prompt").to_string(),
719 PromptType::ReplaceSearch,
720 false,
721 );
722 }
723 Action::QueryReplace => {
724 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 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 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 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 if let Some(events) = self.action_to_events(Action::RemoveSecondaryCursors) {
872 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 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 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 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 self.config.active_keybinding_map = map_name.clone().into();
929
930 *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 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 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 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 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 self.file_open_toggle_hidden();
1072 }
1073 Action::StartMacroRecording => {
1074 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 if let Some(events) = self.action_to_events(Action::DeleteBackward) {
1132 if events.len() > 1 {
1133 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 #[cfg(feature = "plugins")]
1152 if let Some(result) = self.plugin_manager.execute_action_async(&action_name) {
1153 match result {
1154 Ok(receiver) => {
1155 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 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 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 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 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 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 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 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 self.start_shell_command_prompt(false);
1278 }
1279 Action::ShellCommandReplace => {
1280 self.start_shell_command_prompt(true);
1282 }
1283 Action::OpenSettings => {
1284 self.open_settings();
1285 }
1286 Action::CloseSettings => {
1287 let has_changes = self
1289 .settings_state
1290 .as_ref()
1291 .is_some_and(|s| s.has_changes());
1292 if has_changes {
1293 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 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 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 self.apply_action_as_events(action)?;
1440 }
1441 }
1442
1443 Ok(())
1444 }
1445}