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