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