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
443 .active_window()
444 .is_composite_buffer(self.active_buffer())
445 {
446 KeyContext::CompositeBuffer
447 } else {
448 self.active_window().key_context.clone()
450 }
451 }
452
453 pub fn handle_key(
456 &mut self,
457 code: crossterm::event::KeyCode,
458 modifiers: crossterm::event::KeyModifiers,
459 ) -> AnyhowResult<()> {
460 use crate::input::keybindings::Action;
461
462 let _t_total = std::time::Instant::now();
463
464 tracing::trace!(
465 "Editor.handle_key: code={:?}, modifiers={:?}",
466 code,
467 modifiers
468 );
469
470 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
472
473 if self.active_window().is_event_debug_active() {
477 self.active_window_mut()
478 .handle_event_debug_input(&key_event);
479 return Ok(());
480 }
481
482 if self.dispatch_terminal_input(&key_event).is_some() {
484 return Ok(());
485 }
486
487 if self.try_resolve_next_key_callback(&key_event) {
494 return Ok(());
495 }
496
497 if self.floating_widget_panel.is_some()
504 && self.dispatch_floating_widget_key(code, modifiers)
505 {
506 return Ok(());
507 }
508
509 let active_split = self.effective_active_split();
518 if let Some(view_state) = self
519 .windows
520 .get_mut(&self.active_window)
521 .and_then(|w| w.split_view_states_mut())
522 .expect("active window must have a populated split layout")
523 .get_mut(&active_split)
524 {
525 view_state.viewport.clear_skip_ensure_visible();
526 }
527
528 if self.active_window_mut().theme_info_popup.is_some() {
530 self.active_window_mut().theme_info_popup = None;
531 }
532
533 if self
534 .active_window_mut()
535 .file_explorer_context_menu
536 .is_some()
537 {
538 if let Some(result) = self.handle_file_explorer_context_menu_key(code, modifiers) {
539 return result;
540 }
541 }
542
543 let mut context = self.get_key_context();
545
546 let popup_visible_on_screen =
556 self.global_popups.is_visible() || self.active_state().popups.is_visible();
557 if popup_visible_on_screen {
558 let (is_transient_popup, has_selection) = {
562 let popup = self
563 .global_popups
564 .top()
565 .or_else(|| self.active_state().popups.top());
566 (
567 popup.is_some_and(|p| p.transient),
568 popup.is_some_and(|p| p.has_selection()),
569 )
570 };
571
572 let is_copy_key = key_event.code == crossterm::event::KeyCode::Char('c')
574 && key_event
575 .modifiers
576 .contains(crossterm::event::KeyModifiers::CONTROL);
577
578 let resolved_action = self
583 .keybindings
584 .read()
585 .ok()
586 .map(|kb| kb.resolve(&key_event, context.clone()));
587 let is_focus_popup_key = matches!(
588 resolved_action,
589 Some(crate::input::keybindings::Action::PopupFocus)
590 );
591
592 if is_transient_popup && !(has_selection && is_copy_key) && !is_focus_popup_key {
593 self.hide_popup();
595 tracing::debug!("Dismissed transient popup on key press");
596 context = self.get_key_context();
598 }
599 }
600
601 if let Some(action) = self.resolve_unfocused_popup_action(&key_event) {
607 self.handle_action(action)?;
608 return Ok(());
609 }
610
611 if self.dispatch_modal_input(&key_event).is_some() {
613 return Ok(());
614 }
615
616 if context != self.get_key_context() {
619 context = self.get_key_context();
620 }
621
622 let should_check_mode_bindings =
626 matches!(context, crate::input::keybindings::KeyContext::Normal);
627
628 if should_check_mode_bindings {
629 let effective_mode = self.effective_mode().map(|s| s.to_owned());
632
633 if let Some(ref mode_name) = effective_mode {
634 let mode_ctx = crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
635 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
636
637 let (chord_result, resolved_action) = {
639 let keybindings = self.keybindings.read().unwrap();
640 let chord_result = keybindings.resolve_chord(
641 &self.active_window().chord_state,
642 &key_event,
643 mode_ctx.clone(),
644 );
645 let resolved = keybindings.resolve(&key_event, mode_ctx);
646 (chord_result, resolved)
647 };
648 match chord_result {
649 crate::input::keybindings::ChordResolution::Complete(action) => {
650 tracing::debug!("Mode chord resolved to action: {:?}", action);
651 self.active_window_mut().chord_state.clear();
652 return self.handle_action(action);
653 }
654 crate::input::keybindings::ChordResolution::Partial => {
655 tracing::debug!("Potential chord prefix in mode '{}'", mode_name);
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() {
661 tracing::debug!("Chord sequence abandoned in mode, clearing state");
662 self.active_window_mut().chord_state.clear();
663 }
664 }
665 }
666
667 if resolved_action != Action::None {
669 return self.handle_action(resolved_action);
670 }
671 }
672
673 if let Some(ref mode_name) = effective_mode {
685 if self.mode_registry.allows_text_input(mode_name) {
686 if let KeyCode::Char(c) = code {
687 let ch = if modifiers.contains(KeyModifiers::SHIFT) {
688 c.to_uppercase().next().unwrap_or(c)
689 } else {
690 c
691 };
692 if !modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) {
693 let action_name = format!("mode_text_input:{}", ch);
694 return self.handle_action(Action::PluginAction(action_name));
695 }
696 }
697 tracing::debug!("Blocking unbound key in text-input mode '{}'", mode_name);
698 return Ok(());
699 }
700 }
701 if let Some(ref mode_name) = self.active_window().editor_mode {
702 if self.mode_registry.is_read_only(mode_name) {
703 tracing::debug!("Ignoring unbound key in read-only mode '{}'", mode_name);
704 return Ok(());
705 }
706 tracing::debug!(
707 "Mode '{}' is not read-only, allowing key through",
708 mode_name
709 );
710 }
711 }
712
713 {
720 let active_buf = self.active_buffer();
721 let active_split = self.effective_active_split();
722 if self.active_window().is_composite_buffer(active_buf) {
723 if let Some(handled) =
724 self.try_route_composite_key(active_split, active_buf, &key_event)
725 {
726 return handled;
727 }
728 }
729 }
730
731 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
733 let (chord_result, action) = {
734 let keybindings = self.keybindings.read().unwrap();
735 let chord_result = keybindings.resolve_chord(
736 &self.active_window().chord_state,
737 &key_event,
738 context.clone(),
739 );
740 let action = keybindings.resolve(&key_event, context.clone());
741 (chord_result, action)
742 };
743
744 match chord_result {
745 crate::input::keybindings::ChordResolution::Complete(action) => {
746 tracing::debug!("Complete chord match -> Action: {:?}", action);
748 self.active_window_mut().chord_state.clear();
749 return self.handle_action(action);
750 }
751 crate::input::keybindings::ChordResolution::Partial => {
752 tracing::debug!("Partial chord match - waiting for next key");
754 self.active_window_mut().chord_state.push((code, modifiers));
755 return Ok(());
756 }
757 crate::input::keybindings::ChordResolution::NoMatch => {
758 if !self.active_window_mut().chord_state.is_empty() {
760 tracing::debug!("Chord sequence abandoned, clearing state");
761 self.active_window_mut().chord_state.clear();
762 }
763 }
764 }
765
766 tracing::trace!("Context: {:?} -> Action: {:?}", context, action);
768
769 match action {
772 Action::LspCompletion
773 | Action::LspGotoDefinition
774 | Action::LspReferences
775 | Action::LspHover
776 | Action::None => {
777 }
779 _ => {
780 self.active_window_mut().cancel_pending_lsp_requests();
782 }
783 }
784
785 self.handle_action(action)
789 }
790
791 pub(crate) fn handle_action(&mut self, action: Action) -> AnyhowResult<()> {
794 use crate::input::keybindings::Action;
795
796 self.record_macro_action(&action);
798
799 if !matches!(action, Action::DabbrevExpand) {
801 self.reset_dabbrev_state();
802 }
803
804 match action {
805 Action::Quit => self.quit(),
806 Action::ForceQuit => {
807 self.should_quit = true;
808 }
809 Action::Detach => {
810 self.should_detach = true;
811 }
812 Action::Save => {
813 if self.active_state().buffer.file_path().is_none() {
815 self.start_prompt_with_initial_text(
816 t!("file.save_as_prompt").to_string(),
817 PromptType::SaveFileAs,
818 String::new(),
819 );
820 self.init_file_open_state();
821 } else if self.check_save_conflict().is_some() {
822 self.start_prompt(
824 t!("file.file_changed_prompt").to_string(),
825 PromptType::ConfirmSaveConflict,
826 );
827 } else if let Err(e) = self.save() {
828 let msg = format!("{}", e);
829 self.active_window_mut().status_message =
830 Some(t!("file.save_failed", error = &msg).to_string());
831 }
832 }
833 Action::SaveAs => {
834 let current_path = self
836 .active_state()
837 .buffer
838 .file_path()
839 .map(|p| {
840 p.strip_prefix(&self.working_dir)
842 .unwrap_or(p)
843 .to_string_lossy()
844 .to_string()
845 })
846 .unwrap_or_default();
847 self.start_prompt_with_initial_text(
848 t!("file.save_as_prompt").to_string(),
849 PromptType::SaveFileAs,
850 current_path,
851 );
852 self.init_file_open_state();
853 }
854 Action::Open => {
855 self.start_prompt(t!("file.open_prompt").to_string(), PromptType::OpenFile);
856 self.prefill_open_file_prompt();
857 self.init_file_open_state();
858 }
859 Action::SwitchProject => {
860 self.start_prompt(
861 t!("file.switch_project_prompt").to_string(),
862 PromptType::SwitchProject,
863 );
864 self.init_folder_open_state();
865 }
866 Action::GotoLine => {
867 let has_line_index = self
868 .buffers()
869 .get(&self.active_buffer())
870 .is_none_or(|s| s.buffer.line_count().is_some());
871 if has_line_index {
872 self.start_prompt(
873 t!("file.goto_line_prompt").to_string(),
874 PromptType::GotoLine,
875 );
876 } else {
877 self.start_prompt(
878 t!("goto.scan_confirm_prompt", yes = "y", no = "N").to_string(),
879 PromptType::GotoLineScanConfirm,
880 );
881 }
882 }
883 Action::ScanLineIndex => {
884 self.start_incremental_line_scan(false);
885 }
886 Action::New => {
887 self.new_buffer();
888 }
889 Action::Close | Action::CloseTab => {
890 self.close_tab();
895 }
896 Action::Revert => {
897 if self.active_state().buffer.is_modified() {
899 let revert_key = t!("prompt.key.revert").to_string();
900 let cancel_key = t!("prompt.key.cancel").to_string();
901 self.start_prompt(
902 t!(
903 "prompt.revert_confirm",
904 revert_key = revert_key,
905 cancel_key = cancel_key
906 )
907 .to_string(),
908 PromptType::ConfirmRevert,
909 );
910 } else {
911 if let Err(e) = self.revert_file() {
913 self.set_status_message(
914 t!("error.failed_to_revert", error = e.to_string()).to_string(),
915 );
916 }
917 }
918 }
919 Action::ToggleAutoRevert => {
920 self.toggle_auto_revert();
921 }
922 Action::FormatBuffer => {
923 if let Err(e) = self.format_buffer() {
924 self.set_status_message(
925 t!("error.format_failed", error = e.to_string()).to_string(),
926 );
927 }
928 }
929 Action::TrimTrailingWhitespace => match self.trim_trailing_whitespace() {
930 Ok(true) => {
931 self.set_status_message(t!("whitespace.trimmed").to_string());
932 }
933 Ok(false) => {
934 self.set_status_message(t!("whitespace.no_trailing").to_string());
935 }
936 Err(e) => {
937 self.set_status_message(
938 t!("error.trim_whitespace_failed", error = e).to_string(),
939 );
940 }
941 },
942 Action::EnsureFinalNewline => match self.ensure_final_newline() {
943 Ok(true) => {
944 self.set_status_message(t!("whitespace.newline_added").to_string());
945 }
946 Ok(false) => {
947 self.set_status_message(t!("whitespace.already_has_newline").to_string());
948 }
949 Err(e) => {
950 self.set_status_message(
951 t!("error.ensure_newline_failed", error = e).to_string(),
952 );
953 }
954 },
955 Action::Copy => {
956 let popup = self
958 .global_popups
959 .top()
960 .or_else(|| self.active_state().popups.top());
961 if let Some(popup) = popup {
962 if popup.has_selection() {
963 if let Some(text) = popup.get_selected_text() {
964 self.clipboard.copy(text);
965 self.set_status_message(t!("clipboard.copied").to_string());
966 return Ok(());
967 }
968 }
969 }
970 if self.active_window_mut().key_context
971 == crate::input::keybindings::KeyContext::FileExplorer
972 {
973 self.active_window_mut().file_explorer_copy();
974 return Ok(());
975 }
976 let buffer_id = self.active_buffer();
978 if self.active_window().is_composite_buffer(buffer_id) {
979 if let Some(_handled) = self.handle_composite_action(buffer_id, &Action::Copy) {
980 return Ok(());
981 }
982 }
983 self.copy_selection()
984 }
985 Action::CopyWithTheme(theme) => self.copy_selection_with_theme(&theme),
986 Action::CopyFilePath => self.copy_active_buffer_path(false),
987 Action::CopyRelativeFilePath => self.copy_active_buffer_path(true),
988 Action::Cut => {
989 if self.active_window_mut().key_context
990 == crate::input::keybindings::KeyContext::FileExplorer
991 {
992 self.active_window_mut().file_explorer_cut();
993 return Ok(());
994 }
995 if self.active_window().is_editing_disabled() {
996 self.set_status_message(t!("buffer.editing_disabled").to_string());
997 return Ok(());
998 }
999 self.cut_selection()
1000 }
1001 Action::Paste => {
1002 if self.active_window_mut().key_context
1003 == crate::input::keybindings::KeyContext::FileExplorer
1004 {
1005 self.file_explorer_paste();
1006 return Ok(());
1007 }
1008 if self.active_window().is_editing_disabled() {
1009 self.set_status_message(t!("buffer.editing_disabled").to_string());
1010 return Ok(());
1011 }
1012 self.paste()
1013 }
1014 Action::YankWordForward => self.yank_word_forward(),
1015 Action::YankWordBackward => self.yank_word_backward(),
1016 Action::YankToLineEnd => self.yank_to_line_end(),
1017 Action::YankToLineStart => self.yank_to_line_start(),
1018 Action::YankViWordEnd => self.yank_vi_word_end(),
1019 Action::Undo => {
1020 self.handle_undo();
1021 }
1022 Action::Redo => {
1023 self.handle_redo();
1024 }
1025 Action::ShowHelp => {
1026 self.active_window_mut().open_help_manual();
1027 }
1028 Action::ShowKeyboardShortcuts => {
1029 self.active_window_mut().open_keyboard_shortcuts();
1030 }
1031 Action::ShowWarnings => {
1032 self.show_warnings_popup();
1033 }
1034 Action::ShowStatusLog => {
1035 self.open_status_log();
1036 }
1037 Action::ShowLspStatus => {
1038 self.show_lsp_status_popup();
1039 }
1040 Action::ShowRemoteIndicatorMenu => {
1041 self.show_remote_indicator_popup();
1042 }
1043 Action::ClearWarnings => {
1044 self.active_window_mut().clear_warnings();
1045 }
1046 Action::CommandPalette => {
1047 if let Some(prompt) = &self.active_window_mut().prompt {
1050 if prompt.prompt_type == PromptType::QuickOpen {
1051 self.cancel_prompt();
1052 return Ok(());
1053 }
1054 }
1055 self.start_quick_open();
1056 }
1057 Action::QuickOpen => {
1058 if let Some(prompt) = &self.active_window_mut().prompt {
1060 if prompt.prompt_type == PromptType::QuickOpen {
1061 self.cancel_prompt();
1062 return Ok(());
1063 }
1064 }
1065
1066 self.start_quick_open();
1068 }
1069 Action::QuickOpenBuffers => {
1070 if let Some(prompt) = &self.active_window_mut().prompt {
1071 if prompt.prompt_type == PromptType::QuickOpen {
1072 self.cancel_prompt();
1073 return Ok(());
1074 }
1075 }
1076 self.start_quick_open_with_prefix("#");
1077 }
1078 Action::QuickOpenFiles => {
1079 if let Some(prompt) = &self.active_window_mut().prompt {
1080 if prompt.prompt_type == PromptType::QuickOpen {
1081 self.cancel_prompt();
1082 return Ok(());
1083 }
1084 }
1085 self.start_quick_open_with_prefix("");
1086 }
1087 Action::OpenLiveGrep => {
1088 #[cfg(feature = "plugins")]
1094 {
1095 let result = self
1096 .plugin_manager
1097 .read()
1098 .unwrap()
1099 .execute_action_async("start_live_grep");
1100 if let Some(result) = result {
1101 match result {
1102 Ok(receiver) => {
1103 self.pending_plugin_actions
1104 .push(("start_live_grep".to_string(), receiver));
1105 }
1106 Err(e) => {
1107 self.set_status_message(format!("Live Grep unavailable: {}", e));
1108 }
1109 }
1110 } else {
1111 self.set_status_message("Live Grep plugin not loaded".to_string());
1112 }
1113 }
1114 #[cfg(not(feature = "plugins"))]
1115 {
1116 self.set_status_message("Live Grep requires the plugins feature".to_string());
1117 }
1118 }
1119 Action::ResumeLiveGrep => {
1120 let cached = self.active_window_mut().live_grep_last_state.clone();
1126 match cached {
1127 Some(state) if state.cached_results.as_ref().is_some_and(|r| !r.is_empty()) => {
1128 let results = state.cached_results.unwrap_or_default();
1129 let suggestions: Vec<crate::input::commands::Suggestion> = results
1134 .into_iter()
1135 .map(|m| {
1136 let label = format!("{}:{}", m.file, m.line);
1137 let value = format!("{}:{}:{}", m.file, m.line, m.column);
1138 let mut s = crate::input::commands::Suggestion::new(label);
1139 s.description = Some(m.content);
1140 s.value = Some(value);
1141 s
1142 })
1143 .collect();
1144 let mut prompt = crate::view::prompt::Prompt::with_suggestions(
1151 "Live grep: ".to_string(),
1152 PromptType::LiveGrep,
1153 suggestions,
1154 );
1155 prompt.input = state.query;
1156 prompt.cursor_pos = prompt.input.len();
1157 if let Some(idx) = state.selected_index {
1158 if idx < prompt.suggestions.len() {
1159 prompt.selected_suggestion = Some(idx);
1160 }
1161 }
1162 prompt.suggestions_set_for_input = Some(prompt.input.clone());
1163 prompt.overlay = true;
1165 self.active_window_mut().prompt = Some(prompt);
1166 }
1167 _ => {
1168 #[cfg(feature = "plugins")]
1170 {
1171 let result = self
1172 .plugin_manager
1173 .read()
1174 .unwrap()
1175 .execute_action_async("start_live_grep");
1176 if let Some(result) = result {
1177 match result {
1178 Ok(receiver) => {
1179 self.pending_plugin_actions
1180 .push(("start_live_grep".to_string(), receiver));
1181 }
1182 Err(e) => {
1183 self.set_status_message(format!(
1184 "Live Grep unavailable: {}",
1185 e
1186 ));
1187 }
1188 }
1189 }
1190 }
1191 }
1192 }
1193 }
1194 Action::LiveGrepExportQuickfix => {
1195 let is_grep = self
1200 .active_window()
1201 .prompt
1202 .as_ref()
1203 .map(|p| match &p.prompt_type {
1204 PromptType::LiveGrep => true,
1205 PromptType::Plugin { custom_type } => custom_type == "live-grep",
1206 _ => false,
1207 })
1208 .unwrap_or(false);
1209 if !is_grep {
1210 self.set_status_message(
1211 "Quickfix export is only available inside Live Grep".to_string(),
1212 );
1213 return Ok(());
1214 }
1215 let (query, matches) = {
1216 let prompt = self.active_window().prompt.as_ref().unwrap();
1217 (
1218 prompt.input.clone(),
1219 self.snapshot_prompt_results_for_grep(prompt),
1220 )
1221 };
1222 if matches.is_empty() {
1223 self.set_status_message("No Live Grep results to export".to_string());
1224 return Ok(());
1225 }
1226 self.cancel_prompt();
1228 self.install_quickfix_in_dock(query, matches);
1230 }
1231 Action::ToggleUtilityDock => {
1232 use crate::view::split::SplitRole;
1233 if let Some(dock_leaf) = self
1234 .windows
1235 .get(&self.active_window)
1236 .and_then(|w| w.buffers.splits())
1237 .map(|(mgr, _)| mgr)
1238 .expect("active window must have a populated split layout")
1239 .find_leaf_by_role(SplitRole::UtilityDock)
1240 {
1241 let active = self
1242 .windows
1243 .get(&self.active_window)
1244 .and_then(|w| w.buffers.splits())
1245 .map(|(mgr, _)| mgr)
1246 .expect("active window must have a populated split layout")
1247 .active_split();
1248 if active == dock_leaf {
1249 self.next_split();
1254 } else {
1255 self.windows
1256 .get_mut(&self.active_window)
1257 .and_then(|w| w.split_manager_mut())
1258 .expect("active window must have a populated split layout")
1259 .set_active_split(dock_leaf);
1260 }
1261 } else {
1262 self.set_status_message(
1263 "No Utility Dock open — invoke a dock-aware utility (Diagnostics, Search/Replace, …)"
1264 .to_string(),
1265 );
1266 }
1267 }
1268 Action::CycleLiveGrepProvider => {
1269 let in_live_grep = self
1275 .active_window()
1276 .prompt
1277 .as_ref()
1278 .map(|p| match &p.prompt_type {
1279 PromptType::LiveGrep => true,
1280 PromptType::Plugin { custom_type } => custom_type == "live-grep",
1281 _ => false,
1282 })
1283 .unwrap_or(false);
1284 if !in_live_grep {
1285 self.set_status_message(
1286 "Cycle Live Grep provider only works inside Live Grep".to_string(),
1287 );
1288 return Ok(());
1289 }
1290 #[cfg(feature = "plugins")]
1291 {
1292 let result = self
1293 .plugin_manager
1294 .read()
1295 .unwrap()
1296 .execute_action_async("live_grep_cycle_provider");
1297 if let Some(result) = result {
1298 match result {
1299 Ok(receiver) => {
1300 self.pending_plugin_actions
1301 .push(("live_grep_cycle_provider".to_string(), receiver));
1302 }
1303 Err(e) => {
1304 self.set_status_message(format!("Live Grep cycle failed: {}", e));
1305 }
1306 }
1307 } else {
1308 self.set_status_message("Live Grep plugin not loaded".to_string());
1309 }
1310 }
1311 #[cfg(not(feature = "plugins"))]
1312 {
1313 self.set_status_message(
1314 "Live Grep cycle requires the plugins feature".to_string(),
1315 );
1316 }
1317 }
1318 Action::OpenTerminalInDock => {
1319 use crate::model::event::SplitDirection;
1320 use crate::view::split::SplitRole;
1321 if let Some(dock_leaf) = self
1322 .windows
1323 .get(&self.active_window)
1324 .and_then(|w| w.buffers.splits())
1325 .map(|(mgr, _)| mgr)
1326 .expect("active window must have a populated split layout")
1327 .find_leaf_by_role(SplitRole::UtilityDock)
1328 {
1329 self.windows
1332 .get_mut(&self.active_window)
1333 .and_then(|w| w.split_manager_mut())
1334 .expect("active window must have a populated split layout")
1335 .set_active_split(dock_leaf);
1336 self.open_terminal();
1337 } else {
1338 let Some(terminal_id) = self.spawn_terminal_session() else {
1345 return Ok(());
1346 };
1347 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
1348 match self
1351 .windows
1352 .get_mut(&self.active_window)
1353 .and_then(|w| w.split_manager_mut())
1354 .expect("active window must have a populated split layout")
1355 .split_root_positioned(SplitDirection::Horizontal, buffer_id, 0.7, false)
1356 {
1357 Ok(new_leaf) => {
1358 let mut view_state = crate::view::split::SplitViewState::with_buffer(
1359 self.terminal_width,
1360 self.terminal_height,
1361 buffer_id,
1362 );
1363 view_state.apply_config_defaults(
1364 self.config.editor.line_numbers,
1365 self.config.editor.highlight_current_line,
1366 self.active_window().resolve_line_wrap_for_buffer(buffer_id),
1367 self.config.editor.wrap_indent,
1368 self.active_window()
1369 .resolve_wrap_column_for_buffer(buffer_id),
1370 self.config.editor.rulers.clone(),
1371 );
1372 view_state.viewport.line_wrap_enabled = false;
1376 self.windows
1377 .get_mut(&self.active_window)
1378 .and_then(|w| w.split_view_states_mut())
1379 .expect("active window must have a populated split layout")
1380 .insert(new_leaf, view_state);
1381 self.windows
1382 .get_mut(&self.active_window)
1383 .and_then(|w| w.split_manager_mut())
1384 .expect("active window must have a populated split layout")
1385 .set_leaf_role(new_leaf, Some(SplitRole::UtilityDock));
1386 self.windows
1387 .get_mut(&self.active_window)
1388 .and_then(|w| w.split_manager_mut())
1389 .expect("active window must have a populated split layout")
1390 .set_active_split(new_leaf);
1391 self.active_window_mut().terminal_mode = true;
1397 self.active_window_mut().key_context =
1398 crate::input::keybindings::KeyContext::Terminal;
1399 self.active_window_mut().resize_visible_terminals();
1400 let exit_key = self
1401 .keybindings
1402 .read()
1403 .unwrap()
1404 .find_keybinding_for_action(
1405 "terminal_escape",
1406 crate::input::keybindings::KeyContext::Terminal,
1407 )
1408 .unwrap_or_else(|| "Ctrl+Space".to_string());
1409 self.set_status_message(
1410 rust_i18n::t!(
1411 "terminal.opened",
1412 id = terminal_id.0,
1413 exit_key = exit_key
1414 )
1415 .to_string(),
1416 );
1417 tracing::info!(
1418 "Opened terminal {:?} into new dock leaf {:?} (buffer {:?})",
1419 terminal_id,
1420 new_leaf,
1421 buffer_id
1422 );
1423 }
1424 Err(e) => {
1425 self.set_status_message(format!(
1426 "Failed to create dock for terminal: {}",
1427 e
1428 ));
1429 return Ok(());
1430 }
1431 }
1432 }
1433 }
1434 Action::ToggleLineWrap => {
1435 let new_value = !self.config.editor.line_wrap;
1436 self.config_mut().editor.line_wrap = new_value;
1437 self.sync_windows_config();
1445
1446 let leaf_ids: Vec<_> = self
1449 .windows
1450 .get(&self.active_window)
1451 .and_then(|w| w.buffers.splits())
1452 .map(|(_, vs)| vs)
1453 .expect("active window must have a populated split layout")
1454 .keys()
1455 .copied()
1456 .collect();
1457 for leaf_id in leaf_ids {
1458 let buffer_id = self
1459 .split_manager_mut()
1460 .get_buffer_id(leaf_id.into())
1461 .unwrap_or(BufferId(0));
1462 let effective_wrap =
1463 self.active_window().resolve_line_wrap_for_buffer(buffer_id);
1464 let wrap_column = self
1465 .active_window()
1466 .resolve_wrap_column_for_buffer(buffer_id);
1467 if let Some(view_state) = self
1468 .windows
1469 .get_mut(&self.active_window)
1470 .and_then(|w| w.split_view_states_mut())
1471 .expect("active window must have a populated split layout")
1472 .get_mut(&leaf_id)
1473 {
1474 view_state.viewport.line_wrap_enabled = effective_wrap;
1475 view_state.viewport.wrap_indent = self.config.editor.wrap_indent;
1476 view_state.viewport.wrap_column = wrap_column;
1477 }
1478 }
1479
1480 let state = if self.config.editor.line_wrap {
1481 t!("view.state_enabled").to_string()
1482 } else {
1483 t!("view.state_disabled").to_string()
1484 };
1485 self.set_status_message(t!("view.line_wrap_state", state = state).to_string());
1486 }
1487 Action::ToggleCurrentLineHighlight => {
1488 let new_value = !self.config.editor.highlight_current_line;
1489 self.config_mut().editor.highlight_current_line = new_value;
1490
1491 let leaf_ids: Vec<_> = self
1493 .windows
1494 .get(&self.active_window)
1495 .and_then(|w| w.buffers.splits())
1496 .map(|(_, vs)| vs)
1497 .expect("active window must have a populated split layout")
1498 .keys()
1499 .copied()
1500 .collect();
1501 for leaf_id in leaf_ids {
1502 if let Some(view_state) = self
1503 .windows
1504 .get_mut(&self.active_window)
1505 .and_then(|w| w.split_view_states_mut())
1506 .expect("active window must have a populated split layout")
1507 .get_mut(&leaf_id)
1508 {
1509 view_state.highlight_current_line =
1510 self.config.editor.highlight_current_line;
1511 }
1512 }
1513
1514 let state = if self.config.editor.highlight_current_line {
1515 t!("view.state_enabled").to_string()
1516 } else {
1517 t!("view.state_disabled").to_string()
1518 };
1519 self.set_status_message(
1520 t!("view.current_line_highlight_state", state = state).to_string(),
1521 );
1522 }
1523 Action::ToggleReadOnly => {
1524 let buffer_id = self.active_buffer();
1525 let is_now_read_only = self
1526 .active_window()
1527 .buffer_metadata
1528 .get(&buffer_id)
1529 .map(|m| !m.read_only)
1530 .unwrap_or(false);
1531 self.active_window_mut()
1532 .mark_buffer_read_only(buffer_id, is_now_read_only);
1533
1534 let state_str = if is_now_read_only {
1535 t!("view.state_enabled").to_string()
1536 } else {
1537 t!("view.state_disabled").to_string()
1538 };
1539 self.set_status_message(t!("view.read_only_state", state = state_str).to_string());
1540 }
1541 Action::TogglePageView => {
1542 self.active_window_mut().handle_toggle_page_view();
1543 }
1544 Action::SetPageWidth => {
1545 let active_split = self
1546 .windows
1547 .get(&self.active_window)
1548 .and_then(|w| w.buffers.splits())
1549 .map(|(mgr, _)| mgr)
1550 .expect("active window must have a populated split layout")
1551 .active_split();
1552 let current = self
1553 .windows
1554 .get(&self.active_window)
1555 .and_then(|w| w.buffers.splits())
1556 .map(|(_, vs)| vs)
1557 .expect("active window must have a populated split layout")
1558 .get(&active_split)
1559 .and_then(|v| v.compose_width.map(|w| w.to_string()))
1560 .unwrap_or_default();
1561 self.start_prompt_with_initial_text(
1562 "Page width (empty = viewport): ".to_string(),
1563 PromptType::SetPageWidth,
1564 current,
1565 );
1566 }
1567 Action::SetBackground => {
1568 let default_path = self
1569 .ansi_background_path
1570 .as_ref()
1571 .and_then(|p| {
1572 p.strip_prefix(&self.working_dir)
1573 .ok()
1574 .map(|rel| rel.to_string_lossy().to_string())
1575 })
1576 .unwrap_or_else(|| DEFAULT_BACKGROUND_FILE.to_string());
1577
1578 self.start_prompt_with_initial_text(
1579 "Background file: ".to_string(),
1580 PromptType::SetBackgroundFile,
1581 default_path,
1582 );
1583 }
1584 Action::SetBackgroundBlend => {
1585 let default_amount = format!("{:.2}", self.background_fade);
1586 self.start_prompt_with_initial_text(
1587 "Background blend (0-1): ".to_string(),
1588 PromptType::SetBackgroundBlend,
1589 default_amount,
1590 );
1591 }
1592 Action::LspCompletion => {
1593 self.request_completion();
1594 }
1595 Action::DabbrevExpand => {
1596 self.dabbrev_expand();
1597 }
1598 Action::LspGotoDefinition => {
1599 self.request_goto_definition()?;
1600 }
1601 Action::LspRename => {
1602 self.start_rename()?;
1603 }
1604 Action::LspHover => {
1605 self.request_hover()?;
1606 }
1607 Action::LspReferences => {
1608 self.request_references()?;
1609 }
1610 Action::LspSignatureHelp => {
1611 self.request_signature_help();
1612 }
1613 Action::LspCodeActions => {
1614 self.request_code_actions()?;
1615 }
1616 Action::LspRestart => {
1617 self.handle_lsp_restart();
1618 }
1619 Action::LspStop => {
1620 self.handle_lsp_stop();
1621 }
1622 Action::LspToggleForBuffer => {
1623 self.handle_lsp_toggle_for_buffer();
1624 }
1625 Action::ToggleInlayHints => {
1626 self.toggle_inlay_hints();
1627 }
1628 Action::DumpConfig => {
1629 self.dump_config();
1630 }
1631 Action::RedrawScreen => {
1632 self.request_full_redraw();
1633 }
1634 Action::SelectTheme => {
1635 self.start_select_theme_prompt();
1636 }
1637 Action::InspectThemeAtCursor => {
1638 self.inspect_theme_at_cursor();
1639 }
1640 Action::SelectKeybindingMap => {
1641 self.start_select_keybinding_map_prompt();
1642 }
1643 Action::SelectCursorStyle => {
1644 self.start_select_cursor_style_prompt();
1645 }
1646 Action::SelectLocale => {
1647 self.start_select_locale_prompt();
1648 }
1649 Action::Search => {
1650 let is_search_prompt = self.active_window().prompt.as_ref().is_some_and(|p| {
1652 matches!(
1653 p.prompt_type,
1654 PromptType::Search
1655 | PromptType::ReplaceSearch
1656 | PromptType::QueryReplaceSearch
1657 )
1658 });
1659
1660 if is_search_prompt {
1661 self.confirm_prompt();
1662 } else {
1663 self.start_search_prompt(
1664 t!("file.search_prompt").to_string(),
1665 PromptType::Search,
1666 false,
1667 );
1668 }
1669 }
1670 Action::Replace => {
1671 self.start_search_prompt(
1673 t!("file.replace_prompt").to_string(),
1674 PromptType::ReplaceSearch,
1675 false,
1676 );
1677 }
1678 Action::QueryReplace => {
1679 self.active_window_mut().search_confirm_each = true;
1681 self.start_search_prompt(
1682 "Query replace: ".to_string(),
1683 PromptType::QueryReplaceSearch,
1684 false,
1685 );
1686 }
1687 Action::FindInSelection => {
1688 self.start_search_prompt(
1689 t!("file.search_prompt").to_string(),
1690 PromptType::Search,
1691 true,
1692 );
1693 }
1694 Action::FindNext => {
1695 self.find_next();
1696 }
1697 Action::FindPrevious => {
1698 self.find_previous();
1699 }
1700 Action::FindSelectionNext => {
1701 self.find_selection_next();
1702 }
1703 Action::FindSelectionPrevious => {
1704 self.find_selection_previous();
1705 }
1706 Action::AddCursorNextMatch => self.add_cursor_at_next_match(),
1707 Action::AddCursorAbove => self.add_cursor_above(),
1708 Action::AddCursorBelow => self.add_cursor_below(),
1709 Action::NextBuffer => self.next_buffer(),
1710 Action::PrevBuffer => self.prev_buffer(),
1711 Action::SwitchToPreviousTab => self.switch_to_previous_tab(),
1712 Action::SwitchToTabByName => self.start_switch_to_tab_prompt(),
1713
1714 Action::ScrollTabsLeft => {
1716 let active_split_id = self
1717 .windows
1718 .get(&self.active_window)
1719 .and_then(|w| w.buffers.splits())
1720 .map(|(mgr, _)| mgr)
1721 .expect("active window must have a populated split layout")
1722 .active_split();
1723 if let Some(view_state) = self
1724 .windows
1725 .get_mut(&self.active_window)
1726 .and_then(|w| w.split_view_states_mut())
1727 .expect("active window must have a populated split layout")
1728 .get_mut(&active_split_id)
1729 {
1730 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_sub(5);
1731 self.set_status_message(t!("status.scrolled_tabs_left").to_string());
1732 }
1733 }
1734 Action::ScrollTabsRight => {
1735 let active_split_id = self
1736 .windows
1737 .get(&self.active_window)
1738 .and_then(|w| w.buffers.splits())
1739 .map(|(mgr, _)| mgr)
1740 .expect("active window must have a populated split layout")
1741 .active_split();
1742 if let Some(view_state) = self
1743 .windows
1744 .get_mut(&self.active_window)
1745 .and_then(|w| w.split_view_states_mut())
1746 .expect("active window must have a populated split layout")
1747 .get_mut(&active_split_id)
1748 {
1749 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_add(5);
1750 self.set_status_message(t!("status.scrolled_tabs_right").to_string());
1751 }
1752 }
1753 Action::NavigateBack => self.navigate_back(),
1754 Action::NavigateForward => self.navigate_forward(),
1755 Action::SplitHorizontal => self.split_pane_horizontal(),
1756 Action::SplitVertical => self.split_pane_vertical(),
1757 Action::CloseSplit => self.close_active_split(),
1758 Action::NextSplit => self.next_split(),
1759 Action::PrevSplit => self.prev_split(),
1760 Action::IncreaseSplitSize => self.adjust_split_size(0.05),
1761 Action::DecreaseSplitSize => self.adjust_split_size(-0.05),
1762 Action::ToggleMaximizeSplit => self.toggle_maximize_split(),
1763 Action::ToggleFileExplorer => self.toggle_file_explorer(),
1764 Action::ToggleMenuBar => self.toggle_menu_bar(),
1765 Action::ToggleTabBar => self.active_window_mut().toggle_tab_bar(),
1766 Action::ToggleStatusBar => self.active_window_mut().toggle_status_bar(),
1767 Action::TogglePromptLine => self.active_window_mut().toggle_prompt_line(),
1768 Action::ToggleVerticalScrollbar => self.toggle_vertical_scrollbar(),
1769 Action::ToggleHorizontalScrollbar => self.toggle_horizontal_scrollbar(),
1770 Action::ToggleLineNumbers => self.toggle_line_numbers(),
1771 Action::ToggleScrollSync => self.active_window_mut().toggle_scroll_sync(),
1772 Action::ToggleMouseCapture => self.toggle_mouse_capture(),
1773 Action::ToggleMouseHover => self.toggle_mouse_hover(),
1774 Action::ToggleDebugHighlights => self.active_window_mut().toggle_debug_highlights(),
1775 Action::AddRuler => {
1777 self.start_prompt(t!("rulers.add_prompt").to_string(), PromptType::AddRuler);
1778 }
1779 Action::RemoveRuler => {
1780 self.start_remove_ruler_prompt();
1781 }
1782 Action::SetTabSize => {
1784 let current = self
1785 .buffers()
1786 .get(&self.active_buffer())
1787 .map(|s| s.buffer_settings.tab_size.to_string())
1788 .unwrap_or_else(|| "4".to_string());
1789 self.start_prompt_with_initial_text(
1790 "Tab size: ".to_string(),
1791 PromptType::SetTabSize,
1792 current,
1793 );
1794 }
1795 Action::SetLineEnding => {
1796 self.start_set_line_ending_prompt();
1797 }
1798 Action::SetEncoding => {
1799 self.start_set_encoding_prompt();
1800 }
1801 Action::ReloadWithEncoding => {
1802 self.start_reload_with_encoding_prompt();
1803 }
1804 Action::SetLanguage => {
1805 self.start_set_language_prompt();
1806 }
1807 Action::ToggleIndentationStyle => {
1808 let __buffer_id = self.active_buffer();
1809 if let Some(state) = self
1810 .windows
1811 .get_mut(&self.active_window)
1812 .map(|w| &mut w.buffers)
1813 .expect("active window present")
1814 .get_mut(&__buffer_id)
1815 {
1816 state.buffer_settings.use_tabs = !state.buffer_settings.use_tabs;
1817 let status = if state.buffer_settings.use_tabs {
1818 "Indentation: Tabs"
1819 } else {
1820 "Indentation: Spaces"
1821 };
1822 self.set_status_message(status.to_string());
1823 }
1824 }
1825 Action::ToggleTabIndicators | Action::ToggleWhitespaceIndicators => {
1826 let __buffer_id = self.active_buffer();
1827 if let Some(state) = self
1828 .windows
1829 .get_mut(&self.active_window)
1830 .map(|w| &mut w.buffers)
1831 .expect("active window present")
1832 .get_mut(&__buffer_id)
1833 {
1834 state.buffer_settings.whitespace.toggle_all();
1835 let status = if state.buffer_settings.whitespace.any_visible() {
1836 t!("toggle.whitespace_indicators_shown")
1837 } else {
1838 t!("toggle.whitespace_indicators_hidden")
1839 };
1840 self.set_status_message(status.to_string());
1841 }
1842 }
1843 Action::ResetBufferSettings => self.reset_buffer_settings(),
1844 Action::FocusFileExplorer => self.focus_file_explorer(),
1845 Action::FocusEditor => self.active_window_mut().focus_editor(),
1846 Action::FileExplorerUp => self.file_explorer_navigate_up(),
1847 Action::FileExplorerDown => self.file_explorer_navigate_down(),
1848 Action::FileExplorerPageUp => self.file_explorer_page_up(),
1849 Action::FileExplorerPageDown => self.file_explorer_page_down(),
1850 Action::FileExplorerExpand => self.file_explorer_toggle_expand(),
1851 Action::FileExplorerCollapse => self.file_explorer_collapse(),
1852 Action::FileExplorerOpen => self.file_explorer_open_file()?,
1853 Action::FileExplorerRefresh => self.file_explorer_refresh(),
1854 Action::FileExplorerNewFile => self.file_explorer_new_file(),
1855 Action::FileExplorerNewDirectory => self.file_explorer_new_directory(),
1856 Action::FileExplorerDelete => self.file_explorer_delete(),
1857 Action::FileExplorerRename => self.file_explorer_rename(),
1858 Action::FileExplorerToggleHidden => self.file_explorer_toggle_hidden(),
1859 Action::FileExplorerToggleGitignored => self.file_explorer_toggle_gitignored(),
1860 Action::FileExplorerSearchClear => {
1861 self.active_window_mut().file_explorer_search_clear()
1862 }
1863 Action::FileExplorerSearchBackspace => {
1864 self.active_window_mut().file_explorer_search_pop_char()
1865 }
1866 Action::FileExplorerCopy => self.active_window_mut().file_explorer_copy(),
1867 Action::FileExplorerCut => self.active_window_mut().file_explorer_cut(),
1868 Action::FileExplorerPaste => self.file_explorer_paste(),
1869 Action::FileExplorerDuplicate => self.file_explorer_duplicate(),
1870 Action::FileExplorerCopyFullPath => self.file_explorer_copy_path(false),
1871 Action::FileExplorerCopyRelativePath => self.file_explorer_copy_path(true),
1872 Action::FileExplorerExtendSelectionUp => {
1873 self.active_window_mut().file_explorer_extend_selection_up()
1874 }
1875 Action::FileExplorerExtendSelectionDown => self
1876 .active_window_mut()
1877 .file_explorer_extend_selection_down(),
1878 Action::FileExplorerToggleSelect => {
1879 self.active_window_mut().file_explorer_toggle_select()
1880 }
1881 Action::FileExplorerSelectAll => self.active_window_mut().file_explorer_select_all(),
1882 Action::RemoveSecondaryCursors => {
1883 if let Some(events) = self
1885 .active_window_mut()
1886 .action_to_events(Action::RemoveSecondaryCursors)
1887 {
1888 let batch = Event::Batch {
1890 events: events.clone(),
1891 description: "Remove secondary cursors".to_string(),
1892 };
1893 self.active_event_log_mut().append(batch.clone());
1894 self.apply_event_to_active_buffer(&batch);
1895
1896 let active_split = self
1898 .windows
1899 .get(&self.active_window)
1900 .and_then(|w| w.buffers.splits())
1901 .map(|(mgr, _)| mgr)
1902 .expect("active window must have a populated split layout")
1903 .active_split();
1904 let active_buffer = self.active_buffer();
1905 self.active_window_mut()
1906 .ensure_cursor_visible_for_split(active_buffer, active_split);
1907 }
1908 }
1909
1910 Action::MenuActivate => {
1912 self.handle_menu_activate();
1913 }
1914 Action::MenuClose => {
1915 self.handle_menu_close();
1916 }
1917 Action::MenuLeft => {
1918 self.handle_menu_left();
1919 }
1920 Action::MenuRight => {
1921 self.handle_menu_right();
1922 }
1923 Action::MenuUp => {
1924 self.handle_menu_up();
1925 }
1926 Action::MenuDown => {
1927 self.handle_menu_down();
1928 }
1929 Action::MenuExecute => {
1930 if let Some(action) = self.handle_menu_execute() {
1931 return self.handle_action(action);
1932 }
1933 }
1934 Action::MenuOpen(menu_name) => {
1935 if self.config.editor.menu_bar_mnemonics {
1936 self.handle_menu_open(&menu_name);
1937 }
1938 }
1939
1940 Action::SwitchKeybindingMap(map_name) => {
1941 let is_builtin =
1943 matches!(map_name.as_str(), "default" | "emacs" | "vscode" | "macos");
1944 let is_user_defined = self.config.keybinding_maps.contains_key(&map_name);
1945
1946 if is_builtin || is_user_defined {
1947 self.config_mut().active_keybinding_map = map_name.clone().into();
1949
1950 *self.keybindings.write().unwrap() =
1952 crate::input::keybindings::KeybindingResolver::new(&self.config);
1953
1954 self.set_status_message(
1955 t!("view.keybindings_switched", map = map_name).to_string(),
1956 );
1957 } else {
1958 self.set_status_message(
1959 t!("view.keybindings_unknown", map = map_name).to_string(),
1960 );
1961 }
1962 }
1963
1964 Action::SmartHome => {
1965 let buffer_id = self.active_buffer();
1967 if self.active_window().is_composite_buffer(buffer_id) {
1968 if let Some(_handled) =
1969 self.handle_composite_action(buffer_id, &Action::SmartHome)
1970 {
1971 return Ok(());
1972 }
1973 }
1974 self.smart_home();
1975 }
1976 Action::ToggleComment => {
1977 self.toggle_comment();
1978 }
1979 Action::ToggleFold => {
1980 self.active_window_mut().toggle_fold_at_cursor();
1981 }
1982 Action::GoToMatchingBracket => {
1983 self.goto_matching_bracket();
1984 }
1985 Action::JumpToNextError => {
1986 self.jump_to_next_error();
1987 }
1988 Action::JumpToPreviousError => {
1989 self.jump_to_previous_error();
1990 }
1991 Action::SetBookmark(key) => {
1992 self.active_window_mut().set_bookmark(key);
1993 }
1994 Action::JumpToBookmark(key) => {
1995 self.jump_to_bookmark(key);
1996 }
1997 Action::ClearBookmark(key) => {
1998 self.active_window_mut().clear_bookmark(key);
1999 }
2000 Action::ListBookmarks => {
2001 self.active_window_mut().list_bookmarks();
2002 }
2003 Action::ToggleSearchCaseSensitive => {
2004 self.active_window_mut().search_case_sensitive =
2005 !self.active_window().search_case_sensitive;
2006 let state = if self.active_window().search_case_sensitive {
2007 "enabled"
2008 } else {
2009 "disabled"
2010 };
2011 self.set_status_message(
2012 t!("search.case_sensitive_state", state = state).to_string(),
2013 );
2014 if let Some(prompt) = &self.active_window_mut().prompt {
2017 if matches!(
2018 prompt.prompt_type,
2019 PromptType::Search
2020 | PromptType::ReplaceSearch
2021 | PromptType::QueryReplaceSearch
2022 ) {
2023 let query = prompt.input.clone();
2024 self.update_search_highlights(&query);
2025 }
2026 } else if let Some(search_state) = &self.active_window().search_state {
2027 let query = search_state.query.clone();
2028 self.perform_search(&query);
2029 }
2030 }
2031 Action::ToggleSearchWholeWord => {
2032 self.active_window_mut().search_whole_word =
2033 !self.active_window().search_whole_word;
2034 let state = if self.active_window().search_whole_word {
2035 "enabled"
2036 } else {
2037 "disabled"
2038 };
2039 self.set_status_message(t!("search.whole_word_state", state = state).to_string());
2040 if let Some(prompt) = &self.active_window_mut().prompt {
2043 if matches!(
2044 prompt.prompt_type,
2045 PromptType::Search
2046 | PromptType::ReplaceSearch
2047 | PromptType::QueryReplaceSearch
2048 ) {
2049 let query = prompt.input.clone();
2050 self.update_search_highlights(&query);
2051 }
2052 } else if let Some(search_state) = &self.active_window().search_state {
2053 let query = search_state.query.clone();
2054 self.perform_search(&query);
2055 }
2056 }
2057 Action::ToggleSearchRegex => {
2058 self.active_window_mut().search_use_regex = !self.active_window().search_use_regex;
2059 let state = if self.active_window().search_use_regex {
2060 "enabled"
2061 } else {
2062 "disabled"
2063 };
2064 self.set_status_message(t!("search.regex_state", state = state).to_string());
2065 if let Some(prompt) = &self.active_window_mut().prompt {
2068 if matches!(
2069 prompt.prompt_type,
2070 PromptType::Search
2071 | PromptType::ReplaceSearch
2072 | PromptType::QueryReplaceSearch
2073 ) {
2074 let query = prompt.input.clone();
2075 self.update_search_highlights(&query);
2076 }
2077 } else if let Some(search_state) = &self.active_window().search_state {
2078 let query = search_state.query.clone();
2079 self.perform_search(&query);
2080 }
2081 }
2082 Action::ToggleSearchConfirmEach => {
2083 self.active_window_mut().search_confirm_each =
2084 !self.active_window().search_confirm_each;
2085 let state = if self.active_window().search_confirm_each {
2086 "enabled"
2087 } else {
2088 "disabled"
2089 };
2090 self.set_status_message(t!("search.confirm_each_state", state = state).to_string());
2091 }
2092 Action::FileBrowserToggleHidden => {
2093 self.file_open_toggle_hidden();
2095 }
2096 Action::StartMacroRecording => {
2097 self.set_status_message(
2099 "Use Ctrl+Shift+R to start recording (will prompt for register)".to_string(),
2100 );
2101 }
2102 Action::StopMacroRecording => {
2103 self.stop_macro_recording();
2104 }
2105 Action::PlayMacro(key) => {
2106 self.play_macro(key);
2107 }
2108 Action::ToggleMacroRecording(key) => {
2109 self.toggle_macro_recording(key);
2110 }
2111 Action::ShowMacro(key) => {
2112 self.show_macro_in_buffer(key);
2113 }
2114 Action::ListMacros => {
2115 self.list_macros_in_buffer();
2116 }
2117 Action::PromptRecordMacro => {
2118 self.start_prompt("Record macro (0-9): ".to_string(), PromptType::RecordMacro);
2119 }
2120 Action::PromptPlayMacro => {
2121 self.start_prompt("Play macro (0-9): ".to_string(), PromptType::PlayMacro);
2122 }
2123 Action::PlayLastMacro => {
2124 if let Some(key) = self.active_window_mut().macros.last_register() {
2125 self.play_macro(key);
2126 } else {
2127 self.set_status_message(t!("status.no_macro_recorded").to_string());
2128 }
2129 }
2130 Action::PromptSetBookmark => {
2131 self.start_prompt("Set bookmark (0-9): ".to_string(), PromptType::SetBookmark);
2132 }
2133 Action::PromptJumpToBookmark => {
2134 self.start_prompt(
2135 "Jump to bookmark (0-9): ".to_string(),
2136 PromptType::JumpToBookmark,
2137 );
2138 }
2139 Action::CompositeNextHunk => {
2140 let buf = self.active_buffer();
2141 self.active_window_mut().composite_next_hunk_active(buf);
2142 }
2143 Action::CompositePrevHunk => {
2144 let buf = self.active_buffer();
2145 self.active_window_mut().composite_prev_hunk_active(buf);
2146 }
2147 Action::None => {}
2148 Action::DeleteBackward => {
2149 if self.active_window().is_editing_disabled() {
2150 self.set_status_message(t!("buffer.editing_disabled").to_string());
2151 return Ok(());
2152 }
2153 if let Some(events) = self
2155 .active_window_mut()
2156 .action_to_events(Action::DeleteBackward)
2157 {
2158 if events.len() > 1 {
2159 let description = "Delete backward".to_string();
2161 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description)
2162 {
2163 self.active_event_log_mut().append(bulk_edit);
2164 }
2165 } else {
2166 for event in events {
2167 self.active_event_log_mut().append(event.clone());
2168 self.apply_event_to_active_buffer(&event);
2169 }
2170 }
2171 }
2172 }
2173 Action::PluginAction(action_name) => {
2174 tracing::debug!("handle_action: PluginAction('{}')", action_name);
2175 #[cfg(feature = "plugins")]
2178 {
2179 let result = self
2180 .plugin_manager
2181 .read()
2182 .unwrap()
2183 .execute_action_async(&action_name);
2184 if let Some(result) = result {
2185 match result {
2186 Ok(receiver) => {
2187 self.pending_plugin_actions
2189 .push((action_name.clone(), receiver));
2190 }
2191 Err(e) => {
2192 self.set_status_message(
2193 t!("view.plugin_error", error = e.to_string()).to_string(),
2194 );
2195 tracing::error!("Plugin action error: {}", e);
2196 }
2197 }
2198 } else {
2199 self.set_status_message(
2200 t!("status.plugin_manager_unavailable").to_string(),
2201 );
2202 }
2203 }
2204 #[cfg(not(feature = "plugins"))]
2205 {
2206 let _ = action_name;
2207 self.set_status_message(
2208 "Plugins not available (compiled without plugin support)".to_string(),
2209 );
2210 }
2211 }
2212 Action::LoadPluginFromBuffer => {
2213 #[cfg(feature = "plugins")]
2214 {
2215 let buffer_id = self.active_buffer();
2216 let state = self.active_state();
2217 let buffer = &state.buffer;
2218 let total = buffer.total_bytes();
2219 let content =
2220 String::from_utf8_lossy(&buffer.slice_bytes(0..total)).to_string();
2221
2222 let is_ts = buffer
2224 .file_path()
2225 .and_then(|p| p.extension())
2226 .and_then(|e| e.to_str())
2227 .map(|e| e == "ts" || e == "tsx")
2228 .unwrap_or(true);
2229
2230 let name = buffer
2232 .file_path()
2233 .and_then(|p| p.file_name())
2234 .and_then(|s| s.to_str())
2235 .map(|s| s.to_string())
2236 .unwrap_or_else(|| "buffer-plugin".to_string());
2237
2238 let load_result = self
2239 .plugin_manager
2240 .read()
2241 .unwrap()
2242 .load_plugin_from_source(&content, &name, is_ts);
2243 match load_result {
2244 Ok(()) => {
2245 self.set_status_message(format!(
2246 "Plugin '{}' loaded from buffer",
2247 name
2248 ));
2249 }
2250 Err(e) => {
2251 self.set_status_message(format!("Failed to load plugin: {}", e));
2252 tracing::error!("LoadPluginFromBuffer error: {}", e);
2253 }
2254 }
2255
2256 self.setup_plugin_dev_lsp(buffer_id, &content);
2258 }
2259 #[cfg(not(feature = "plugins"))]
2260 {
2261 self.set_status_message(
2262 "Plugins not available (compiled without plugin support)".to_string(),
2263 );
2264 }
2265 }
2266 Action::InitReload => {
2267 self.load_init_script(true);
2272 self.fire_plugins_loaded_hook();
2275 }
2276 Action::InitEdit => {
2277 let config_dir = self.dir_context.config_dir.clone();
2280 match crate::init_script::ensure_starter(&config_dir) {
2281 Ok(path) => {
2282 let declarations =
2292 self.plugin_manager.read().unwrap().plugin_declarations();
2293 crate::init_script::write_plugin_declarations(&config_dir, &declarations);
2294 match self.open_file(&path) {
2295 Ok(_) => {
2296 self.set_status_message(format!("init.ts: {}", path.display()));
2297 }
2298 Err(e) => {
2299 self.set_status_message(format!("init.ts: open failed: {e}"));
2300 }
2301 }
2302 }
2303 Err(e) => {
2304 self.set_status_message(format!("init.ts: create failed: {e}"));
2305 }
2306 }
2307 }
2308 Action::InitCheck => {
2309 let report = crate::init_script::check(&self.dir_context.config_dir);
2312 if report.ok && report.diagnostics.is_empty() {
2313 self.set_status_message("init.ts: ok".into());
2314 } else if !report.ok {
2315 let first = report
2316 .diagnostics
2317 .first()
2318 .map(|d| format!("{}:{}: {}", d.line, d.column, d.message))
2319 .unwrap_or_else(|| "unknown error".into());
2320 self.set_status_message(format!(
2321 "init.ts: {} error(s) — first: {first}",
2322 report.diagnostics.len()
2323 ));
2324 } else {
2325 self.set_status_message(format!(
2326 "init.ts: {} warning(s)",
2327 report.diagnostics.len()
2328 ));
2329 }
2330 }
2331 Action::OpenTerminal => {
2332 self.open_terminal();
2333 }
2334 Action::CloseTerminal => {
2335 self.close_terminal();
2336 }
2337 Action::FocusTerminal => {
2338 if self
2340 .active_window()
2341 .is_terminal_buffer(self.active_buffer())
2342 {
2343 self.active_window_mut().terminal_mode = true;
2344 self.active_window_mut().key_context = KeyContext::Terminal;
2345 self.set_status_message(t!("status.terminal_mode_enabled").to_string());
2346 }
2347 }
2348 Action::TerminalEscape => {
2349 if self.active_window().terminal_mode {
2351 self.active_window_mut().terminal_mode = false;
2352 self.active_window_mut().key_context = KeyContext::Normal;
2353 self.set_status_message(t!("status.terminal_mode_disabled").to_string());
2354 }
2355 }
2356 Action::ToggleKeyboardCapture => {
2357 if self.active_window().terminal_mode {
2359 self.active_window_mut().keyboard_capture =
2360 !self.active_window_mut().keyboard_capture;
2361 if self.active_window_mut().keyboard_capture {
2362 self.set_status_message(
2363 "Keyboard capture ON - all keys go to terminal (F9 to toggle)"
2364 .to_string(),
2365 );
2366 } else {
2367 self.set_status_message(
2368 "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
2369 );
2370 }
2371 }
2372 }
2373 Action::TerminalPaste => {
2374 if self.active_window().terminal_mode {
2376 if let Some(text) = self.clipboard.paste() {
2377 self.active_window_mut()
2378 .send_terminal_input(text.as_bytes());
2379 }
2380 }
2381 }
2382 Action::ShellCommand => {
2383 self.start_shell_command_prompt(false);
2385 }
2386 Action::ShellCommandReplace => {
2387 self.start_shell_command_prompt(true);
2389 }
2390 Action::OpenSettings => {
2391 self.open_settings();
2392 }
2393 Action::CloseSettings => {
2394 let has_changes = self
2396 .settings_state
2397 .as_ref()
2398 .is_some_and(|s| s.has_changes());
2399 if has_changes {
2400 if let Some(ref mut state) = self.settings_state {
2402 state.show_confirm_dialog();
2403 }
2404 } else {
2405 self.close_settings(false);
2406 }
2407 }
2408 Action::SettingsSave => {
2409 self.save_settings();
2410 }
2411 Action::SettingsReset => {
2412 if let Some(ref mut state) = self.settings_state {
2413 state.reset_current_to_default();
2414 }
2415 }
2416 Action::SettingsInherit => {
2417 if let Some(ref mut state) = self.settings_state {
2418 state.set_current_to_null();
2419 }
2420 }
2421 Action::SettingsToggleFocus => {
2422 if let Some(ref mut state) = self.settings_state {
2423 state.toggle_focus();
2424 }
2425 }
2426 Action::SettingsActivate => {
2427 self.settings_activate_current();
2428 }
2429 Action::SettingsSearch => {
2430 if let Some(ref mut state) = self.settings_state {
2431 state.start_search();
2432 }
2433 }
2434 Action::SettingsHelp => {
2435 if let Some(ref mut state) = self.settings_state {
2436 state.toggle_help();
2437 }
2438 }
2439 Action::SettingsIncrement => {
2440 self.settings_increment_current();
2441 }
2442 Action::SettingsDecrement => {
2443 self.settings_decrement_current();
2444 }
2445 Action::CalibrateInput => {
2446 self.open_calibration_wizard();
2447 }
2448 Action::EventDebug => {
2449 self.active_window_mut().open_event_debug();
2450 }
2451 Action::SuspendProcess => {
2452 self.request_suspend();
2453 }
2454 Action::OpenKeybindingEditor => {
2455 self.open_keybinding_editor();
2456 }
2457 Action::PromptConfirm => {
2458 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
2459 use super::prompt_actions::PromptResult;
2460 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
2461 PromptResult::ExecuteAction(action) => {
2462 return self.handle_action(action);
2463 }
2464 PromptResult::EarlyReturn => {
2465 return Ok(());
2466 }
2467 PromptResult::Done => {}
2468 }
2469 }
2470 }
2471 Action::PromptConfirmWithText(ref text) => {
2472 if let Some(ref mut prompt) = self.active_window_mut().prompt {
2474 prompt.set_input(text.clone());
2475 self.update_prompt_suggestions();
2476 }
2477 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
2478 use super::prompt_actions::PromptResult;
2479 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
2480 PromptResult::ExecuteAction(action) => {
2481 return self.handle_action(action);
2482 }
2483 PromptResult::EarlyReturn => {
2484 return Ok(());
2485 }
2486 PromptResult::Done => {}
2487 }
2488 }
2489 }
2490 Action::PopupConfirm => {
2491 use super::popup_actions::PopupConfirmResult;
2492 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
2493 return Ok(());
2494 }
2495 }
2496 Action::PopupCancel => {
2497 self.handle_popup_cancel();
2498 }
2499 Action::PopupFocus => {
2500 self.handle_popup_focus();
2501 }
2502 Action::CompletionAccept => {
2503 use super::popup_actions::PopupConfirmResult;
2504 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
2505 return Ok(());
2506 }
2507 }
2508 Action::CompletionDismiss => {
2509 self.handle_popup_cancel();
2510 }
2511 Action::InsertChar(c) => {
2512 if self.is_prompting() {
2513 return self.handle_insert_char_prompt(c);
2514 } else if self.active_window_mut().key_context == KeyContext::FileExplorer {
2515 self.active_window_mut().file_explorer_search_push_char(c);
2516 } else {
2517 self.handle_insert_char_editor(c)?;
2518 }
2519 }
2520 Action::PromptCopy => {
2522 if let Some(prompt) = &self.active_window_mut().prompt {
2523 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
2524 if !text.is_empty() {
2525 self.clipboard.copy(text);
2526 self.set_status_message(t!("clipboard.copied").to_string());
2527 }
2528 }
2529 }
2530 Action::PromptCut => {
2531 if let Some(prompt) = &self.active_window_mut().prompt {
2532 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
2533 if !text.is_empty() {
2534 self.clipboard.copy(text);
2535 }
2536 }
2537 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
2538 if prompt.has_selection() {
2539 prompt.delete_selection();
2540 } else {
2541 prompt.clear();
2542 }
2543 }
2544 self.set_status_message(t!("clipboard.cut").to_string());
2545 self.update_prompt_suggestions();
2546 }
2547 Action::PromptPaste => {
2548 if let Some(text) = self.clipboard.paste() {
2549 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
2550 prompt.insert_str(&text);
2551 }
2552 self.update_prompt_suggestions();
2553 }
2554 }
2555 _ => {
2556 self.apply_action_as_events(action)?;
2562 }
2563 }
2564
2565 Ok(())
2566 }
2567
2568 fn dispatch_floating_widget_key(
2579 &mut self,
2580 code: crossterm::event::KeyCode,
2581 modifiers: crossterm::event::KeyModifiers,
2582 ) -> bool {
2583 use crossterm::event::{KeyCode, KeyModifiers};
2584 let panel_id = match self.floating_widget_panel.as_ref() {
2585 Some(fwp) => fwp.panel_id,
2586 None => return false,
2587 };
2588 let key_name: Option<&str> = match code {
2589 KeyCode::Esc => {
2590 let widget_key = self
2591 .widget_registry
2592 .get(panel_id)
2593 .map(|p| p.focus_key.clone())
2594 .unwrap_or_default();
2595 if self
2596 .plugin_manager
2597 .read()
2598 .unwrap()
2599 .has_hook_handlers("widget_event")
2600 {
2601 self.plugin_manager.read().unwrap().run_hook(
2602 "widget_event",
2603 crate::services::plugins::hooks::HookArgs::WidgetEvent {
2604 panel_id,
2605 widget_key,
2606 event_type: "cancel".to_string(),
2607 payload: serde_json::json!({}),
2608 },
2609 );
2610 }
2611 self.floating_widget_panel = None;
2612 let _ = self.widget_registry.unmount(panel_id);
2613 return true;
2614 }
2615 KeyCode::Tab => Some(if modifiers.contains(KeyModifiers::SHIFT) {
2616 "Shift+Tab"
2617 } else {
2618 "Tab"
2619 }),
2620 KeyCode::BackTab => Some("Shift+Tab"),
2621 KeyCode::Enter => Some("Enter"),
2622 KeyCode::Backspace => Some("Backspace"),
2623 KeyCode::Delete => Some("Delete"),
2624 KeyCode::Home => Some("Home"),
2625 KeyCode::End => Some("End"),
2626 KeyCode::Left => Some("Left"),
2627 KeyCode::Right => Some("Right"),
2628 KeyCode::Up => Some("Up"),
2629 KeyCode::Down => Some("Down"),
2630 KeyCode::PageUp => Some("PageUp"),
2631 KeyCode::PageDown => Some("PageDown"),
2632 _ => None,
2633 };
2634 if let Some(name) = key_name {
2635 self.handle_widget_command(
2636 panel_id,
2637 fresh_core::api::WidgetAction::Key {
2638 key: name.to_string(),
2639 },
2640 );
2641 return true;
2642 }
2643 if let KeyCode::Char(c) = code {
2644 if modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) {
2650 return true;
2651 }
2652 let ch = if modifiers.contains(KeyModifiers::SHIFT) {
2653 c.to_uppercase().next().unwrap_or(c)
2654 } else {
2655 c
2656 };
2657 self.handle_widget_command(
2658 panel_id,
2659 fresh_core::api::WidgetAction::TextInputChar {
2660 text: ch.to_string(),
2661 },
2662 );
2663 return true;
2664 }
2665 true
2670 }
2671}