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
60 .active_window_mut()
61 .pending_next_key_callbacks
62 .pop_front()
63 {
64 let json = serde_json::to_string(&payload).unwrap_or_else(|_| "null".to_string());
65 self.plugin_manager
66 .read()
67 .unwrap()
68 .resolve_callback(callback_id, json);
69 return true;
70 }
71 if self.active_window_mut().key_capture_active {
72 self.active_window_mut()
73 .pending_key_capture_buffer
74 .push_back(payload);
75 return true;
76 }
77 false
78 }
79}
80
81impl Editor {
82 pub(crate) fn popups_capture_keys(&self) -> bool {
101 use crate::input::keybindings::KeyContext;
102 use crate::view::popup::PopupResolver;
103 let trust_prompt_up = self
109 .global_popups
110 .top()
111 .is_some_and(|p| p.focused && matches!(p.resolver, PopupResolver::WorkspaceTrust));
112 if trust_prompt_up {
113 return true;
114 }
115 if matches!(self.active_window().key_context, KeyContext::FileExplorer) {
116 return false;
117 }
118 self.topmost_popup_focused()
119 }
120
121 pub(crate) fn topmost_popup_focused(&self) -> bool {
126 if let Some(popup) = self.global_popups.top() {
127 return popup.focused;
128 }
129 if let Some(popup) = self.active_state().popups.top() {
130 return popup.focused;
131 }
132 false
135 }
136
137 pub(crate) fn resolve_unfocused_popup_action(
147 &self,
148 event: &crossterm::event::KeyEvent,
149 ) -> Option<crate::input::keybindings::Action> {
150 use crate::input::keybindings::{Action, KeyContext};
151
152 let popup_visible =
153 self.global_popups.is_visible() || self.active_state().popups.is_visible();
154 if !popup_visible || self.topmost_popup_focused() {
155 return None;
156 }
157
158 if self.settings_state.as_ref().is_some_and(|s| s.visible)
164 || self.menu_state.active_menu.is_some()
165 || self.is_prompting()
166 {
167 return None;
168 }
169
170 let kb = self.keybindings.read().ok()?;
171
172 let popup_focus_match = matches!(
181 kb.resolve_in_context_only(event, self.active_window().key_context.clone()),
182 Some(Action::PopupFocus),
183 );
184 if popup_focus_match {
185 return Some(Action::PopupFocus);
186 }
187
188 let resolved_popup = kb.resolve_in_context_only(event, KeyContext::Popup);
194 match resolved_popup {
195 Some(action @ (Action::PopupCancel | Action::PopupFocus)) => Some(action),
196 _ => None,
197 }
198 }
199
200 pub(crate) fn resolve_completion_popup_action(
206 &self,
207 event: &crossterm::event::KeyEvent,
208 ) -> Option<crate::input::keybindings::Action> {
209 use crate::input::keybindings::{Action, KeyContext};
210 use crate::view::popup::PopupKind;
211
212 let topmost_kind = if self.global_popups.is_visible() {
213 self.global_popups.top().map(|p| p.kind)
214 } else if self.active_state().popups.is_visible() {
215 self.active_state().popups.top().map(|p| p.kind)
216 } else {
217 None
218 };
219
220 if topmost_kind != Some(PopupKind::Completion) {
221 return None;
222 }
223
224 match self
225 .keybindings
226 .read()
227 .unwrap()
228 .resolve_in_context_only(event, KeyContext::Completion)
229 {
230 Some(action @ (Action::CompletionAccept | Action::CompletionDismiss)) => Some(action),
231 _ => None,
232 }
233 }
234
235 pub fn get_key_context(&self) -> crate::input::keybindings::KeyContext {
237 use crate::input::keybindings::KeyContext;
238
239 if self.settings_state.as_ref().is_some_and(|s| s.visible) {
243 KeyContext::Settings
244 } else if self.menu_state.active_menu.is_some() {
245 KeyContext::Menu
246 } else if self.is_prompting() {
247 KeyContext::Prompt
248 } else if self.popups_capture_keys()
249 && (self.global_popups.is_visible() || self.active_state().popups.is_visible())
250 {
251 KeyContext::Popup
252 } else if self.floating_widget_panel.is_some() {
253 KeyContext::Normal
263 } else if self
264 .active_window()
265 .is_composite_buffer(self.active_buffer())
266 {
267 KeyContext::CompositeBuffer
268 } else {
269 self.active_window().key_context.clone()
271 }
272 }
273
274 pub fn handle_key(
277 &mut self,
278 code: crossterm::event::KeyCode,
279 modifiers: crossterm::event::KeyModifiers,
280 ) -> AnyhowResult<()> {
281 use crate::input::keybindings::Action;
282
283 let _t_total = std::time::Instant::now();
284
285 tracing::trace!(
286 "Editor.handle_key: code={:?}, modifiers={:?}",
287 code,
288 modifiers
289 );
290
291 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
293
294 if self.active_window().is_event_debug_active() {
298 self.active_window_mut()
299 .handle_event_debug_input(&key_event);
300 return Ok(());
301 }
302
303 if self.dispatch_terminal_input(&key_event).is_some() {
308 return Ok(());
309 }
310
311 if self.try_resolve_next_key_callback(&key_event) {
318 return Ok(());
319 }
320
321 if self.floating_widget_panel.is_some()
328 && self.dispatch_floating_widget_key(code, modifiers)
329 {
330 return Ok(());
331 }
332
333 let active_split = self.effective_active_split();
342 if let Some(view_state) = self
343 .windows
344 .get_mut(&self.active_window)
345 .and_then(|w| w.split_view_states_mut())
346 .expect("active window must have a populated split layout")
347 .get_mut(&active_split)
348 {
349 view_state.viewport.clear_skip_ensure_visible();
350 }
351
352 if self.active_window_mut().theme_info_popup.is_some() {
354 self.active_window_mut().theme_info_popup = None;
355 }
356
357 if self
358 .active_window_mut()
359 .file_explorer_context_menu
360 .is_some()
361 {
362 if let Some(result) = self.handle_file_explorer_context_menu_key(code, modifiers) {
363 return result;
364 }
365 }
366
367 let mut context = self.get_key_context();
369
370 let popup_visible_on_screen =
380 self.global_popups.is_visible() || self.active_state().popups.is_visible();
381 if popup_visible_on_screen {
382 let (is_transient_popup, has_selection) = {
386 let popup = self
387 .global_popups
388 .top()
389 .or_else(|| self.active_state().popups.top());
390 (
391 popup.is_some_and(|p| p.transient),
392 popup.is_some_and(|p| p.has_selection()),
393 )
394 };
395
396 let is_copy_key = key_event.code == crossterm::event::KeyCode::Char('c')
398 && key_event
399 .modifiers
400 .contains(crossterm::event::KeyModifiers::CONTROL);
401
402 let resolved_action = self
407 .keybindings
408 .read()
409 .ok()
410 .map(|kb| kb.resolve(&key_event, context.clone()));
411 let is_focus_popup_key = matches!(
412 resolved_action,
413 Some(crate::input::keybindings::Action::PopupFocus)
414 );
415
416 if is_transient_popup && !(has_selection && is_copy_key) && !is_focus_popup_key {
417 self.hide_popup();
419 tracing::debug!("Dismissed transient popup on key press");
420 context = self.get_key_context();
422 }
423 }
424
425 if let Some(action) = self.resolve_unfocused_popup_action(&key_event) {
431 self.handle_action(action)?;
432 return Ok(());
433 }
434
435 if self.dispatch_modal_input(&key_event).is_some() {
437 return Ok(());
438 }
439
440 if context != self.get_key_context() {
443 context = self.get_key_context();
444 }
445
446 let should_check_mode_bindings =
450 matches!(context, crate::input::keybindings::KeyContext::Normal);
451
452 if should_check_mode_bindings {
453 let effective_mode = self.effective_mode().map(|s| s.to_owned());
456
457 if let Some(ref mode_name) = effective_mode {
458 let mode_ctx = crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
459 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
460
461 let (chord_result, resolved_action) = {
463 let keybindings = self.keybindings.read().unwrap();
464 let chord_result = keybindings.resolve_chord(
465 &self.active_window().chord_state,
466 &key_event,
467 mode_ctx.clone(),
468 );
469 let resolved = keybindings.resolve(&key_event, mode_ctx);
470 (chord_result, resolved)
471 };
472 match chord_result {
473 crate::input::keybindings::ChordResolution::Complete(action) => {
474 tracing::debug!("Mode chord resolved to action: {:?}", action);
475 self.active_window_mut().chord_state.clear();
476 return self.handle_action(action);
477 }
478 crate::input::keybindings::ChordResolution::Partial => {
479 tracing::debug!("Potential chord prefix in mode '{}'", mode_name);
480 self.active_window_mut().chord_state.push((code, modifiers));
481 return Ok(());
482 }
483 crate::input::keybindings::ChordResolution::NoMatch => {
484 if !self.active_window_mut().chord_state.is_empty() {
485 tracing::debug!("Chord sequence abandoned in mode, clearing state");
486 self.active_window_mut().chord_state.clear();
487 }
488 }
489 }
490
491 if resolved_action != Action::None {
493 return self.handle_action(resolved_action);
494 }
495 }
496
497 if let Some(ref mode_name) = effective_mode {
509 if self.mode_registry.allows_text_input(mode_name) {
510 if let KeyCode::Char(c) = code {
511 let ch = if modifiers.contains(KeyModifiers::SHIFT) {
512 c.to_uppercase().next().unwrap_or(c)
513 } else {
514 c
515 };
516 if !modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) {
517 let action_name = format!("mode_text_input:{}", ch);
518 return self.handle_action(Action::PluginAction(action_name));
519 }
520 }
521 let normal_ctx = crate::input::keybindings::KeyContext::Normal;
530 let resolved = {
531 let keybindings = self.keybindings.read().unwrap();
532 keybindings.resolve(&key_event, normal_ctx)
533 };
534 match resolved {
535 Action::Paste | Action::Copy | Action::Cut | Action::SelectAll => {
536 return self.handle_action(resolved);
537 }
538 _ => {}
539 }
540 if modifiers.contains(KeyModifiers::SHIFT) {
549 let buffer_id = self.active_buffer();
550 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id)
551 {
552 let ctrl = modifiers.contains(KeyModifiers::CONTROL);
553 let handled = match code {
554 KeyCode::Left if ctrl => self
555 .with_focused_text_editor(panel_id, |e| {
556 e.move_word_left_selecting()
557 }),
558 KeyCode::Right if ctrl => self
559 .with_focused_text_editor(panel_id, |e| {
560 e.move_word_right_selecting()
561 }),
562 KeyCode::Left => self.with_focused_text_editor(panel_id, |e| {
563 e.move_left_selecting()
564 }),
565 KeyCode::Right => self.with_focused_text_editor(panel_id, |e| {
566 e.move_right_selecting()
567 }),
568 KeyCode::Up => self
569 .with_focused_text_editor(panel_id, |e| e.move_up_selecting()),
570 KeyCode::Down => self.with_focused_text_editor(panel_id, |e| {
571 e.move_down_selecting()
572 }),
573 KeyCode::Home => self.with_focused_text_editor(panel_id, |e| {
574 e.move_home_selecting()
575 }),
576 KeyCode::End => self
577 .with_focused_text_editor(panel_id, |e| e.move_end_selecting()),
578 _ => false,
579 };
580 if matches!(
586 code,
587 KeyCode::Left
588 | KeyCode::Right
589 | KeyCode::Up
590 | KeyCode::Down
591 | KeyCode::Home
592 | KeyCode::End
593 ) {
594 let _ = handled;
595 return Ok(());
596 }
597 }
598 }
599 tracing::debug!("Blocking unbound key in text-input mode '{}'", mode_name);
600 return Ok(());
601 }
602 }
603 if let Some(ref mode_name) = self.active_window().editor_mode {
604 if self.mode_registry.is_read_only(mode_name) {
605 tracing::debug!("Ignoring unbound key in read-only mode '{}'", mode_name);
606 return Ok(());
607 }
608 tracing::debug!(
609 "Mode '{}' is not read-only, allowing key through",
610 mode_name
611 );
612 }
613 }
614
615 {
622 let active_buf = self.active_buffer();
623 let active_split = self.effective_active_split();
624 if self.active_window().is_composite_buffer(active_buf) {
625 if let Some(handled) =
626 self.try_route_composite_key(active_split, active_buf, &key_event)
627 {
628 return handled;
629 }
630 }
631 }
632
633 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
635 let (chord_result, action) = {
636 let keybindings = self.keybindings.read().unwrap();
637 let chord_result = keybindings.resolve_chord(
638 &self.active_window().chord_state,
639 &key_event,
640 context.clone(),
641 );
642 let action = keybindings.resolve(&key_event, context.clone());
643 (chord_result, action)
644 };
645
646 match chord_result {
647 crate::input::keybindings::ChordResolution::Complete(action) => {
648 tracing::debug!("Complete chord match -> Action: {:?}", action);
650 self.active_window_mut().chord_state.clear();
651 return self.handle_action(action);
652 }
653 crate::input::keybindings::ChordResolution::Partial => {
654 tracing::debug!("Partial chord match - waiting for next key");
656 self.active_window_mut().chord_state.push((code, modifiers));
657 return Ok(());
658 }
659 crate::input::keybindings::ChordResolution::NoMatch => {
660 if !self.active_window_mut().chord_state.is_empty() {
662 tracing::debug!("Chord sequence abandoned, clearing state");
663 self.active_window_mut().chord_state.clear();
664 }
665 }
666 }
667
668 tracing::trace!("Context: {:?} -> Action: {:?}", context, action);
670
671 match action {
674 Action::LspCompletion
675 | Action::LspGotoDefinition
676 | Action::LspReferences
677 | Action::LspHover
678 | Action::None => {
679 }
681 _ => {
682 self.active_window_mut().cancel_pending_lsp_requests();
684 }
685 }
686
687 self.handle_action(action)
691 }
692
693 pub(crate) fn set_workspace_trust_level(
701 &mut self,
702 level: crate::services::workspace_trust::TrustLevel,
703 ) {
704 use crate::services::workspace_trust::TrustLevel;
705 let trust = &self.authority.workspace_trust;
706 let changed = trust.level() != level;
707 trust.set_level(level);
708 let msg = match level {
709 TrustLevel::Trusted => t!("trust.now_trusted"),
710 TrustLevel::Restricted => t!("trust.now_restricted"),
711 TrustLevel::Blocked => t!("trust.now_blocked"),
712 }
713 .to_string();
714 self.active_window_mut().status_message = Some(msg);
715 if changed {
718 self.request_restart(self.working_dir().to_path_buf());
719 }
720 }
721
722 pub(crate) fn handle_action(&mut self, action: Action) -> AnyhowResult<()> {
723 use crate::input::keybindings::Action;
724
725 self.record_macro_action(&action);
727
728 if !matches!(action, Action::DabbrevExpand) {
730 self.reset_dabbrev_state();
731 }
732
733 match action {
734 Action::Quit => self.quit(),
735 Action::ForceQuit => {
736 self.should_quit = true;
737 }
738 Action::Detach => {
739 self.should_detach = true;
740 }
741 Action::WorkspaceTrustTrust => {
742 self.set_workspace_trust_level(
743 crate::services::workspace_trust::TrustLevel::Trusted,
744 );
745 }
746 Action::WorkspaceTrustRestrict => {
747 self.set_workspace_trust_level(
748 crate::services::workspace_trust::TrustLevel::Restricted,
749 );
750 }
751 Action::WorkspaceTrustBlock => {
752 self.set_workspace_trust_level(
753 crate::services::workspace_trust::TrustLevel::Blocked,
754 );
755 }
756 Action::WorkspaceTrustPrompt => {
757 self.show_workspace_trust_popup(true);
759 }
760 Action::Save => {
761 if self.active_state().buffer.file_path().is_none() {
763 self.start_prompt_with_initial_text(
764 t!("file.save_as_prompt").to_string(),
765 PromptType::SaveFileAs,
766 String::new(),
767 );
768 self.init_file_open_state();
769 } else if self.check_save_conflict().is_some() {
770 self.start_prompt(
772 t!("file.file_changed_prompt").to_string(),
773 PromptType::ConfirmSaveConflict,
774 );
775 } else if let Err(e) = self.save() {
776 let msg = format!("{}", e);
777 self.active_window_mut().status_message =
778 Some(t!("file.save_failed", error = &msg).to_string());
779 }
780 }
781 Action::SaveAs => {
782 let current_path = self
784 .active_state()
785 .buffer
786 .file_path()
787 .map(|p| {
788 p.strip_prefix(self.working_dir())
790 .unwrap_or(p)
791 .to_string_lossy()
792 .to_string()
793 })
794 .unwrap_or_default();
795 self.start_prompt_with_initial_text(
796 t!("file.save_as_prompt").to_string(),
797 PromptType::SaveFileAs,
798 current_path,
799 );
800 self.init_file_open_state();
801 }
802 Action::Open => {
803 self.start_prompt(t!("file.open_prompt").to_string(), PromptType::OpenFile);
804 self.prefill_open_file_prompt();
805 self.init_file_open_state();
806 }
807 Action::SwitchProject => {
808 self.start_prompt(
809 t!("file.switch_project_prompt").to_string(),
810 PromptType::SwitchProject,
811 );
812 self.init_folder_open_state();
813 }
814 Action::GotoLine => {
815 let has_line_index = self
816 .buffers()
817 .get(&self.active_buffer())
818 .is_none_or(|s| s.buffer.line_count().is_some());
819 if has_line_index {
820 self.start_prompt(
821 t!("file.goto_line_prompt").to_string(),
822 PromptType::GotoLine,
823 );
824 } else {
825 self.start_prompt(
826 t!("goto.scan_confirm_prompt", yes = "y", no = "N").to_string(),
827 PromptType::GotoLineScanConfirm,
828 );
829 }
830 }
831 Action::ScanLineIndex => {
832 self.start_incremental_line_scan(false);
833 }
834 Action::New => {
835 self.new_buffer();
836 }
837 Action::Close | Action::CloseTab => {
838 self.close_tab();
843 }
844 Action::Revert => {
845 if self.active_state().buffer.is_modified() {
847 let revert_key = t!("prompt.key.revert").to_string();
848 let cancel_key = t!("prompt.key.cancel").to_string();
849 self.start_prompt(
850 t!(
851 "prompt.revert_confirm",
852 revert_key = revert_key,
853 cancel_key = cancel_key
854 )
855 .to_string(),
856 PromptType::ConfirmRevert,
857 );
858 } else {
859 if let Err(e) = self.revert_file() {
861 self.set_status_message(
862 t!("error.failed_to_revert", error = e.to_string()).to_string(),
863 );
864 }
865 }
866 }
867 Action::ToggleAutoRevert => {
868 self.toggle_auto_revert();
869 }
870 Action::FormatBuffer => {
871 if let Err(e) = self.format_buffer() {
872 self.set_status_message(
873 t!("error.format_failed", error = e.to_string()).to_string(),
874 );
875 }
876 }
877 Action::TrimTrailingWhitespace => match self.trim_trailing_whitespace() {
878 Ok(true) => {
879 self.set_status_message(t!("whitespace.trimmed").to_string());
880 }
881 Ok(false) => {
882 self.set_status_message(t!("whitespace.no_trailing").to_string());
883 }
884 Err(e) => {
885 self.set_status_message(
886 t!("error.trim_whitespace_failed", error = e).to_string(),
887 );
888 }
889 },
890 Action::EnsureFinalNewline => match self.ensure_final_newline() {
891 Ok(true) => {
892 self.set_status_message(t!("whitespace.newline_added").to_string());
893 }
894 Ok(false) => {
895 self.set_status_message(t!("whitespace.already_has_newline").to_string());
896 }
897 Err(e) => {
898 self.set_status_message(
899 t!("error.ensure_newline_failed", error = e).to_string(),
900 );
901 }
902 },
903 Action::Copy => {
904 let popup = self
906 .global_popups
907 .top()
908 .or_else(|| self.active_state().popups.top());
909 if let Some(popup) = popup {
910 if popup.has_selection() {
911 if let Some(text) = popup.get_selected_text() {
912 self.clipboard.copy(text);
913 self.set_status_message(t!("clipboard.copied").to_string());
914 return Ok(());
915 }
916 }
917 }
918 if self.active_window_mut().key_context
919 == crate::input::keybindings::KeyContext::FileExplorer
920 {
921 self.active_window_mut().file_explorer_copy();
922 return Ok(());
923 }
924 let buffer_id = self.active_buffer();
931 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
932 if self.handle_widget_copy(panel_id) {
933 self.set_status_message(t!("clipboard.copied").to_string());
934 return Ok(());
935 }
936 }
937 if self.active_window().is_composite_buffer(buffer_id) {
939 if let Some(_handled) = self.handle_composite_action(buffer_id, &Action::Copy) {
940 return Ok(());
941 }
942 }
943 self.copy_selection()
944 }
945 Action::CopyWithTheme(theme) => self.copy_selection_with_theme(&theme),
946 Action::CopyFilePath => self.copy_active_buffer_path(false),
947 Action::CopyRelativeFilePath => self.copy_active_buffer_path(true),
948 Action::Cut => {
949 if self.active_window_mut().key_context
950 == crate::input::keybindings::KeyContext::FileExplorer
951 {
952 self.active_window_mut().file_explorer_cut();
953 return Ok(());
954 }
955 let buffer_id = self.active_buffer();
959 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
960 if self.handle_widget_cut(panel_id) {
961 return Ok(());
962 }
963 }
964 if self.active_window().is_editing_disabled() {
965 self.set_status_message(t!("buffer.editing_disabled").to_string());
966 return Ok(());
967 }
968 self.cut_selection()
969 }
970 Action::Paste => {
971 if self.active_window_mut().key_context
972 == crate::input::keybindings::KeyContext::FileExplorer
973 {
974 self.file_explorer_paste();
975 return Ok(());
976 }
977 let buffer_id = self.active_buffer();
983 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
984 if let Some(text) = self.clipboard.paste() {
985 let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
986 self.handle_widget_insert_str(panel_id, &normalized);
987 self.set_status_message(t!("clipboard.pasted").to_string());
988 }
989 return Ok(());
990 }
991 if self.active_window().is_editing_disabled() {
992 self.set_status_message(t!("buffer.editing_disabled").to_string());
993 return Ok(());
994 }
995 self.paste()
996 }
997 Action::SelectAll => {
998 let buffer_id = self.active_buffer();
1003 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
1004 self.handle_widget_select_all(panel_id);
1005 return Ok(());
1006 }
1007 self.apply_action_as_events(Action::SelectAll)?;
1008 }
1009 Action::YankWordForward => self.yank_word_forward(),
1010 Action::YankWordBackward => self.yank_word_backward(),
1011 Action::YankToLineEnd => self.yank_to_line_end(),
1012 Action::YankToLineStart => self.yank_to_line_start(),
1013 Action::YankViWordEnd => self.yank_vi_word_end(),
1014 Action::Undo => {
1015 self.handle_undo();
1016 }
1017 Action::Redo => {
1018 self.handle_redo();
1019 }
1020 Action::ShowHelp => {
1021 self.active_window_mut().open_help_manual();
1022 }
1023 Action::ShowKeyboardShortcuts => {
1024 self.active_window_mut().open_keyboard_shortcuts();
1025 }
1026 Action::ShowWarnings => {
1027 self.show_warnings_popup();
1028 }
1029 Action::ShowStatusLog => {
1030 self.open_status_log();
1031 }
1032 Action::ShowLspStatus => {
1033 self.show_lsp_status_popup();
1034 }
1035 Action::ShowRemoteIndicatorMenu => {
1036 self.show_remote_indicator_popup();
1037 }
1038 Action::ClearWarnings => {
1039 self.active_window_mut().clear_warnings();
1040 }
1041 Action::CommandPalette => {
1042 if let Some(prompt) = &self.active_window_mut().prompt {
1045 if prompt.prompt_type == PromptType::QuickOpen {
1046 self.cancel_prompt();
1047 return Ok(());
1048 }
1049 }
1050 self.start_quick_open();
1051 }
1052 Action::QuickOpen => {
1053 if let Some(prompt) = &self.active_window_mut().prompt {
1055 if prompt.prompt_type == PromptType::QuickOpen {
1056 self.cancel_prompt();
1057 return Ok(());
1058 }
1059 }
1060
1061 self.start_quick_open();
1063 }
1064 Action::QuickOpenBuffers => {
1065 if let Some(prompt) = &self.active_window_mut().prompt {
1066 if prompt.prompt_type == PromptType::QuickOpen {
1067 self.cancel_prompt();
1068 return Ok(());
1069 }
1070 }
1071 self.start_quick_open_with_prefix("#");
1072 }
1073 Action::QuickOpenFiles => {
1074 if let Some(prompt) = &self.active_window_mut().prompt {
1075 if prompt.prompt_type == PromptType::QuickOpen {
1076 self.cancel_prompt();
1077 return Ok(());
1078 }
1079 }
1080 self.start_quick_open_with_prefix("");
1081 }
1082 Action::OpenLiveGrep => {
1083 #[cfg(feature = "plugins")]
1089 {
1090 let result = self
1091 .plugin_manager
1092 .read()
1093 .unwrap()
1094 .execute_action_async("start_live_grep");
1095 if let Some(result) = result {
1096 match result {
1097 Ok(receiver) => {
1098 self.pending_plugin_actions
1099 .push(("start_live_grep".to_string(), receiver));
1100 }
1101 Err(e) => {
1102 self.set_status_message(format!("Live Grep unavailable: {}", e));
1103 }
1104 }
1105 } else {
1106 self.set_status_message("Live Grep plugin not loaded".to_string());
1107 }
1108 }
1109 #[cfg(not(feature = "plugins"))]
1110 {
1111 self.set_status_message("Live Grep requires the plugins feature".to_string());
1112 }
1113 }
1114 Action::ResumeLiveGrep => {
1115 #[cfg(feature = "plugins")]
1121 {
1122 let result = self
1123 .plugin_manager
1124 .read()
1125 .unwrap()
1126 .execute_action_async("resume_live_grep");
1127 if let Some(result) = result {
1128 match result {
1129 Ok(receiver) => {
1130 self.pending_plugin_actions
1131 .push(("resume_live_grep".to_string(), receiver));
1132 }
1133 Err(e) => {
1134 self.set_status_message(format!("Live Grep unavailable: {}", e));
1135 }
1136 }
1137 }
1138 }
1139 }
1140 Action::ToggleUtilityDock => {
1141 use crate::view::split::SplitRole;
1142 if let Some(dock_leaf) = self
1143 .windows
1144 .get(&self.active_window)
1145 .and_then(|w| w.buffers.splits())
1146 .map(|(mgr, _)| mgr)
1147 .expect("active window must have a populated split layout")
1148 .find_leaf_by_role(SplitRole::UtilityDock)
1149 {
1150 let active = self
1151 .windows
1152 .get(&self.active_window)
1153 .and_then(|w| w.buffers.splits())
1154 .map(|(mgr, _)| mgr)
1155 .expect("active window must have a populated split layout")
1156 .active_split();
1157 if active == dock_leaf {
1158 self.next_split();
1163 } else {
1164 self.windows
1165 .get_mut(&self.active_window)
1166 .and_then(|w| w.split_manager_mut())
1167 .expect("active window must have a populated split layout")
1168 .set_active_split(dock_leaf);
1169 }
1170 } else {
1171 self.set_status_message(
1172 "No Utility Dock open — invoke a dock-aware utility (Diagnostics, Search/Replace, …)"
1173 .to_string(),
1174 );
1175 }
1176 }
1177 Action::CycleLiveGrepProvider => {
1178 let in_live_grep = self
1184 .active_window()
1185 .prompt
1186 .as_ref()
1187 .map(|p| match &p.prompt_type {
1188 PromptType::LiveGrep => true,
1189 PromptType::Plugin { custom_type } => custom_type == "live-grep",
1190 _ => false,
1191 })
1192 .unwrap_or(false);
1193 if !in_live_grep {
1194 self.set_status_message(
1195 "Cycle Live Grep provider only works inside Live Grep".to_string(),
1196 );
1197 return Ok(());
1198 }
1199 #[cfg(feature = "plugins")]
1200 {
1201 let result = self
1202 .plugin_manager
1203 .read()
1204 .unwrap()
1205 .execute_action_async("live_grep_cycle_provider");
1206 if let Some(result) = result {
1207 match result {
1208 Ok(receiver) => {
1209 self.pending_plugin_actions
1210 .push(("live_grep_cycle_provider".to_string(), receiver));
1211 }
1212 Err(e) => {
1213 self.set_status_message(format!("Live Grep cycle failed: {}", e));
1214 }
1215 }
1216 } else {
1217 self.set_status_message("Live Grep plugin not loaded".to_string());
1218 }
1219 }
1220 #[cfg(not(feature = "plugins"))]
1221 {
1222 self.set_status_message(
1223 "Live Grep cycle requires the plugins feature".to_string(),
1224 );
1225 }
1226 }
1227 Action::OpenTerminalInDock => {
1228 use crate::model::event::SplitDirection;
1229 use crate::view::split::SplitRole;
1230 if let Some(dock_leaf) = self
1231 .windows
1232 .get(&self.active_window)
1233 .and_then(|w| w.buffers.splits())
1234 .map(|(mgr, _)| mgr)
1235 .expect("active window must have a populated split layout")
1236 .find_leaf_by_role(SplitRole::UtilityDock)
1237 {
1238 self.windows
1241 .get_mut(&self.active_window)
1242 .and_then(|w| w.split_manager_mut())
1243 .expect("active window must have a populated split layout")
1244 .set_active_split(dock_leaf);
1245 self.open_terminal();
1246 } else {
1247 let Some(terminal_id) = self.spawn_terminal_session() else {
1254 return Ok(());
1255 };
1256 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
1257 match self
1260 .windows
1261 .get_mut(&self.active_window)
1262 .and_then(|w| w.split_manager_mut())
1263 .expect("active window must have a populated split layout")
1264 .split_root_positioned(SplitDirection::Horizontal, buffer_id, 0.7, false)
1265 {
1266 Ok(new_leaf) => {
1267 let mut view_state = crate::view::split::SplitViewState::with_buffer(
1268 self.terminal_width,
1269 self.terminal_height,
1270 buffer_id,
1271 );
1272 view_state.apply_config_defaults(
1278 false,
1279 false,
1280 self.active_window().resolve_line_wrap_for_buffer(buffer_id),
1281 self.config.editor.wrap_indent,
1282 self.active_window()
1283 .resolve_wrap_column_for_buffer(buffer_id),
1284 self.config.editor.rulers.clone(),
1285 );
1286 view_state.viewport.line_wrap_enabled = false;
1290 self.windows
1291 .get_mut(&self.active_window)
1292 .and_then(|w| w.split_view_states_mut())
1293 .expect("active window must have a populated split layout")
1294 .insert(new_leaf, view_state);
1295 self.windows
1296 .get_mut(&self.active_window)
1297 .and_then(|w| w.split_manager_mut())
1298 .expect("active window must have a populated split layout")
1299 .set_leaf_role(new_leaf, Some(SplitRole::UtilityDock));
1300 self.windows
1301 .get_mut(&self.active_window)
1302 .and_then(|w| w.split_manager_mut())
1303 .expect("active window must have a populated split layout")
1304 .set_active_split(new_leaf);
1305 self.active_window_mut().terminal_mode = true;
1311 self.active_window_mut().key_context =
1312 crate::input::keybindings::KeyContext::Terminal;
1313 self.active_window_mut().resize_visible_terminals();
1314 let exit_key = self
1315 .keybindings
1316 .read()
1317 .unwrap()
1318 .find_keybinding_for_action(
1319 "terminal_escape",
1320 crate::input::keybindings::KeyContext::Terminal,
1321 )
1322 .unwrap_or_else(|| "Ctrl+Space".to_string());
1323 self.set_status_message(
1324 rust_i18n::t!(
1325 "terminal.opened",
1326 id = terminal_id.0,
1327 exit_key = exit_key
1328 )
1329 .to_string(),
1330 );
1331 tracing::info!(
1332 "Opened terminal {:?} into new dock leaf {:?} (buffer {:?})",
1333 terminal_id,
1334 new_leaf,
1335 buffer_id
1336 );
1337 }
1338 Err(e) => {
1339 self.set_status_message(format!(
1340 "Failed to create dock for terminal: {}",
1341 e
1342 ));
1343 return Ok(());
1344 }
1345 }
1346 }
1347 }
1348 Action::ToggleLineWrap => {
1349 let new_value = !self.config.editor.line_wrap;
1350 self.config_mut().editor.line_wrap = new_value;
1351 self.sync_windows_config();
1359
1360 let leaf_ids: Vec<_> = self
1363 .windows
1364 .get(&self.active_window)
1365 .and_then(|w| w.buffers.splits())
1366 .map(|(_, vs)| vs)
1367 .expect("active window must have a populated split layout")
1368 .keys()
1369 .copied()
1370 .collect();
1371 for leaf_id in leaf_ids {
1372 let buffer_id = self
1373 .split_manager_mut()
1374 .get_buffer_id(leaf_id.into())
1375 .unwrap_or(BufferId(0));
1376 let effective_wrap =
1377 self.active_window().resolve_line_wrap_for_buffer(buffer_id);
1378 let wrap_column = self
1379 .active_window()
1380 .resolve_wrap_column_for_buffer(buffer_id);
1381 if let Some(view_state) = self
1382 .windows
1383 .get_mut(&self.active_window)
1384 .and_then(|w| w.split_view_states_mut())
1385 .expect("active window must have a populated split layout")
1386 .get_mut(&leaf_id)
1387 {
1388 view_state.viewport.line_wrap_enabled = effective_wrap;
1389 view_state.viewport.wrap_indent = self.config.editor.wrap_indent;
1390 view_state.viewport.wrap_column = wrap_column;
1391 }
1392 }
1393
1394 let state = if self.config.editor.line_wrap {
1395 t!("view.state_enabled").to_string()
1396 } else {
1397 t!("view.state_disabled").to_string()
1398 };
1399 self.set_status_message(t!("view.line_wrap_state", state = state).to_string());
1400 }
1401 Action::ToggleCurrentLineHighlight => {
1402 let new_value = !self.config.editor.highlight_current_line;
1403 self.config_mut().editor.highlight_current_line = new_value;
1404
1405 let leaf_ids: Vec<_> = self
1407 .windows
1408 .get(&self.active_window)
1409 .and_then(|w| w.buffers.splits())
1410 .map(|(_, vs)| vs)
1411 .expect("active window must have a populated split layout")
1412 .keys()
1413 .copied()
1414 .collect();
1415 for leaf_id in leaf_ids {
1416 if let Some(view_state) = self
1417 .windows
1418 .get_mut(&self.active_window)
1419 .and_then(|w| w.split_view_states_mut())
1420 .expect("active window must have a populated split layout")
1421 .get_mut(&leaf_id)
1422 {
1423 view_state.highlight_current_line =
1424 self.config.editor.highlight_current_line;
1425 }
1426 }
1427
1428 let state = if self.config.editor.highlight_current_line {
1429 t!("view.state_enabled").to_string()
1430 } else {
1431 t!("view.state_disabled").to_string()
1432 };
1433 self.set_status_message(
1434 t!("view.current_line_highlight_state", state = state).to_string(),
1435 );
1436 }
1437 Action::ToggleReadOnly => {
1438 let buffer_id = self.active_buffer();
1439 let is_now_read_only = self
1440 .active_window()
1441 .buffer_metadata
1442 .get(&buffer_id)
1443 .map(|m| !m.read_only)
1444 .unwrap_or(false);
1445 self.active_window_mut()
1446 .mark_buffer_read_only(buffer_id, is_now_read_only);
1447
1448 let state_str = if is_now_read_only {
1449 t!("view.state_enabled").to_string()
1450 } else {
1451 t!("view.state_disabled").to_string()
1452 };
1453 self.set_status_message(t!("view.read_only_state", state = state_str).to_string());
1454 }
1455 Action::TogglePageView => {
1456 self.active_window_mut().handle_toggle_page_view();
1457 }
1458 Action::SetPageWidth => {
1459 let active_split = self
1460 .windows
1461 .get(&self.active_window)
1462 .and_then(|w| w.buffers.splits())
1463 .map(|(mgr, _)| mgr)
1464 .expect("active window must have a populated split layout")
1465 .active_split();
1466 let current = self
1467 .windows
1468 .get(&self.active_window)
1469 .and_then(|w| w.buffers.splits())
1470 .map(|(_, vs)| vs)
1471 .expect("active window must have a populated split layout")
1472 .get(&active_split)
1473 .and_then(|v| v.compose_width.map(|w| w.to_string()))
1474 .unwrap_or_default();
1475 self.start_prompt_with_initial_text(
1476 "Page width (empty = viewport): ".to_string(),
1477 PromptType::SetPageWidth,
1478 current,
1479 );
1480 }
1481 Action::SetBackground => {
1482 let default_path = self
1483 .ansi_background_path
1484 .as_ref()
1485 .and_then(|p| {
1486 p.strip_prefix(self.working_dir())
1487 .ok()
1488 .map(|rel| rel.to_string_lossy().to_string())
1489 })
1490 .unwrap_or_else(|| DEFAULT_BACKGROUND_FILE.to_string());
1491
1492 self.start_prompt_with_initial_text(
1493 "Background file: ".to_string(),
1494 PromptType::SetBackgroundFile,
1495 default_path,
1496 );
1497 }
1498 Action::SetBackgroundBlend => {
1499 let default_amount = format!("{:.2}", self.background_fade);
1500 self.start_prompt_with_initial_text(
1501 "Background blend (0-1): ".to_string(),
1502 PromptType::SetBackgroundBlend,
1503 default_amount,
1504 );
1505 }
1506 Action::LspCompletion => {
1507 self.request_completion();
1508 }
1509 Action::DabbrevExpand => {
1510 self.dabbrev_expand();
1511 }
1512 Action::LspGotoDefinition => {
1513 self.request_goto_definition()?;
1514 }
1515 Action::LspRename => {
1516 self.start_rename()?;
1517 }
1518 Action::LspHover => {
1519 self.request_hover()?;
1520 }
1521 Action::LspReferences => {
1522 self.request_references()?;
1523 }
1524 Action::LspSignatureHelp => {
1525 self.request_signature_help();
1526 }
1527 Action::LspCodeActions => {
1528 self.request_code_actions()?;
1529 }
1530 Action::LspRestart => {
1531 self.handle_lsp_restart();
1532 }
1533 Action::LspStop => {
1534 self.handle_lsp_stop();
1535 }
1536 Action::LspToggleForBuffer => {
1537 self.handle_lsp_toggle_for_buffer();
1538 }
1539 Action::ToggleInlayHints => {
1540 self.toggle_inlay_hints();
1541 }
1542 Action::DumpConfig => {
1543 self.dump_config();
1544 }
1545 Action::RedrawScreen => {
1546 self.request_full_redraw();
1547 }
1548 Action::SelectTheme => {
1549 self.start_select_theme_prompt();
1550 }
1551 Action::InspectThemeAtCursor => {
1552 self.inspect_theme_at_cursor();
1553 }
1554 Action::SelectKeybindingMap => {
1555 self.start_select_keybinding_map_prompt();
1556 }
1557 Action::SelectCursorStyle => {
1558 self.start_select_cursor_style_prompt();
1559 }
1560 Action::SelectLocale => {
1561 self.start_select_locale_prompt();
1562 }
1563 Action::Search => {
1564 let is_search_prompt = self.active_window().prompt.as_ref().is_some_and(|p| {
1566 matches!(
1567 p.prompt_type,
1568 PromptType::Search
1569 | PromptType::ReplaceSearch
1570 | PromptType::QueryReplaceSearch
1571 )
1572 });
1573
1574 if is_search_prompt {
1575 self.confirm_prompt();
1576 } else {
1577 self.start_search_prompt(
1578 t!("file.search_prompt").to_string(),
1579 PromptType::Search,
1580 false,
1581 );
1582 }
1583 }
1584 Action::Replace => {
1585 self.start_search_prompt(
1587 t!("file.replace_prompt").to_string(),
1588 PromptType::ReplaceSearch,
1589 false,
1590 );
1591 }
1592 Action::QueryReplace => {
1593 self.active_window_mut().search_confirm_each = true;
1595 self.start_search_prompt(
1596 "Query replace: ".to_string(),
1597 PromptType::QueryReplaceSearch,
1598 false,
1599 );
1600 }
1601 Action::FindInSelection => {
1602 self.start_search_prompt(
1603 t!("file.search_prompt").to_string(),
1604 PromptType::Search,
1605 true,
1606 );
1607 }
1608 Action::FindNext => {
1609 self.find_next();
1610 }
1611 Action::FindPrevious => {
1612 self.find_previous();
1613 }
1614 Action::FindSelectionNext => {
1615 self.find_selection_next();
1616 }
1617 Action::FindSelectionPrevious => {
1618 self.find_selection_previous();
1619 }
1620 Action::AddCursorNextMatch => self.add_cursor_at_next_match(),
1621 Action::AddCursorAbove => self.add_cursor_above(),
1622 Action::AddCursorBelow => self.add_cursor_below(),
1623 Action::AddCursorsToLineEnds => self.add_cursors_to_line_ends(),
1624 Action::NextBuffer => self.next_buffer(),
1625 Action::PrevBuffer => self.prev_buffer(),
1626 Action::SwitchToPreviousTab => self.switch_to_previous_tab(),
1627 Action::SwitchToTabByName => self.start_switch_to_tab_prompt(),
1628
1629 Action::ScrollTabsLeft => {
1631 let active_split_id = self
1632 .windows
1633 .get(&self.active_window)
1634 .and_then(|w| w.buffers.splits())
1635 .map(|(mgr, _)| mgr)
1636 .expect("active window must have a populated split layout")
1637 .active_split();
1638 if let Some(view_state) = self
1639 .windows
1640 .get_mut(&self.active_window)
1641 .and_then(|w| w.split_view_states_mut())
1642 .expect("active window must have a populated split layout")
1643 .get_mut(&active_split_id)
1644 {
1645 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_sub(5);
1646 self.set_status_message(t!("status.scrolled_tabs_left").to_string());
1647 }
1648 }
1649 Action::ScrollTabsRight => {
1650 let active_split_id = self
1651 .windows
1652 .get(&self.active_window)
1653 .and_then(|w| w.buffers.splits())
1654 .map(|(mgr, _)| mgr)
1655 .expect("active window must have a populated split layout")
1656 .active_split();
1657 if let Some(view_state) = self
1658 .windows
1659 .get_mut(&self.active_window)
1660 .and_then(|w| w.split_view_states_mut())
1661 .expect("active window must have a populated split layout")
1662 .get_mut(&active_split_id)
1663 {
1664 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_add(5);
1665 self.set_status_message(t!("status.scrolled_tabs_right").to_string());
1666 }
1667 }
1668 Action::NavigateBack => self.navigate_back(),
1669 Action::NavigateForward => self.navigate_forward(),
1670 Action::SplitHorizontal => self.split_pane_horizontal(),
1671 Action::SplitVertical => self.split_pane_vertical(),
1672 Action::CloseSplit => self.close_active_split(),
1673 Action::NextSplit => self.next_split(),
1674 Action::PrevSplit => self.prev_split(),
1675 Action::NextWindow => self.next_window(),
1676 Action::PrevWindow => self.prev_window(),
1677 Action::IncreaseSplitSize => self.adjust_split_size(0.05),
1678 Action::DecreaseSplitSize => self.adjust_split_size(-0.05),
1679 Action::ToggleMaximizeSplit => self.toggle_maximize_split(),
1680 Action::ToggleFileExplorer => self.toggle_file_explorer(),
1681 Action::ToggleFileExplorerSide => self.toggle_file_explorer_side(),
1682 Action::ToggleMenuBar => self.toggle_menu_bar(),
1683 Action::ToggleTabBar => self.active_window_mut().toggle_tab_bar(),
1684 Action::ToggleStatusBar => self.active_window_mut().toggle_status_bar(),
1685 Action::TogglePromptLine => self.active_window_mut().toggle_prompt_line(),
1686 Action::ToggleVerticalScrollbar => self.toggle_vertical_scrollbar(),
1687 Action::ToggleHorizontalScrollbar => self.toggle_horizontal_scrollbar(),
1688 Action::ToggleLineNumbers => self.toggle_line_numbers(),
1689 Action::ToggleScrollSync => self.active_window_mut().toggle_scroll_sync(),
1690 Action::ToggleMouseCapture => self.toggle_mouse_capture(),
1691 Action::ToggleMouseHover => self.toggle_mouse_hover(),
1692 Action::ToggleDebugHighlights => self.active_window_mut().toggle_debug_highlights(),
1693 Action::AddRuler => {
1695 self.start_prompt(t!("rulers.add_prompt").to_string(), PromptType::AddRuler);
1696 }
1697 Action::RemoveRuler => {
1698 self.start_remove_ruler_prompt();
1699 }
1700 Action::SetTabSize => {
1702 let current = self
1703 .buffers()
1704 .get(&self.active_buffer())
1705 .map(|s| s.buffer_settings.tab_size.to_string())
1706 .unwrap_or_else(|| "4".to_string());
1707 self.start_prompt_with_initial_text(
1708 "Tab size: ".to_string(),
1709 PromptType::SetTabSize,
1710 current,
1711 );
1712 }
1713 Action::SetLineEnding => {
1714 self.start_set_line_ending_prompt();
1715 }
1716 Action::SetEncoding => {
1717 self.start_set_encoding_prompt();
1718 }
1719 Action::ReloadWithEncoding => {
1720 self.start_reload_with_encoding_prompt();
1721 }
1722 Action::SetLanguage => {
1723 self.start_set_language_prompt();
1724 }
1725 Action::ToggleIndentationStyle => {
1726 let __buffer_id = self.active_buffer();
1727 if let Some(state) = self
1728 .windows
1729 .get_mut(&self.active_window)
1730 .map(|w| &mut w.buffers)
1731 .expect("active window present")
1732 .get_mut(&__buffer_id)
1733 {
1734 state.buffer_settings.use_tabs = !state.buffer_settings.use_tabs;
1735 let status = if state.buffer_settings.use_tabs {
1736 "Indentation: Tabs"
1737 } else {
1738 "Indentation: Spaces"
1739 };
1740 self.set_status_message(status.to_string());
1741 }
1742 }
1743 Action::ToggleTabIndicators | Action::ToggleWhitespaceIndicators => {
1744 let __buffer_id = self.active_buffer();
1745 if let Some(state) = self
1746 .windows
1747 .get_mut(&self.active_window)
1748 .map(|w| &mut w.buffers)
1749 .expect("active window present")
1750 .get_mut(&__buffer_id)
1751 {
1752 state.buffer_settings.whitespace.toggle_all();
1753 let status = if state.buffer_settings.whitespace.any_visible() {
1754 t!("toggle.whitespace_indicators_shown")
1755 } else {
1756 t!("toggle.whitespace_indicators_hidden")
1757 };
1758 self.set_status_message(status.to_string());
1759 }
1760 }
1761 Action::ResetBufferSettings => self.reset_buffer_settings(),
1762 Action::FocusFileExplorer => self.focus_file_explorer(),
1763 Action::FocusEditor => self.active_window_mut().focus_editor(),
1764 Action::FileExplorerUp => self.file_explorer_navigate_up(),
1765 Action::FileExplorerDown => self.file_explorer_navigate_down(),
1766 Action::FileExplorerPageUp => self.file_explorer_page_up(),
1767 Action::FileExplorerPageDown => self.file_explorer_page_down(),
1768 Action::FileExplorerExpand => self.file_explorer_toggle_expand(),
1769 Action::FileExplorerCollapse => self.file_explorer_collapse(),
1770 Action::FileExplorerOpen => self.file_explorer_open_file()?,
1771 Action::FileExplorerRefresh => self.file_explorer_refresh(),
1772 Action::FileExplorerNewFile => self.file_explorer_new_file(),
1773 Action::FileExplorerNewDirectory => self.file_explorer_new_directory(),
1774 Action::FileExplorerDelete => self.file_explorer_delete(),
1775 Action::FileExplorerRename => self.file_explorer_rename(),
1776 Action::FileExplorerToggleHidden => self.file_explorer_toggle_hidden(),
1777 Action::FileExplorerToggleGitignored => self.file_explorer_toggle_gitignored(),
1778 Action::FileExplorerSearchClear => {
1779 self.active_window_mut().file_explorer_search_clear()
1780 }
1781 Action::FileExplorerSearchBackspace => {
1782 self.active_window_mut().file_explorer_search_pop_char()
1783 }
1784 Action::FileExplorerCopy => self.active_window_mut().file_explorer_copy(),
1785 Action::FileExplorerCut => self.active_window_mut().file_explorer_cut(),
1786 Action::FileExplorerPaste => self.file_explorer_paste(),
1787 Action::FileExplorerDuplicate => self.file_explorer_duplicate(),
1788 Action::FileExplorerCopyFullPath => self.file_explorer_copy_path(false),
1789 Action::FileExplorerCopyRelativePath => self.file_explorer_copy_path(true),
1790 Action::FileExplorerExtendSelectionUp => {
1791 self.active_window_mut().file_explorer_extend_selection_up()
1792 }
1793 Action::FileExplorerExtendSelectionDown => self
1794 .active_window_mut()
1795 .file_explorer_extend_selection_down(),
1796 Action::FileExplorerToggleSelect => {
1797 self.active_window_mut().file_explorer_toggle_select()
1798 }
1799 Action::FileExplorerSelectAll => self.active_window_mut().file_explorer_select_all(),
1800 Action::RemoveSecondaryCursors => {
1801 if let Some(events) = self
1803 .active_window_mut()
1804 .action_to_events(Action::RemoveSecondaryCursors)
1805 {
1806 let batch = Event::Batch {
1808 events: events.clone(),
1809 description: "Remove secondary cursors".to_string(),
1810 };
1811 self.active_event_log_mut().append(batch.clone());
1812 self.apply_event_to_active_buffer(&batch);
1813
1814 let active_split = self
1816 .windows
1817 .get(&self.active_window)
1818 .and_then(|w| w.buffers.splits())
1819 .map(|(mgr, _)| mgr)
1820 .expect("active window must have a populated split layout")
1821 .active_split();
1822 let active_buffer = self.active_buffer();
1823 self.active_window_mut()
1824 .ensure_cursor_visible_for_split(active_buffer, active_split);
1825 }
1826 }
1827
1828 Action::MenuActivate => {
1830 self.handle_menu_activate();
1831 }
1832 Action::MenuClose => {
1833 self.handle_menu_close();
1834 }
1835 Action::MenuLeft => {
1836 self.handle_menu_left();
1837 }
1838 Action::MenuRight => {
1839 self.handle_menu_right();
1840 }
1841 Action::MenuUp => {
1842 self.handle_menu_up();
1843 }
1844 Action::MenuDown => {
1845 self.handle_menu_down();
1846 }
1847 Action::MenuExecute => {
1848 if let Some(action) = self.handle_menu_execute() {
1849 return self.handle_action(action);
1850 }
1851 }
1852 Action::MenuOpen(menu_name) => {
1853 if self.config.editor.menu_bar_mnemonics {
1854 self.handle_menu_open(&menu_name);
1855 }
1856 }
1857
1858 Action::SwitchKeybindingMap(map_name) => {
1859 let is_builtin =
1861 matches!(map_name.as_str(), "default" | "emacs" | "vscode" | "macos");
1862 let is_user_defined = self.config.keybinding_maps.contains_key(&map_name);
1863
1864 if is_builtin || is_user_defined {
1865 self.config_mut().active_keybinding_map = map_name.clone().into();
1867
1868 *self.keybindings.write().unwrap() =
1870 crate::input::keybindings::KeybindingResolver::new(&self.config);
1871
1872 self.set_status_message(
1873 t!("view.keybindings_switched", map = map_name).to_string(),
1874 );
1875 } else {
1876 self.set_status_message(
1877 t!("view.keybindings_unknown", map = map_name).to_string(),
1878 );
1879 }
1880 }
1881
1882 Action::SmartHome => {
1883 let buffer_id = self.active_buffer();
1885 if self.active_window().is_composite_buffer(buffer_id) {
1886 if let Some(_handled) =
1887 self.handle_composite_action(buffer_id, &Action::SmartHome)
1888 {
1889 return Ok(());
1890 }
1891 }
1892 self.smart_home();
1893 }
1894 Action::ToggleComment => {
1895 self.toggle_comment();
1896 }
1897 Action::ToggleFold => {
1898 self.active_window_mut().toggle_fold_at_cursor();
1899 }
1900 Action::GoToMatchingBracket => {
1901 self.goto_matching_bracket();
1902 }
1903 Action::JumpToNextError => {
1904 self.jump_to_next_error();
1905 }
1906 Action::JumpToPreviousError => {
1907 self.jump_to_previous_error();
1908 }
1909 Action::SetBookmark(key) => {
1910 self.active_window_mut().set_bookmark(key);
1911 }
1912 Action::JumpToBookmark(key) => {
1913 self.jump_to_bookmark(key);
1914 }
1915 Action::ClearBookmark(key) => {
1916 self.active_window_mut().clear_bookmark(key);
1917 }
1918 Action::ListBookmarks => {
1919 self.active_window_mut().list_bookmarks();
1920 }
1921 Action::ToggleSearchCaseSensitive => {
1922 self.active_window_mut().search_case_sensitive =
1923 !self.active_window().search_case_sensitive;
1924 let state = if self.active_window().search_case_sensitive {
1925 "enabled"
1926 } else {
1927 "disabled"
1928 };
1929 self.set_status_message(
1930 t!("search.case_sensitive_state", state = state).to_string(),
1931 );
1932 if let Some(prompt) = &self.active_window_mut().prompt {
1935 if matches!(
1936 prompt.prompt_type,
1937 PromptType::Search
1938 | PromptType::ReplaceSearch
1939 | PromptType::QueryReplaceSearch
1940 ) {
1941 let query = prompt.input.clone();
1942 self.update_search_highlights(&query);
1943 }
1944 } else if let Some(search_state) = &self.active_window().search_state {
1945 let query = search_state.query.clone();
1946 self.perform_search(&query);
1947 }
1948 }
1949 Action::ToggleSearchWholeWord => {
1950 self.active_window_mut().search_whole_word =
1951 !self.active_window().search_whole_word;
1952 let state = if self.active_window().search_whole_word {
1953 "enabled"
1954 } else {
1955 "disabled"
1956 };
1957 self.set_status_message(t!("search.whole_word_state", state = state).to_string());
1958 if let Some(prompt) = &self.active_window_mut().prompt {
1961 if matches!(
1962 prompt.prompt_type,
1963 PromptType::Search
1964 | PromptType::ReplaceSearch
1965 | PromptType::QueryReplaceSearch
1966 ) {
1967 let query = prompt.input.clone();
1968 self.update_search_highlights(&query);
1969 }
1970 } else if let Some(search_state) = &self.active_window().search_state {
1971 let query = search_state.query.clone();
1972 self.perform_search(&query);
1973 }
1974 }
1975 Action::ToggleSearchRegex => {
1976 self.active_window_mut().search_use_regex = !self.active_window().search_use_regex;
1977 let state = if self.active_window().search_use_regex {
1978 "enabled"
1979 } else {
1980 "disabled"
1981 };
1982 self.set_status_message(t!("search.regex_state", state = state).to_string());
1983 if let Some(prompt) = &self.active_window_mut().prompt {
1986 if matches!(
1987 prompt.prompt_type,
1988 PromptType::Search
1989 | PromptType::ReplaceSearch
1990 | PromptType::QueryReplaceSearch
1991 ) {
1992 let query = prompt.input.clone();
1993 self.update_search_highlights(&query);
1994 }
1995 } else if let Some(search_state) = &self.active_window().search_state {
1996 let query = search_state.query.clone();
1997 self.perform_search(&query);
1998 }
1999 }
2000 Action::ToggleSearchConfirmEach => {
2001 self.active_window_mut().search_confirm_each =
2002 !self.active_window().search_confirm_each;
2003 let state = if self.active_window().search_confirm_each {
2004 "enabled"
2005 } else {
2006 "disabled"
2007 };
2008 self.set_status_message(t!("search.confirm_each_state", state = state).to_string());
2009 }
2010 Action::FileBrowserToggleHidden => {
2011 self.file_open_toggle_hidden();
2013 }
2014 Action::StartMacroRecording => {
2015 self.set_status_message(
2017 "Use Ctrl+Shift+R to start recording (will prompt for register)".to_string(),
2018 );
2019 }
2020 Action::StopMacroRecording => {
2021 self.stop_macro_recording();
2022 }
2023 Action::PlayMacro(key) => {
2024 self.play_macro(key);
2025 }
2026 Action::ToggleMacroRecording(key) => {
2027 self.toggle_macro_recording(key);
2028 }
2029 Action::ShowMacro(key) => {
2030 self.show_macro_in_buffer(key);
2031 }
2032 Action::ListMacros => {
2033 self.list_macros_in_buffer();
2034 }
2035 Action::PromptRecordMacro => {
2036 self.start_prompt("Record macro (0-9): ".to_string(), PromptType::RecordMacro);
2037 }
2038 Action::PromptPlayMacro => {
2039 self.start_prompt("Play macro (0-9): ".to_string(), PromptType::PlayMacro);
2040 }
2041 Action::PlayLastMacro => {
2042 if let Some(key) = self.active_window_mut().macros.last_register() {
2043 self.play_macro(key);
2044 } else {
2045 self.set_status_message(t!("status.no_macro_recorded").to_string());
2046 }
2047 }
2048 Action::PromptSetBookmark => {
2049 self.start_prompt("Set bookmark (0-9): ".to_string(), PromptType::SetBookmark);
2050 }
2051 Action::PromptJumpToBookmark => {
2052 self.start_prompt(
2053 "Jump to bookmark (0-9): ".to_string(),
2054 PromptType::JumpToBookmark,
2055 );
2056 }
2057 Action::CompositeNextHunk => {
2058 let buf = self.active_buffer();
2059 self.active_window_mut().composite_next_hunk_active(buf);
2060 }
2061 Action::CompositePrevHunk => {
2062 let buf = self.active_buffer();
2063 self.active_window_mut().composite_prev_hunk_active(buf);
2064 }
2065 Action::None => {}
2066 Action::DeleteBackward => {
2067 if self.active_window().is_editing_disabled() {
2068 self.set_status_message(t!("buffer.editing_disabled").to_string());
2069 return Ok(());
2070 }
2071 if let Some(events) = self
2073 .active_window_mut()
2074 .action_to_events(Action::DeleteBackward)
2075 {
2076 if events.len() > 1 {
2077 let description = "Delete backward".to_string();
2079 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description)
2080 {
2081 self.active_event_log_mut().append(bulk_edit);
2082 }
2083 } else {
2084 for event in events {
2085 self.active_event_log_mut().append(event.clone());
2086 self.apply_event_to_active_buffer(&event);
2087 }
2088 }
2089 }
2090 }
2091 Action::PluginAction(action_name) => {
2092 tracing::debug!("handle_action: PluginAction('{}')", action_name);
2093 #[cfg(feature = "plugins")]
2096 {
2097 let result = self
2098 .plugin_manager
2099 .read()
2100 .unwrap()
2101 .execute_action_async(&action_name);
2102 if let Some(result) = result {
2103 match result {
2104 Ok(receiver) => {
2105 self.pending_plugin_actions
2107 .push((action_name.clone(), receiver));
2108 }
2109 Err(e) => {
2110 self.set_status_message(
2111 t!("view.plugin_error", error = e.to_string()).to_string(),
2112 );
2113 tracing::error!("Plugin action error: {}", e);
2114 }
2115 }
2116 } else {
2117 self.set_status_message(
2118 t!("status.plugin_manager_unavailable").to_string(),
2119 );
2120 }
2121 }
2122 #[cfg(not(feature = "plugins"))]
2123 {
2124 let _ = action_name;
2125 self.set_status_message(
2126 "Plugins not available (compiled without plugin support)".to_string(),
2127 );
2128 }
2129 }
2130 Action::LoadPluginFromBuffer => {
2131 #[cfg(feature = "plugins")]
2132 {
2133 let buffer_id = self.active_buffer();
2134 let state = self.active_state();
2135 let buffer = &state.buffer;
2136 let total = buffer.total_bytes();
2137 let content =
2138 String::from_utf8_lossy(&buffer.slice_bytes(0..total)).to_string();
2139
2140 let is_ts = buffer
2142 .file_path()
2143 .and_then(|p| p.extension())
2144 .and_then(|e| e.to_str())
2145 .map(|e| e == "ts" || e == "tsx")
2146 .unwrap_or(true);
2147
2148 let name = buffer
2150 .file_path()
2151 .and_then(|p| p.file_name())
2152 .and_then(|s| s.to_str())
2153 .map(|s| s.to_string())
2154 .unwrap_or_else(|| "buffer-plugin".to_string());
2155
2156 let load_result = self
2157 .plugin_manager
2158 .read()
2159 .unwrap()
2160 .load_plugin_from_source(&content, &name, is_ts);
2161 match load_result {
2162 Ok(()) => {
2163 self.set_status_message(format!(
2164 "Plugin '{}' loaded from buffer",
2165 name
2166 ));
2167 }
2168 Err(e) => {
2169 self.set_status_message(format!("Failed to load plugin: {}", e));
2170 tracing::error!("LoadPluginFromBuffer error: {}", e);
2171 }
2172 }
2173
2174 self.setup_plugin_dev_lsp(buffer_id, &content);
2176 }
2177 #[cfg(not(feature = "plugins"))]
2178 {
2179 self.set_status_message(
2180 "Plugins not available (compiled without plugin support)".to_string(),
2181 );
2182 }
2183 }
2184 Action::InitReload => {
2185 self.load_init_script(true);
2190 self.fire_plugins_loaded_hook();
2193 }
2194 Action::InitEdit => {
2195 let config_dir = self.dir_context.config_dir.clone();
2198 match crate::init_script::ensure_starter(&config_dir) {
2199 Ok(path) => {
2200 let declarations =
2210 self.plugin_manager.read().unwrap().plugin_declarations();
2211 crate::init_script::write_plugin_declarations(&config_dir, &declarations);
2212 match self.open_file(&path) {
2213 Ok(_) => {
2214 self.set_status_message(format!("init.ts: {}", path.display()));
2215 }
2216 Err(e) => {
2217 self.set_status_message(format!("init.ts: open failed: {e}"));
2218 }
2219 }
2220 }
2221 Err(e) => {
2222 self.set_status_message(format!("init.ts: create failed: {e}"));
2223 }
2224 }
2225 }
2226 Action::InitCheck => {
2227 let report = crate::init_script::check(&self.dir_context.config_dir);
2230 if report.ok && report.diagnostics.is_empty() {
2231 self.set_status_message("init.ts: ok".into());
2232 } else if !report.ok {
2233 let first = report
2234 .diagnostics
2235 .first()
2236 .map(|d| format!("{}:{}: {}", d.line, d.column, d.message))
2237 .unwrap_or_else(|| "unknown error".into());
2238 self.set_status_message(format!(
2239 "init.ts: {} error(s) — first: {first}",
2240 report.diagnostics.len()
2241 ));
2242 } else {
2243 self.set_status_message(format!(
2244 "init.ts: {} warning(s)",
2245 report.diagnostics.len()
2246 ));
2247 }
2248 }
2249 Action::OpenTerminal => {
2250 self.open_terminal();
2251 }
2252 Action::CloseTerminal => {
2253 self.close_terminal();
2254 }
2255 Action::FocusTerminal => {
2256 if self
2258 .active_window()
2259 .is_terminal_buffer(self.active_buffer())
2260 {
2261 self.active_window_mut().terminal_mode = true;
2262 self.active_window_mut().key_context = KeyContext::Terminal;
2263 self.set_status_message(t!("status.terminal_mode_enabled").to_string());
2264 }
2265 }
2266 Action::TerminalEscape => {
2267 if self.active_window().terminal_mode {
2269 self.active_window_mut().terminal_mode = false;
2270 self.active_window_mut().key_context = KeyContext::Normal;
2271 self.set_status_message(t!("status.terminal_mode_disabled").to_string());
2272 }
2273 }
2274 Action::ToggleKeyboardCapture => {
2275 if self.active_window().terminal_mode {
2277 self.active_window_mut().keyboard_capture =
2278 !self.active_window_mut().keyboard_capture;
2279 if self.active_window_mut().keyboard_capture {
2280 self.set_status_message(
2281 "Keyboard capture ON - all keys go to terminal (F9 to toggle)"
2282 .to_string(),
2283 );
2284 } else {
2285 self.set_status_message(
2286 "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
2287 );
2288 }
2289 }
2290 }
2291 Action::TerminalPaste => {
2292 if self.active_window().terminal_mode {
2294 if let Some(text) = self.clipboard.paste() {
2295 self.active_window_mut()
2296 .send_terminal_input(text.as_bytes());
2297 }
2298 }
2299 }
2300 Action::ShellCommand => {
2301 self.start_shell_command_prompt(false);
2303 }
2304 Action::ShellCommandReplace => {
2305 self.start_shell_command_prompt(true);
2307 }
2308 Action::OpenSettings => {
2309 self.open_settings();
2310 }
2311 Action::CloseSettings => {
2312 let has_changes = self
2314 .settings_state
2315 .as_ref()
2316 .is_some_and(|s| s.has_changes());
2317 if has_changes {
2318 if let Some(ref mut state) = self.settings_state {
2320 state.show_confirm_dialog();
2321 }
2322 } else {
2323 self.close_settings(false);
2324 }
2325 }
2326 Action::SettingsSave => {
2327 self.save_settings();
2328 }
2329 Action::SettingsReset => {
2330 if let Some(ref mut state) = self.settings_state {
2331 state.reset_current_to_default();
2332 }
2333 }
2334 Action::SettingsInherit => {
2335 if let Some(ref mut state) = self.settings_state {
2336 state.set_current_to_null();
2337 }
2338 }
2339 Action::SettingsToggleFocus => {
2340 if let Some(ref mut state) = self.settings_state {
2341 state.toggle_focus();
2342 }
2343 }
2344 Action::SettingsActivate => {
2345 self.settings_activate_current();
2346 }
2347 Action::SettingsSearch => {
2348 if let Some(ref mut state) = self.settings_state {
2349 state.start_search();
2350 }
2351 }
2352 Action::SettingsHelp => {
2353 if let Some(ref mut state) = self.settings_state {
2354 state.toggle_help();
2355 }
2356 }
2357 Action::SettingsIncrement => {
2358 self.settings_increment_current();
2359 }
2360 Action::SettingsDecrement => {
2361 self.settings_decrement_current();
2362 }
2363 Action::CalibrateInput => {
2364 self.open_calibration_wizard();
2365 }
2366 Action::EventDebug => {
2367 self.active_window_mut().open_event_debug();
2368 }
2369 Action::SuspendProcess => {
2370 self.request_suspend();
2371 }
2372 Action::OpenKeybindingEditor => {
2373 self.open_keybinding_editor();
2374 }
2375 Action::PromptConfirm => {
2376 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
2377 use super::prompt_actions::PromptResult;
2378 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
2379 PromptResult::ExecuteAction(action) => {
2380 return self.handle_action(action);
2381 }
2382 PromptResult::EarlyReturn => {
2383 return Ok(());
2384 }
2385 PromptResult::Done => {}
2386 }
2387 }
2388 }
2389 Action::PromptConfirmWithText(ref text) => {
2390 if let Some(ref mut prompt) = self.active_window_mut().prompt {
2392 prompt.set_input(text.clone());
2393 self.update_prompt_suggestions();
2394 }
2395 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
2396 use super::prompt_actions::PromptResult;
2397 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
2398 PromptResult::ExecuteAction(action) => {
2399 return self.handle_action(action);
2400 }
2401 PromptResult::EarlyReturn => {
2402 return Ok(());
2403 }
2404 PromptResult::Done => {}
2405 }
2406 }
2407 }
2408 Action::PopupConfirm => {
2409 use super::popup_actions::PopupConfirmResult;
2410 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
2411 return Ok(());
2412 }
2413 }
2414 Action::PopupCancel => {
2415 self.handle_popup_cancel();
2416 }
2417 Action::PopupFocus => {
2418 self.handle_popup_focus();
2419 }
2420 Action::CompletionAccept => {
2421 use super::popup_actions::PopupConfirmResult;
2422 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
2423 return Ok(());
2424 }
2425 }
2426 Action::CompletionDismiss => {
2427 self.handle_popup_cancel();
2428 }
2429 Action::InsertChar(c) => {
2430 if self.is_prompting() {
2431 return self.handle_insert_char_prompt(c);
2432 } else if self.active_window_mut().key_context == KeyContext::FileExplorer {
2433 self.active_window_mut().file_explorer_search_push_char(c);
2434 } else {
2435 self.handle_insert_char_editor(c)?;
2436 }
2437 }
2438 Action::PromptCopy => {
2440 if let Some(prompt) = &self.active_window_mut().prompt {
2441 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
2442 if !text.is_empty() {
2443 self.clipboard.copy(text);
2444 self.set_status_message(t!("clipboard.copied").to_string());
2445 }
2446 }
2447 }
2448 Action::PromptCut => {
2449 if let Some(prompt) = &self.active_window_mut().prompt {
2450 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
2451 if !text.is_empty() {
2452 self.clipboard.copy(text);
2453 }
2454 }
2455 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
2456 if prompt.has_selection() {
2457 prompt.delete_selection();
2458 } else {
2459 prompt.clear();
2460 }
2461 }
2462 self.set_status_message(t!("clipboard.cut").to_string());
2463 self.update_prompt_suggestions();
2464 }
2465 Action::PromptPaste => {
2466 if let Some(text) = self.clipboard.paste() {
2467 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
2468 prompt.insert_str(&text);
2469 }
2470 self.update_prompt_suggestions();
2471 }
2472 }
2473 _ => {
2474 self.apply_action_as_events(action)?;
2480 }
2481 }
2482
2483 Ok(())
2484 }
2485
2486 fn dispatch_floating_widget_key(
2497 &mut self,
2498 code: crossterm::event::KeyCode,
2499 modifiers: crossterm::event::KeyModifiers,
2500 ) -> bool {
2501 use crossterm::event::{KeyCode, KeyModifiers};
2502 let panel_id = match self.floating_widget_panel.as_ref() {
2503 Some(fwp) => fwp.panel_id,
2504 None => return false,
2505 };
2506 let key_name: Option<&str> = match code {
2507 KeyCode::Esc => {
2508 let mode_has_binding = self
2517 .active_window()
2518 .editor_mode
2519 .as_ref()
2520 .map(|mode_name| {
2521 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
2522 let mode_ctx =
2523 crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
2524 let keybindings = self.keybindings.read().unwrap();
2525 keybindings.has_explicit_binding(&key_event, &mode_ctx)
2526 })
2527 .unwrap_or(false);
2528 if mode_has_binding {
2529 return false;
2530 }
2531 let widget_key = self
2532 .widget_registry
2533 .get(panel_id)
2534 .map(|p| p.focus_key.clone())
2535 .unwrap_or_default();
2536 if self
2537 .plugin_manager
2538 .read()
2539 .unwrap()
2540 .has_hook_handlers("widget_event")
2541 {
2542 self.plugin_manager.read().unwrap().run_hook(
2543 "widget_event",
2544 crate::services::plugins::hooks::HookArgs::WidgetEvent {
2545 panel_id,
2546 widget_key,
2547 event_type: "cancel".to_string(),
2548 payload: serde_json::json!({}),
2549 },
2550 );
2551 }
2552 self.floating_widget_panel = None;
2553 let _ = self.widget_registry.unmount(panel_id);
2554 return true;
2555 }
2556 KeyCode::Tab => Some(if modifiers.contains(KeyModifiers::SHIFT) {
2557 "Shift+Tab"
2558 } else {
2559 "Tab"
2560 }),
2561 KeyCode::BackTab => Some("Shift+Tab"),
2562 KeyCode::Enter => Some("Enter"),
2563 KeyCode::Backspace => Some("Backspace"),
2564 KeyCode::Delete => Some("Delete"),
2565 KeyCode::Home => Some("Home"),
2566 KeyCode::End => Some("End"),
2567 KeyCode::Left => Some("Left"),
2568 KeyCode::Right => Some("Right"),
2569 KeyCode::Up => Some("Up"),
2570 KeyCode::Down => Some("Down"),
2571 KeyCode::PageUp => Some("PageUp"),
2572 KeyCode::PageDown => Some("PageDown"),
2573 _ => None,
2574 };
2575 if let Some(name) = key_name {
2576 let mode_has_binding = self
2595 .active_window()
2596 .editor_mode
2597 .as_ref()
2598 .map(|mode_name| {
2599 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
2600 let mode_ctx =
2601 crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
2602 let keybindings = self.keybindings.read().unwrap();
2603 keybindings.has_explicit_binding(&key_event, &mode_ctx)
2604 })
2605 .unwrap_or(false);
2606 if mode_has_binding {
2607 return false;
2608 }
2609 self.handle_widget_command(
2610 panel_id,
2611 fresh_core::api::WidgetAction::Key {
2612 key: name.to_string(),
2613 },
2614 );
2615 return true;
2616 }
2617 if let KeyCode::Char(c) = code {
2618 {
2629 let mode_has_binding = self
2630 .active_window()
2631 .editor_mode
2632 .as_ref()
2633 .map(|mode_name| {
2634 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
2635 let mode_ctx =
2636 crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
2637 let keybindings = self.keybindings.read().unwrap();
2638 keybindings.has_explicit_binding(&key_event, &mode_ctx)
2639 })
2640 .unwrap_or(false);
2641 if mode_has_binding {
2642 return false;
2643 }
2644 }
2645 if modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) {
2651 return true;
2652 }
2653 let ch = if modifiers.contains(KeyModifiers::SHIFT) {
2654 c.to_uppercase().next().unwrap_or(c)
2655 } else {
2656 c
2657 };
2658 if ch == ' ' {
2669 self.handle_widget_command(
2670 panel_id,
2671 fresh_core::api::WidgetAction::Key {
2672 key: "Space".to_string(),
2673 },
2674 );
2675 return true;
2676 }
2677 self.handle_widget_command(
2678 panel_id,
2679 fresh_core::api::WidgetAction::TextInputChar {
2680 text: ch.to_string(),
2681 },
2682 );
2683 return true;
2684 }
2685 true
2690 }
2691}