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 fn install_quickfix_in_dock(
91 &mut self,
92 query: String,
93 matches: Vec<crate::services::live_grep_state::GrepMatch>,
94 ) {
95 use crate::model::event::SplitDirection;
96 use crate::primitives::text_property::TextPropertyEntry;
97 use crate::view::split::SplitRole;
98
99 let mut entries = Vec::with_capacity(matches.len() + 2);
102 let header = format!("Quickfix: {} ({} matches)\n", query, matches.len());
103 entries.push(TextPropertyEntry::text(header));
104 for m in &matches {
105 let line = format!("{}:{}:{} {}\n", m.file, m.line, m.column, m.content.trim());
106 entries.push(TextPropertyEntry::text(line));
107 }
108
109 let panel_key = "quickfix".to_string();
112 if let Some(&existing) = self.panel_ids().get(&panel_key) {
113 if self
114 .windows
115 .get(&self.active_window)
116 .map(|w| &w.buffers)
117 .expect("active window present")
118 .contains_key(&existing)
119 {
120 if let Err(e) = self.set_virtual_buffer_content(existing, entries) {
121 tracing::error!("Failed to update quickfix buffer: {}", e);
122 return;
123 }
124 if let Some(dock_leaf) = self
126 .windows
127 .get(&self.active_window)
128 .and_then(|w| w.buffers.splits())
129 .map(|(mgr, _)| mgr)
130 .expect("active window must have a populated split layout")
131 .find_leaf_by_role(SplitRole::UtilityDock)
132 {
133 self.windows
134 .get_mut(&self.active_window)
135 .and_then(|w| w.split_manager_mut())
136 .expect("active window must have a populated split layout")
137 .set_active_split(dock_leaf);
138 self.active_window_mut()
139 .set_pane_buffer(dock_leaf, existing);
140 }
141 self.set_status_message(format!("Quickfix updated: {} matches", matches.len()));
142 return;
143 }
144 self.panel_ids_mut().remove(&panel_key);
146 }
147
148 let buffer_id = self.active_window_mut().create_virtual_buffer_detached(
154 "*Quickfix*".to_string(),
155 "quickfix-list".to_string(),
156 true,
157 );
158 if let Some(state) = self
159 .windows
160 .get_mut(&self.active_window)
161 .map(|w| &mut w.buffers)
162 .expect("active window present")
163 .get_mut(&buffer_id)
164 {
165 state.margins.configure_for_line_numbers(false);
166 state.show_cursors = true;
167 state.editing_disabled = true;
168 }
169 self.panel_ids_mut().insert(panel_key, buffer_id);
170 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
171 tracing::error!("Failed to set quickfix buffer content: {}", e);
172 return;
173 }
174
175 if let Some(dock_leaf) = self
179 .windows
180 .get(&self.active_window)
181 .and_then(|w| w.buffers.splits())
182 .map(|(mgr, _)| mgr)
183 .expect("active window must have a populated split layout")
184 .find_leaf_by_role(SplitRole::UtilityDock)
185 {
186 self.windows
187 .get_mut(&self.active_window)
188 .and_then(|w| w.split_manager_mut())
189 .expect("active window must have a populated split layout")
190 .set_active_split(dock_leaf);
191 self.active_window_mut()
192 .set_pane_buffer(dock_leaf, buffer_id);
193 let line_numbers = self.config.editor.line_numbers;
201 let highlight_current_line = self.config.editor.highlight_current_line;
202 let line_wrap = self.active_window().resolve_line_wrap_for_buffer(buffer_id);
203 let wrap_indent = self.config.editor.wrap_indent;
204 let wrap_column = self
205 .active_window()
206 .resolve_wrap_column_for_buffer(buffer_id);
207 let rulers = self.config.editor.rulers.clone();
208 if let Some(view_state) = self
209 .windows
210 .get_mut(&self.active_window)
211 .and_then(|w| w.split_view_states_mut())
212 .expect("active window must have a populated split layout")
213 .get_mut(&dock_leaf)
214 {
215 let buf_state = view_state.ensure_buffer_state(buffer_id);
216 buf_state.apply_config_defaults(
217 line_numbers,
218 highlight_current_line,
219 line_wrap,
220 wrap_indent,
221 wrap_column,
222 rulers,
223 );
224 buf_state.show_line_numbers = false;
225 }
226 } else {
227 match self
230 .windows
231 .get_mut(&self.active_window)
232 .and_then(|w| w.split_manager_mut())
233 .expect("active window must have a populated split layout")
234 .split_root_positioned(
235 SplitDirection::Horizontal,
236 buffer_id,
237 0.7,
238 false, ) {
240 Ok(new_leaf) => {
241 let mut view_state = crate::view::split::SplitViewState::with_buffer(
242 self.terminal_width,
243 self.terminal_height,
244 buffer_id,
245 );
246 view_state.apply_config_defaults(
247 self.config.editor.line_numbers,
248 self.config.editor.highlight_current_line,
249 self.active_window().resolve_line_wrap_for_buffer(buffer_id),
250 self.config.editor.wrap_indent,
251 self.active_window()
252 .resolve_wrap_column_for_buffer(buffer_id),
253 self.config.editor.rulers.clone(),
254 );
255 view_state.ensure_buffer_state(buffer_id).show_line_numbers = false;
256 self.windows
257 .get_mut(&self.active_window)
258 .and_then(|w| w.split_view_states_mut())
259 .expect("active window must have a populated split layout")
260 .insert(new_leaf, view_state);
261 self.windows
262 .get_mut(&self.active_window)
263 .and_then(|w| w.split_manager_mut())
264 .expect("active window must have a populated split layout")
265 .set_leaf_role(new_leaf, Some(SplitRole::UtilityDock));
266 self.windows
267 .get_mut(&self.active_window)
268 .and_then(|w| w.split_manager_mut())
269 .expect("active window must have a populated split layout")
270 .set_active_split(new_leaf);
271 }
272 Err(e) => {
273 tracing::error!("Failed to create dock split for quickfix: {}", e);
274 return;
275 }
276 }
277 }
278
279 self.set_status_message(format!(
280 "Quickfix exported: {} matches in dock",
281 matches.len()
282 ));
283 }
284
285 pub(crate) fn popups_capture_keys(&self) -> bool {
304 use crate::input::keybindings::KeyContext;
305 if matches!(self.active_window().key_context, KeyContext::FileExplorer) {
306 return false;
307 }
308 self.topmost_popup_focused()
309 }
310
311 pub(crate) fn topmost_popup_focused(&self) -> bool {
316 if let Some(popup) = self.global_popups.top() {
317 return popup.focused;
318 }
319 if let Some(popup) = self.active_state().popups.top() {
320 return popup.focused;
321 }
322 false
325 }
326
327 pub(crate) fn resolve_unfocused_popup_action(
337 &self,
338 event: &crossterm::event::KeyEvent,
339 ) -> Option<crate::input::keybindings::Action> {
340 use crate::input::keybindings::{Action, KeyContext};
341
342 let popup_visible =
343 self.global_popups.is_visible() || self.active_state().popups.is_visible();
344 if !popup_visible || self.topmost_popup_focused() {
345 return None;
346 }
347
348 if self.settings_state.as_ref().is_some_and(|s| s.visible)
354 || self.menu_state.active_menu.is_some()
355 || self.is_prompting()
356 {
357 return None;
358 }
359
360 let kb = self.keybindings.read().ok()?;
361
362 let popup_focus_match = matches!(
371 kb.resolve_in_context_only(event, self.active_window().key_context.clone()),
372 Some(Action::PopupFocus),
373 );
374 if popup_focus_match {
375 return Some(Action::PopupFocus);
376 }
377
378 let resolved_popup = kb.resolve_in_context_only(event, KeyContext::Popup);
384 match resolved_popup {
385 Some(action @ (Action::PopupCancel | Action::PopupFocus)) => Some(action),
386 _ => None,
387 }
388 }
389
390 pub(crate) fn resolve_completion_popup_action(
396 &self,
397 event: &crossterm::event::KeyEvent,
398 ) -> Option<crate::input::keybindings::Action> {
399 use crate::input::keybindings::{Action, KeyContext};
400 use crate::view::popup::PopupKind;
401
402 let topmost_kind = if self.global_popups.is_visible() {
403 self.global_popups.top().map(|p| p.kind)
404 } else if self.active_state().popups.is_visible() {
405 self.active_state().popups.top().map(|p| p.kind)
406 } else {
407 None
408 };
409
410 if topmost_kind != Some(PopupKind::Completion) {
411 return None;
412 }
413
414 match self
415 .keybindings
416 .read()
417 .unwrap()
418 .resolve_in_context_only(event, KeyContext::Completion)
419 {
420 Some(action @ (Action::CompletionAccept | Action::CompletionDismiss)) => Some(action),
421 _ => None,
422 }
423 }
424
425 pub fn get_key_context(&self) -> crate::input::keybindings::KeyContext {
427 use crate::input::keybindings::KeyContext;
428
429 if self.settings_state.as_ref().is_some_and(|s| s.visible) {
433 KeyContext::Settings
434 } else if self.menu_state.active_menu.is_some() {
435 KeyContext::Menu
436 } else if self.is_prompting() {
437 KeyContext::Prompt
438 } else if self.popups_capture_keys()
439 && (self.global_popups.is_visible() || self.active_state().popups.is_visible())
440 {
441 KeyContext::Popup
442 } else if self.floating_widget_panel.is_some() {
443 KeyContext::Normal
453 } else if self
454 .active_window()
455 .is_composite_buffer(self.active_buffer())
456 {
457 KeyContext::CompositeBuffer
458 } else {
459 self.active_window().key_context.clone()
461 }
462 }
463
464 pub fn handle_key(
467 &mut self,
468 code: crossterm::event::KeyCode,
469 modifiers: crossterm::event::KeyModifiers,
470 ) -> AnyhowResult<()> {
471 use crate::input::keybindings::Action;
472
473 let _t_total = std::time::Instant::now();
474
475 tracing::trace!(
476 "Editor.handle_key: code={:?}, modifiers={:?}",
477 code,
478 modifiers
479 );
480
481 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
483
484 if self.active_window().is_event_debug_active() {
488 self.active_window_mut()
489 .handle_event_debug_input(&key_event);
490 return Ok(());
491 }
492
493 if self.dispatch_terminal_input(&key_event).is_some() {
498 return Ok(());
499 }
500
501 if self.try_resolve_next_key_callback(&key_event) {
508 return Ok(());
509 }
510
511 if self.floating_widget_panel.is_some()
518 && self.dispatch_floating_widget_key(code, modifiers)
519 {
520 return Ok(());
521 }
522
523 let active_split = self.effective_active_split();
532 if let Some(view_state) = self
533 .windows
534 .get_mut(&self.active_window)
535 .and_then(|w| w.split_view_states_mut())
536 .expect("active window must have a populated split layout")
537 .get_mut(&active_split)
538 {
539 view_state.viewport.clear_skip_ensure_visible();
540 }
541
542 if self.active_window_mut().theme_info_popup.is_some() {
544 self.active_window_mut().theme_info_popup = None;
545 }
546
547 if self
548 .active_window_mut()
549 .file_explorer_context_menu
550 .is_some()
551 {
552 if let Some(result) = self.handle_file_explorer_context_menu_key(code, modifiers) {
553 return result;
554 }
555 }
556
557 let mut context = self.get_key_context();
559
560 let popup_visible_on_screen =
570 self.global_popups.is_visible() || self.active_state().popups.is_visible();
571 if popup_visible_on_screen {
572 let (is_transient_popup, has_selection) = {
576 let popup = self
577 .global_popups
578 .top()
579 .or_else(|| self.active_state().popups.top());
580 (
581 popup.is_some_and(|p| p.transient),
582 popup.is_some_and(|p| p.has_selection()),
583 )
584 };
585
586 let is_copy_key = key_event.code == crossterm::event::KeyCode::Char('c')
588 && key_event
589 .modifiers
590 .contains(crossterm::event::KeyModifiers::CONTROL);
591
592 let resolved_action = self
597 .keybindings
598 .read()
599 .ok()
600 .map(|kb| kb.resolve(&key_event, context.clone()));
601 let is_focus_popup_key = matches!(
602 resolved_action,
603 Some(crate::input::keybindings::Action::PopupFocus)
604 );
605
606 if is_transient_popup && !(has_selection && is_copy_key) && !is_focus_popup_key {
607 self.hide_popup();
609 tracing::debug!("Dismissed transient popup on key press");
610 context = self.get_key_context();
612 }
613 }
614
615 if let Some(action) = self.resolve_unfocused_popup_action(&key_event) {
621 self.handle_action(action)?;
622 return Ok(());
623 }
624
625 if self.dispatch_modal_input(&key_event).is_some() {
627 return Ok(());
628 }
629
630 if context != self.get_key_context() {
633 context = self.get_key_context();
634 }
635
636 let should_check_mode_bindings =
640 matches!(context, crate::input::keybindings::KeyContext::Normal);
641
642 if should_check_mode_bindings {
643 let effective_mode = self.effective_mode().map(|s| s.to_owned());
646
647 if let Some(ref mode_name) = effective_mode {
648 let mode_ctx = crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
649 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
650
651 let (chord_result, resolved_action) = {
653 let keybindings = self.keybindings.read().unwrap();
654 let chord_result = keybindings.resolve_chord(
655 &self.active_window().chord_state,
656 &key_event,
657 mode_ctx.clone(),
658 );
659 let resolved = keybindings.resolve(&key_event, mode_ctx);
660 (chord_result, resolved)
661 };
662 match chord_result {
663 crate::input::keybindings::ChordResolution::Complete(action) => {
664 tracing::debug!("Mode chord resolved to action: {:?}", action);
665 self.active_window_mut().chord_state.clear();
666 return self.handle_action(action);
667 }
668 crate::input::keybindings::ChordResolution::Partial => {
669 tracing::debug!("Potential chord prefix in mode '{}'", mode_name);
670 self.active_window_mut().chord_state.push((code, modifiers));
671 return Ok(());
672 }
673 crate::input::keybindings::ChordResolution::NoMatch => {
674 if !self.active_window_mut().chord_state.is_empty() {
675 tracing::debug!("Chord sequence abandoned in mode, clearing state");
676 self.active_window_mut().chord_state.clear();
677 }
678 }
679 }
680
681 if resolved_action != Action::None {
683 return self.handle_action(resolved_action);
684 }
685 }
686
687 if let Some(ref mode_name) = effective_mode {
699 if self.mode_registry.allows_text_input(mode_name) {
700 if let KeyCode::Char(c) = code {
701 let ch = if modifiers.contains(KeyModifiers::SHIFT) {
702 c.to_uppercase().next().unwrap_or(c)
703 } else {
704 c
705 };
706 if !modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) {
707 let action_name = format!("mode_text_input:{}", ch);
708 return self.handle_action(Action::PluginAction(action_name));
709 }
710 }
711 let normal_ctx = crate::input::keybindings::KeyContext::Normal;
720 let resolved = {
721 let keybindings = self.keybindings.read().unwrap();
722 keybindings.resolve(&key_event, normal_ctx)
723 };
724 match resolved {
725 Action::Paste | Action::Copy | Action::Cut | Action::SelectAll => {
726 return self.handle_action(resolved);
727 }
728 _ => {}
729 }
730 if modifiers.contains(KeyModifiers::SHIFT) {
739 let buffer_id = self.active_buffer();
740 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id)
741 {
742 let ctrl = modifiers.contains(KeyModifiers::CONTROL);
743 let handled = match code {
744 KeyCode::Left if ctrl => self
745 .with_focused_text_editor(panel_id, |e| {
746 e.move_word_left_selecting()
747 }),
748 KeyCode::Right if ctrl => self
749 .with_focused_text_editor(panel_id, |e| {
750 e.move_word_right_selecting()
751 }),
752 KeyCode::Left => self.with_focused_text_editor(panel_id, |e| {
753 e.move_left_selecting()
754 }),
755 KeyCode::Right => self.with_focused_text_editor(panel_id, |e| {
756 e.move_right_selecting()
757 }),
758 KeyCode::Up => self
759 .with_focused_text_editor(panel_id, |e| e.move_up_selecting()),
760 KeyCode::Down => self.with_focused_text_editor(panel_id, |e| {
761 e.move_down_selecting()
762 }),
763 KeyCode::Home => self.with_focused_text_editor(panel_id, |e| {
764 e.move_home_selecting()
765 }),
766 KeyCode::End => self
767 .with_focused_text_editor(panel_id, |e| e.move_end_selecting()),
768 _ => false,
769 };
770 if matches!(
776 code,
777 KeyCode::Left
778 | KeyCode::Right
779 | KeyCode::Up
780 | KeyCode::Down
781 | KeyCode::Home
782 | KeyCode::End
783 ) {
784 let _ = handled;
785 return Ok(());
786 }
787 }
788 }
789 tracing::debug!("Blocking unbound key in text-input mode '{}'", mode_name);
790 return Ok(());
791 }
792 }
793 if let Some(ref mode_name) = self.active_window().editor_mode {
794 if self.mode_registry.is_read_only(mode_name) {
795 tracing::debug!("Ignoring unbound key in read-only mode '{}'", mode_name);
796 return Ok(());
797 }
798 tracing::debug!(
799 "Mode '{}' is not read-only, allowing key through",
800 mode_name
801 );
802 }
803 }
804
805 {
812 let active_buf = self.active_buffer();
813 let active_split = self.effective_active_split();
814 if self.active_window().is_composite_buffer(active_buf) {
815 if let Some(handled) =
816 self.try_route_composite_key(active_split, active_buf, &key_event)
817 {
818 return handled;
819 }
820 }
821 }
822
823 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
825 let (chord_result, action) = {
826 let keybindings = self.keybindings.read().unwrap();
827 let chord_result = keybindings.resolve_chord(
828 &self.active_window().chord_state,
829 &key_event,
830 context.clone(),
831 );
832 let action = keybindings.resolve(&key_event, context.clone());
833 (chord_result, action)
834 };
835
836 match chord_result {
837 crate::input::keybindings::ChordResolution::Complete(action) => {
838 tracing::debug!("Complete chord match -> Action: {:?}", action);
840 self.active_window_mut().chord_state.clear();
841 return self.handle_action(action);
842 }
843 crate::input::keybindings::ChordResolution::Partial => {
844 tracing::debug!("Partial chord match - waiting for next key");
846 self.active_window_mut().chord_state.push((code, modifiers));
847 return Ok(());
848 }
849 crate::input::keybindings::ChordResolution::NoMatch => {
850 if !self.active_window_mut().chord_state.is_empty() {
852 tracing::debug!("Chord sequence abandoned, clearing state");
853 self.active_window_mut().chord_state.clear();
854 }
855 }
856 }
857
858 tracing::trace!("Context: {:?} -> Action: {:?}", context, action);
860
861 match action {
864 Action::LspCompletion
865 | Action::LspGotoDefinition
866 | Action::LspReferences
867 | Action::LspHover
868 | Action::None => {
869 }
871 _ => {
872 self.active_window_mut().cancel_pending_lsp_requests();
874 }
875 }
876
877 self.handle_action(action)
881 }
882
883 pub(crate) fn handle_action(&mut self, action: Action) -> AnyhowResult<()> {
886 use crate::input::keybindings::Action;
887
888 self.record_macro_action(&action);
890
891 if !matches!(action, Action::DabbrevExpand) {
893 self.reset_dabbrev_state();
894 }
895
896 match action {
897 Action::Quit => self.quit(),
898 Action::ForceQuit => {
899 self.should_quit = true;
900 }
901 Action::Detach => {
902 self.should_detach = true;
903 }
904 Action::Save => {
905 if self.active_state().buffer.file_path().is_none() {
907 self.start_prompt_with_initial_text(
908 t!("file.save_as_prompt").to_string(),
909 PromptType::SaveFileAs,
910 String::new(),
911 );
912 self.init_file_open_state();
913 } else if self.check_save_conflict().is_some() {
914 self.start_prompt(
916 t!("file.file_changed_prompt").to_string(),
917 PromptType::ConfirmSaveConflict,
918 );
919 } else if let Err(e) = self.save() {
920 let msg = format!("{}", e);
921 self.active_window_mut().status_message =
922 Some(t!("file.save_failed", error = &msg).to_string());
923 }
924 }
925 Action::SaveAs => {
926 let current_path = self
928 .active_state()
929 .buffer
930 .file_path()
931 .map(|p| {
932 p.strip_prefix(&self.working_dir)
934 .unwrap_or(p)
935 .to_string_lossy()
936 .to_string()
937 })
938 .unwrap_or_default();
939 self.start_prompt_with_initial_text(
940 t!("file.save_as_prompt").to_string(),
941 PromptType::SaveFileAs,
942 current_path,
943 );
944 self.init_file_open_state();
945 }
946 Action::Open => {
947 self.start_prompt(t!("file.open_prompt").to_string(), PromptType::OpenFile);
948 self.prefill_open_file_prompt();
949 self.init_file_open_state();
950 }
951 Action::SwitchProject => {
952 self.start_prompt(
953 t!("file.switch_project_prompt").to_string(),
954 PromptType::SwitchProject,
955 );
956 self.init_folder_open_state();
957 }
958 Action::GotoLine => {
959 let has_line_index = self
960 .buffers()
961 .get(&self.active_buffer())
962 .is_none_or(|s| s.buffer.line_count().is_some());
963 if has_line_index {
964 self.start_prompt(
965 t!("file.goto_line_prompt").to_string(),
966 PromptType::GotoLine,
967 );
968 } else {
969 self.start_prompt(
970 t!("goto.scan_confirm_prompt", yes = "y", no = "N").to_string(),
971 PromptType::GotoLineScanConfirm,
972 );
973 }
974 }
975 Action::ScanLineIndex => {
976 self.start_incremental_line_scan(false);
977 }
978 Action::New => {
979 self.new_buffer();
980 }
981 Action::Close | Action::CloseTab => {
982 self.close_tab();
987 }
988 Action::Revert => {
989 if self.active_state().buffer.is_modified() {
991 let revert_key = t!("prompt.key.revert").to_string();
992 let cancel_key = t!("prompt.key.cancel").to_string();
993 self.start_prompt(
994 t!(
995 "prompt.revert_confirm",
996 revert_key = revert_key,
997 cancel_key = cancel_key
998 )
999 .to_string(),
1000 PromptType::ConfirmRevert,
1001 );
1002 } else {
1003 if let Err(e) = self.revert_file() {
1005 self.set_status_message(
1006 t!("error.failed_to_revert", error = e.to_string()).to_string(),
1007 );
1008 }
1009 }
1010 }
1011 Action::ToggleAutoRevert => {
1012 self.toggle_auto_revert();
1013 }
1014 Action::FormatBuffer => {
1015 if let Err(e) = self.format_buffer() {
1016 self.set_status_message(
1017 t!("error.format_failed", error = e.to_string()).to_string(),
1018 );
1019 }
1020 }
1021 Action::TrimTrailingWhitespace => match self.trim_trailing_whitespace() {
1022 Ok(true) => {
1023 self.set_status_message(t!("whitespace.trimmed").to_string());
1024 }
1025 Ok(false) => {
1026 self.set_status_message(t!("whitespace.no_trailing").to_string());
1027 }
1028 Err(e) => {
1029 self.set_status_message(
1030 t!("error.trim_whitespace_failed", error = e).to_string(),
1031 );
1032 }
1033 },
1034 Action::EnsureFinalNewline => match self.ensure_final_newline() {
1035 Ok(true) => {
1036 self.set_status_message(t!("whitespace.newline_added").to_string());
1037 }
1038 Ok(false) => {
1039 self.set_status_message(t!("whitespace.already_has_newline").to_string());
1040 }
1041 Err(e) => {
1042 self.set_status_message(
1043 t!("error.ensure_newline_failed", error = e).to_string(),
1044 );
1045 }
1046 },
1047 Action::Copy => {
1048 let popup = self
1050 .global_popups
1051 .top()
1052 .or_else(|| self.active_state().popups.top());
1053 if let Some(popup) = popup {
1054 if popup.has_selection() {
1055 if let Some(text) = popup.get_selected_text() {
1056 self.clipboard.copy(text);
1057 self.set_status_message(t!("clipboard.copied").to_string());
1058 return Ok(());
1059 }
1060 }
1061 }
1062 if self.active_window_mut().key_context
1063 == crate::input::keybindings::KeyContext::FileExplorer
1064 {
1065 self.active_window_mut().file_explorer_copy();
1066 return Ok(());
1067 }
1068 let buffer_id = self.active_buffer();
1075 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
1076 if self.handle_widget_copy(panel_id) {
1077 self.set_status_message(t!("clipboard.copied").to_string());
1078 return Ok(());
1079 }
1080 }
1081 if self.active_window().is_composite_buffer(buffer_id) {
1083 if let Some(_handled) = self.handle_composite_action(buffer_id, &Action::Copy) {
1084 return Ok(());
1085 }
1086 }
1087 self.copy_selection()
1088 }
1089 Action::CopyWithTheme(theme) => self.copy_selection_with_theme(&theme),
1090 Action::CopyFilePath => self.copy_active_buffer_path(false),
1091 Action::CopyRelativeFilePath => self.copy_active_buffer_path(true),
1092 Action::Cut => {
1093 if self.active_window_mut().key_context
1094 == crate::input::keybindings::KeyContext::FileExplorer
1095 {
1096 self.active_window_mut().file_explorer_cut();
1097 return Ok(());
1098 }
1099 let buffer_id = self.active_buffer();
1103 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
1104 if self.handle_widget_cut(panel_id) {
1105 return Ok(());
1106 }
1107 }
1108 if self.active_window().is_editing_disabled() {
1109 self.set_status_message(t!("buffer.editing_disabled").to_string());
1110 return Ok(());
1111 }
1112 self.cut_selection()
1113 }
1114 Action::Paste => {
1115 if self.active_window_mut().key_context
1116 == crate::input::keybindings::KeyContext::FileExplorer
1117 {
1118 self.file_explorer_paste();
1119 return Ok(());
1120 }
1121 let buffer_id = self.active_buffer();
1127 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
1128 if let Some(text) = self.clipboard.paste() {
1129 let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
1130 self.handle_widget_insert_str(panel_id, &normalized);
1131 self.set_status_message(t!("clipboard.pasted").to_string());
1132 }
1133 return Ok(());
1134 }
1135 if self.active_window().is_editing_disabled() {
1136 self.set_status_message(t!("buffer.editing_disabled").to_string());
1137 return Ok(());
1138 }
1139 self.paste()
1140 }
1141 Action::SelectAll => {
1142 let buffer_id = self.active_buffer();
1147 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
1148 self.handle_widget_select_all(panel_id);
1149 return Ok(());
1150 }
1151 self.apply_action_as_events(Action::SelectAll)?;
1152 }
1153 Action::YankWordForward => self.yank_word_forward(),
1154 Action::YankWordBackward => self.yank_word_backward(),
1155 Action::YankToLineEnd => self.yank_to_line_end(),
1156 Action::YankToLineStart => self.yank_to_line_start(),
1157 Action::YankViWordEnd => self.yank_vi_word_end(),
1158 Action::Undo => {
1159 self.handle_undo();
1160 }
1161 Action::Redo => {
1162 self.handle_redo();
1163 }
1164 Action::ShowHelp => {
1165 self.active_window_mut().open_help_manual();
1166 }
1167 Action::ShowKeyboardShortcuts => {
1168 self.active_window_mut().open_keyboard_shortcuts();
1169 }
1170 Action::ShowWarnings => {
1171 self.show_warnings_popup();
1172 }
1173 Action::ShowStatusLog => {
1174 self.open_status_log();
1175 }
1176 Action::ShowLspStatus => {
1177 self.show_lsp_status_popup();
1178 }
1179 Action::ShowRemoteIndicatorMenu => {
1180 self.show_remote_indicator_popup();
1181 }
1182 Action::ClearWarnings => {
1183 self.active_window_mut().clear_warnings();
1184 }
1185 Action::CommandPalette => {
1186 if let Some(prompt) = &self.active_window_mut().prompt {
1189 if prompt.prompt_type == PromptType::QuickOpen {
1190 self.cancel_prompt();
1191 return Ok(());
1192 }
1193 }
1194 self.start_quick_open();
1195 }
1196 Action::QuickOpen => {
1197 if let Some(prompt) = &self.active_window_mut().prompt {
1199 if prompt.prompt_type == PromptType::QuickOpen {
1200 self.cancel_prompt();
1201 return Ok(());
1202 }
1203 }
1204
1205 self.start_quick_open();
1207 }
1208 Action::QuickOpenBuffers => {
1209 if let Some(prompt) = &self.active_window_mut().prompt {
1210 if prompt.prompt_type == PromptType::QuickOpen {
1211 self.cancel_prompt();
1212 return Ok(());
1213 }
1214 }
1215 self.start_quick_open_with_prefix("#");
1216 }
1217 Action::QuickOpenFiles => {
1218 if let Some(prompt) = &self.active_window_mut().prompt {
1219 if prompt.prompt_type == PromptType::QuickOpen {
1220 self.cancel_prompt();
1221 return Ok(());
1222 }
1223 }
1224 self.start_quick_open_with_prefix("");
1225 }
1226 Action::OpenLiveGrep => {
1227 #[cfg(feature = "plugins")]
1233 {
1234 let result = self
1235 .plugin_manager
1236 .read()
1237 .unwrap()
1238 .execute_action_async("start_live_grep");
1239 if let Some(result) = result {
1240 match result {
1241 Ok(receiver) => {
1242 self.pending_plugin_actions
1243 .push(("start_live_grep".to_string(), receiver));
1244 }
1245 Err(e) => {
1246 self.set_status_message(format!("Live Grep unavailable: {}", e));
1247 }
1248 }
1249 } else {
1250 self.set_status_message("Live Grep plugin not loaded".to_string());
1251 }
1252 }
1253 #[cfg(not(feature = "plugins"))]
1254 {
1255 self.set_status_message("Live Grep requires the plugins feature".to_string());
1256 }
1257 }
1258 Action::ResumeLiveGrep => {
1259 let cached = self.active_window_mut().live_grep_last_state.clone();
1265 match cached {
1266 Some(state) if state.cached_results.as_ref().is_some_and(|r| !r.is_empty()) => {
1267 let results = state.cached_results.unwrap_or_default();
1268 let suggestions: Vec<crate::input::commands::Suggestion> = results
1273 .into_iter()
1274 .map(|m| {
1275 let label = format!("{}:{}", m.file, m.line);
1276 let value = format!("{}:{}:{}", m.file, m.line, m.column);
1277 let mut s = crate::input::commands::Suggestion::new(label);
1278 s.description = Some(m.content);
1279 s.value = Some(value);
1280 s
1281 })
1282 .collect();
1283 let mut prompt = crate::view::prompt::Prompt::with_suggestions(
1290 "Live grep: ".to_string(),
1291 PromptType::LiveGrep,
1292 suggestions,
1293 );
1294 prompt.input = state.query;
1295 prompt.cursor_pos = prompt.input.len();
1296 if let Some(idx) = state.selected_index {
1297 if idx < prompt.suggestions.len() {
1298 prompt.selected_suggestion = Some(idx);
1299 }
1300 }
1301 prompt.suggestions_set_for_input = Some(prompt.input.clone());
1302 prompt.overlay = true;
1304 self.active_window_mut().prompt = Some(prompt);
1305 }
1306 _ => {
1307 #[cfg(feature = "plugins")]
1309 {
1310 let result = self
1311 .plugin_manager
1312 .read()
1313 .unwrap()
1314 .execute_action_async("start_live_grep");
1315 if let Some(result) = result {
1316 match result {
1317 Ok(receiver) => {
1318 self.pending_plugin_actions
1319 .push(("start_live_grep".to_string(), receiver));
1320 }
1321 Err(e) => {
1322 self.set_status_message(format!(
1323 "Live Grep unavailable: {}",
1324 e
1325 ));
1326 }
1327 }
1328 }
1329 }
1330 }
1331 }
1332 }
1333 Action::LiveGrepExportQuickfix => {
1334 let is_grep = self
1339 .active_window()
1340 .prompt
1341 .as_ref()
1342 .map(|p| match &p.prompt_type {
1343 PromptType::LiveGrep => true,
1344 PromptType::Plugin { custom_type } => custom_type == "live-grep",
1345 _ => false,
1346 })
1347 .unwrap_or(false);
1348 if !is_grep {
1349 self.set_status_message(
1350 "Quickfix export is only available inside Live Grep".to_string(),
1351 );
1352 return Ok(());
1353 }
1354 let (query, matches) = {
1355 let prompt = self.active_window().prompt.as_ref().unwrap();
1356 (
1357 prompt.input.clone(),
1358 self.snapshot_prompt_results_for_grep(prompt),
1359 )
1360 };
1361 if matches.is_empty() {
1362 self.set_status_message("No Live Grep results to export".to_string());
1363 return Ok(());
1364 }
1365 self.cancel_prompt();
1367 self.install_quickfix_in_dock(query, matches);
1369 }
1370 Action::ToggleUtilityDock => {
1371 use crate::view::split::SplitRole;
1372 if let Some(dock_leaf) = self
1373 .windows
1374 .get(&self.active_window)
1375 .and_then(|w| w.buffers.splits())
1376 .map(|(mgr, _)| mgr)
1377 .expect("active window must have a populated split layout")
1378 .find_leaf_by_role(SplitRole::UtilityDock)
1379 {
1380 let active = self
1381 .windows
1382 .get(&self.active_window)
1383 .and_then(|w| w.buffers.splits())
1384 .map(|(mgr, _)| mgr)
1385 .expect("active window must have a populated split layout")
1386 .active_split();
1387 if active == dock_leaf {
1388 self.next_split();
1393 } else {
1394 self.windows
1395 .get_mut(&self.active_window)
1396 .and_then(|w| w.split_manager_mut())
1397 .expect("active window must have a populated split layout")
1398 .set_active_split(dock_leaf);
1399 }
1400 } else {
1401 self.set_status_message(
1402 "No Utility Dock open — invoke a dock-aware utility (Diagnostics, Search/Replace, …)"
1403 .to_string(),
1404 );
1405 }
1406 }
1407 Action::CycleLiveGrepProvider => {
1408 let in_live_grep = self
1414 .active_window()
1415 .prompt
1416 .as_ref()
1417 .map(|p| match &p.prompt_type {
1418 PromptType::LiveGrep => true,
1419 PromptType::Plugin { custom_type } => custom_type == "live-grep",
1420 _ => false,
1421 })
1422 .unwrap_or(false);
1423 if !in_live_grep {
1424 self.set_status_message(
1425 "Cycle Live Grep provider only works inside Live Grep".to_string(),
1426 );
1427 return Ok(());
1428 }
1429 #[cfg(feature = "plugins")]
1430 {
1431 let result = self
1432 .plugin_manager
1433 .read()
1434 .unwrap()
1435 .execute_action_async("live_grep_cycle_provider");
1436 if let Some(result) = result {
1437 match result {
1438 Ok(receiver) => {
1439 self.pending_plugin_actions
1440 .push(("live_grep_cycle_provider".to_string(), receiver));
1441 }
1442 Err(e) => {
1443 self.set_status_message(format!("Live Grep cycle failed: {}", e));
1444 }
1445 }
1446 } else {
1447 self.set_status_message("Live Grep plugin not loaded".to_string());
1448 }
1449 }
1450 #[cfg(not(feature = "plugins"))]
1451 {
1452 self.set_status_message(
1453 "Live Grep cycle requires the plugins feature".to_string(),
1454 );
1455 }
1456 }
1457 Action::OpenTerminalInDock => {
1458 use crate::model::event::SplitDirection;
1459 use crate::view::split::SplitRole;
1460 if let Some(dock_leaf) = self
1461 .windows
1462 .get(&self.active_window)
1463 .and_then(|w| w.buffers.splits())
1464 .map(|(mgr, _)| mgr)
1465 .expect("active window must have a populated split layout")
1466 .find_leaf_by_role(SplitRole::UtilityDock)
1467 {
1468 self.windows
1471 .get_mut(&self.active_window)
1472 .and_then(|w| w.split_manager_mut())
1473 .expect("active window must have a populated split layout")
1474 .set_active_split(dock_leaf);
1475 self.open_terminal();
1476 } else {
1477 let Some(terminal_id) = self.spawn_terminal_session() else {
1484 return Ok(());
1485 };
1486 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
1487 match self
1490 .windows
1491 .get_mut(&self.active_window)
1492 .and_then(|w| w.split_manager_mut())
1493 .expect("active window must have a populated split layout")
1494 .split_root_positioned(SplitDirection::Horizontal, buffer_id, 0.7, false)
1495 {
1496 Ok(new_leaf) => {
1497 let mut view_state = crate::view::split::SplitViewState::with_buffer(
1498 self.terminal_width,
1499 self.terminal_height,
1500 buffer_id,
1501 );
1502 view_state.apply_config_defaults(
1503 self.config.editor.line_numbers,
1504 self.config.editor.highlight_current_line,
1505 self.active_window().resolve_line_wrap_for_buffer(buffer_id),
1506 self.config.editor.wrap_indent,
1507 self.active_window()
1508 .resolve_wrap_column_for_buffer(buffer_id),
1509 self.config.editor.rulers.clone(),
1510 );
1511 view_state.viewport.line_wrap_enabled = false;
1515 self.windows
1516 .get_mut(&self.active_window)
1517 .and_then(|w| w.split_view_states_mut())
1518 .expect("active window must have a populated split layout")
1519 .insert(new_leaf, view_state);
1520 self.windows
1521 .get_mut(&self.active_window)
1522 .and_then(|w| w.split_manager_mut())
1523 .expect("active window must have a populated split layout")
1524 .set_leaf_role(new_leaf, Some(SplitRole::UtilityDock));
1525 self.windows
1526 .get_mut(&self.active_window)
1527 .and_then(|w| w.split_manager_mut())
1528 .expect("active window must have a populated split layout")
1529 .set_active_split(new_leaf);
1530 self.active_window_mut().terminal_mode = true;
1536 self.active_window_mut().key_context =
1537 crate::input::keybindings::KeyContext::Terminal;
1538 self.active_window_mut().resize_visible_terminals();
1539 let exit_key = self
1540 .keybindings
1541 .read()
1542 .unwrap()
1543 .find_keybinding_for_action(
1544 "terminal_escape",
1545 crate::input::keybindings::KeyContext::Terminal,
1546 )
1547 .unwrap_or_else(|| "Ctrl+Space".to_string());
1548 self.set_status_message(
1549 rust_i18n::t!(
1550 "terminal.opened",
1551 id = terminal_id.0,
1552 exit_key = exit_key
1553 )
1554 .to_string(),
1555 );
1556 tracing::info!(
1557 "Opened terminal {:?} into new dock leaf {:?} (buffer {:?})",
1558 terminal_id,
1559 new_leaf,
1560 buffer_id
1561 );
1562 }
1563 Err(e) => {
1564 self.set_status_message(format!(
1565 "Failed to create dock for terminal: {}",
1566 e
1567 ));
1568 return Ok(());
1569 }
1570 }
1571 }
1572 }
1573 Action::ToggleLineWrap => {
1574 let new_value = !self.config.editor.line_wrap;
1575 self.config_mut().editor.line_wrap = new_value;
1576 self.sync_windows_config();
1584
1585 let leaf_ids: Vec<_> = self
1588 .windows
1589 .get(&self.active_window)
1590 .and_then(|w| w.buffers.splits())
1591 .map(|(_, vs)| vs)
1592 .expect("active window must have a populated split layout")
1593 .keys()
1594 .copied()
1595 .collect();
1596 for leaf_id in leaf_ids {
1597 let buffer_id = self
1598 .split_manager_mut()
1599 .get_buffer_id(leaf_id.into())
1600 .unwrap_or(BufferId(0));
1601 let effective_wrap =
1602 self.active_window().resolve_line_wrap_for_buffer(buffer_id);
1603 let wrap_column = self
1604 .active_window()
1605 .resolve_wrap_column_for_buffer(buffer_id);
1606 if let Some(view_state) = self
1607 .windows
1608 .get_mut(&self.active_window)
1609 .and_then(|w| w.split_view_states_mut())
1610 .expect("active window must have a populated split layout")
1611 .get_mut(&leaf_id)
1612 {
1613 view_state.viewport.line_wrap_enabled = effective_wrap;
1614 view_state.viewport.wrap_indent = self.config.editor.wrap_indent;
1615 view_state.viewport.wrap_column = wrap_column;
1616 }
1617 }
1618
1619 let state = if self.config.editor.line_wrap {
1620 t!("view.state_enabled").to_string()
1621 } else {
1622 t!("view.state_disabled").to_string()
1623 };
1624 self.set_status_message(t!("view.line_wrap_state", state = state).to_string());
1625 }
1626 Action::ToggleCurrentLineHighlight => {
1627 let new_value = !self.config.editor.highlight_current_line;
1628 self.config_mut().editor.highlight_current_line = new_value;
1629
1630 let leaf_ids: Vec<_> = self
1632 .windows
1633 .get(&self.active_window)
1634 .and_then(|w| w.buffers.splits())
1635 .map(|(_, vs)| vs)
1636 .expect("active window must have a populated split layout")
1637 .keys()
1638 .copied()
1639 .collect();
1640 for leaf_id in leaf_ids {
1641 if let Some(view_state) = self
1642 .windows
1643 .get_mut(&self.active_window)
1644 .and_then(|w| w.split_view_states_mut())
1645 .expect("active window must have a populated split layout")
1646 .get_mut(&leaf_id)
1647 {
1648 view_state.highlight_current_line =
1649 self.config.editor.highlight_current_line;
1650 }
1651 }
1652
1653 let state = if self.config.editor.highlight_current_line {
1654 t!("view.state_enabled").to_string()
1655 } else {
1656 t!("view.state_disabled").to_string()
1657 };
1658 self.set_status_message(
1659 t!("view.current_line_highlight_state", state = state).to_string(),
1660 );
1661 }
1662 Action::ToggleReadOnly => {
1663 let buffer_id = self.active_buffer();
1664 let is_now_read_only = self
1665 .active_window()
1666 .buffer_metadata
1667 .get(&buffer_id)
1668 .map(|m| !m.read_only)
1669 .unwrap_or(false);
1670 self.active_window_mut()
1671 .mark_buffer_read_only(buffer_id, is_now_read_only);
1672
1673 let state_str = if is_now_read_only {
1674 t!("view.state_enabled").to_string()
1675 } else {
1676 t!("view.state_disabled").to_string()
1677 };
1678 self.set_status_message(t!("view.read_only_state", state = state_str).to_string());
1679 }
1680 Action::TogglePageView => {
1681 self.active_window_mut().handle_toggle_page_view();
1682 }
1683 Action::SetPageWidth => {
1684 let active_split = self
1685 .windows
1686 .get(&self.active_window)
1687 .and_then(|w| w.buffers.splits())
1688 .map(|(mgr, _)| mgr)
1689 .expect("active window must have a populated split layout")
1690 .active_split();
1691 let current = self
1692 .windows
1693 .get(&self.active_window)
1694 .and_then(|w| w.buffers.splits())
1695 .map(|(_, vs)| vs)
1696 .expect("active window must have a populated split layout")
1697 .get(&active_split)
1698 .and_then(|v| v.compose_width.map(|w| w.to_string()))
1699 .unwrap_or_default();
1700 self.start_prompt_with_initial_text(
1701 "Page width (empty = viewport): ".to_string(),
1702 PromptType::SetPageWidth,
1703 current,
1704 );
1705 }
1706 Action::SetBackground => {
1707 let default_path = self
1708 .ansi_background_path
1709 .as_ref()
1710 .and_then(|p| {
1711 p.strip_prefix(&self.working_dir)
1712 .ok()
1713 .map(|rel| rel.to_string_lossy().to_string())
1714 })
1715 .unwrap_or_else(|| DEFAULT_BACKGROUND_FILE.to_string());
1716
1717 self.start_prompt_with_initial_text(
1718 "Background file: ".to_string(),
1719 PromptType::SetBackgroundFile,
1720 default_path,
1721 );
1722 }
1723 Action::SetBackgroundBlend => {
1724 let default_amount = format!("{:.2}", self.background_fade);
1725 self.start_prompt_with_initial_text(
1726 "Background blend (0-1): ".to_string(),
1727 PromptType::SetBackgroundBlend,
1728 default_amount,
1729 );
1730 }
1731 Action::LspCompletion => {
1732 self.request_completion();
1733 }
1734 Action::DabbrevExpand => {
1735 self.dabbrev_expand();
1736 }
1737 Action::LspGotoDefinition => {
1738 self.request_goto_definition()?;
1739 }
1740 Action::LspRename => {
1741 self.start_rename()?;
1742 }
1743 Action::LspHover => {
1744 self.request_hover()?;
1745 }
1746 Action::LspReferences => {
1747 self.request_references()?;
1748 }
1749 Action::LspSignatureHelp => {
1750 self.request_signature_help();
1751 }
1752 Action::LspCodeActions => {
1753 self.request_code_actions()?;
1754 }
1755 Action::LspRestart => {
1756 self.handle_lsp_restart();
1757 }
1758 Action::LspStop => {
1759 self.handle_lsp_stop();
1760 }
1761 Action::LspToggleForBuffer => {
1762 self.handle_lsp_toggle_for_buffer();
1763 }
1764 Action::ToggleInlayHints => {
1765 self.toggle_inlay_hints();
1766 }
1767 Action::DumpConfig => {
1768 self.dump_config();
1769 }
1770 Action::RedrawScreen => {
1771 self.request_full_redraw();
1772 }
1773 Action::SelectTheme => {
1774 self.start_select_theme_prompt();
1775 }
1776 Action::InspectThemeAtCursor => {
1777 self.inspect_theme_at_cursor();
1778 }
1779 Action::SelectKeybindingMap => {
1780 self.start_select_keybinding_map_prompt();
1781 }
1782 Action::SelectCursorStyle => {
1783 self.start_select_cursor_style_prompt();
1784 }
1785 Action::SelectLocale => {
1786 self.start_select_locale_prompt();
1787 }
1788 Action::Search => {
1789 let is_search_prompt = self.active_window().prompt.as_ref().is_some_and(|p| {
1791 matches!(
1792 p.prompt_type,
1793 PromptType::Search
1794 | PromptType::ReplaceSearch
1795 | PromptType::QueryReplaceSearch
1796 )
1797 });
1798
1799 if is_search_prompt {
1800 self.confirm_prompt();
1801 } else {
1802 self.start_search_prompt(
1803 t!("file.search_prompt").to_string(),
1804 PromptType::Search,
1805 false,
1806 );
1807 }
1808 }
1809 Action::Replace => {
1810 self.start_search_prompt(
1812 t!("file.replace_prompt").to_string(),
1813 PromptType::ReplaceSearch,
1814 false,
1815 );
1816 }
1817 Action::QueryReplace => {
1818 self.active_window_mut().search_confirm_each = true;
1820 self.start_search_prompt(
1821 "Query replace: ".to_string(),
1822 PromptType::QueryReplaceSearch,
1823 false,
1824 );
1825 }
1826 Action::FindInSelection => {
1827 self.start_search_prompt(
1828 t!("file.search_prompt").to_string(),
1829 PromptType::Search,
1830 true,
1831 );
1832 }
1833 Action::FindNext => {
1834 self.find_next();
1835 }
1836 Action::FindPrevious => {
1837 self.find_previous();
1838 }
1839 Action::FindSelectionNext => {
1840 self.find_selection_next();
1841 }
1842 Action::FindSelectionPrevious => {
1843 self.find_selection_previous();
1844 }
1845 Action::AddCursorNextMatch => self.add_cursor_at_next_match(),
1846 Action::AddCursorAbove => self.add_cursor_above(),
1847 Action::AddCursorBelow => self.add_cursor_below(),
1848 Action::AddCursorsToLineEnds => self.add_cursors_to_line_ends(),
1849 Action::NextBuffer => self.next_buffer(),
1850 Action::PrevBuffer => self.prev_buffer(),
1851 Action::SwitchToPreviousTab => self.switch_to_previous_tab(),
1852 Action::SwitchToTabByName => self.start_switch_to_tab_prompt(),
1853
1854 Action::ScrollTabsLeft => {
1856 let active_split_id = self
1857 .windows
1858 .get(&self.active_window)
1859 .and_then(|w| w.buffers.splits())
1860 .map(|(mgr, _)| mgr)
1861 .expect("active window must have a populated split layout")
1862 .active_split();
1863 if let Some(view_state) = self
1864 .windows
1865 .get_mut(&self.active_window)
1866 .and_then(|w| w.split_view_states_mut())
1867 .expect("active window must have a populated split layout")
1868 .get_mut(&active_split_id)
1869 {
1870 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_sub(5);
1871 self.set_status_message(t!("status.scrolled_tabs_left").to_string());
1872 }
1873 }
1874 Action::ScrollTabsRight => {
1875 let active_split_id = self
1876 .windows
1877 .get(&self.active_window)
1878 .and_then(|w| w.buffers.splits())
1879 .map(|(mgr, _)| mgr)
1880 .expect("active window must have a populated split layout")
1881 .active_split();
1882 if let Some(view_state) = self
1883 .windows
1884 .get_mut(&self.active_window)
1885 .and_then(|w| w.split_view_states_mut())
1886 .expect("active window must have a populated split layout")
1887 .get_mut(&active_split_id)
1888 {
1889 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_add(5);
1890 self.set_status_message(t!("status.scrolled_tabs_right").to_string());
1891 }
1892 }
1893 Action::NavigateBack => self.navigate_back(),
1894 Action::NavigateForward => self.navigate_forward(),
1895 Action::SplitHorizontal => self.split_pane_horizontal(),
1896 Action::SplitVertical => self.split_pane_vertical(),
1897 Action::CloseSplit => self.close_active_split(),
1898 Action::NextSplit => self.next_split(),
1899 Action::PrevSplit => self.prev_split(),
1900 Action::IncreaseSplitSize => self.adjust_split_size(0.05),
1901 Action::DecreaseSplitSize => self.adjust_split_size(-0.05),
1902 Action::ToggleMaximizeSplit => self.toggle_maximize_split(),
1903 Action::ToggleFileExplorer => self.toggle_file_explorer(),
1904 Action::ToggleMenuBar => self.toggle_menu_bar(),
1905 Action::ToggleTabBar => self.active_window_mut().toggle_tab_bar(),
1906 Action::ToggleStatusBar => self.active_window_mut().toggle_status_bar(),
1907 Action::TogglePromptLine => self.active_window_mut().toggle_prompt_line(),
1908 Action::ToggleVerticalScrollbar => self.toggle_vertical_scrollbar(),
1909 Action::ToggleHorizontalScrollbar => self.toggle_horizontal_scrollbar(),
1910 Action::ToggleLineNumbers => self.toggle_line_numbers(),
1911 Action::ToggleScrollSync => self.active_window_mut().toggle_scroll_sync(),
1912 Action::ToggleMouseCapture => self.toggle_mouse_capture(),
1913 Action::ToggleMouseHover => self.toggle_mouse_hover(),
1914 Action::ToggleDebugHighlights => self.active_window_mut().toggle_debug_highlights(),
1915 Action::AddRuler => {
1917 self.start_prompt(t!("rulers.add_prompt").to_string(), PromptType::AddRuler);
1918 }
1919 Action::RemoveRuler => {
1920 self.start_remove_ruler_prompt();
1921 }
1922 Action::SetTabSize => {
1924 let current = self
1925 .buffers()
1926 .get(&self.active_buffer())
1927 .map(|s| s.buffer_settings.tab_size.to_string())
1928 .unwrap_or_else(|| "4".to_string());
1929 self.start_prompt_with_initial_text(
1930 "Tab size: ".to_string(),
1931 PromptType::SetTabSize,
1932 current,
1933 );
1934 }
1935 Action::SetLineEnding => {
1936 self.start_set_line_ending_prompt();
1937 }
1938 Action::SetEncoding => {
1939 self.start_set_encoding_prompt();
1940 }
1941 Action::ReloadWithEncoding => {
1942 self.start_reload_with_encoding_prompt();
1943 }
1944 Action::SetLanguage => {
1945 self.start_set_language_prompt();
1946 }
1947 Action::ToggleIndentationStyle => {
1948 let __buffer_id = self.active_buffer();
1949 if let Some(state) = self
1950 .windows
1951 .get_mut(&self.active_window)
1952 .map(|w| &mut w.buffers)
1953 .expect("active window present")
1954 .get_mut(&__buffer_id)
1955 {
1956 state.buffer_settings.use_tabs = !state.buffer_settings.use_tabs;
1957 let status = if state.buffer_settings.use_tabs {
1958 "Indentation: Tabs"
1959 } else {
1960 "Indentation: Spaces"
1961 };
1962 self.set_status_message(status.to_string());
1963 }
1964 }
1965 Action::ToggleTabIndicators | Action::ToggleWhitespaceIndicators => {
1966 let __buffer_id = self.active_buffer();
1967 if let Some(state) = self
1968 .windows
1969 .get_mut(&self.active_window)
1970 .map(|w| &mut w.buffers)
1971 .expect("active window present")
1972 .get_mut(&__buffer_id)
1973 {
1974 state.buffer_settings.whitespace.toggle_all();
1975 let status = if state.buffer_settings.whitespace.any_visible() {
1976 t!("toggle.whitespace_indicators_shown")
1977 } else {
1978 t!("toggle.whitespace_indicators_hidden")
1979 };
1980 self.set_status_message(status.to_string());
1981 }
1982 }
1983 Action::ResetBufferSettings => self.reset_buffer_settings(),
1984 Action::FocusFileExplorer => self.focus_file_explorer(),
1985 Action::FocusEditor => self.active_window_mut().focus_editor(),
1986 Action::FileExplorerUp => self.file_explorer_navigate_up(),
1987 Action::FileExplorerDown => self.file_explorer_navigate_down(),
1988 Action::FileExplorerPageUp => self.file_explorer_page_up(),
1989 Action::FileExplorerPageDown => self.file_explorer_page_down(),
1990 Action::FileExplorerExpand => self.file_explorer_toggle_expand(),
1991 Action::FileExplorerCollapse => self.file_explorer_collapse(),
1992 Action::FileExplorerOpen => self.file_explorer_open_file()?,
1993 Action::FileExplorerRefresh => self.file_explorer_refresh(),
1994 Action::FileExplorerNewFile => self.file_explorer_new_file(),
1995 Action::FileExplorerNewDirectory => self.file_explorer_new_directory(),
1996 Action::FileExplorerDelete => self.file_explorer_delete(),
1997 Action::FileExplorerRename => self.file_explorer_rename(),
1998 Action::FileExplorerToggleHidden => self.file_explorer_toggle_hidden(),
1999 Action::FileExplorerToggleGitignored => self.file_explorer_toggle_gitignored(),
2000 Action::FileExplorerSearchClear => {
2001 self.active_window_mut().file_explorer_search_clear()
2002 }
2003 Action::FileExplorerSearchBackspace => {
2004 self.active_window_mut().file_explorer_search_pop_char()
2005 }
2006 Action::FileExplorerCopy => self.active_window_mut().file_explorer_copy(),
2007 Action::FileExplorerCut => self.active_window_mut().file_explorer_cut(),
2008 Action::FileExplorerPaste => self.file_explorer_paste(),
2009 Action::FileExplorerDuplicate => self.file_explorer_duplicate(),
2010 Action::FileExplorerCopyFullPath => self.file_explorer_copy_path(false),
2011 Action::FileExplorerCopyRelativePath => self.file_explorer_copy_path(true),
2012 Action::FileExplorerExtendSelectionUp => {
2013 self.active_window_mut().file_explorer_extend_selection_up()
2014 }
2015 Action::FileExplorerExtendSelectionDown => self
2016 .active_window_mut()
2017 .file_explorer_extend_selection_down(),
2018 Action::FileExplorerToggleSelect => {
2019 self.active_window_mut().file_explorer_toggle_select()
2020 }
2021 Action::FileExplorerSelectAll => self.active_window_mut().file_explorer_select_all(),
2022 Action::RemoveSecondaryCursors => {
2023 if let Some(events) = self
2025 .active_window_mut()
2026 .action_to_events(Action::RemoveSecondaryCursors)
2027 {
2028 let batch = Event::Batch {
2030 events: events.clone(),
2031 description: "Remove secondary cursors".to_string(),
2032 };
2033 self.active_event_log_mut().append(batch.clone());
2034 self.apply_event_to_active_buffer(&batch);
2035
2036 let active_split = self
2038 .windows
2039 .get(&self.active_window)
2040 .and_then(|w| w.buffers.splits())
2041 .map(|(mgr, _)| mgr)
2042 .expect("active window must have a populated split layout")
2043 .active_split();
2044 let active_buffer = self.active_buffer();
2045 self.active_window_mut()
2046 .ensure_cursor_visible_for_split(active_buffer, active_split);
2047 }
2048 }
2049
2050 Action::MenuActivate => {
2052 self.handle_menu_activate();
2053 }
2054 Action::MenuClose => {
2055 self.handle_menu_close();
2056 }
2057 Action::MenuLeft => {
2058 self.handle_menu_left();
2059 }
2060 Action::MenuRight => {
2061 self.handle_menu_right();
2062 }
2063 Action::MenuUp => {
2064 self.handle_menu_up();
2065 }
2066 Action::MenuDown => {
2067 self.handle_menu_down();
2068 }
2069 Action::MenuExecute => {
2070 if let Some(action) = self.handle_menu_execute() {
2071 return self.handle_action(action);
2072 }
2073 }
2074 Action::MenuOpen(menu_name) => {
2075 if self.config.editor.menu_bar_mnemonics {
2076 self.handle_menu_open(&menu_name);
2077 }
2078 }
2079
2080 Action::SwitchKeybindingMap(map_name) => {
2081 let is_builtin =
2083 matches!(map_name.as_str(), "default" | "emacs" | "vscode" | "macos");
2084 let is_user_defined = self.config.keybinding_maps.contains_key(&map_name);
2085
2086 if is_builtin || is_user_defined {
2087 self.config_mut().active_keybinding_map = map_name.clone().into();
2089
2090 *self.keybindings.write().unwrap() =
2092 crate::input::keybindings::KeybindingResolver::new(&self.config);
2093
2094 self.set_status_message(
2095 t!("view.keybindings_switched", map = map_name).to_string(),
2096 );
2097 } else {
2098 self.set_status_message(
2099 t!("view.keybindings_unknown", map = map_name).to_string(),
2100 );
2101 }
2102 }
2103
2104 Action::SmartHome => {
2105 let buffer_id = self.active_buffer();
2107 if self.active_window().is_composite_buffer(buffer_id) {
2108 if let Some(_handled) =
2109 self.handle_composite_action(buffer_id, &Action::SmartHome)
2110 {
2111 return Ok(());
2112 }
2113 }
2114 self.smart_home();
2115 }
2116 Action::ToggleComment => {
2117 self.toggle_comment();
2118 }
2119 Action::ToggleFold => {
2120 self.active_window_mut().toggle_fold_at_cursor();
2121 }
2122 Action::GoToMatchingBracket => {
2123 self.goto_matching_bracket();
2124 }
2125 Action::JumpToNextError => {
2126 self.jump_to_next_error();
2127 }
2128 Action::JumpToPreviousError => {
2129 self.jump_to_previous_error();
2130 }
2131 Action::SetBookmark(key) => {
2132 self.active_window_mut().set_bookmark(key);
2133 }
2134 Action::JumpToBookmark(key) => {
2135 self.jump_to_bookmark(key);
2136 }
2137 Action::ClearBookmark(key) => {
2138 self.active_window_mut().clear_bookmark(key);
2139 }
2140 Action::ListBookmarks => {
2141 self.active_window_mut().list_bookmarks();
2142 }
2143 Action::ToggleSearchCaseSensitive => {
2144 self.active_window_mut().search_case_sensitive =
2145 !self.active_window().search_case_sensitive;
2146 let state = if self.active_window().search_case_sensitive {
2147 "enabled"
2148 } else {
2149 "disabled"
2150 };
2151 self.set_status_message(
2152 t!("search.case_sensitive_state", state = state).to_string(),
2153 );
2154 if let Some(prompt) = &self.active_window_mut().prompt {
2157 if matches!(
2158 prompt.prompt_type,
2159 PromptType::Search
2160 | PromptType::ReplaceSearch
2161 | PromptType::QueryReplaceSearch
2162 ) {
2163 let query = prompt.input.clone();
2164 self.update_search_highlights(&query);
2165 }
2166 } else if let Some(search_state) = &self.active_window().search_state {
2167 let query = search_state.query.clone();
2168 self.perform_search(&query);
2169 }
2170 }
2171 Action::ToggleSearchWholeWord => {
2172 self.active_window_mut().search_whole_word =
2173 !self.active_window().search_whole_word;
2174 let state = if self.active_window().search_whole_word {
2175 "enabled"
2176 } else {
2177 "disabled"
2178 };
2179 self.set_status_message(t!("search.whole_word_state", state = state).to_string());
2180 if let Some(prompt) = &self.active_window_mut().prompt {
2183 if matches!(
2184 prompt.prompt_type,
2185 PromptType::Search
2186 | PromptType::ReplaceSearch
2187 | PromptType::QueryReplaceSearch
2188 ) {
2189 let query = prompt.input.clone();
2190 self.update_search_highlights(&query);
2191 }
2192 } else if let Some(search_state) = &self.active_window().search_state {
2193 let query = search_state.query.clone();
2194 self.perform_search(&query);
2195 }
2196 }
2197 Action::ToggleSearchRegex => {
2198 self.active_window_mut().search_use_regex = !self.active_window().search_use_regex;
2199 let state = if self.active_window().search_use_regex {
2200 "enabled"
2201 } else {
2202 "disabled"
2203 };
2204 self.set_status_message(t!("search.regex_state", state = state).to_string());
2205 if let Some(prompt) = &self.active_window_mut().prompt {
2208 if matches!(
2209 prompt.prompt_type,
2210 PromptType::Search
2211 | PromptType::ReplaceSearch
2212 | PromptType::QueryReplaceSearch
2213 ) {
2214 let query = prompt.input.clone();
2215 self.update_search_highlights(&query);
2216 }
2217 } else if let Some(search_state) = &self.active_window().search_state {
2218 let query = search_state.query.clone();
2219 self.perform_search(&query);
2220 }
2221 }
2222 Action::ToggleSearchConfirmEach => {
2223 self.active_window_mut().search_confirm_each =
2224 !self.active_window().search_confirm_each;
2225 let state = if self.active_window().search_confirm_each {
2226 "enabled"
2227 } else {
2228 "disabled"
2229 };
2230 self.set_status_message(t!("search.confirm_each_state", state = state).to_string());
2231 }
2232 Action::FileBrowserToggleHidden => {
2233 self.file_open_toggle_hidden();
2235 }
2236 Action::StartMacroRecording => {
2237 self.set_status_message(
2239 "Use Ctrl+Shift+R to start recording (will prompt for register)".to_string(),
2240 );
2241 }
2242 Action::StopMacroRecording => {
2243 self.stop_macro_recording();
2244 }
2245 Action::PlayMacro(key) => {
2246 self.play_macro(key);
2247 }
2248 Action::ToggleMacroRecording(key) => {
2249 self.toggle_macro_recording(key);
2250 }
2251 Action::ShowMacro(key) => {
2252 self.show_macro_in_buffer(key);
2253 }
2254 Action::ListMacros => {
2255 self.list_macros_in_buffer();
2256 }
2257 Action::PromptRecordMacro => {
2258 self.start_prompt("Record macro (0-9): ".to_string(), PromptType::RecordMacro);
2259 }
2260 Action::PromptPlayMacro => {
2261 self.start_prompt("Play macro (0-9): ".to_string(), PromptType::PlayMacro);
2262 }
2263 Action::PlayLastMacro => {
2264 if let Some(key) = self.active_window_mut().macros.last_register() {
2265 self.play_macro(key);
2266 } else {
2267 self.set_status_message(t!("status.no_macro_recorded").to_string());
2268 }
2269 }
2270 Action::PromptSetBookmark => {
2271 self.start_prompt("Set bookmark (0-9): ".to_string(), PromptType::SetBookmark);
2272 }
2273 Action::PromptJumpToBookmark => {
2274 self.start_prompt(
2275 "Jump to bookmark (0-9): ".to_string(),
2276 PromptType::JumpToBookmark,
2277 );
2278 }
2279 Action::CompositeNextHunk => {
2280 let buf = self.active_buffer();
2281 self.active_window_mut().composite_next_hunk_active(buf);
2282 }
2283 Action::CompositePrevHunk => {
2284 let buf = self.active_buffer();
2285 self.active_window_mut().composite_prev_hunk_active(buf);
2286 }
2287 Action::None => {}
2288 Action::DeleteBackward => {
2289 if self.active_window().is_editing_disabled() {
2290 self.set_status_message(t!("buffer.editing_disabled").to_string());
2291 return Ok(());
2292 }
2293 if let Some(events) = self
2295 .active_window_mut()
2296 .action_to_events(Action::DeleteBackward)
2297 {
2298 if events.len() > 1 {
2299 let description = "Delete backward".to_string();
2301 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description)
2302 {
2303 self.active_event_log_mut().append(bulk_edit);
2304 }
2305 } else {
2306 for event in events {
2307 self.active_event_log_mut().append(event.clone());
2308 self.apply_event_to_active_buffer(&event);
2309 }
2310 }
2311 }
2312 }
2313 Action::PluginAction(action_name) => {
2314 tracing::debug!("handle_action: PluginAction('{}')", action_name);
2315 #[cfg(feature = "plugins")]
2318 {
2319 let result = self
2320 .plugin_manager
2321 .read()
2322 .unwrap()
2323 .execute_action_async(&action_name);
2324 if let Some(result) = result {
2325 match result {
2326 Ok(receiver) => {
2327 self.pending_plugin_actions
2329 .push((action_name.clone(), receiver));
2330 }
2331 Err(e) => {
2332 self.set_status_message(
2333 t!("view.plugin_error", error = e.to_string()).to_string(),
2334 );
2335 tracing::error!("Plugin action error: {}", e);
2336 }
2337 }
2338 } else {
2339 self.set_status_message(
2340 t!("status.plugin_manager_unavailable").to_string(),
2341 );
2342 }
2343 }
2344 #[cfg(not(feature = "plugins"))]
2345 {
2346 let _ = action_name;
2347 self.set_status_message(
2348 "Plugins not available (compiled without plugin support)".to_string(),
2349 );
2350 }
2351 }
2352 Action::LoadPluginFromBuffer => {
2353 #[cfg(feature = "plugins")]
2354 {
2355 let buffer_id = self.active_buffer();
2356 let state = self.active_state();
2357 let buffer = &state.buffer;
2358 let total = buffer.total_bytes();
2359 let content =
2360 String::from_utf8_lossy(&buffer.slice_bytes(0..total)).to_string();
2361
2362 let is_ts = buffer
2364 .file_path()
2365 .and_then(|p| p.extension())
2366 .and_then(|e| e.to_str())
2367 .map(|e| e == "ts" || e == "tsx")
2368 .unwrap_or(true);
2369
2370 let name = buffer
2372 .file_path()
2373 .and_then(|p| p.file_name())
2374 .and_then(|s| s.to_str())
2375 .map(|s| s.to_string())
2376 .unwrap_or_else(|| "buffer-plugin".to_string());
2377
2378 let load_result = self
2379 .plugin_manager
2380 .read()
2381 .unwrap()
2382 .load_plugin_from_source(&content, &name, is_ts);
2383 match load_result {
2384 Ok(()) => {
2385 self.set_status_message(format!(
2386 "Plugin '{}' loaded from buffer",
2387 name
2388 ));
2389 }
2390 Err(e) => {
2391 self.set_status_message(format!("Failed to load plugin: {}", e));
2392 tracing::error!("LoadPluginFromBuffer error: {}", e);
2393 }
2394 }
2395
2396 self.setup_plugin_dev_lsp(buffer_id, &content);
2398 }
2399 #[cfg(not(feature = "plugins"))]
2400 {
2401 self.set_status_message(
2402 "Plugins not available (compiled without plugin support)".to_string(),
2403 );
2404 }
2405 }
2406 Action::InitReload => {
2407 self.load_init_script(true);
2412 self.fire_plugins_loaded_hook();
2415 }
2416 Action::InitEdit => {
2417 let config_dir = self.dir_context.config_dir.clone();
2420 match crate::init_script::ensure_starter(&config_dir) {
2421 Ok(path) => {
2422 let declarations =
2432 self.plugin_manager.read().unwrap().plugin_declarations();
2433 crate::init_script::write_plugin_declarations(&config_dir, &declarations);
2434 match self.open_file(&path) {
2435 Ok(_) => {
2436 self.set_status_message(format!("init.ts: {}", path.display()));
2437 }
2438 Err(e) => {
2439 self.set_status_message(format!("init.ts: open failed: {e}"));
2440 }
2441 }
2442 }
2443 Err(e) => {
2444 self.set_status_message(format!("init.ts: create failed: {e}"));
2445 }
2446 }
2447 }
2448 Action::InitCheck => {
2449 let report = crate::init_script::check(&self.dir_context.config_dir);
2452 if report.ok && report.diagnostics.is_empty() {
2453 self.set_status_message("init.ts: ok".into());
2454 } else if !report.ok {
2455 let first = report
2456 .diagnostics
2457 .first()
2458 .map(|d| format!("{}:{}: {}", d.line, d.column, d.message))
2459 .unwrap_or_else(|| "unknown error".into());
2460 self.set_status_message(format!(
2461 "init.ts: {} error(s) — first: {first}",
2462 report.diagnostics.len()
2463 ));
2464 } else {
2465 self.set_status_message(format!(
2466 "init.ts: {} warning(s)",
2467 report.diagnostics.len()
2468 ));
2469 }
2470 }
2471 Action::OpenTerminal => {
2472 self.open_terminal();
2473 }
2474 Action::CloseTerminal => {
2475 self.close_terminal();
2476 }
2477 Action::FocusTerminal => {
2478 if self
2480 .active_window()
2481 .is_terminal_buffer(self.active_buffer())
2482 {
2483 self.active_window_mut().terminal_mode = true;
2484 self.active_window_mut().key_context = KeyContext::Terminal;
2485 self.set_status_message(t!("status.terminal_mode_enabled").to_string());
2486 }
2487 }
2488 Action::TerminalEscape => {
2489 if self.active_window().terminal_mode {
2491 self.active_window_mut().terminal_mode = false;
2492 self.active_window_mut().key_context = KeyContext::Normal;
2493 self.set_status_message(t!("status.terminal_mode_disabled").to_string());
2494 }
2495 }
2496 Action::ToggleKeyboardCapture => {
2497 if self.active_window().terminal_mode {
2499 self.active_window_mut().keyboard_capture =
2500 !self.active_window_mut().keyboard_capture;
2501 if self.active_window_mut().keyboard_capture {
2502 self.set_status_message(
2503 "Keyboard capture ON - all keys go to terminal (F9 to toggle)"
2504 .to_string(),
2505 );
2506 } else {
2507 self.set_status_message(
2508 "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
2509 );
2510 }
2511 }
2512 }
2513 Action::TerminalPaste => {
2514 if self.active_window().terminal_mode {
2516 if let Some(text) = self.clipboard.paste() {
2517 self.active_window_mut()
2518 .send_terminal_input(text.as_bytes());
2519 }
2520 }
2521 }
2522 Action::ShellCommand => {
2523 self.start_shell_command_prompt(false);
2525 }
2526 Action::ShellCommandReplace => {
2527 self.start_shell_command_prompt(true);
2529 }
2530 Action::OpenSettings => {
2531 self.open_settings();
2532 }
2533 Action::CloseSettings => {
2534 let has_changes = self
2536 .settings_state
2537 .as_ref()
2538 .is_some_and(|s| s.has_changes());
2539 if has_changes {
2540 if let Some(ref mut state) = self.settings_state {
2542 state.show_confirm_dialog();
2543 }
2544 } else {
2545 self.close_settings(false);
2546 }
2547 }
2548 Action::SettingsSave => {
2549 self.save_settings();
2550 }
2551 Action::SettingsReset => {
2552 if let Some(ref mut state) = self.settings_state {
2553 state.reset_current_to_default();
2554 }
2555 }
2556 Action::SettingsInherit => {
2557 if let Some(ref mut state) = self.settings_state {
2558 state.set_current_to_null();
2559 }
2560 }
2561 Action::SettingsToggleFocus => {
2562 if let Some(ref mut state) = self.settings_state {
2563 state.toggle_focus();
2564 }
2565 }
2566 Action::SettingsActivate => {
2567 self.settings_activate_current();
2568 }
2569 Action::SettingsSearch => {
2570 if let Some(ref mut state) = self.settings_state {
2571 state.start_search();
2572 }
2573 }
2574 Action::SettingsHelp => {
2575 if let Some(ref mut state) = self.settings_state {
2576 state.toggle_help();
2577 }
2578 }
2579 Action::SettingsIncrement => {
2580 self.settings_increment_current();
2581 }
2582 Action::SettingsDecrement => {
2583 self.settings_decrement_current();
2584 }
2585 Action::CalibrateInput => {
2586 self.open_calibration_wizard();
2587 }
2588 Action::EventDebug => {
2589 self.active_window_mut().open_event_debug();
2590 }
2591 Action::SuspendProcess => {
2592 self.request_suspend();
2593 }
2594 Action::OpenKeybindingEditor => {
2595 self.open_keybinding_editor();
2596 }
2597 Action::PromptConfirm => {
2598 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
2599 use super::prompt_actions::PromptResult;
2600 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
2601 PromptResult::ExecuteAction(action) => {
2602 return self.handle_action(action);
2603 }
2604 PromptResult::EarlyReturn => {
2605 return Ok(());
2606 }
2607 PromptResult::Done => {}
2608 }
2609 }
2610 }
2611 Action::PromptConfirmWithText(ref text) => {
2612 if let Some(ref mut prompt) = self.active_window_mut().prompt {
2614 prompt.set_input(text.clone());
2615 self.update_prompt_suggestions();
2616 }
2617 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
2618 use super::prompt_actions::PromptResult;
2619 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
2620 PromptResult::ExecuteAction(action) => {
2621 return self.handle_action(action);
2622 }
2623 PromptResult::EarlyReturn => {
2624 return Ok(());
2625 }
2626 PromptResult::Done => {}
2627 }
2628 }
2629 }
2630 Action::PopupConfirm => {
2631 use super::popup_actions::PopupConfirmResult;
2632 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
2633 return Ok(());
2634 }
2635 }
2636 Action::PopupCancel => {
2637 self.handle_popup_cancel();
2638 }
2639 Action::PopupFocus => {
2640 self.handle_popup_focus();
2641 }
2642 Action::CompletionAccept => {
2643 use super::popup_actions::PopupConfirmResult;
2644 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
2645 return Ok(());
2646 }
2647 }
2648 Action::CompletionDismiss => {
2649 self.handle_popup_cancel();
2650 }
2651 Action::InsertChar(c) => {
2652 if self.is_prompting() {
2653 return self.handle_insert_char_prompt(c);
2654 } else if self.active_window_mut().key_context == KeyContext::FileExplorer {
2655 self.active_window_mut().file_explorer_search_push_char(c);
2656 } else {
2657 self.handle_insert_char_editor(c)?;
2658 }
2659 }
2660 Action::PromptCopy => {
2662 if let Some(prompt) = &self.active_window_mut().prompt {
2663 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
2664 if !text.is_empty() {
2665 self.clipboard.copy(text);
2666 self.set_status_message(t!("clipboard.copied").to_string());
2667 }
2668 }
2669 }
2670 Action::PromptCut => {
2671 if let Some(prompt) = &self.active_window_mut().prompt {
2672 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
2673 if !text.is_empty() {
2674 self.clipboard.copy(text);
2675 }
2676 }
2677 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
2678 if prompt.has_selection() {
2679 prompt.delete_selection();
2680 } else {
2681 prompt.clear();
2682 }
2683 }
2684 self.set_status_message(t!("clipboard.cut").to_string());
2685 self.update_prompt_suggestions();
2686 }
2687 Action::PromptPaste => {
2688 if let Some(text) = self.clipboard.paste() {
2689 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
2690 prompt.insert_str(&text);
2691 }
2692 self.update_prompt_suggestions();
2693 }
2694 }
2695 _ => {
2696 self.apply_action_as_events(action)?;
2702 }
2703 }
2704
2705 Ok(())
2706 }
2707
2708 fn dispatch_floating_widget_key(
2719 &mut self,
2720 code: crossterm::event::KeyCode,
2721 modifiers: crossterm::event::KeyModifiers,
2722 ) -> bool {
2723 use crossterm::event::{KeyCode, KeyModifiers};
2724 let panel_id = match self.floating_widget_panel.as_ref() {
2725 Some(fwp) => fwp.panel_id,
2726 None => return false,
2727 };
2728 let key_name: Option<&str> = match code {
2729 KeyCode::Esc => {
2730 let mode_has_binding = self
2739 .active_window()
2740 .editor_mode
2741 .as_ref()
2742 .map(|mode_name| {
2743 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
2744 let mode_ctx =
2745 crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
2746 let keybindings = self.keybindings.read().unwrap();
2747 keybindings.has_explicit_binding(&key_event, &mode_ctx)
2748 })
2749 .unwrap_or(false);
2750 if mode_has_binding {
2751 return false;
2752 }
2753 let widget_key = self
2754 .widget_registry
2755 .get(panel_id)
2756 .map(|p| p.focus_key.clone())
2757 .unwrap_or_default();
2758 if self
2759 .plugin_manager
2760 .read()
2761 .unwrap()
2762 .has_hook_handlers("widget_event")
2763 {
2764 self.plugin_manager.read().unwrap().run_hook(
2765 "widget_event",
2766 crate::services::plugins::hooks::HookArgs::WidgetEvent {
2767 panel_id,
2768 widget_key,
2769 event_type: "cancel".to_string(),
2770 payload: serde_json::json!({}),
2771 },
2772 );
2773 }
2774 self.floating_widget_panel = None;
2775 let _ = self.widget_registry.unmount(panel_id);
2776 return true;
2777 }
2778 KeyCode::Tab => Some(if modifiers.contains(KeyModifiers::SHIFT) {
2779 "Shift+Tab"
2780 } else {
2781 "Tab"
2782 }),
2783 KeyCode::BackTab => Some("Shift+Tab"),
2784 KeyCode::Enter => Some("Enter"),
2785 KeyCode::Backspace => Some("Backspace"),
2786 KeyCode::Delete => Some("Delete"),
2787 KeyCode::Home => Some("Home"),
2788 KeyCode::End => Some("End"),
2789 KeyCode::Left => Some("Left"),
2790 KeyCode::Right => Some("Right"),
2791 KeyCode::Up => Some("Up"),
2792 KeyCode::Down => Some("Down"),
2793 KeyCode::PageUp => Some("PageUp"),
2794 KeyCode::PageDown => Some("PageDown"),
2795 _ => None,
2796 };
2797 if let Some(name) = key_name {
2798 let mode_has_binding = self
2817 .active_window()
2818 .editor_mode
2819 .as_ref()
2820 .map(|mode_name| {
2821 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
2822 let mode_ctx =
2823 crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
2824 let keybindings = self.keybindings.read().unwrap();
2825 keybindings.has_explicit_binding(&key_event, &mode_ctx)
2826 })
2827 .unwrap_or(false);
2828 if mode_has_binding {
2829 return false;
2830 }
2831 self.handle_widget_command(
2832 panel_id,
2833 fresh_core::api::WidgetAction::Key {
2834 key: name.to_string(),
2835 },
2836 );
2837 return true;
2838 }
2839 if let KeyCode::Char(c) = code {
2840 if modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) {
2853 let mode_has_binding = self
2854 .active_window()
2855 .editor_mode
2856 .as_ref()
2857 .map(|mode_name| {
2858 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
2859 let mode_ctx =
2860 crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
2861 let keybindings = self.keybindings.read().unwrap();
2862 keybindings.has_explicit_binding(&key_event, &mode_ctx)
2863 })
2864 .unwrap_or(false);
2865 if mode_has_binding {
2866 return false;
2867 }
2868 return true;
2869 }
2870 let ch = if modifiers.contains(KeyModifiers::SHIFT) {
2871 c.to_uppercase().next().unwrap_or(c)
2872 } else {
2873 c
2874 };
2875 if ch == ' ' {
2886 self.handle_widget_command(
2887 panel_id,
2888 fresh_core::api::WidgetAction::Key {
2889 key: "Space".to_string(),
2890 },
2891 );
2892 return true;
2893 }
2894 self.handle_widget_command(
2895 panel_id,
2896 fresh_core::api::WidgetAction::TextInputChar {
2897 text: ch.to_string(),
2898 },
2899 );
2900 return true;
2901 }
2902 true
2907 }
2908}