1use super::*;
2use anyhow::Result as AnyhowResult;
3use rust_i18n::t;
4
5fn key_event_to_payload(ev: &crossterm::event::KeyEvent) -> fresh_core::api::KeyEventPayload {
14 use crossterm::event::{KeyCode, KeyModifiers};
15 let key = match ev.code {
16 KeyCode::Char(c) => c.to_string(),
17 KeyCode::Esc => "escape".to_string(),
18 KeyCode::Enter => "enter".to_string(),
19 KeyCode::Tab => "tab".to_string(),
20 KeyCode::BackTab => "backtab".to_string(),
21 KeyCode::Backspace => "backspace".to_string(),
22 KeyCode::Delete => "delete".to_string(),
23 KeyCode::Left => "left".to_string(),
24 KeyCode::Right => "right".to_string(),
25 KeyCode::Up => "up".to_string(),
26 KeyCode::Down => "down".to_string(),
27 KeyCode::Home => "home".to_string(),
28 KeyCode::End => "end".to_string(),
29 KeyCode::PageUp => "pageup".to_string(),
30 KeyCode::PageDown => "pagedown".to_string(),
31 KeyCode::Insert => "insert".to_string(),
32 KeyCode::F(n) => format!("f{}", n),
33 _ => String::new(),
34 };
35 fresh_core::api::KeyEventPayload {
36 key,
37 ctrl: ev.modifiers.contains(KeyModifiers::CONTROL),
38 alt: ev.modifiers.contains(KeyModifiers::ALT),
39 shift: ev.modifiers.contains(KeyModifiers::SHIFT),
40 meta: ev.modifiers.contains(KeyModifiers::SUPER),
41 }
42}
43
44impl Editor {
45 fn try_resolve_next_key_callback(&mut self, key_event: &crossterm::event::KeyEvent) -> bool {
58 let payload = key_event_to_payload(key_event);
59 if let Some(callback_id) = self.pending_next_key_callbacks.pop_front() {
60 let json = serde_json::to_string(&payload).unwrap_or_else(|_| "null".to_string());
61 self.plugin_manager.resolve_callback(callback_id, json);
62 return true;
63 }
64 if self.key_capture_active {
65 self.pending_key_capture_buffer.push_back(payload);
66 return true;
67 }
68 false
69 }
70}
71
72impl Editor {
73 fn install_quickfix_in_dock(
82 &mut self,
83 query: String,
84 matches: Vec<crate::services::live_grep_state::GrepMatch>,
85 ) {
86 use crate::model::event::SplitDirection;
87 use crate::primitives::text_property::TextPropertyEntry;
88 use crate::view::split::SplitRole;
89
90 let mut entries = Vec::with_capacity(matches.len() + 2);
93 let header = format!("Quickfix: {} ({} matches)\n", query, matches.len());
94 entries.push(TextPropertyEntry::text(header));
95 for m in &matches {
96 let line = format!("{}:{}:{} {}\n", m.file, m.line, m.column, m.content.trim());
97 entries.push(TextPropertyEntry::text(line));
98 }
99
100 let panel_key = "quickfix".to_string();
103 if let Some(&existing) = self.panel_ids.get(&panel_key) {
104 if self.buffers.contains_key(&existing) {
105 if let Err(e) = self.set_virtual_buffer_content(existing, entries) {
106 tracing::error!("Failed to update quickfix buffer: {}", e);
107 return;
108 }
109 if let Some(dock_leaf) =
111 self.split_manager.find_leaf_by_role(SplitRole::UtilityDock)
112 {
113 self.split_manager.set_active_split(dock_leaf);
114 self.set_pane_buffer(dock_leaf, existing);
115 }
116 self.set_status_message(format!("Quickfix updated: {} matches", matches.len()));
117 return;
118 }
119 self.panel_ids.remove(&panel_key);
121 }
122
123 let buffer_id = self.create_virtual_buffer_detached(
129 "*Quickfix*".to_string(),
130 "quickfix-list".to_string(),
131 true,
132 );
133 if let Some(state) = self.buffers.get_mut(&buffer_id) {
134 state.margins.configure_for_line_numbers(false);
135 state.show_cursors = true;
136 state.editing_disabled = true;
137 }
138 self.panel_ids.insert(panel_key, buffer_id);
139 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
140 tracing::error!("Failed to set quickfix buffer content: {}", e);
141 return;
142 }
143
144 if let Some(dock_leaf) = self.split_manager.find_leaf_by_role(SplitRole::UtilityDock) {
148 self.split_manager.set_active_split(dock_leaf);
149 self.set_pane_buffer(dock_leaf, buffer_id);
150 let line_numbers = self.config.editor.line_numbers;
158 let highlight_current_line = self.config.editor.highlight_current_line;
159 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
160 let wrap_indent = self.config.editor.wrap_indent;
161 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
162 let rulers = self.config.editor.rulers.clone();
163 if let Some(view_state) = self.split_view_states.get_mut(&dock_leaf) {
164 let buf_state = view_state.ensure_buffer_state(buffer_id);
165 buf_state.apply_config_defaults(
166 line_numbers,
167 highlight_current_line,
168 line_wrap,
169 wrap_indent,
170 wrap_column,
171 rulers,
172 );
173 buf_state.show_line_numbers = false;
174 }
175 } else {
176 match self.split_manager.split_root_positioned(
179 SplitDirection::Horizontal,
180 buffer_id,
181 0.7,
182 false, ) {
184 Ok(new_leaf) => {
185 let mut view_state = crate::view::split::SplitViewState::with_buffer(
186 self.terminal_width,
187 self.terminal_height,
188 buffer_id,
189 );
190 view_state.apply_config_defaults(
191 self.config.editor.line_numbers,
192 self.config.editor.highlight_current_line,
193 self.resolve_line_wrap_for_buffer(buffer_id),
194 self.config.editor.wrap_indent,
195 self.resolve_wrap_column_for_buffer(buffer_id),
196 self.config.editor.rulers.clone(),
197 );
198 view_state.ensure_buffer_state(buffer_id).show_line_numbers = false;
199 self.split_view_states.insert(new_leaf, view_state);
200 self.split_manager
201 .set_leaf_role(new_leaf, Some(SplitRole::UtilityDock));
202 self.split_manager.set_active_split(new_leaf);
203 }
204 Err(e) => {
205 tracing::error!("Failed to create dock split for quickfix: {}", e);
206 return;
207 }
208 }
209 }
210
211 self.set_status_message(format!(
212 "Quickfix exported: {} matches in dock",
213 matches.len()
214 ));
215 }
216
217 pub(crate) fn popups_capture_keys(&self) -> bool {
236 use crate::input::keybindings::KeyContext;
237 if matches!(self.key_context, KeyContext::FileExplorer) {
238 return false;
239 }
240 self.topmost_popup_focused()
241 }
242
243 pub(crate) fn topmost_popup_focused(&self) -> bool {
248 if let Some(popup) = self.global_popups.top() {
249 return popup.focused;
250 }
251 if let Some(popup) = self.active_state().popups.top() {
252 return popup.focused;
253 }
254 false
257 }
258
259 pub(crate) fn resolve_unfocused_popup_action(
269 &self,
270 event: &crossterm::event::KeyEvent,
271 ) -> Option<crate::input::keybindings::Action> {
272 use crate::input::keybindings::{Action, KeyContext};
273
274 let popup_visible =
275 self.global_popups.is_visible() || self.active_state().popups.is_visible();
276 if !popup_visible || self.topmost_popup_focused() {
277 return None;
278 }
279
280 if self.settings_state.as_ref().is_some_and(|s| s.visible)
286 || self.menu_state.active_menu.is_some()
287 || self.is_prompting()
288 {
289 return None;
290 }
291
292 let kb = self.keybindings.read().ok()?;
293
294 let popup_focus_match = matches!(
303 kb.resolve_in_context_only(event, self.key_context.clone()),
304 Some(Action::PopupFocus),
305 );
306 if popup_focus_match {
307 return Some(Action::PopupFocus);
308 }
309
310 let resolved_popup = kb.resolve_in_context_only(event, KeyContext::Popup);
316 match resolved_popup {
317 Some(action @ (Action::PopupCancel | Action::PopupFocus)) => Some(action),
318 _ => None,
319 }
320 }
321
322 pub(crate) fn resolve_completion_popup_action(
328 &self,
329 event: &crossterm::event::KeyEvent,
330 ) -> Option<crate::input::keybindings::Action> {
331 use crate::input::keybindings::{Action, KeyContext};
332 use crate::view::popup::PopupKind;
333
334 let topmost_kind = if self.global_popups.is_visible() {
335 self.global_popups.top().map(|p| p.kind)
336 } else if self.active_state().popups.is_visible() {
337 self.active_state().popups.top().map(|p| p.kind)
338 } else {
339 None
340 };
341
342 if topmost_kind != Some(PopupKind::Completion) {
343 return None;
344 }
345
346 match self
347 .keybindings
348 .read()
349 .unwrap()
350 .resolve_in_context_only(event, KeyContext::Completion)
351 {
352 Some(action @ (Action::CompletionAccept | Action::CompletionDismiss)) => Some(action),
353 _ => None,
354 }
355 }
356
357 pub fn get_key_context(&self) -> crate::input::keybindings::KeyContext {
359 use crate::input::keybindings::KeyContext;
360
361 if self.settings_state.as_ref().is_some_and(|s| s.visible) {
365 KeyContext::Settings
366 } else if self.menu_state.active_menu.is_some() {
367 KeyContext::Menu
368 } else if self.is_prompting() {
369 KeyContext::Prompt
370 } else if self.popups_capture_keys()
371 && (self.global_popups.is_visible() || self.active_state().popups.is_visible())
372 {
373 KeyContext::Popup
374 } else if self.is_composite_buffer(self.active_buffer()) {
375 KeyContext::CompositeBuffer
376 } else {
377 self.key_context.clone()
379 }
380 }
381
382 pub fn handle_key(
385 &mut self,
386 code: crossterm::event::KeyCode,
387 modifiers: crossterm::event::KeyModifiers,
388 ) -> AnyhowResult<()> {
389 use crate::input::keybindings::Action;
390
391 let _t_total = std::time::Instant::now();
392
393 tracing::trace!(
394 "Editor.handle_key: code={:?}, modifiers={:?}",
395 code,
396 modifiers
397 );
398
399 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
401
402 if self.is_event_debug_active() {
406 self.handle_event_debug_input(&key_event);
407 return Ok(());
408 }
409
410 if self.dispatch_terminal_input(&key_event).is_some() {
412 return Ok(());
413 }
414
415 if self.try_resolve_next_key_callback(&key_event) {
422 return Ok(());
423 }
424
425 let active_split = self.effective_active_split();
434 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
435 view_state.viewport.clear_skip_ensure_visible();
436 }
437
438 if self.theme_info_popup.is_some() {
440 self.theme_info_popup = None;
441 }
442
443 if self.file_explorer_context_menu.is_some() {
444 if let Some(result) = self.handle_file_explorer_context_menu_key(code, modifiers) {
445 return result;
446 }
447 }
448
449 let mut context = self.get_key_context();
451
452 let popup_visible_on_screen =
462 self.global_popups.is_visible() || self.active_state().popups.is_visible();
463 if popup_visible_on_screen {
464 let (is_transient_popup, has_selection) = {
468 let popup = self
469 .global_popups
470 .top()
471 .or_else(|| self.active_state().popups.top());
472 (
473 popup.is_some_and(|p| p.transient),
474 popup.is_some_and(|p| p.has_selection()),
475 )
476 };
477
478 let is_copy_key = key_event.code == crossterm::event::KeyCode::Char('c')
480 && key_event
481 .modifiers
482 .contains(crossterm::event::KeyModifiers::CONTROL);
483
484 let resolved_action = self
489 .keybindings
490 .read()
491 .ok()
492 .map(|kb| kb.resolve(&key_event, context.clone()));
493 let is_focus_popup_key = matches!(
494 resolved_action,
495 Some(crate::input::keybindings::Action::PopupFocus)
496 );
497
498 if is_transient_popup && !(has_selection && is_copy_key) && !is_focus_popup_key {
499 self.hide_popup();
501 tracing::debug!("Dismissed transient popup on key press");
502 context = self.get_key_context();
504 }
505 }
506
507 if let Some(action) = self.resolve_unfocused_popup_action(&key_event) {
513 self.handle_action(action)?;
514 return Ok(());
515 }
516
517 if self.dispatch_modal_input(&key_event).is_some() {
519 return Ok(());
520 }
521
522 if context != self.get_key_context() {
525 context = self.get_key_context();
526 }
527
528 let should_check_mode_bindings =
532 matches!(context, crate::input::keybindings::KeyContext::Normal);
533
534 if should_check_mode_bindings {
535 let effective_mode = self.effective_mode().map(|s| s.to_owned());
538
539 if let Some(ref mode_name) = effective_mode {
540 let mode_ctx = crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
541 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
542
543 let (chord_result, resolved_action) = {
545 let keybindings = self.keybindings.read().unwrap();
546 let chord_result =
547 keybindings.resolve_chord(&self.chord_state, &key_event, mode_ctx.clone());
548 let resolved = keybindings.resolve(&key_event, mode_ctx);
549 (chord_result, resolved)
550 };
551 match chord_result {
552 crate::input::keybindings::ChordResolution::Complete(action) => {
553 tracing::debug!("Mode chord resolved to action: {:?}", action);
554 self.chord_state.clear();
555 return self.handle_action(action);
556 }
557 crate::input::keybindings::ChordResolution::Partial => {
558 tracing::debug!("Potential chord prefix in mode '{}'", mode_name);
559 self.chord_state.push((code, modifiers));
560 return Ok(());
561 }
562 crate::input::keybindings::ChordResolution::NoMatch => {
563 if !self.chord_state.is_empty() {
564 tracing::debug!("Chord sequence abandoned in mode, clearing state");
565 self.chord_state.clear();
566 }
567 }
568 }
569
570 if resolved_action != Action::None {
572 return self.handle_action(resolved_action);
573 }
574 }
575
576 if let Some(ref mode_name) = effective_mode {
588 if self.mode_registry.allows_text_input(mode_name) {
589 if let KeyCode::Char(c) = code {
590 let ch = if modifiers.contains(KeyModifiers::SHIFT) {
591 c.to_uppercase().next().unwrap_or(c)
592 } else {
593 c
594 };
595 if !modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) {
596 let action_name = format!("mode_text_input:{}", ch);
597 return self.handle_action(Action::PluginAction(action_name));
598 }
599 }
600 tracing::debug!("Blocking unbound key in text-input mode '{}'", mode_name);
601 return Ok(());
602 }
603 }
604 if let Some(ref mode_name) = self.editor_mode {
605 if self.mode_registry.is_read_only(mode_name) {
606 tracing::debug!("Ignoring unbound key in read-only mode '{}'", mode_name);
607 return Ok(());
608 }
609 tracing::debug!(
610 "Mode '{}' is not read-only, allowing key through",
611 mode_name
612 );
613 }
614 }
615
616 {
623 let active_buf = self.active_buffer();
624 let active_split = self.effective_active_split();
625 if self.is_composite_buffer(active_buf) {
626 if let Some(handled) =
627 self.try_route_composite_key(active_split, active_buf, &key_event)
628 {
629 return handled;
630 }
631 }
632 }
633
634 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
636 let (chord_result, action) = {
637 let keybindings = self.keybindings.read().unwrap();
638 let chord_result =
639 keybindings.resolve_chord(&self.chord_state, &key_event, context.clone());
640 let action = keybindings.resolve(&key_event, context.clone());
641 (chord_result, action)
642 };
643
644 match chord_result {
645 crate::input::keybindings::ChordResolution::Complete(action) => {
646 tracing::debug!("Complete chord match -> Action: {:?}", action);
648 self.chord_state.clear();
649 return self.handle_action(action);
650 }
651 crate::input::keybindings::ChordResolution::Partial => {
652 tracing::debug!("Partial chord match - waiting for next key");
654 self.chord_state.push((code, modifiers));
655 return Ok(());
656 }
657 crate::input::keybindings::ChordResolution::NoMatch => {
658 if !self.chord_state.is_empty() {
660 tracing::debug!("Chord sequence abandoned, clearing state");
661 self.chord_state.clear();
662 }
663 }
664 }
665
666 tracing::trace!("Context: {:?} -> Action: {:?}", context, action);
668
669 match action {
672 Action::LspCompletion
673 | Action::LspGotoDefinition
674 | Action::LspReferences
675 | Action::LspHover
676 | Action::None => {
677 }
679 _ => {
680 self.cancel_pending_lsp_requests();
682 }
683 }
684
685 self.handle_action(action)
689 }
690
691 pub(crate) fn handle_action(&mut self, action: Action) -> AnyhowResult<()> {
694 use crate::input::keybindings::Action;
695
696 self.record_macro_action(&action);
698
699 if !matches!(action, Action::DabbrevExpand) {
701 self.reset_dabbrev_state();
702 }
703
704 match action {
705 Action::Quit => self.quit(),
706 Action::ForceQuit => {
707 self.should_quit = true;
708 }
709 Action::Detach => {
710 self.should_detach = true;
711 }
712 Action::Save => {
713 if self.active_state().buffer.file_path().is_none() {
715 self.start_prompt_with_initial_text(
716 t!("file.save_as_prompt").to_string(),
717 PromptType::SaveFileAs,
718 String::new(),
719 );
720 self.init_file_open_state();
721 } else if self.check_save_conflict().is_some() {
722 self.start_prompt(
724 t!("file.file_changed_prompt").to_string(),
725 PromptType::ConfirmSaveConflict,
726 );
727 } else if let Err(e) = self.save() {
728 let msg = format!("{}", e);
729 self.status_message = Some(t!("file.save_failed", error = &msg).to_string());
730 }
731 }
732 Action::SaveAs => {
733 let current_path = self
735 .active_state()
736 .buffer
737 .file_path()
738 .map(|p| {
739 p.strip_prefix(&self.working_dir)
741 .unwrap_or(p)
742 .to_string_lossy()
743 .to_string()
744 })
745 .unwrap_or_default();
746 self.start_prompt_with_initial_text(
747 t!("file.save_as_prompt").to_string(),
748 PromptType::SaveFileAs,
749 current_path,
750 );
751 self.init_file_open_state();
752 }
753 Action::Open => {
754 self.start_prompt(t!("file.open_prompt").to_string(), PromptType::OpenFile);
755 self.prefill_open_file_prompt();
756 self.init_file_open_state();
757 }
758 Action::SwitchProject => {
759 self.start_prompt(
760 t!("file.switch_project_prompt").to_string(),
761 PromptType::SwitchProject,
762 );
763 self.init_folder_open_state();
764 }
765 Action::GotoLine => {
766 let has_line_index = self
767 .buffers
768 .get(&self.active_buffer())
769 .is_none_or(|s| s.buffer.line_count().is_some());
770 if has_line_index {
771 self.start_prompt(
772 t!("file.goto_line_prompt").to_string(),
773 PromptType::GotoLine,
774 );
775 } else {
776 self.start_prompt(
777 t!("goto.scan_confirm_prompt", yes = "y", no = "N").to_string(),
778 PromptType::GotoLineScanConfirm,
779 );
780 }
781 }
782 Action::ScanLineIndex => {
783 self.start_incremental_line_scan(false);
784 }
785 Action::New => {
786 self.new_buffer();
787 }
788 Action::Close | Action::CloseTab => {
789 self.close_tab();
794 }
795 Action::Revert => {
796 if self.active_state().buffer.is_modified() {
798 let revert_key = t!("prompt.key.revert").to_string();
799 let cancel_key = t!("prompt.key.cancel").to_string();
800 self.start_prompt(
801 t!(
802 "prompt.revert_confirm",
803 revert_key = revert_key,
804 cancel_key = cancel_key
805 )
806 .to_string(),
807 PromptType::ConfirmRevert,
808 );
809 } else {
810 if let Err(e) = self.revert_file() {
812 self.set_status_message(
813 t!("error.failed_to_revert", error = e.to_string()).to_string(),
814 );
815 }
816 }
817 }
818 Action::ToggleAutoRevert => {
819 self.toggle_auto_revert();
820 }
821 Action::FormatBuffer => {
822 if let Err(e) = self.format_buffer() {
823 self.set_status_message(
824 t!("error.format_failed", error = e.to_string()).to_string(),
825 );
826 }
827 }
828 Action::TrimTrailingWhitespace => match self.trim_trailing_whitespace() {
829 Ok(true) => {
830 self.set_status_message(t!("whitespace.trimmed").to_string());
831 }
832 Ok(false) => {
833 self.set_status_message(t!("whitespace.no_trailing").to_string());
834 }
835 Err(e) => {
836 self.set_status_message(
837 t!("error.trim_whitespace_failed", error = e).to_string(),
838 );
839 }
840 },
841 Action::EnsureFinalNewline => match self.ensure_final_newline() {
842 Ok(true) => {
843 self.set_status_message(t!("whitespace.newline_added").to_string());
844 }
845 Ok(false) => {
846 self.set_status_message(t!("whitespace.already_has_newline").to_string());
847 }
848 Err(e) => {
849 self.set_status_message(
850 t!("error.ensure_newline_failed", error = e).to_string(),
851 );
852 }
853 },
854 Action::Copy => {
855 let popup = self
857 .global_popups
858 .top()
859 .or_else(|| self.active_state().popups.top());
860 if let Some(popup) = popup {
861 if popup.has_selection() {
862 if let Some(text) = popup.get_selected_text() {
863 self.clipboard.copy(text);
864 self.set_status_message(t!("clipboard.copied").to_string());
865 return Ok(());
866 }
867 }
868 }
869 if self.key_context == crate::input::keybindings::KeyContext::FileExplorer {
870 self.file_explorer_copy();
871 return Ok(());
872 }
873 let buffer_id = self.active_buffer();
875 if self.is_composite_buffer(buffer_id) {
876 if let Some(_handled) = self.handle_composite_action(buffer_id, &Action::Copy) {
877 return Ok(());
878 }
879 }
880 self.copy_selection()
881 }
882 Action::CopyWithTheme(theme) => self.copy_selection_with_theme(&theme),
883 Action::CopyFilePath => self.copy_active_buffer_path(false),
884 Action::CopyRelativeFilePath => self.copy_active_buffer_path(true),
885 Action::Cut => {
886 if self.key_context == crate::input::keybindings::KeyContext::FileExplorer {
887 self.file_explorer_cut();
888 return Ok(());
889 }
890 if self.is_editing_disabled() {
891 self.set_status_message(t!("buffer.editing_disabled").to_string());
892 return Ok(());
893 }
894 self.cut_selection()
895 }
896 Action::Paste => {
897 if self.key_context == crate::input::keybindings::KeyContext::FileExplorer {
898 self.file_explorer_paste();
899 return Ok(());
900 }
901 if self.is_editing_disabled() {
902 self.set_status_message(t!("buffer.editing_disabled").to_string());
903 return Ok(());
904 }
905 self.paste()
906 }
907 Action::YankWordForward => self.yank_word_forward(),
908 Action::YankWordBackward => self.yank_word_backward(),
909 Action::YankToLineEnd => self.yank_to_line_end(),
910 Action::YankToLineStart => self.yank_to_line_start(),
911 Action::YankViWordEnd => self.yank_vi_word_end(),
912 Action::Undo => {
913 self.handle_undo();
914 }
915 Action::Redo => {
916 self.handle_redo();
917 }
918 Action::ShowHelp => {
919 self.open_help_manual();
920 }
921 Action::ShowKeyboardShortcuts => {
922 self.open_keyboard_shortcuts();
923 }
924 Action::ShowWarnings => {
925 self.show_warnings_popup();
926 }
927 Action::ShowStatusLog => {
928 self.open_status_log();
929 }
930 Action::ShowLspStatus => {
931 self.show_lsp_status_popup();
932 }
933 Action::ShowRemoteIndicatorMenu => {
934 self.show_remote_indicator_popup();
935 }
936 Action::ClearWarnings => {
937 self.clear_warnings();
938 }
939 Action::CommandPalette => {
940 if let Some(prompt) = &self.prompt {
943 if prompt.prompt_type == PromptType::QuickOpen {
944 self.cancel_prompt();
945 return Ok(());
946 }
947 }
948 self.start_quick_open();
949 }
950 Action::QuickOpen => {
951 if let Some(prompt) = &self.prompt {
953 if prompt.prompt_type == PromptType::QuickOpen {
954 self.cancel_prompt();
955 return Ok(());
956 }
957 }
958
959 self.start_quick_open();
961 }
962 Action::QuickOpenBuffers => {
963 if let Some(prompt) = &self.prompt {
964 if prompt.prompt_type == PromptType::QuickOpen {
965 self.cancel_prompt();
966 return Ok(());
967 }
968 }
969 self.start_quick_open_with_prefix("#");
970 }
971 Action::QuickOpenFiles => {
972 if let Some(prompt) = &self.prompt {
973 if prompt.prompt_type == PromptType::QuickOpen {
974 self.cancel_prompt();
975 return Ok(());
976 }
977 }
978 self.start_quick_open_with_prefix("");
979 }
980 Action::OpenLiveGrep => {
981 #[cfg(feature = "plugins")]
987 {
988 if let Some(result) =
989 self.plugin_manager.execute_action_async("start_live_grep")
990 {
991 match result {
992 Ok(receiver) => {
993 self.pending_plugin_actions
994 .push(("start_live_grep".to_string(), receiver));
995 }
996 Err(e) => {
997 self.set_status_message(format!("Live Grep unavailable: {}", e));
998 }
999 }
1000 } else {
1001 self.set_status_message("Live Grep plugin not loaded".to_string());
1002 }
1003 }
1004 #[cfg(not(feature = "plugins"))]
1005 {
1006 self.set_status_message("Live Grep requires the plugins feature".to_string());
1007 }
1008 }
1009 Action::ResumeLiveGrep => {
1010 let cached = self.live_grep_last_state.clone();
1016 match cached {
1017 Some(state) if state.cached_results.as_ref().is_some_and(|r| !r.is_empty()) => {
1018 let results = state.cached_results.unwrap_or_default();
1019 let suggestions: Vec<crate::input::commands::Suggestion> = results
1024 .into_iter()
1025 .map(|m| {
1026 let label = format!("{}:{}", m.file, m.line);
1027 let value = format!("{}:{}:{}", m.file, m.line, m.column);
1028 let mut s = crate::input::commands::Suggestion::new(label);
1029 s.description = Some(m.content);
1030 s.value = Some(value);
1031 s
1032 })
1033 .collect();
1034 let mut prompt = crate::view::prompt::Prompt::with_suggestions(
1041 "Live grep: ".to_string(),
1042 PromptType::LiveGrep,
1043 suggestions,
1044 );
1045 prompt.input = state.query;
1046 prompt.cursor_pos = prompt.input.len();
1047 if let Some(idx) = state.selected_index {
1048 if idx < prompt.suggestions.len() {
1049 prompt.selected_suggestion = Some(idx);
1050 }
1051 }
1052 prompt.suggestions_set_for_input = Some(prompt.input.clone());
1053 prompt.overlay = true;
1055 self.prompt = Some(prompt);
1056 }
1057 _ => {
1058 #[cfg(feature = "plugins")]
1060 if let Some(result) =
1061 self.plugin_manager.execute_action_async("start_live_grep")
1062 {
1063 match result {
1064 Ok(receiver) => {
1065 self.pending_plugin_actions
1066 .push(("start_live_grep".to_string(), receiver));
1067 }
1068 Err(e) => {
1069 self.set_status_message(format!(
1070 "Live Grep unavailable: {}",
1071 e
1072 ));
1073 }
1074 }
1075 }
1076 }
1077 }
1078 }
1079 Action::LiveGrepExportQuickfix => {
1080 let is_grep = self
1085 .prompt
1086 .as_ref()
1087 .map(|p| match &p.prompt_type {
1088 PromptType::LiveGrep => true,
1089 PromptType::Plugin { custom_type } => custom_type == "live-grep",
1090 _ => false,
1091 })
1092 .unwrap_or(false);
1093 if !is_grep {
1094 self.set_status_message(
1095 "Quickfix export is only available inside Live Grep".to_string(),
1096 );
1097 return Ok(());
1098 }
1099 let (query, matches) = {
1100 let prompt = self.prompt.as_ref().unwrap();
1101 (
1102 prompt.input.clone(),
1103 self.snapshot_prompt_results_for_grep(prompt),
1104 )
1105 };
1106 if matches.is_empty() {
1107 self.set_status_message("No Live Grep results to export".to_string());
1108 return Ok(());
1109 }
1110 self.cancel_prompt();
1112 self.install_quickfix_in_dock(query, matches);
1114 }
1115 Action::ToggleUtilityDock => {
1116 use crate::view::split::SplitRole;
1117 if let Some(dock_leaf) =
1118 self.split_manager.find_leaf_by_role(SplitRole::UtilityDock)
1119 {
1120 let active = self.split_manager.active_split();
1121 if active == dock_leaf {
1122 self.next_split();
1127 } else {
1128 self.split_manager.set_active_split(dock_leaf);
1129 }
1130 } else {
1131 self.set_status_message(
1132 "No Utility Dock open — invoke a dock-aware utility (Diagnostics, Search/Replace, …)"
1133 .to_string(),
1134 );
1135 }
1136 }
1137 Action::CycleLiveGrepProvider => {
1138 let in_live_grep = self
1144 .prompt
1145 .as_ref()
1146 .map(|p| match &p.prompt_type {
1147 PromptType::LiveGrep => true,
1148 PromptType::Plugin { custom_type } => custom_type == "live-grep",
1149 _ => false,
1150 })
1151 .unwrap_or(false);
1152 if !in_live_grep {
1153 self.set_status_message(
1154 "Cycle Live Grep provider only works inside Live Grep".to_string(),
1155 );
1156 return Ok(());
1157 }
1158 #[cfg(feature = "plugins")]
1159 {
1160 if let Some(result) = self
1161 .plugin_manager
1162 .execute_action_async("live_grep_cycle_provider")
1163 {
1164 match result {
1165 Ok(receiver) => {
1166 self.pending_plugin_actions
1167 .push(("live_grep_cycle_provider".to_string(), receiver));
1168 }
1169 Err(e) => {
1170 self.set_status_message(format!("Live Grep cycle failed: {}", e));
1171 }
1172 }
1173 } else {
1174 self.set_status_message("Live Grep plugin not loaded".to_string());
1175 }
1176 }
1177 #[cfg(not(feature = "plugins"))]
1178 {
1179 self.set_status_message(
1180 "Live Grep cycle requires the plugins feature".to_string(),
1181 );
1182 }
1183 }
1184 Action::OpenTerminalInDock => {
1185 use crate::model::event::SplitDirection;
1186 use crate::view::split::SplitRole;
1187 if let Some(dock_leaf) =
1188 self.split_manager.find_leaf_by_role(SplitRole::UtilityDock)
1189 {
1190 self.split_manager.set_active_split(dock_leaf);
1193 self.open_terminal();
1194 } else {
1195 let Some(terminal_id) = self.spawn_terminal_session() else {
1202 return Ok(());
1203 };
1204 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
1205 match self.split_manager.split_root_positioned(
1208 SplitDirection::Horizontal,
1209 buffer_id,
1210 0.7,
1211 false,
1212 ) {
1213 Ok(new_leaf) => {
1214 let mut view_state = crate::view::split::SplitViewState::with_buffer(
1215 self.terminal_width,
1216 self.terminal_height,
1217 buffer_id,
1218 );
1219 view_state.apply_config_defaults(
1220 self.config.editor.line_numbers,
1221 self.config.editor.highlight_current_line,
1222 self.resolve_line_wrap_for_buffer(buffer_id),
1223 self.config.editor.wrap_indent,
1224 self.resolve_wrap_column_for_buffer(buffer_id),
1225 self.config.editor.rulers.clone(),
1226 );
1227 view_state.viewport.line_wrap_enabled = false;
1231 self.split_view_states.insert(new_leaf, view_state);
1232 self.split_manager
1233 .set_leaf_role(new_leaf, Some(SplitRole::UtilityDock));
1234 self.split_manager.set_active_split(new_leaf);
1235 self.terminal_mode = true;
1241 self.key_context = crate::input::keybindings::KeyContext::Terminal;
1242 self.resize_visible_terminals();
1243 let exit_key = self
1244 .keybindings
1245 .read()
1246 .unwrap()
1247 .find_keybinding_for_action(
1248 "terminal_escape",
1249 crate::input::keybindings::KeyContext::Terminal,
1250 )
1251 .unwrap_or_else(|| "Ctrl+Space".to_string());
1252 self.set_status_message(
1253 rust_i18n::t!(
1254 "terminal.opened",
1255 id = terminal_id.0,
1256 exit_key = exit_key
1257 )
1258 .to_string(),
1259 );
1260 tracing::info!(
1261 "Opened terminal {:?} into new dock leaf {:?} (buffer {:?})",
1262 terminal_id,
1263 new_leaf,
1264 buffer_id
1265 );
1266 }
1267 Err(e) => {
1268 self.set_status_message(format!(
1269 "Failed to create dock for terminal: {}",
1270 e
1271 ));
1272 return Ok(());
1273 }
1274 }
1275 }
1276 }
1277 Action::ToggleLineWrap => {
1278 let new_value = !self.config.editor.line_wrap;
1279 self.config_mut().editor.line_wrap = new_value;
1280
1281 let leaf_ids: Vec<_> = self.split_view_states.keys().copied().collect();
1284 for leaf_id in leaf_ids {
1285 let buffer_id = self
1286 .split_manager
1287 .get_buffer_id(leaf_id.into())
1288 .unwrap_or(BufferId(0));
1289 let effective_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
1290 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
1291 if let Some(view_state) = self.split_view_states.get_mut(&leaf_id) {
1292 view_state.viewport.line_wrap_enabled = effective_wrap;
1293 view_state.viewport.wrap_indent = self.config.editor.wrap_indent;
1294 view_state.viewport.wrap_column = wrap_column;
1295 }
1296 }
1297
1298 let state = if self.config.editor.line_wrap {
1299 t!("view.state_enabled").to_string()
1300 } else {
1301 t!("view.state_disabled").to_string()
1302 };
1303 self.set_status_message(t!("view.line_wrap_state", state = state).to_string());
1304 }
1305 Action::ToggleCurrentLineHighlight => {
1306 let new_value = !self.config.editor.highlight_current_line;
1307 self.config_mut().editor.highlight_current_line = new_value;
1308
1309 let leaf_ids: Vec<_> = self.split_view_states.keys().copied().collect();
1311 for leaf_id in leaf_ids {
1312 if let Some(view_state) = self.split_view_states.get_mut(&leaf_id) {
1313 view_state.highlight_current_line =
1314 self.config.editor.highlight_current_line;
1315 }
1316 }
1317
1318 let state = if self.config.editor.highlight_current_line {
1319 t!("view.state_enabled").to_string()
1320 } else {
1321 t!("view.state_disabled").to_string()
1322 };
1323 self.set_status_message(
1324 t!("view.current_line_highlight_state", state = state).to_string(),
1325 );
1326 }
1327 Action::ToggleReadOnly => {
1328 let buffer_id = self.active_buffer();
1329 let is_now_read_only = self
1330 .buffer_metadata
1331 .get(&buffer_id)
1332 .map(|m| !m.read_only)
1333 .unwrap_or(false);
1334 self.mark_buffer_read_only(buffer_id, is_now_read_only);
1335
1336 let state_str = if is_now_read_only {
1337 t!("view.state_enabled").to_string()
1338 } else {
1339 t!("view.state_disabled").to_string()
1340 };
1341 self.set_status_message(t!("view.read_only_state", state = state_str).to_string());
1342 }
1343 Action::TogglePageView => {
1344 self.handle_toggle_page_view();
1345 }
1346 Action::SetPageWidth => {
1347 let active_split = self.split_manager.active_split();
1348 let current = self
1349 .split_view_states
1350 .get(&active_split)
1351 .and_then(|v| v.compose_width.map(|w| w.to_string()))
1352 .unwrap_or_default();
1353 self.start_prompt_with_initial_text(
1354 "Page width (empty = viewport): ".to_string(),
1355 PromptType::SetPageWidth,
1356 current,
1357 );
1358 }
1359 Action::SetBackground => {
1360 let default_path = self
1361 .ansi_background_path
1362 .as_ref()
1363 .and_then(|p| {
1364 p.strip_prefix(&self.working_dir)
1365 .ok()
1366 .map(|rel| rel.to_string_lossy().to_string())
1367 })
1368 .unwrap_or_else(|| DEFAULT_BACKGROUND_FILE.to_string());
1369
1370 self.start_prompt_with_initial_text(
1371 "Background file: ".to_string(),
1372 PromptType::SetBackgroundFile,
1373 default_path,
1374 );
1375 }
1376 Action::SetBackgroundBlend => {
1377 let default_amount = format!("{:.2}", self.background_fade);
1378 self.start_prompt_with_initial_text(
1379 "Background blend (0-1): ".to_string(),
1380 PromptType::SetBackgroundBlend,
1381 default_amount,
1382 );
1383 }
1384 Action::LspCompletion => {
1385 self.request_completion();
1386 }
1387 Action::DabbrevExpand => {
1388 self.dabbrev_expand();
1389 }
1390 Action::LspGotoDefinition => {
1391 self.request_goto_definition()?;
1392 }
1393 Action::LspRename => {
1394 self.start_rename()?;
1395 }
1396 Action::LspHover => {
1397 self.request_hover()?;
1398 }
1399 Action::LspReferences => {
1400 self.request_references()?;
1401 }
1402 Action::LspSignatureHelp => {
1403 self.request_signature_help();
1404 }
1405 Action::LspCodeActions => {
1406 self.request_code_actions()?;
1407 }
1408 Action::LspRestart => {
1409 self.handle_lsp_restart();
1410 }
1411 Action::LspStop => {
1412 self.handle_lsp_stop();
1413 }
1414 Action::LspToggleForBuffer => {
1415 self.handle_lsp_toggle_for_buffer();
1416 }
1417 Action::ToggleInlayHints => {
1418 self.toggle_inlay_hints();
1419 }
1420 Action::DumpConfig => {
1421 self.dump_config();
1422 }
1423 Action::RedrawScreen => {
1424 self.request_full_redraw();
1425 }
1426 Action::SelectTheme => {
1427 self.start_select_theme_prompt();
1428 }
1429 Action::InspectThemeAtCursor => {
1430 self.inspect_theme_at_cursor();
1431 }
1432 Action::SelectKeybindingMap => {
1433 self.start_select_keybinding_map_prompt();
1434 }
1435 Action::SelectCursorStyle => {
1436 self.start_select_cursor_style_prompt();
1437 }
1438 Action::SelectLocale => {
1439 self.start_select_locale_prompt();
1440 }
1441 Action::Search => {
1442 let is_search_prompt = self.prompt.as_ref().is_some_and(|p| {
1444 matches!(
1445 p.prompt_type,
1446 PromptType::Search
1447 | PromptType::ReplaceSearch
1448 | PromptType::QueryReplaceSearch
1449 )
1450 });
1451
1452 if is_search_prompt {
1453 self.confirm_prompt();
1454 } else {
1455 self.start_search_prompt(
1456 t!("file.search_prompt").to_string(),
1457 PromptType::Search,
1458 false,
1459 );
1460 }
1461 }
1462 Action::Replace => {
1463 self.start_search_prompt(
1465 t!("file.replace_prompt").to_string(),
1466 PromptType::ReplaceSearch,
1467 false,
1468 );
1469 }
1470 Action::QueryReplace => {
1471 self.search_confirm_each = true;
1473 self.start_search_prompt(
1474 "Query replace: ".to_string(),
1475 PromptType::QueryReplaceSearch,
1476 false,
1477 );
1478 }
1479 Action::FindInSelection => {
1480 self.start_search_prompt(
1481 t!("file.search_prompt").to_string(),
1482 PromptType::Search,
1483 true,
1484 );
1485 }
1486 Action::FindNext => {
1487 self.find_next();
1488 }
1489 Action::FindPrevious => {
1490 self.find_previous();
1491 }
1492 Action::FindSelectionNext => {
1493 self.find_selection_next();
1494 }
1495 Action::FindSelectionPrevious => {
1496 self.find_selection_previous();
1497 }
1498 Action::AddCursorNextMatch => self.add_cursor_at_next_match(),
1499 Action::AddCursorAbove => self.add_cursor_above(),
1500 Action::AddCursorBelow => self.add_cursor_below(),
1501 Action::NextBuffer => self.next_buffer(),
1502 Action::PrevBuffer => self.prev_buffer(),
1503 Action::SwitchToPreviousTab => self.switch_to_previous_tab(),
1504 Action::SwitchToTabByName => self.start_switch_to_tab_prompt(),
1505
1506 Action::ScrollTabsLeft => {
1508 let active_split_id = self.split_manager.active_split();
1509 if let Some(view_state) = self.split_view_states.get_mut(&active_split_id) {
1510 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_sub(5);
1511 self.set_status_message(t!("status.scrolled_tabs_left").to_string());
1512 }
1513 }
1514 Action::ScrollTabsRight => {
1515 let active_split_id = self.split_manager.active_split();
1516 if let Some(view_state) = self.split_view_states.get_mut(&active_split_id) {
1517 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_add(5);
1518 self.set_status_message(t!("status.scrolled_tabs_right").to_string());
1519 }
1520 }
1521 Action::NavigateBack => self.navigate_back(),
1522 Action::NavigateForward => self.navigate_forward(),
1523 Action::SplitHorizontal => self.split_pane_horizontal(),
1524 Action::SplitVertical => self.split_pane_vertical(),
1525 Action::CloseSplit => self.close_active_split(),
1526 Action::NextSplit => self.next_split(),
1527 Action::PrevSplit => self.prev_split(),
1528 Action::IncreaseSplitSize => self.adjust_split_size(0.05),
1529 Action::DecreaseSplitSize => self.adjust_split_size(-0.05),
1530 Action::ToggleMaximizeSplit => self.toggle_maximize_split(),
1531 Action::ToggleFileExplorer => self.toggle_file_explorer(),
1532 Action::ToggleMenuBar => self.toggle_menu_bar(),
1533 Action::ToggleTabBar => self.toggle_tab_bar(),
1534 Action::ToggleStatusBar => self.toggle_status_bar(),
1535 Action::TogglePromptLine => self.toggle_prompt_line(),
1536 Action::ToggleVerticalScrollbar => self.toggle_vertical_scrollbar(),
1537 Action::ToggleHorizontalScrollbar => self.toggle_horizontal_scrollbar(),
1538 Action::ToggleLineNumbers => self.toggle_line_numbers(),
1539 Action::ToggleScrollSync => self.toggle_scroll_sync(),
1540 Action::ToggleMouseCapture => self.toggle_mouse_capture(),
1541 Action::ToggleMouseHover => self.toggle_mouse_hover(),
1542 Action::ToggleDebugHighlights => self.toggle_debug_highlights(),
1543 Action::AddRuler => {
1545 self.start_prompt(t!("rulers.add_prompt").to_string(), PromptType::AddRuler);
1546 }
1547 Action::RemoveRuler => {
1548 self.start_remove_ruler_prompt();
1549 }
1550 Action::SetTabSize => {
1552 let current = self
1553 .buffers
1554 .get(&self.active_buffer())
1555 .map(|s| s.buffer_settings.tab_size.to_string())
1556 .unwrap_or_else(|| "4".to_string());
1557 self.start_prompt_with_initial_text(
1558 "Tab size: ".to_string(),
1559 PromptType::SetTabSize,
1560 current,
1561 );
1562 }
1563 Action::SetLineEnding => {
1564 self.start_set_line_ending_prompt();
1565 }
1566 Action::SetEncoding => {
1567 self.start_set_encoding_prompt();
1568 }
1569 Action::ReloadWithEncoding => {
1570 self.start_reload_with_encoding_prompt();
1571 }
1572 Action::SetLanguage => {
1573 self.start_set_language_prompt();
1574 }
1575 Action::ToggleIndentationStyle => {
1576 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
1577 state.buffer_settings.use_tabs = !state.buffer_settings.use_tabs;
1578 let status = if state.buffer_settings.use_tabs {
1579 "Indentation: Tabs"
1580 } else {
1581 "Indentation: Spaces"
1582 };
1583 self.set_status_message(status.to_string());
1584 }
1585 }
1586 Action::ToggleTabIndicators | Action::ToggleWhitespaceIndicators => {
1587 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
1588 state.buffer_settings.whitespace.toggle_all();
1589 let status = if state.buffer_settings.whitespace.any_visible() {
1590 t!("toggle.whitespace_indicators_shown")
1591 } else {
1592 t!("toggle.whitespace_indicators_hidden")
1593 };
1594 self.set_status_message(status.to_string());
1595 }
1596 }
1597 Action::ResetBufferSettings => self.reset_buffer_settings(),
1598 Action::FocusFileExplorer => self.focus_file_explorer(),
1599 Action::FocusEditor => self.focus_editor(),
1600 Action::FileExplorerUp => self.file_explorer_navigate_up(),
1601 Action::FileExplorerDown => self.file_explorer_navigate_down(),
1602 Action::FileExplorerPageUp => self.file_explorer_page_up(),
1603 Action::FileExplorerPageDown => self.file_explorer_page_down(),
1604 Action::FileExplorerExpand => self.file_explorer_toggle_expand(),
1605 Action::FileExplorerCollapse => self.file_explorer_collapse(),
1606 Action::FileExplorerOpen => self.file_explorer_open_file()?,
1607 Action::FileExplorerRefresh => self.file_explorer_refresh(),
1608 Action::FileExplorerNewFile => self.file_explorer_new_file(),
1609 Action::FileExplorerNewDirectory => self.file_explorer_new_directory(),
1610 Action::FileExplorerDelete => self.file_explorer_delete(),
1611 Action::FileExplorerRename => self.file_explorer_rename(),
1612 Action::FileExplorerToggleHidden => self.file_explorer_toggle_hidden(),
1613 Action::FileExplorerToggleGitignored => self.file_explorer_toggle_gitignored(),
1614 Action::FileExplorerSearchClear => self.file_explorer_search_clear(),
1615 Action::FileExplorerSearchBackspace => self.file_explorer_search_pop_char(),
1616 Action::FileExplorerCopy => self.file_explorer_copy(),
1617 Action::FileExplorerCut => self.file_explorer_cut(),
1618 Action::FileExplorerPaste => self.file_explorer_paste(),
1619 Action::FileExplorerDuplicate => self.file_explorer_duplicate(),
1620 Action::FileExplorerCopyFullPath => self.file_explorer_copy_path(false),
1621 Action::FileExplorerCopyRelativePath => self.file_explorer_copy_path(true),
1622 Action::FileExplorerExtendSelectionUp => self.file_explorer_extend_selection_up(),
1623 Action::FileExplorerExtendSelectionDown => self.file_explorer_extend_selection_down(),
1624 Action::FileExplorerToggleSelect => self.file_explorer_toggle_select(),
1625 Action::FileExplorerSelectAll => self.file_explorer_select_all(),
1626 Action::RemoveSecondaryCursors => {
1627 if let Some(events) = self.action_to_events(Action::RemoveSecondaryCursors) {
1629 let batch = Event::Batch {
1631 events: events.clone(),
1632 description: "Remove secondary cursors".to_string(),
1633 };
1634 self.active_event_log_mut().append(batch.clone());
1635 self.apply_event_to_active_buffer(&batch);
1636
1637 let active_split = self.split_manager.active_split();
1639 let active_buffer = self.active_buffer();
1640 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
1641 let state = self.buffers.get_mut(&active_buffer).unwrap();
1642 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
1643 }
1644 }
1645 }
1646
1647 Action::MenuActivate => {
1649 self.handle_menu_activate();
1650 }
1651 Action::MenuClose => {
1652 self.handle_menu_close();
1653 }
1654 Action::MenuLeft => {
1655 self.handle_menu_left();
1656 }
1657 Action::MenuRight => {
1658 self.handle_menu_right();
1659 }
1660 Action::MenuUp => {
1661 self.handle_menu_up();
1662 }
1663 Action::MenuDown => {
1664 self.handle_menu_down();
1665 }
1666 Action::MenuExecute => {
1667 if let Some(action) = self.handle_menu_execute() {
1668 return self.handle_action(action);
1669 }
1670 }
1671 Action::MenuOpen(menu_name) => {
1672 if self.config.editor.menu_bar_mnemonics {
1673 self.handle_menu_open(&menu_name);
1674 }
1675 }
1676
1677 Action::SwitchKeybindingMap(map_name) => {
1678 let is_builtin =
1680 matches!(map_name.as_str(), "default" | "emacs" | "vscode" | "macos");
1681 let is_user_defined = self.config.keybinding_maps.contains_key(&map_name);
1682
1683 if is_builtin || is_user_defined {
1684 self.config_mut().active_keybinding_map = map_name.clone().into();
1686
1687 *self.keybindings.write().unwrap() =
1689 crate::input::keybindings::KeybindingResolver::new(&self.config);
1690
1691 self.set_status_message(
1692 t!("view.keybindings_switched", map = map_name).to_string(),
1693 );
1694 } else {
1695 self.set_status_message(
1696 t!("view.keybindings_unknown", map = map_name).to_string(),
1697 );
1698 }
1699 }
1700
1701 Action::SmartHome => {
1702 let buffer_id = self.active_buffer();
1704 if self.is_composite_buffer(buffer_id) {
1705 if let Some(_handled) =
1706 self.handle_composite_action(buffer_id, &Action::SmartHome)
1707 {
1708 return Ok(());
1709 }
1710 }
1711 self.smart_home();
1712 }
1713 Action::ToggleComment => {
1714 self.toggle_comment();
1715 }
1716 Action::ToggleFold => {
1717 self.toggle_fold_at_cursor();
1718 }
1719 Action::GoToMatchingBracket => {
1720 self.goto_matching_bracket();
1721 }
1722 Action::JumpToNextError => {
1723 self.jump_to_next_error();
1724 }
1725 Action::JumpToPreviousError => {
1726 self.jump_to_previous_error();
1727 }
1728 Action::SetBookmark(key) => {
1729 self.set_bookmark(key);
1730 }
1731 Action::JumpToBookmark(key) => {
1732 self.jump_to_bookmark(key);
1733 }
1734 Action::ClearBookmark(key) => {
1735 self.clear_bookmark(key);
1736 }
1737 Action::ListBookmarks => {
1738 self.list_bookmarks();
1739 }
1740 Action::ToggleSearchCaseSensitive => {
1741 self.search_case_sensitive = !self.search_case_sensitive;
1742 let state = if self.search_case_sensitive {
1743 "enabled"
1744 } else {
1745 "disabled"
1746 };
1747 self.set_status_message(
1748 t!("search.case_sensitive_state", state = state).to_string(),
1749 );
1750 if let Some(prompt) = &self.prompt {
1753 if matches!(
1754 prompt.prompt_type,
1755 PromptType::Search
1756 | PromptType::ReplaceSearch
1757 | PromptType::QueryReplaceSearch
1758 ) {
1759 let query = prompt.input.clone();
1760 self.update_search_highlights(&query);
1761 }
1762 } else if let Some(search_state) = &self.search_state {
1763 let query = search_state.query.clone();
1764 self.perform_search(&query);
1765 }
1766 }
1767 Action::ToggleSearchWholeWord => {
1768 self.search_whole_word = !self.search_whole_word;
1769 let state = if self.search_whole_word {
1770 "enabled"
1771 } else {
1772 "disabled"
1773 };
1774 self.set_status_message(t!("search.whole_word_state", state = state).to_string());
1775 if let Some(prompt) = &self.prompt {
1778 if matches!(
1779 prompt.prompt_type,
1780 PromptType::Search
1781 | PromptType::ReplaceSearch
1782 | PromptType::QueryReplaceSearch
1783 ) {
1784 let query = prompt.input.clone();
1785 self.update_search_highlights(&query);
1786 }
1787 } else if let Some(search_state) = &self.search_state {
1788 let query = search_state.query.clone();
1789 self.perform_search(&query);
1790 }
1791 }
1792 Action::ToggleSearchRegex => {
1793 self.search_use_regex = !self.search_use_regex;
1794 let state = if self.search_use_regex {
1795 "enabled"
1796 } else {
1797 "disabled"
1798 };
1799 self.set_status_message(t!("search.regex_state", state = state).to_string());
1800 if let Some(prompt) = &self.prompt {
1803 if matches!(
1804 prompt.prompt_type,
1805 PromptType::Search
1806 | PromptType::ReplaceSearch
1807 | PromptType::QueryReplaceSearch
1808 ) {
1809 let query = prompt.input.clone();
1810 self.update_search_highlights(&query);
1811 }
1812 } else if let Some(search_state) = &self.search_state {
1813 let query = search_state.query.clone();
1814 self.perform_search(&query);
1815 }
1816 }
1817 Action::ToggleSearchConfirmEach => {
1818 self.search_confirm_each = !self.search_confirm_each;
1819 let state = if self.search_confirm_each {
1820 "enabled"
1821 } else {
1822 "disabled"
1823 };
1824 self.set_status_message(t!("search.confirm_each_state", state = state).to_string());
1825 }
1826 Action::FileBrowserToggleHidden => {
1827 self.file_open_toggle_hidden();
1829 }
1830 Action::StartMacroRecording => {
1831 self.set_status_message(
1833 "Use Ctrl+Shift+R to start recording (will prompt for register)".to_string(),
1834 );
1835 }
1836 Action::StopMacroRecording => {
1837 self.stop_macro_recording();
1838 }
1839 Action::PlayMacro(key) => {
1840 self.play_macro(key);
1841 }
1842 Action::ToggleMacroRecording(key) => {
1843 self.toggle_macro_recording(key);
1844 }
1845 Action::ShowMacro(key) => {
1846 self.show_macro_in_buffer(key);
1847 }
1848 Action::ListMacros => {
1849 self.list_macros_in_buffer();
1850 }
1851 Action::PromptRecordMacro => {
1852 self.start_prompt("Record macro (0-9): ".to_string(), PromptType::RecordMacro);
1853 }
1854 Action::PromptPlayMacro => {
1855 self.start_prompt("Play macro (0-9): ".to_string(), PromptType::PlayMacro);
1856 }
1857 Action::PlayLastMacro => {
1858 if let Some(key) = self.macros.last_register() {
1859 self.play_macro(key);
1860 } else {
1861 self.set_status_message(t!("status.no_macro_recorded").to_string());
1862 }
1863 }
1864 Action::PromptSetBookmark => {
1865 self.start_prompt("Set bookmark (0-9): ".to_string(), PromptType::SetBookmark);
1866 }
1867 Action::PromptJumpToBookmark => {
1868 self.start_prompt(
1869 "Jump to bookmark (0-9): ".to_string(),
1870 PromptType::JumpToBookmark,
1871 );
1872 }
1873 Action::CompositeNextHunk => {
1874 let buf = self.active_buffer();
1875 self.composite_next_hunk_active(buf);
1876 }
1877 Action::CompositePrevHunk => {
1878 let buf = self.active_buffer();
1879 self.composite_prev_hunk_active(buf);
1880 }
1881 Action::None => {}
1882 Action::DeleteBackward => {
1883 if self.is_editing_disabled() {
1884 self.set_status_message(t!("buffer.editing_disabled").to_string());
1885 return Ok(());
1886 }
1887 if let Some(events) = self.action_to_events(Action::DeleteBackward) {
1889 if events.len() > 1 {
1890 let description = "Delete backward".to_string();
1892 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description)
1893 {
1894 self.active_event_log_mut().append(bulk_edit);
1895 }
1896 } else {
1897 for event in events {
1898 self.active_event_log_mut().append(event.clone());
1899 self.apply_event_to_active_buffer(&event);
1900 }
1901 }
1902 }
1903 }
1904 Action::PluginAction(action_name) => {
1905 tracing::debug!("handle_action: PluginAction('{}')", action_name);
1906 #[cfg(feature = "plugins")]
1909 if let Some(result) = self.plugin_manager.execute_action_async(&action_name) {
1910 match result {
1911 Ok(receiver) => {
1912 self.pending_plugin_actions
1914 .push((action_name.clone(), receiver));
1915 }
1916 Err(e) => {
1917 self.set_status_message(
1918 t!("view.plugin_error", error = e.to_string()).to_string(),
1919 );
1920 tracing::error!("Plugin action error: {}", e);
1921 }
1922 }
1923 } else {
1924 self.set_status_message(t!("status.plugin_manager_unavailable").to_string());
1925 }
1926 #[cfg(not(feature = "plugins"))]
1927 {
1928 let _ = action_name;
1929 self.set_status_message(
1930 "Plugins not available (compiled without plugin support)".to_string(),
1931 );
1932 }
1933 }
1934 Action::LoadPluginFromBuffer => {
1935 #[cfg(feature = "plugins")]
1936 {
1937 let buffer_id = self.active_buffer();
1938 let state = self.active_state();
1939 let buffer = &state.buffer;
1940 let total = buffer.total_bytes();
1941 let content =
1942 String::from_utf8_lossy(&buffer.slice_bytes(0..total)).to_string();
1943
1944 let is_ts = buffer
1946 .file_path()
1947 .and_then(|p| p.extension())
1948 .and_then(|e| e.to_str())
1949 .map(|e| e == "ts" || e == "tsx")
1950 .unwrap_or(true);
1951
1952 let name = buffer
1954 .file_path()
1955 .and_then(|p| p.file_name())
1956 .and_then(|s| s.to_str())
1957 .map(|s| s.to_string())
1958 .unwrap_or_else(|| "buffer-plugin".to_string());
1959
1960 match self
1961 .plugin_manager
1962 .load_plugin_from_source(&content, &name, is_ts)
1963 {
1964 Ok(()) => {
1965 self.set_status_message(format!(
1966 "Plugin '{}' loaded from buffer",
1967 name
1968 ));
1969 }
1970 Err(e) => {
1971 self.set_status_message(format!("Failed to load plugin: {}", e));
1972 tracing::error!("LoadPluginFromBuffer error: {}", e);
1973 }
1974 }
1975
1976 self.setup_plugin_dev_lsp(buffer_id, &content);
1978 }
1979 #[cfg(not(feature = "plugins"))]
1980 {
1981 self.set_status_message(
1982 "Plugins not available (compiled without plugin support)".to_string(),
1983 );
1984 }
1985 }
1986 Action::InitReload => {
1987 self.load_init_script(true);
1992 self.fire_plugins_loaded_hook();
1995 }
1996 Action::InitEdit => {
1997 let config_dir = self.dir_context.config_dir.clone();
2000 match crate::init_script::ensure_starter(&config_dir) {
2001 Ok(path) => {
2002 let declarations = self.plugin_manager.plugin_declarations();
2012 crate::init_script::write_plugin_declarations(&config_dir, &declarations);
2013 match self.open_file(&path) {
2014 Ok(_) => {
2015 self.set_status_message(format!("init.ts: {}", path.display()));
2016 }
2017 Err(e) => {
2018 self.set_status_message(format!("init.ts: open failed: {e}"));
2019 }
2020 }
2021 }
2022 Err(e) => {
2023 self.set_status_message(format!("init.ts: create failed: {e}"));
2024 }
2025 }
2026 }
2027 Action::InitCheck => {
2028 let report = crate::init_script::check(&self.dir_context.config_dir);
2031 if report.ok && report.diagnostics.is_empty() {
2032 self.set_status_message("init.ts: ok".into());
2033 } else if !report.ok {
2034 let first = report
2035 .diagnostics
2036 .first()
2037 .map(|d| format!("{}:{}: {}", d.line, d.column, d.message))
2038 .unwrap_or_else(|| "unknown error".into());
2039 self.set_status_message(format!(
2040 "init.ts: {} error(s) — first: {first}",
2041 report.diagnostics.len()
2042 ));
2043 } else {
2044 self.set_status_message(format!(
2045 "init.ts: {} warning(s)",
2046 report.diagnostics.len()
2047 ));
2048 }
2049 }
2050 Action::OpenTerminal => {
2051 self.open_terminal();
2052 }
2053 Action::CloseTerminal => {
2054 self.close_terminal();
2055 }
2056 Action::FocusTerminal => {
2057 if self.is_terminal_buffer(self.active_buffer()) {
2059 self.terminal_mode = true;
2060 self.key_context = KeyContext::Terminal;
2061 self.set_status_message(t!("status.terminal_mode_enabled").to_string());
2062 }
2063 }
2064 Action::TerminalEscape => {
2065 if self.terminal_mode {
2067 self.terminal_mode = false;
2068 self.key_context = KeyContext::Normal;
2069 self.set_status_message(t!("status.terminal_mode_disabled").to_string());
2070 }
2071 }
2072 Action::ToggleKeyboardCapture => {
2073 if self.terminal_mode {
2075 self.keyboard_capture = !self.keyboard_capture;
2076 if self.keyboard_capture {
2077 self.set_status_message(
2078 "Keyboard capture ON - all keys go to terminal (F9 to toggle)"
2079 .to_string(),
2080 );
2081 } else {
2082 self.set_status_message(
2083 "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
2084 );
2085 }
2086 }
2087 }
2088 Action::TerminalPaste => {
2089 if self.terminal_mode {
2091 if let Some(text) = self.clipboard.paste() {
2092 self.send_terminal_input(text.as_bytes());
2093 }
2094 }
2095 }
2096 Action::ShellCommand => {
2097 self.start_shell_command_prompt(false);
2099 }
2100 Action::ShellCommandReplace => {
2101 self.start_shell_command_prompt(true);
2103 }
2104 Action::OpenSettings => {
2105 self.open_settings();
2106 }
2107 Action::CloseSettings => {
2108 let has_changes = self
2110 .settings_state
2111 .as_ref()
2112 .is_some_and(|s| s.has_changes());
2113 if has_changes {
2114 if let Some(ref mut state) = self.settings_state {
2116 state.show_confirm_dialog();
2117 }
2118 } else {
2119 self.close_settings(false);
2120 }
2121 }
2122 Action::SettingsSave => {
2123 self.save_settings();
2124 }
2125 Action::SettingsReset => {
2126 if let Some(ref mut state) = self.settings_state {
2127 state.reset_current_to_default();
2128 }
2129 }
2130 Action::SettingsInherit => {
2131 if let Some(ref mut state) = self.settings_state {
2132 state.set_current_to_null();
2133 }
2134 }
2135 Action::SettingsToggleFocus => {
2136 if let Some(ref mut state) = self.settings_state {
2137 state.toggle_focus();
2138 }
2139 }
2140 Action::SettingsActivate => {
2141 self.settings_activate_current();
2142 }
2143 Action::SettingsSearch => {
2144 if let Some(ref mut state) = self.settings_state {
2145 state.start_search();
2146 }
2147 }
2148 Action::SettingsHelp => {
2149 if let Some(ref mut state) = self.settings_state {
2150 state.toggle_help();
2151 }
2152 }
2153 Action::SettingsIncrement => {
2154 self.settings_increment_current();
2155 }
2156 Action::SettingsDecrement => {
2157 self.settings_decrement_current();
2158 }
2159 Action::CalibrateInput => {
2160 self.open_calibration_wizard();
2161 }
2162 Action::EventDebug => {
2163 self.open_event_debug();
2164 }
2165 Action::SuspendProcess => {
2166 self.request_suspend();
2167 }
2168 Action::OpenKeybindingEditor => {
2169 self.open_keybinding_editor();
2170 }
2171 Action::PromptConfirm => {
2172 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
2173 use super::prompt_actions::PromptResult;
2174 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
2175 PromptResult::ExecuteAction(action) => {
2176 return self.handle_action(action);
2177 }
2178 PromptResult::EarlyReturn => {
2179 return Ok(());
2180 }
2181 PromptResult::Done => {}
2182 }
2183 }
2184 }
2185 Action::PromptConfirmWithText(ref text) => {
2186 if let Some(ref mut prompt) = self.prompt {
2188 prompt.set_input(text.clone());
2189 self.update_prompt_suggestions();
2190 }
2191 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
2192 use super::prompt_actions::PromptResult;
2193 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
2194 PromptResult::ExecuteAction(action) => {
2195 return self.handle_action(action);
2196 }
2197 PromptResult::EarlyReturn => {
2198 return Ok(());
2199 }
2200 PromptResult::Done => {}
2201 }
2202 }
2203 }
2204 Action::PopupConfirm => {
2205 use super::popup_actions::PopupConfirmResult;
2206 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
2207 return Ok(());
2208 }
2209 }
2210 Action::PopupCancel => {
2211 self.handle_popup_cancel();
2212 }
2213 Action::PopupFocus => {
2214 self.handle_popup_focus();
2215 }
2216 Action::CompletionAccept => {
2217 use super::popup_actions::PopupConfirmResult;
2218 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
2219 return Ok(());
2220 }
2221 }
2222 Action::CompletionDismiss => {
2223 self.handle_popup_cancel();
2224 }
2225 Action::InsertChar(c) => {
2226 if self.is_prompting() {
2227 return self.handle_insert_char_prompt(c);
2228 } else if self.key_context == KeyContext::FileExplorer {
2229 self.file_explorer_search_push_char(c);
2230 } else {
2231 self.handle_insert_char_editor(c)?;
2232 }
2233 }
2234 Action::PromptCopy => {
2236 if let Some(prompt) = &self.prompt {
2237 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
2238 if !text.is_empty() {
2239 self.clipboard.copy(text);
2240 self.set_status_message(t!("clipboard.copied").to_string());
2241 }
2242 }
2243 }
2244 Action::PromptCut => {
2245 if let Some(prompt) = &self.prompt {
2246 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
2247 if !text.is_empty() {
2248 self.clipboard.copy(text);
2249 }
2250 }
2251 if let Some(prompt) = self.prompt.as_mut() {
2252 if prompt.has_selection() {
2253 prompt.delete_selection();
2254 } else {
2255 prompt.clear();
2256 }
2257 }
2258 self.set_status_message(t!("clipboard.cut").to_string());
2259 self.update_prompt_suggestions();
2260 }
2261 Action::PromptPaste => {
2262 if let Some(text) = self.clipboard.paste() {
2263 if let Some(prompt) = self.prompt.as_mut() {
2264 prompt.insert_str(&text);
2265 }
2266 self.update_prompt_suggestions();
2267 }
2268 }
2269 _ => {
2270 self.apply_action_as_events(action)?;
2276 }
2277 }
2278
2279 Ok(())
2280 }
2281}