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 use crate::view::popup::PopupResolver;
306 let trust_prompt_up = self
312 .global_popups
313 .top()
314 .is_some_and(|p| p.focused && matches!(p.resolver, PopupResolver::WorkspaceTrust));
315 if trust_prompt_up {
316 return true;
317 }
318 if matches!(self.active_window().key_context, KeyContext::FileExplorer) {
319 return false;
320 }
321 self.topmost_popup_focused()
322 }
323
324 pub(crate) fn topmost_popup_focused(&self) -> bool {
329 if let Some(popup) = self.global_popups.top() {
330 return popup.focused;
331 }
332 if let Some(popup) = self.active_state().popups.top() {
333 return popup.focused;
334 }
335 false
338 }
339
340 pub(crate) fn resolve_unfocused_popup_action(
350 &self,
351 event: &crossterm::event::KeyEvent,
352 ) -> Option<crate::input::keybindings::Action> {
353 use crate::input::keybindings::{Action, KeyContext};
354
355 let popup_visible =
356 self.global_popups.is_visible() || self.active_state().popups.is_visible();
357 if !popup_visible || self.topmost_popup_focused() {
358 return None;
359 }
360
361 if self.settings_state.as_ref().is_some_and(|s| s.visible)
367 || self.menu_state.active_menu.is_some()
368 || self.is_prompting()
369 {
370 return None;
371 }
372
373 let kb = self.keybindings.read().ok()?;
374
375 let popup_focus_match = matches!(
384 kb.resolve_in_context_only(event, self.active_window().key_context.clone()),
385 Some(Action::PopupFocus),
386 );
387 if popup_focus_match {
388 return Some(Action::PopupFocus);
389 }
390
391 let resolved_popup = kb.resolve_in_context_only(event, KeyContext::Popup);
397 match resolved_popup {
398 Some(action @ (Action::PopupCancel | Action::PopupFocus)) => Some(action),
399 _ => None,
400 }
401 }
402
403 pub(crate) fn resolve_completion_popup_action(
409 &self,
410 event: &crossterm::event::KeyEvent,
411 ) -> Option<crate::input::keybindings::Action> {
412 use crate::input::keybindings::{Action, KeyContext};
413 use crate::view::popup::PopupKind;
414
415 let topmost_kind = if self.global_popups.is_visible() {
416 self.global_popups.top().map(|p| p.kind)
417 } else if self.active_state().popups.is_visible() {
418 self.active_state().popups.top().map(|p| p.kind)
419 } else {
420 None
421 };
422
423 if topmost_kind != Some(PopupKind::Completion) {
424 return None;
425 }
426
427 match self
428 .keybindings
429 .read()
430 .unwrap()
431 .resolve_in_context_only(event, KeyContext::Completion)
432 {
433 Some(action @ (Action::CompletionAccept | Action::CompletionDismiss)) => Some(action),
434 _ => None,
435 }
436 }
437
438 pub fn get_key_context(&self) -> crate::input::keybindings::KeyContext {
440 use crate::input::keybindings::KeyContext;
441
442 if self.settings_state.as_ref().is_some_and(|s| s.visible) {
446 KeyContext::Settings
447 } else if self.menu_state.active_menu.is_some() {
448 KeyContext::Menu
449 } else if self.is_prompting() {
450 KeyContext::Prompt
451 } else if self.popups_capture_keys()
452 && (self.global_popups.is_visible() || self.active_state().popups.is_visible())
453 {
454 KeyContext::Popup
455 } else if self.floating_widget_panel.is_some() {
456 KeyContext::Normal
466 } else if self
467 .active_window()
468 .is_composite_buffer(self.active_buffer())
469 {
470 KeyContext::CompositeBuffer
471 } else {
472 self.active_window().key_context.clone()
474 }
475 }
476
477 pub fn handle_key(
480 &mut self,
481 code: crossterm::event::KeyCode,
482 modifiers: crossterm::event::KeyModifiers,
483 ) -> AnyhowResult<()> {
484 use crate::input::keybindings::Action;
485
486 let _t_total = std::time::Instant::now();
487
488 tracing::trace!(
489 "Editor.handle_key: code={:?}, modifiers={:?}",
490 code,
491 modifiers
492 );
493
494 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
496
497 if self.active_window().is_event_debug_active() {
501 self.active_window_mut()
502 .handle_event_debug_input(&key_event);
503 return Ok(());
504 }
505
506 if self.dispatch_terminal_input(&key_event).is_some() {
511 return Ok(());
512 }
513
514 if self.try_resolve_next_key_callback(&key_event) {
521 return Ok(());
522 }
523
524 if self.floating_widget_panel.is_some()
531 && self.dispatch_floating_widget_key(code, modifiers)
532 {
533 return Ok(());
534 }
535
536 let active_split = self.effective_active_split();
545 if let Some(view_state) = self
546 .windows
547 .get_mut(&self.active_window)
548 .and_then(|w| w.split_view_states_mut())
549 .expect("active window must have a populated split layout")
550 .get_mut(&active_split)
551 {
552 view_state.viewport.clear_skip_ensure_visible();
553 }
554
555 if self.active_window_mut().theme_info_popup.is_some() {
557 self.active_window_mut().theme_info_popup = None;
558 }
559
560 if self
561 .active_window_mut()
562 .file_explorer_context_menu
563 .is_some()
564 {
565 if let Some(result) = self.handle_file_explorer_context_menu_key(code, modifiers) {
566 return result;
567 }
568 }
569
570 let mut context = self.get_key_context();
572
573 let popup_visible_on_screen =
583 self.global_popups.is_visible() || self.active_state().popups.is_visible();
584 if popup_visible_on_screen {
585 let (is_transient_popup, has_selection) = {
589 let popup = self
590 .global_popups
591 .top()
592 .or_else(|| self.active_state().popups.top());
593 (
594 popup.is_some_and(|p| p.transient),
595 popup.is_some_and(|p| p.has_selection()),
596 )
597 };
598
599 let is_copy_key = key_event.code == crossterm::event::KeyCode::Char('c')
601 && key_event
602 .modifiers
603 .contains(crossterm::event::KeyModifiers::CONTROL);
604
605 let resolved_action = self
610 .keybindings
611 .read()
612 .ok()
613 .map(|kb| kb.resolve(&key_event, context.clone()));
614 let is_focus_popup_key = matches!(
615 resolved_action,
616 Some(crate::input::keybindings::Action::PopupFocus)
617 );
618
619 if is_transient_popup && !(has_selection && is_copy_key) && !is_focus_popup_key {
620 self.hide_popup();
622 tracing::debug!("Dismissed transient popup on key press");
623 context = self.get_key_context();
625 }
626 }
627
628 if let Some(action) = self.resolve_unfocused_popup_action(&key_event) {
634 self.handle_action(action)?;
635 return Ok(());
636 }
637
638 if self.dispatch_modal_input(&key_event).is_some() {
640 return Ok(());
641 }
642
643 if context != self.get_key_context() {
646 context = self.get_key_context();
647 }
648
649 let should_check_mode_bindings =
653 matches!(context, crate::input::keybindings::KeyContext::Normal);
654
655 if should_check_mode_bindings {
656 let effective_mode = self.effective_mode().map(|s| s.to_owned());
659
660 if let Some(ref mode_name) = effective_mode {
661 let mode_ctx = crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
662 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
663
664 let (chord_result, resolved_action) = {
666 let keybindings = self.keybindings.read().unwrap();
667 let chord_result = keybindings.resolve_chord(
668 &self.active_window().chord_state,
669 &key_event,
670 mode_ctx.clone(),
671 );
672 let resolved = keybindings.resolve(&key_event, mode_ctx);
673 (chord_result, resolved)
674 };
675 match chord_result {
676 crate::input::keybindings::ChordResolution::Complete(action) => {
677 tracing::debug!("Mode chord resolved to action: {:?}", action);
678 self.active_window_mut().chord_state.clear();
679 return self.handle_action(action);
680 }
681 crate::input::keybindings::ChordResolution::Partial => {
682 tracing::debug!("Potential chord prefix in mode '{}'", mode_name);
683 self.active_window_mut().chord_state.push((code, modifiers));
684 return Ok(());
685 }
686 crate::input::keybindings::ChordResolution::NoMatch => {
687 if !self.active_window_mut().chord_state.is_empty() {
688 tracing::debug!("Chord sequence abandoned in mode, clearing state");
689 self.active_window_mut().chord_state.clear();
690 }
691 }
692 }
693
694 if resolved_action != Action::None {
696 return self.handle_action(resolved_action);
697 }
698 }
699
700 if let Some(ref mode_name) = effective_mode {
712 if self.mode_registry.allows_text_input(mode_name) {
713 if let KeyCode::Char(c) = code {
714 let ch = if modifiers.contains(KeyModifiers::SHIFT) {
715 c.to_uppercase().next().unwrap_or(c)
716 } else {
717 c
718 };
719 if !modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) {
720 let action_name = format!("mode_text_input:{}", ch);
721 return self.handle_action(Action::PluginAction(action_name));
722 }
723 }
724 let normal_ctx = crate::input::keybindings::KeyContext::Normal;
733 let resolved = {
734 let keybindings = self.keybindings.read().unwrap();
735 keybindings.resolve(&key_event, normal_ctx)
736 };
737 match resolved {
738 Action::Paste | Action::Copy | Action::Cut | Action::SelectAll => {
739 return self.handle_action(resolved);
740 }
741 _ => {}
742 }
743 if modifiers.contains(KeyModifiers::SHIFT) {
752 let buffer_id = self.active_buffer();
753 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id)
754 {
755 let ctrl = modifiers.contains(KeyModifiers::CONTROL);
756 let handled = match code {
757 KeyCode::Left if ctrl => self
758 .with_focused_text_editor(panel_id, |e| {
759 e.move_word_left_selecting()
760 }),
761 KeyCode::Right if ctrl => self
762 .with_focused_text_editor(panel_id, |e| {
763 e.move_word_right_selecting()
764 }),
765 KeyCode::Left => self.with_focused_text_editor(panel_id, |e| {
766 e.move_left_selecting()
767 }),
768 KeyCode::Right => self.with_focused_text_editor(panel_id, |e| {
769 e.move_right_selecting()
770 }),
771 KeyCode::Up => self
772 .with_focused_text_editor(panel_id, |e| e.move_up_selecting()),
773 KeyCode::Down => self.with_focused_text_editor(panel_id, |e| {
774 e.move_down_selecting()
775 }),
776 KeyCode::Home => self.with_focused_text_editor(panel_id, |e| {
777 e.move_home_selecting()
778 }),
779 KeyCode::End => self
780 .with_focused_text_editor(panel_id, |e| e.move_end_selecting()),
781 _ => false,
782 };
783 if matches!(
789 code,
790 KeyCode::Left
791 | KeyCode::Right
792 | KeyCode::Up
793 | KeyCode::Down
794 | KeyCode::Home
795 | KeyCode::End
796 ) {
797 let _ = handled;
798 return Ok(());
799 }
800 }
801 }
802 tracing::debug!("Blocking unbound key in text-input mode '{}'", mode_name);
803 return Ok(());
804 }
805 }
806 if let Some(ref mode_name) = self.active_window().editor_mode {
807 if self.mode_registry.is_read_only(mode_name) {
808 tracing::debug!("Ignoring unbound key in read-only mode '{}'", mode_name);
809 return Ok(());
810 }
811 tracing::debug!(
812 "Mode '{}' is not read-only, allowing key through",
813 mode_name
814 );
815 }
816 }
817
818 {
825 let active_buf = self.active_buffer();
826 let active_split = self.effective_active_split();
827 if self.active_window().is_composite_buffer(active_buf) {
828 if let Some(handled) =
829 self.try_route_composite_key(active_split, active_buf, &key_event)
830 {
831 return handled;
832 }
833 }
834 }
835
836 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
838 let (chord_result, action) = {
839 let keybindings = self.keybindings.read().unwrap();
840 let chord_result = keybindings.resolve_chord(
841 &self.active_window().chord_state,
842 &key_event,
843 context.clone(),
844 );
845 let action = keybindings.resolve(&key_event, context.clone());
846 (chord_result, action)
847 };
848
849 match chord_result {
850 crate::input::keybindings::ChordResolution::Complete(action) => {
851 tracing::debug!("Complete chord match -> Action: {:?}", action);
853 self.active_window_mut().chord_state.clear();
854 return self.handle_action(action);
855 }
856 crate::input::keybindings::ChordResolution::Partial => {
857 tracing::debug!("Partial chord match - waiting for next key");
859 self.active_window_mut().chord_state.push((code, modifiers));
860 return Ok(());
861 }
862 crate::input::keybindings::ChordResolution::NoMatch => {
863 if !self.active_window_mut().chord_state.is_empty() {
865 tracing::debug!("Chord sequence abandoned, clearing state");
866 self.active_window_mut().chord_state.clear();
867 }
868 }
869 }
870
871 tracing::trace!("Context: {:?} -> Action: {:?}", context, action);
873
874 match action {
877 Action::LspCompletion
878 | Action::LspGotoDefinition
879 | Action::LspReferences
880 | Action::LspHover
881 | Action::None => {
882 }
884 _ => {
885 self.active_window_mut().cancel_pending_lsp_requests();
887 }
888 }
889
890 self.handle_action(action)
894 }
895
896 pub(crate) fn set_workspace_trust_level(
904 &mut self,
905 level: crate::services::workspace_trust::TrustLevel,
906 ) {
907 use crate::services::workspace_trust::TrustLevel;
908 let trust = &self.authority.workspace_trust;
909 let changed = trust.level() != level;
910 trust.set_level(level);
911 let msg = match level {
912 TrustLevel::Trusted => t!("trust.now_trusted"),
913 TrustLevel::Restricted => t!("trust.now_restricted"),
914 TrustLevel::Blocked => t!("trust.now_blocked"),
915 }
916 .to_string();
917 self.active_window_mut().status_message = Some(msg);
918 if changed {
921 self.request_restart(self.working_dir().to_path_buf());
922 }
923 }
924
925 pub(crate) fn handle_action(&mut self, action: Action) -> AnyhowResult<()> {
926 use crate::input::keybindings::Action;
927
928 self.record_macro_action(&action);
930
931 if !matches!(action, Action::DabbrevExpand) {
933 self.reset_dabbrev_state();
934 }
935
936 match action {
937 Action::Quit => self.quit(),
938 Action::ForceQuit => {
939 self.should_quit = true;
940 }
941 Action::Detach => {
942 self.should_detach = true;
943 }
944 Action::WorkspaceTrustTrust => {
945 self.set_workspace_trust_level(
946 crate::services::workspace_trust::TrustLevel::Trusted,
947 );
948 }
949 Action::WorkspaceTrustRestrict => {
950 self.set_workspace_trust_level(
951 crate::services::workspace_trust::TrustLevel::Restricted,
952 );
953 }
954 Action::WorkspaceTrustBlock => {
955 self.set_workspace_trust_level(
956 crate::services::workspace_trust::TrustLevel::Blocked,
957 );
958 }
959 Action::WorkspaceTrustPrompt => {
960 self.show_workspace_trust_popup(true);
962 }
963 Action::Save => {
964 if self.active_state().buffer.file_path().is_none() {
966 self.start_prompt_with_initial_text(
967 t!("file.save_as_prompt").to_string(),
968 PromptType::SaveFileAs,
969 String::new(),
970 );
971 self.init_file_open_state();
972 } else if self.check_save_conflict().is_some() {
973 self.start_prompt(
975 t!("file.file_changed_prompt").to_string(),
976 PromptType::ConfirmSaveConflict,
977 );
978 } else if let Err(e) = self.save() {
979 let msg = format!("{}", e);
980 self.active_window_mut().status_message =
981 Some(t!("file.save_failed", error = &msg).to_string());
982 }
983 }
984 Action::SaveAs => {
985 let current_path = self
987 .active_state()
988 .buffer
989 .file_path()
990 .map(|p| {
991 p.strip_prefix(self.working_dir())
993 .unwrap_or(p)
994 .to_string_lossy()
995 .to_string()
996 })
997 .unwrap_or_default();
998 self.start_prompt_with_initial_text(
999 t!("file.save_as_prompt").to_string(),
1000 PromptType::SaveFileAs,
1001 current_path,
1002 );
1003 self.init_file_open_state();
1004 }
1005 Action::Open => {
1006 self.start_prompt(t!("file.open_prompt").to_string(), PromptType::OpenFile);
1007 self.prefill_open_file_prompt();
1008 self.init_file_open_state();
1009 }
1010 Action::SwitchProject => {
1011 self.start_prompt(
1012 t!("file.switch_project_prompt").to_string(),
1013 PromptType::SwitchProject,
1014 );
1015 self.init_folder_open_state();
1016 }
1017 Action::GotoLine => {
1018 let has_line_index = self
1019 .buffers()
1020 .get(&self.active_buffer())
1021 .is_none_or(|s| s.buffer.line_count().is_some());
1022 if has_line_index {
1023 self.start_prompt(
1024 t!("file.goto_line_prompt").to_string(),
1025 PromptType::GotoLine,
1026 );
1027 } else {
1028 self.start_prompt(
1029 t!("goto.scan_confirm_prompt", yes = "y", no = "N").to_string(),
1030 PromptType::GotoLineScanConfirm,
1031 );
1032 }
1033 }
1034 Action::ScanLineIndex => {
1035 self.start_incremental_line_scan(false);
1036 }
1037 Action::New => {
1038 self.new_buffer();
1039 }
1040 Action::Close | Action::CloseTab => {
1041 self.close_tab();
1046 }
1047 Action::Revert => {
1048 if self.active_state().buffer.is_modified() {
1050 let revert_key = t!("prompt.key.revert").to_string();
1051 let cancel_key = t!("prompt.key.cancel").to_string();
1052 self.start_prompt(
1053 t!(
1054 "prompt.revert_confirm",
1055 revert_key = revert_key,
1056 cancel_key = cancel_key
1057 )
1058 .to_string(),
1059 PromptType::ConfirmRevert,
1060 );
1061 } else {
1062 if let Err(e) = self.revert_file() {
1064 self.set_status_message(
1065 t!("error.failed_to_revert", error = e.to_string()).to_string(),
1066 );
1067 }
1068 }
1069 }
1070 Action::ToggleAutoRevert => {
1071 self.toggle_auto_revert();
1072 }
1073 Action::FormatBuffer => {
1074 if let Err(e) = self.format_buffer() {
1075 self.set_status_message(
1076 t!("error.format_failed", error = e.to_string()).to_string(),
1077 );
1078 }
1079 }
1080 Action::TrimTrailingWhitespace => match self.trim_trailing_whitespace() {
1081 Ok(true) => {
1082 self.set_status_message(t!("whitespace.trimmed").to_string());
1083 }
1084 Ok(false) => {
1085 self.set_status_message(t!("whitespace.no_trailing").to_string());
1086 }
1087 Err(e) => {
1088 self.set_status_message(
1089 t!("error.trim_whitespace_failed", error = e).to_string(),
1090 );
1091 }
1092 },
1093 Action::EnsureFinalNewline => match self.ensure_final_newline() {
1094 Ok(true) => {
1095 self.set_status_message(t!("whitespace.newline_added").to_string());
1096 }
1097 Ok(false) => {
1098 self.set_status_message(t!("whitespace.already_has_newline").to_string());
1099 }
1100 Err(e) => {
1101 self.set_status_message(
1102 t!("error.ensure_newline_failed", error = e).to_string(),
1103 );
1104 }
1105 },
1106 Action::Copy => {
1107 let popup = self
1109 .global_popups
1110 .top()
1111 .or_else(|| self.active_state().popups.top());
1112 if let Some(popup) = popup {
1113 if popup.has_selection() {
1114 if let Some(text) = popup.get_selected_text() {
1115 self.clipboard.copy(text);
1116 self.set_status_message(t!("clipboard.copied").to_string());
1117 return Ok(());
1118 }
1119 }
1120 }
1121 if self.active_window_mut().key_context
1122 == crate::input::keybindings::KeyContext::FileExplorer
1123 {
1124 self.active_window_mut().file_explorer_copy();
1125 return Ok(());
1126 }
1127 let buffer_id = self.active_buffer();
1134 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
1135 if self.handle_widget_copy(panel_id) {
1136 self.set_status_message(t!("clipboard.copied").to_string());
1137 return Ok(());
1138 }
1139 }
1140 if self.active_window().is_composite_buffer(buffer_id) {
1142 if let Some(_handled) = self.handle_composite_action(buffer_id, &Action::Copy) {
1143 return Ok(());
1144 }
1145 }
1146 self.copy_selection()
1147 }
1148 Action::CopyWithTheme(theme) => self.copy_selection_with_theme(&theme),
1149 Action::CopyFilePath => self.copy_active_buffer_path(false),
1150 Action::CopyRelativeFilePath => self.copy_active_buffer_path(true),
1151 Action::Cut => {
1152 if self.active_window_mut().key_context
1153 == crate::input::keybindings::KeyContext::FileExplorer
1154 {
1155 self.active_window_mut().file_explorer_cut();
1156 return Ok(());
1157 }
1158 let buffer_id = self.active_buffer();
1162 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
1163 if self.handle_widget_cut(panel_id) {
1164 return Ok(());
1165 }
1166 }
1167 if self.active_window().is_editing_disabled() {
1168 self.set_status_message(t!("buffer.editing_disabled").to_string());
1169 return Ok(());
1170 }
1171 self.cut_selection()
1172 }
1173 Action::Paste => {
1174 if self.active_window_mut().key_context
1175 == crate::input::keybindings::KeyContext::FileExplorer
1176 {
1177 self.file_explorer_paste();
1178 return Ok(());
1179 }
1180 let buffer_id = self.active_buffer();
1186 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
1187 if let Some(text) = self.clipboard.paste() {
1188 let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
1189 self.handle_widget_insert_str(panel_id, &normalized);
1190 self.set_status_message(t!("clipboard.pasted").to_string());
1191 }
1192 return Ok(());
1193 }
1194 if self.active_window().is_editing_disabled() {
1195 self.set_status_message(t!("buffer.editing_disabled").to_string());
1196 return Ok(());
1197 }
1198 self.paste()
1199 }
1200 Action::SelectAll => {
1201 let buffer_id = self.active_buffer();
1206 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
1207 self.handle_widget_select_all(panel_id);
1208 return Ok(());
1209 }
1210 self.apply_action_as_events(Action::SelectAll)?;
1211 }
1212 Action::YankWordForward => self.yank_word_forward(),
1213 Action::YankWordBackward => self.yank_word_backward(),
1214 Action::YankToLineEnd => self.yank_to_line_end(),
1215 Action::YankToLineStart => self.yank_to_line_start(),
1216 Action::YankViWordEnd => self.yank_vi_word_end(),
1217 Action::Undo => {
1218 self.handle_undo();
1219 }
1220 Action::Redo => {
1221 self.handle_redo();
1222 }
1223 Action::ShowHelp => {
1224 self.active_window_mut().open_help_manual();
1225 }
1226 Action::ShowKeyboardShortcuts => {
1227 self.active_window_mut().open_keyboard_shortcuts();
1228 }
1229 Action::ShowWarnings => {
1230 self.show_warnings_popup();
1231 }
1232 Action::ShowStatusLog => {
1233 self.open_status_log();
1234 }
1235 Action::ShowLspStatus => {
1236 self.show_lsp_status_popup();
1237 }
1238 Action::ShowRemoteIndicatorMenu => {
1239 self.show_remote_indicator_popup();
1240 }
1241 Action::ClearWarnings => {
1242 self.active_window_mut().clear_warnings();
1243 }
1244 Action::CommandPalette => {
1245 if let Some(prompt) = &self.active_window_mut().prompt {
1248 if prompt.prompt_type == PromptType::QuickOpen {
1249 self.cancel_prompt();
1250 return Ok(());
1251 }
1252 }
1253 self.start_quick_open();
1254 }
1255 Action::QuickOpen => {
1256 if let Some(prompt) = &self.active_window_mut().prompt {
1258 if prompt.prompt_type == PromptType::QuickOpen {
1259 self.cancel_prompt();
1260 return Ok(());
1261 }
1262 }
1263
1264 self.start_quick_open();
1266 }
1267 Action::QuickOpenBuffers => {
1268 if let Some(prompt) = &self.active_window_mut().prompt {
1269 if prompt.prompt_type == PromptType::QuickOpen {
1270 self.cancel_prompt();
1271 return Ok(());
1272 }
1273 }
1274 self.start_quick_open_with_prefix("#");
1275 }
1276 Action::QuickOpenFiles => {
1277 if let Some(prompt) = &self.active_window_mut().prompt {
1278 if prompt.prompt_type == PromptType::QuickOpen {
1279 self.cancel_prompt();
1280 return Ok(());
1281 }
1282 }
1283 self.start_quick_open_with_prefix("");
1284 }
1285 Action::OpenLiveGrep => {
1286 #[cfg(feature = "plugins")]
1292 {
1293 let result = self
1294 .plugin_manager
1295 .read()
1296 .unwrap()
1297 .execute_action_async("start_live_grep");
1298 if let Some(result) = result {
1299 match result {
1300 Ok(receiver) => {
1301 self.pending_plugin_actions
1302 .push(("start_live_grep".to_string(), receiver));
1303 }
1304 Err(e) => {
1305 self.set_status_message(format!("Live Grep unavailable: {}", e));
1306 }
1307 }
1308 } else {
1309 self.set_status_message("Live Grep plugin not loaded".to_string());
1310 }
1311 }
1312 #[cfg(not(feature = "plugins"))]
1313 {
1314 self.set_status_message("Live Grep requires the plugins feature".to_string());
1315 }
1316 }
1317 Action::ResumeLiveGrep => {
1318 #[cfg(feature = "plugins")]
1324 {
1325 let result = self
1326 .plugin_manager
1327 .read()
1328 .unwrap()
1329 .execute_action_async("resume_live_grep");
1330 if let Some(result) = result {
1331 match result {
1332 Ok(receiver) => {
1333 self.pending_plugin_actions
1334 .push(("resume_live_grep".to_string(), receiver));
1335 }
1336 Err(e) => {
1337 self.set_status_message(format!("Live Grep unavailable: {}", e));
1338 }
1339 }
1340 }
1341 }
1342 }
1343 Action::LiveGrepExportQuickfix => {
1344 let is_grep = self
1349 .active_window()
1350 .prompt
1351 .as_ref()
1352 .map(|p| match &p.prompt_type {
1353 PromptType::LiveGrep => true,
1354 PromptType::Plugin { custom_type } => custom_type == "live-grep",
1355 _ => false,
1356 })
1357 .unwrap_or(false);
1358 if !is_grep {
1359 self.set_status_message(
1360 "Quickfix export is only available inside Live Grep".to_string(),
1361 );
1362 return Ok(());
1363 }
1364 let (query, matches) = {
1365 let prompt = self.active_window().prompt.as_ref().unwrap();
1366 (
1367 prompt.input.clone(),
1368 self.snapshot_prompt_results_for_grep(prompt),
1369 )
1370 };
1371 if matches.is_empty() {
1372 self.set_status_message("No Live Grep results to export".to_string());
1373 return Ok(());
1374 }
1375 self.cancel_prompt();
1377 self.install_quickfix_in_dock(query, matches);
1379 }
1380 Action::ToggleUtilityDock => {
1381 use crate::view::split::SplitRole;
1382 if let Some(dock_leaf) = self
1383 .windows
1384 .get(&self.active_window)
1385 .and_then(|w| w.buffers.splits())
1386 .map(|(mgr, _)| mgr)
1387 .expect("active window must have a populated split layout")
1388 .find_leaf_by_role(SplitRole::UtilityDock)
1389 {
1390 let active = self
1391 .windows
1392 .get(&self.active_window)
1393 .and_then(|w| w.buffers.splits())
1394 .map(|(mgr, _)| mgr)
1395 .expect("active window must have a populated split layout")
1396 .active_split();
1397 if active == dock_leaf {
1398 self.next_split();
1403 } else {
1404 self.windows
1405 .get_mut(&self.active_window)
1406 .and_then(|w| w.split_manager_mut())
1407 .expect("active window must have a populated split layout")
1408 .set_active_split(dock_leaf);
1409 }
1410 } else {
1411 self.set_status_message(
1412 "No Utility Dock open — invoke a dock-aware utility (Diagnostics, Search/Replace, …)"
1413 .to_string(),
1414 );
1415 }
1416 }
1417 Action::CycleLiveGrepProvider => {
1418 let in_live_grep = self
1424 .active_window()
1425 .prompt
1426 .as_ref()
1427 .map(|p| match &p.prompt_type {
1428 PromptType::LiveGrep => true,
1429 PromptType::Plugin { custom_type } => custom_type == "live-grep",
1430 _ => false,
1431 })
1432 .unwrap_or(false);
1433 if !in_live_grep {
1434 self.set_status_message(
1435 "Cycle Live Grep provider only works inside Live Grep".to_string(),
1436 );
1437 return Ok(());
1438 }
1439 #[cfg(feature = "plugins")]
1440 {
1441 let result = self
1442 .plugin_manager
1443 .read()
1444 .unwrap()
1445 .execute_action_async("live_grep_cycle_provider");
1446 if let Some(result) = result {
1447 match result {
1448 Ok(receiver) => {
1449 self.pending_plugin_actions
1450 .push(("live_grep_cycle_provider".to_string(), receiver));
1451 }
1452 Err(e) => {
1453 self.set_status_message(format!("Live Grep cycle failed: {}", e));
1454 }
1455 }
1456 } else {
1457 self.set_status_message("Live Grep plugin not loaded".to_string());
1458 }
1459 }
1460 #[cfg(not(feature = "plugins"))]
1461 {
1462 self.set_status_message(
1463 "Live Grep cycle requires the plugins feature".to_string(),
1464 );
1465 }
1466 }
1467 Action::OpenTerminalInDock => {
1468 use crate::model::event::SplitDirection;
1469 use crate::view::split::SplitRole;
1470 if let Some(dock_leaf) = self
1471 .windows
1472 .get(&self.active_window)
1473 .and_then(|w| w.buffers.splits())
1474 .map(|(mgr, _)| mgr)
1475 .expect("active window must have a populated split layout")
1476 .find_leaf_by_role(SplitRole::UtilityDock)
1477 {
1478 self.windows
1481 .get_mut(&self.active_window)
1482 .and_then(|w| w.split_manager_mut())
1483 .expect("active window must have a populated split layout")
1484 .set_active_split(dock_leaf);
1485 self.open_terminal();
1486 } else {
1487 let Some(terminal_id) = self.spawn_terminal_session() else {
1494 return Ok(());
1495 };
1496 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
1497 match self
1500 .windows
1501 .get_mut(&self.active_window)
1502 .and_then(|w| w.split_manager_mut())
1503 .expect("active window must have a populated split layout")
1504 .split_root_positioned(SplitDirection::Horizontal, buffer_id, 0.7, false)
1505 {
1506 Ok(new_leaf) => {
1507 let mut view_state = crate::view::split::SplitViewState::with_buffer(
1508 self.terminal_width,
1509 self.terminal_height,
1510 buffer_id,
1511 );
1512 view_state.apply_config_defaults(
1518 false,
1519 false,
1520 self.active_window().resolve_line_wrap_for_buffer(buffer_id),
1521 self.config.editor.wrap_indent,
1522 self.active_window()
1523 .resolve_wrap_column_for_buffer(buffer_id),
1524 self.config.editor.rulers.clone(),
1525 );
1526 view_state.viewport.line_wrap_enabled = false;
1530 self.windows
1531 .get_mut(&self.active_window)
1532 .and_then(|w| w.split_view_states_mut())
1533 .expect("active window must have a populated split layout")
1534 .insert(new_leaf, view_state);
1535 self.windows
1536 .get_mut(&self.active_window)
1537 .and_then(|w| w.split_manager_mut())
1538 .expect("active window must have a populated split layout")
1539 .set_leaf_role(new_leaf, Some(SplitRole::UtilityDock));
1540 self.windows
1541 .get_mut(&self.active_window)
1542 .and_then(|w| w.split_manager_mut())
1543 .expect("active window must have a populated split layout")
1544 .set_active_split(new_leaf);
1545 self.active_window_mut().terminal_mode = true;
1551 self.active_window_mut().key_context =
1552 crate::input::keybindings::KeyContext::Terminal;
1553 self.active_window_mut().resize_visible_terminals();
1554 let exit_key = self
1555 .keybindings
1556 .read()
1557 .unwrap()
1558 .find_keybinding_for_action(
1559 "terminal_escape",
1560 crate::input::keybindings::KeyContext::Terminal,
1561 )
1562 .unwrap_or_else(|| "Ctrl+Space".to_string());
1563 self.set_status_message(
1564 rust_i18n::t!(
1565 "terminal.opened",
1566 id = terminal_id.0,
1567 exit_key = exit_key
1568 )
1569 .to_string(),
1570 );
1571 tracing::info!(
1572 "Opened terminal {:?} into new dock leaf {:?} (buffer {:?})",
1573 terminal_id,
1574 new_leaf,
1575 buffer_id
1576 );
1577 }
1578 Err(e) => {
1579 self.set_status_message(format!(
1580 "Failed to create dock for terminal: {}",
1581 e
1582 ));
1583 return Ok(());
1584 }
1585 }
1586 }
1587 }
1588 Action::ToggleLineWrap => {
1589 let new_value = !self.config.editor.line_wrap;
1590 self.config_mut().editor.line_wrap = new_value;
1591 self.sync_windows_config();
1599
1600 let leaf_ids: Vec<_> = self
1603 .windows
1604 .get(&self.active_window)
1605 .and_then(|w| w.buffers.splits())
1606 .map(|(_, vs)| vs)
1607 .expect("active window must have a populated split layout")
1608 .keys()
1609 .copied()
1610 .collect();
1611 for leaf_id in leaf_ids {
1612 let buffer_id = self
1613 .split_manager_mut()
1614 .get_buffer_id(leaf_id.into())
1615 .unwrap_or(BufferId(0));
1616 let effective_wrap =
1617 self.active_window().resolve_line_wrap_for_buffer(buffer_id);
1618 let wrap_column = self
1619 .active_window()
1620 .resolve_wrap_column_for_buffer(buffer_id);
1621 if let Some(view_state) = self
1622 .windows
1623 .get_mut(&self.active_window)
1624 .and_then(|w| w.split_view_states_mut())
1625 .expect("active window must have a populated split layout")
1626 .get_mut(&leaf_id)
1627 {
1628 view_state.viewport.line_wrap_enabled = effective_wrap;
1629 view_state.viewport.wrap_indent = self.config.editor.wrap_indent;
1630 view_state.viewport.wrap_column = wrap_column;
1631 }
1632 }
1633
1634 let state = if self.config.editor.line_wrap {
1635 t!("view.state_enabled").to_string()
1636 } else {
1637 t!("view.state_disabled").to_string()
1638 };
1639 self.set_status_message(t!("view.line_wrap_state", state = state).to_string());
1640 }
1641 Action::ToggleCurrentLineHighlight => {
1642 let new_value = !self.config.editor.highlight_current_line;
1643 self.config_mut().editor.highlight_current_line = new_value;
1644
1645 let leaf_ids: Vec<_> = self
1647 .windows
1648 .get(&self.active_window)
1649 .and_then(|w| w.buffers.splits())
1650 .map(|(_, vs)| vs)
1651 .expect("active window must have a populated split layout")
1652 .keys()
1653 .copied()
1654 .collect();
1655 for leaf_id in leaf_ids {
1656 if let Some(view_state) = self
1657 .windows
1658 .get_mut(&self.active_window)
1659 .and_then(|w| w.split_view_states_mut())
1660 .expect("active window must have a populated split layout")
1661 .get_mut(&leaf_id)
1662 {
1663 view_state.highlight_current_line =
1664 self.config.editor.highlight_current_line;
1665 }
1666 }
1667
1668 let state = if self.config.editor.highlight_current_line {
1669 t!("view.state_enabled").to_string()
1670 } else {
1671 t!("view.state_disabled").to_string()
1672 };
1673 self.set_status_message(
1674 t!("view.current_line_highlight_state", state = state).to_string(),
1675 );
1676 }
1677 Action::ToggleReadOnly => {
1678 let buffer_id = self.active_buffer();
1679 let is_now_read_only = self
1680 .active_window()
1681 .buffer_metadata
1682 .get(&buffer_id)
1683 .map(|m| !m.read_only)
1684 .unwrap_or(false);
1685 self.active_window_mut()
1686 .mark_buffer_read_only(buffer_id, is_now_read_only);
1687
1688 let state_str = if is_now_read_only {
1689 t!("view.state_enabled").to_string()
1690 } else {
1691 t!("view.state_disabled").to_string()
1692 };
1693 self.set_status_message(t!("view.read_only_state", state = state_str).to_string());
1694 }
1695 Action::TogglePageView => {
1696 self.active_window_mut().handle_toggle_page_view();
1697 }
1698 Action::SetPageWidth => {
1699 let active_split = self
1700 .windows
1701 .get(&self.active_window)
1702 .and_then(|w| w.buffers.splits())
1703 .map(|(mgr, _)| mgr)
1704 .expect("active window must have a populated split layout")
1705 .active_split();
1706 let current = self
1707 .windows
1708 .get(&self.active_window)
1709 .and_then(|w| w.buffers.splits())
1710 .map(|(_, vs)| vs)
1711 .expect("active window must have a populated split layout")
1712 .get(&active_split)
1713 .and_then(|v| v.compose_width.map(|w| w.to_string()))
1714 .unwrap_or_default();
1715 self.start_prompt_with_initial_text(
1716 "Page width (empty = viewport): ".to_string(),
1717 PromptType::SetPageWidth,
1718 current,
1719 );
1720 }
1721 Action::SetBackground => {
1722 let default_path = self
1723 .ansi_background_path
1724 .as_ref()
1725 .and_then(|p| {
1726 p.strip_prefix(self.working_dir())
1727 .ok()
1728 .map(|rel| rel.to_string_lossy().to_string())
1729 })
1730 .unwrap_or_else(|| DEFAULT_BACKGROUND_FILE.to_string());
1731
1732 self.start_prompt_with_initial_text(
1733 "Background file: ".to_string(),
1734 PromptType::SetBackgroundFile,
1735 default_path,
1736 );
1737 }
1738 Action::SetBackgroundBlend => {
1739 let default_amount = format!("{:.2}", self.background_fade);
1740 self.start_prompt_with_initial_text(
1741 "Background blend (0-1): ".to_string(),
1742 PromptType::SetBackgroundBlend,
1743 default_amount,
1744 );
1745 }
1746 Action::LspCompletion => {
1747 self.request_completion();
1748 }
1749 Action::DabbrevExpand => {
1750 self.dabbrev_expand();
1751 }
1752 Action::LspGotoDefinition => {
1753 self.request_goto_definition()?;
1754 }
1755 Action::LspRename => {
1756 self.start_rename()?;
1757 }
1758 Action::LspHover => {
1759 self.request_hover()?;
1760 }
1761 Action::LspReferences => {
1762 self.request_references()?;
1763 }
1764 Action::LspSignatureHelp => {
1765 self.request_signature_help();
1766 }
1767 Action::LspCodeActions => {
1768 self.request_code_actions()?;
1769 }
1770 Action::LspRestart => {
1771 self.handle_lsp_restart();
1772 }
1773 Action::LspStop => {
1774 self.handle_lsp_stop();
1775 }
1776 Action::LspToggleForBuffer => {
1777 self.handle_lsp_toggle_for_buffer();
1778 }
1779 Action::ToggleInlayHints => {
1780 self.toggle_inlay_hints();
1781 }
1782 Action::DumpConfig => {
1783 self.dump_config();
1784 }
1785 Action::RedrawScreen => {
1786 self.request_full_redraw();
1787 }
1788 Action::SelectTheme => {
1789 self.start_select_theme_prompt();
1790 }
1791 Action::InspectThemeAtCursor => {
1792 self.inspect_theme_at_cursor();
1793 }
1794 Action::SelectKeybindingMap => {
1795 self.start_select_keybinding_map_prompt();
1796 }
1797 Action::SelectCursorStyle => {
1798 self.start_select_cursor_style_prompt();
1799 }
1800 Action::SelectLocale => {
1801 self.start_select_locale_prompt();
1802 }
1803 Action::Search => {
1804 let is_search_prompt = self.active_window().prompt.as_ref().is_some_and(|p| {
1806 matches!(
1807 p.prompt_type,
1808 PromptType::Search
1809 | PromptType::ReplaceSearch
1810 | PromptType::QueryReplaceSearch
1811 )
1812 });
1813
1814 if is_search_prompt {
1815 self.confirm_prompt();
1816 } else {
1817 self.start_search_prompt(
1818 t!("file.search_prompt").to_string(),
1819 PromptType::Search,
1820 false,
1821 );
1822 }
1823 }
1824 Action::Replace => {
1825 self.start_search_prompt(
1827 t!("file.replace_prompt").to_string(),
1828 PromptType::ReplaceSearch,
1829 false,
1830 );
1831 }
1832 Action::QueryReplace => {
1833 self.active_window_mut().search_confirm_each = true;
1835 self.start_search_prompt(
1836 "Query replace: ".to_string(),
1837 PromptType::QueryReplaceSearch,
1838 false,
1839 );
1840 }
1841 Action::FindInSelection => {
1842 self.start_search_prompt(
1843 t!("file.search_prompt").to_string(),
1844 PromptType::Search,
1845 true,
1846 );
1847 }
1848 Action::FindNext => {
1849 self.find_next();
1850 }
1851 Action::FindPrevious => {
1852 self.find_previous();
1853 }
1854 Action::FindSelectionNext => {
1855 self.find_selection_next();
1856 }
1857 Action::FindSelectionPrevious => {
1858 self.find_selection_previous();
1859 }
1860 Action::AddCursorNextMatch => self.add_cursor_at_next_match(),
1861 Action::AddCursorAbove => self.add_cursor_above(),
1862 Action::AddCursorBelow => self.add_cursor_below(),
1863 Action::AddCursorsToLineEnds => self.add_cursors_to_line_ends(),
1864 Action::NextBuffer => self.next_buffer(),
1865 Action::PrevBuffer => self.prev_buffer(),
1866 Action::SwitchToPreviousTab => self.switch_to_previous_tab(),
1867 Action::SwitchToTabByName => self.start_switch_to_tab_prompt(),
1868
1869 Action::ScrollTabsLeft => {
1871 let active_split_id = self
1872 .windows
1873 .get(&self.active_window)
1874 .and_then(|w| w.buffers.splits())
1875 .map(|(mgr, _)| mgr)
1876 .expect("active window must have a populated split layout")
1877 .active_split();
1878 if let Some(view_state) = self
1879 .windows
1880 .get_mut(&self.active_window)
1881 .and_then(|w| w.split_view_states_mut())
1882 .expect("active window must have a populated split layout")
1883 .get_mut(&active_split_id)
1884 {
1885 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_sub(5);
1886 self.set_status_message(t!("status.scrolled_tabs_left").to_string());
1887 }
1888 }
1889 Action::ScrollTabsRight => {
1890 let active_split_id = self
1891 .windows
1892 .get(&self.active_window)
1893 .and_then(|w| w.buffers.splits())
1894 .map(|(mgr, _)| mgr)
1895 .expect("active window must have a populated split layout")
1896 .active_split();
1897 if let Some(view_state) = self
1898 .windows
1899 .get_mut(&self.active_window)
1900 .and_then(|w| w.split_view_states_mut())
1901 .expect("active window must have a populated split layout")
1902 .get_mut(&active_split_id)
1903 {
1904 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_add(5);
1905 self.set_status_message(t!("status.scrolled_tabs_right").to_string());
1906 }
1907 }
1908 Action::NavigateBack => self.navigate_back(),
1909 Action::NavigateForward => self.navigate_forward(),
1910 Action::SplitHorizontal => self.split_pane_horizontal(),
1911 Action::SplitVertical => self.split_pane_vertical(),
1912 Action::CloseSplit => self.close_active_split(),
1913 Action::NextSplit => self.next_split(),
1914 Action::PrevSplit => self.prev_split(),
1915 Action::NextWindow => self.next_window(),
1916 Action::PrevWindow => self.prev_window(),
1917 Action::IncreaseSplitSize => self.adjust_split_size(0.05),
1918 Action::DecreaseSplitSize => self.adjust_split_size(-0.05),
1919 Action::ToggleMaximizeSplit => self.toggle_maximize_split(),
1920 Action::ToggleFileExplorer => self.toggle_file_explorer(),
1921 Action::ToggleFileExplorerSide => self.toggle_file_explorer_side(),
1922 Action::ToggleMenuBar => self.toggle_menu_bar(),
1923 Action::ToggleTabBar => self.active_window_mut().toggle_tab_bar(),
1924 Action::ToggleStatusBar => self.active_window_mut().toggle_status_bar(),
1925 Action::TogglePromptLine => self.active_window_mut().toggle_prompt_line(),
1926 Action::ToggleVerticalScrollbar => self.toggle_vertical_scrollbar(),
1927 Action::ToggleHorizontalScrollbar => self.toggle_horizontal_scrollbar(),
1928 Action::ToggleLineNumbers => self.toggle_line_numbers(),
1929 Action::ToggleScrollSync => self.active_window_mut().toggle_scroll_sync(),
1930 Action::ToggleMouseCapture => self.toggle_mouse_capture(),
1931 Action::ToggleMouseHover => self.toggle_mouse_hover(),
1932 Action::ToggleDebugHighlights => self.active_window_mut().toggle_debug_highlights(),
1933 Action::AddRuler => {
1935 self.start_prompt(t!("rulers.add_prompt").to_string(), PromptType::AddRuler);
1936 }
1937 Action::RemoveRuler => {
1938 self.start_remove_ruler_prompt();
1939 }
1940 Action::SetTabSize => {
1942 let current = self
1943 .buffers()
1944 .get(&self.active_buffer())
1945 .map(|s| s.buffer_settings.tab_size.to_string())
1946 .unwrap_or_else(|| "4".to_string());
1947 self.start_prompt_with_initial_text(
1948 "Tab size: ".to_string(),
1949 PromptType::SetTabSize,
1950 current,
1951 );
1952 }
1953 Action::SetLineEnding => {
1954 self.start_set_line_ending_prompt();
1955 }
1956 Action::SetEncoding => {
1957 self.start_set_encoding_prompt();
1958 }
1959 Action::ReloadWithEncoding => {
1960 self.start_reload_with_encoding_prompt();
1961 }
1962 Action::SetLanguage => {
1963 self.start_set_language_prompt();
1964 }
1965 Action::ToggleIndentationStyle => {
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.use_tabs = !state.buffer_settings.use_tabs;
1975 let status = if state.buffer_settings.use_tabs {
1976 "Indentation: Tabs"
1977 } else {
1978 "Indentation: Spaces"
1979 };
1980 self.set_status_message(status.to_string());
1981 }
1982 }
1983 Action::ToggleTabIndicators | Action::ToggleWhitespaceIndicators => {
1984 let __buffer_id = self.active_buffer();
1985 if let Some(state) = self
1986 .windows
1987 .get_mut(&self.active_window)
1988 .map(|w| &mut w.buffers)
1989 .expect("active window present")
1990 .get_mut(&__buffer_id)
1991 {
1992 state.buffer_settings.whitespace.toggle_all();
1993 let status = if state.buffer_settings.whitespace.any_visible() {
1994 t!("toggle.whitespace_indicators_shown")
1995 } else {
1996 t!("toggle.whitespace_indicators_hidden")
1997 };
1998 self.set_status_message(status.to_string());
1999 }
2000 }
2001 Action::ResetBufferSettings => self.reset_buffer_settings(),
2002 Action::FocusFileExplorer => self.focus_file_explorer(),
2003 Action::FocusEditor => self.active_window_mut().focus_editor(),
2004 Action::FileExplorerUp => self.file_explorer_navigate_up(),
2005 Action::FileExplorerDown => self.file_explorer_navigate_down(),
2006 Action::FileExplorerPageUp => self.file_explorer_page_up(),
2007 Action::FileExplorerPageDown => self.file_explorer_page_down(),
2008 Action::FileExplorerExpand => self.file_explorer_toggle_expand(),
2009 Action::FileExplorerCollapse => self.file_explorer_collapse(),
2010 Action::FileExplorerOpen => self.file_explorer_open_file()?,
2011 Action::FileExplorerRefresh => self.file_explorer_refresh(),
2012 Action::FileExplorerNewFile => self.file_explorer_new_file(),
2013 Action::FileExplorerNewDirectory => self.file_explorer_new_directory(),
2014 Action::FileExplorerDelete => self.file_explorer_delete(),
2015 Action::FileExplorerRename => self.file_explorer_rename(),
2016 Action::FileExplorerToggleHidden => self.file_explorer_toggle_hidden(),
2017 Action::FileExplorerToggleGitignored => self.file_explorer_toggle_gitignored(),
2018 Action::FileExplorerSearchClear => {
2019 self.active_window_mut().file_explorer_search_clear()
2020 }
2021 Action::FileExplorerSearchBackspace => {
2022 self.active_window_mut().file_explorer_search_pop_char()
2023 }
2024 Action::FileExplorerCopy => self.active_window_mut().file_explorer_copy(),
2025 Action::FileExplorerCut => self.active_window_mut().file_explorer_cut(),
2026 Action::FileExplorerPaste => self.file_explorer_paste(),
2027 Action::FileExplorerDuplicate => self.file_explorer_duplicate(),
2028 Action::FileExplorerCopyFullPath => self.file_explorer_copy_path(false),
2029 Action::FileExplorerCopyRelativePath => self.file_explorer_copy_path(true),
2030 Action::FileExplorerExtendSelectionUp => {
2031 self.active_window_mut().file_explorer_extend_selection_up()
2032 }
2033 Action::FileExplorerExtendSelectionDown => self
2034 .active_window_mut()
2035 .file_explorer_extend_selection_down(),
2036 Action::FileExplorerToggleSelect => {
2037 self.active_window_mut().file_explorer_toggle_select()
2038 }
2039 Action::FileExplorerSelectAll => self.active_window_mut().file_explorer_select_all(),
2040 Action::RemoveSecondaryCursors => {
2041 if let Some(events) = self
2043 .active_window_mut()
2044 .action_to_events(Action::RemoveSecondaryCursors)
2045 {
2046 let batch = Event::Batch {
2048 events: events.clone(),
2049 description: "Remove secondary cursors".to_string(),
2050 };
2051 self.active_event_log_mut().append(batch.clone());
2052 self.apply_event_to_active_buffer(&batch);
2053
2054 let active_split = self
2056 .windows
2057 .get(&self.active_window)
2058 .and_then(|w| w.buffers.splits())
2059 .map(|(mgr, _)| mgr)
2060 .expect("active window must have a populated split layout")
2061 .active_split();
2062 let active_buffer = self.active_buffer();
2063 self.active_window_mut()
2064 .ensure_cursor_visible_for_split(active_buffer, active_split);
2065 }
2066 }
2067
2068 Action::MenuActivate => {
2070 self.handle_menu_activate();
2071 }
2072 Action::MenuClose => {
2073 self.handle_menu_close();
2074 }
2075 Action::MenuLeft => {
2076 self.handle_menu_left();
2077 }
2078 Action::MenuRight => {
2079 self.handle_menu_right();
2080 }
2081 Action::MenuUp => {
2082 self.handle_menu_up();
2083 }
2084 Action::MenuDown => {
2085 self.handle_menu_down();
2086 }
2087 Action::MenuExecute => {
2088 if let Some(action) = self.handle_menu_execute() {
2089 return self.handle_action(action);
2090 }
2091 }
2092 Action::MenuOpen(menu_name) => {
2093 if self.config.editor.menu_bar_mnemonics {
2094 self.handle_menu_open(&menu_name);
2095 }
2096 }
2097
2098 Action::SwitchKeybindingMap(map_name) => {
2099 let is_builtin =
2101 matches!(map_name.as_str(), "default" | "emacs" | "vscode" | "macos");
2102 let is_user_defined = self.config.keybinding_maps.contains_key(&map_name);
2103
2104 if is_builtin || is_user_defined {
2105 self.config_mut().active_keybinding_map = map_name.clone().into();
2107
2108 *self.keybindings.write().unwrap() =
2110 crate::input::keybindings::KeybindingResolver::new(&self.config);
2111
2112 self.set_status_message(
2113 t!("view.keybindings_switched", map = map_name).to_string(),
2114 );
2115 } else {
2116 self.set_status_message(
2117 t!("view.keybindings_unknown", map = map_name).to_string(),
2118 );
2119 }
2120 }
2121
2122 Action::SmartHome => {
2123 let buffer_id = self.active_buffer();
2125 if self.active_window().is_composite_buffer(buffer_id) {
2126 if let Some(_handled) =
2127 self.handle_composite_action(buffer_id, &Action::SmartHome)
2128 {
2129 return Ok(());
2130 }
2131 }
2132 self.smart_home();
2133 }
2134 Action::ToggleComment => {
2135 self.toggle_comment();
2136 }
2137 Action::ToggleFold => {
2138 self.active_window_mut().toggle_fold_at_cursor();
2139 }
2140 Action::GoToMatchingBracket => {
2141 self.goto_matching_bracket();
2142 }
2143 Action::JumpToNextError => {
2144 self.jump_to_next_error();
2145 }
2146 Action::JumpToPreviousError => {
2147 self.jump_to_previous_error();
2148 }
2149 Action::SetBookmark(key) => {
2150 self.active_window_mut().set_bookmark(key);
2151 }
2152 Action::JumpToBookmark(key) => {
2153 self.jump_to_bookmark(key);
2154 }
2155 Action::ClearBookmark(key) => {
2156 self.active_window_mut().clear_bookmark(key);
2157 }
2158 Action::ListBookmarks => {
2159 self.active_window_mut().list_bookmarks();
2160 }
2161 Action::ToggleSearchCaseSensitive => {
2162 self.active_window_mut().search_case_sensitive =
2163 !self.active_window().search_case_sensitive;
2164 let state = if self.active_window().search_case_sensitive {
2165 "enabled"
2166 } else {
2167 "disabled"
2168 };
2169 self.set_status_message(
2170 t!("search.case_sensitive_state", state = state).to_string(),
2171 );
2172 if let Some(prompt) = &self.active_window_mut().prompt {
2175 if matches!(
2176 prompt.prompt_type,
2177 PromptType::Search
2178 | PromptType::ReplaceSearch
2179 | PromptType::QueryReplaceSearch
2180 ) {
2181 let query = prompt.input.clone();
2182 self.update_search_highlights(&query);
2183 }
2184 } else if let Some(search_state) = &self.active_window().search_state {
2185 let query = search_state.query.clone();
2186 self.perform_search(&query);
2187 }
2188 }
2189 Action::ToggleSearchWholeWord => {
2190 self.active_window_mut().search_whole_word =
2191 !self.active_window().search_whole_word;
2192 let state = if self.active_window().search_whole_word {
2193 "enabled"
2194 } else {
2195 "disabled"
2196 };
2197 self.set_status_message(t!("search.whole_word_state", state = state).to_string());
2198 if let Some(prompt) = &self.active_window_mut().prompt {
2201 if matches!(
2202 prompt.prompt_type,
2203 PromptType::Search
2204 | PromptType::ReplaceSearch
2205 | PromptType::QueryReplaceSearch
2206 ) {
2207 let query = prompt.input.clone();
2208 self.update_search_highlights(&query);
2209 }
2210 } else if let Some(search_state) = &self.active_window().search_state {
2211 let query = search_state.query.clone();
2212 self.perform_search(&query);
2213 }
2214 }
2215 Action::ToggleSearchRegex => {
2216 self.active_window_mut().search_use_regex = !self.active_window().search_use_regex;
2217 let state = if self.active_window().search_use_regex {
2218 "enabled"
2219 } else {
2220 "disabled"
2221 };
2222 self.set_status_message(t!("search.regex_state", state = state).to_string());
2223 if let Some(prompt) = &self.active_window_mut().prompt {
2226 if matches!(
2227 prompt.prompt_type,
2228 PromptType::Search
2229 | PromptType::ReplaceSearch
2230 | PromptType::QueryReplaceSearch
2231 ) {
2232 let query = prompt.input.clone();
2233 self.update_search_highlights(&query);
2234 }
2235 } else if let Some(search_state) = &self.active_window().search_state {
2236 let query = search_state.query.clone();
2237 self.perform_search(&query);
2238 }
2239 }
2240 Action::ToggleSearchConfirmEach => {
2241 self.active_window_mut().search_confirm_each =
2242 !self.active_window().search_confirm_each;
2243 let state = if self.active_window().search_confirm_each {
2244 "enabled"
2245 } else {
2246 "disabled"
2247 };
2248 self.set_status_message(t!("search.confirm_each_state", state = state).to_string());
2249 }
2250 Action::FileBrowserToggleHidden => {
2251 self.file_open_toggle_hidden();
2253 }
2254 Action::StartMacroRecording => {
2255 self.set_status_message(
2257 "Use Ctrl+Shift+R to start recording (will prompt for register)".to_string(),
2258 );
2259 }
2260 Action::StopMacroRecording => {
2261 self.stop_macro_recording();
2262 }
2263 Action::PlayMacro(key) => {
2264 self.play_macro(key);
2265 }
2266 Action::ToggleMacroRecording(key) => {
2267 self.toggle_macro_recording(key);
2268 }
2269 Action::ShowMacro(key) => {
2270 self.show_macro_in_buffer(key);
2271 }
2272 Action::ListMacros => {
2273 self.list_macros_in_buffer();
2274 }
2275 Action::PromptRecordMacro => {
2276 self.start_prompt("Record macro (0-9): ".to_string(), PromptType::RecordMacro);
2277 }
2278 Action::PromptPlayMacro => {
2279 self.start_prompt("Play macro (0-9): ".to_string(), PromptType::PlayMacro);
2280 }
2281 Action::PlayLastMacro => {
2282 if let Some(key) = self.active_window_mut().macros.last_register() {
2283 self.play_macro(key);
2284 } else {
2285 self.set_status_message(t!("status.no_macro_recorded").to_string());
2286 }
2287 }
2288 Action::PromptSetBookmark => {
2289 self.start_prompt("Set bookmark (0-9): ".to_string(), PromptType::SetBookmark);
2290 }
2291 Action::PromptJumpToBookmark => {
2292 self.start_prompt(
2293 "Jump to bookmark (0-9): ".to_string(),
2294 PromptType::JumpToBookmark,
2295 );
2296 }
2297 Action::CompositeNextHunk => {
2298 let buf = self.active_buffer();
2299 self.active_window_mut().composite_next_hunk_active(buf);
2300 }
2301 Action::CompositePrevHunk => {
2302 let buf = self.active_buffer();
2303 self.active_window_mut().composite_prev_hunk_active(buf);
2304 }
2305 Action::None => {}
2306 Action::DeleteBackward => {
2307 if self.active_window().is_editing_disabled() {
2308 self.set_status_message(t!("buffer.editing_disabled").to_string());
2309 return Ok(());
2310 }
2311 if let Some(events) = self
2313 .active_window_mut()
2314 .action_to_events(Action::DeleteBackward)
2315 {
2316 if events.len() > 1 {
2317 let description = "Delete backward".to_string();
2319 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description)
2320 {
2321 self.active_event_log_mut().append(bulk_edit);
2322 }
2323 } else {
2324 for event in events {
2325 self.active_event_log_mut().append(event.clone());
2326 self.apply_event_to_active_buffer(&event);
2327 }
2328 }
2329 }
2330 }
2331 Action::PluginAction(action_name) => {
2332 tracing::debug!("handle_action: PluginAction('{}')", action_name);
2333 #[cfg(feature = "plugins")]
2336 {
2337 let result = self
2338 .plugin_manager
2339 .read()
2340 .unwrap()
2341 .execute_action_async(&action_name);
2342 if let Some(result) = result {
2343 match result {
2344 Ok(receiver) => {
2345 self.pending_plugin_actions
2347 .push((action_name.clone(), receiver));
2348 }
2349 Err(e) => {
2350 self.set_status_message(
2351 t!("view.plugin_error", error = e.to_string()).to_string(),
2352 );
2353 tracing::error!("Plugin action error: {}", e);
2354 }
2355 }
2356 } else {
2357 self.set_status_message(
2358 t!("status.plugin_manager_unavailable").to_string(),
2359 );
2360 }
2361 }
2362 #[cfg(not(feature = "plugins"))]
2363 {
2364 let _ = action_name;
2365 self.set_status_message(
2366 "Plugins not available (compiled without plugin support)".to_string(),
2367 );
2368 }
2369 }
2370 Action::LoadPluginFromBuffer => {
2371 #[cfg(feature = "plugins")]
2372 {
2373 let buffer_id = self.active_buffer();
2374 let state = self.active_state();
2375 let buffer = &state.buffer;
2376 let total = buffer.total_bytes();
2377 let content =
2378 String::from_utf8_lossy(&buffer.slice_bytes(0..total)).to_string();
2379
2380 let is_ts = buffer
2382 .file_path()
2383 .and_then(|p| p.extension())
2384 .and_then(|e| e.to_str())
2385 .map(|e| e == "ts" || e == "tsx")
2386 .unwrap_or(true);
2387
2388 let name = buffer
2390 .file_path()
2391 .and_then(|p| p.file_name())
2392 .and_then(|s| s.to_str())
2393 .map(|s| s.to_string())
2394 .unwrap_or_else(|| "buffer-plugin".to_string());
2395
2396 let load_result = self
2397 .plugin_manager
2398 .read()
2399 .unwrap()
2400 .load_plugin_from_source(&content, &name, is_ts);
2401 match load_result {
2402 Ok(()) => {
2403 self.set_status_message(format!(
2404 "Plugin '{}' loaded from buffer",
2405 name
2406 ));
2407 }
2408 Err(e) => {
2409 self.set_status_message(format!("Failed to load plugin: {}", e));
2410 tracing::error!("LoadPluginFromBuffer error: {}", e);
2411 }
2412 }
2413
2414 self.setup_plugin_dev_lsp(buffer_id, &content);
2416 }
2417 #[cfg(not(feature = "plugins"))]
2418 {
2419 self.set_status_message(
2420 "Plugins not available (compiled without plugin support)".to_string(),
2421 );
2422 }
2423 }
2424 Action::InitReload => {
2425 self.load_init_script(true);
2430 self.fire_plugins_loaded_hook();
2433 }
2434 Action::InitEdit => {
2435 let config_dir = self.dir_context.config_dir.clone();
2438 match crate::init_script::ensure_starter(&config_dir) {
2439 Ok(path) => {
2440 let declarations =
2450 self.plugin_manager.read().unwrap().plugin_declarations();
2451 crate::init_script::write_plugin_declarations(&config_dir, &declarations);
2452 match self.open_file(&path) {
2453 Ok(_) => {
2454 self.set_status_message(format!("init.ts: {}", path.display()));
2455 }
2456 Err(e) => {
2457 self.set_status_message(format!("init.ts: open failed: {e}"));
2458 }
2459 }
2460 }
2461 Err(e) => {
2462 self.set_status_message(format!("init.ts: create failed: {e}"));
2463 }
2464 }
2465 }
2466 Action::InitCheck => {
2467 let report = crate::init_script::check(&self.dir_context.config_dir);
2470 if report.ok && report.diagnostics.is_empty() {
2471 self.set_status_message("init.ts: ok".into());
2472 } else if !report.ok {
2473 let first = report
2474 .diagnostics
2475 .first()
2476 .map(|d| format!("{}:{}: {}", d.line, d.column, d.message))
2477 .unwrap_or_else(|| "unknown error".into());
2478 self.set_status_message(format!(
2479 "init.ts: {} error(s) — first: {first}",
2480 report.diagnostics.len()
2481 ));
2482 } else {
2483 self.set_status_message(format!(
2484 "init.ts: {} warning(s)",
2485 report.diagnostics.len()
2486 ));
2487 }
2488 }
2489 Action::OpenTerminal => {
2490 self.open_terminal();
2491 }
2492 Action::CloseTerminal => {
2493 self.close_terminal();
2494 }
2495 Action::FocusTerminal => {
2496 if self
2498 .active_window()
2499 .is_terminal_buffer(self.active_buffer())
2500 {
2501 self.active_window_mut().terminal_mode = true;
2502 self.active_window_mut().key_context = KeyContext::Terminal;
2503 self.set_status_message(t!("status.terminal_mode_enabled").to_string());
2504 }
2505 }
2506 Action::TerminalEscape => {
2507 if self.active_window().terminal_mode {
2509 self.active_window_mut().terminal_mode = false;
2510 self.active_window_mut().key_context = KeyContext::Normal;
2511 self.set_status_message(t!("status.terminal_mode_disabled").to_string());
2512 }
2513 }
2514 Action::ToggleKeyboardCapture => {
2515 if self.active_window().terminal_mode {
2517 self.active_window_mut().keyboard_capture =
2518 !self.active_window_mut().keyboard_capture;
2519 if self.active_window_mut().keyboard_capture {
2520 self.set_status_message(
2521 "Keyboard capture ON - all keys go to terminal (F9 to toggle)"
2522 .to_string(),
2523 );
2524 } else {
2525 self.set_status_message(
2526 "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
2527 );
2528 }
2529 }
2530 }
2531 Action::TerminalPaste => {
2532 if self.active_window().terminal_mode {
2534 if let Some(text) = self.clipboard.paste() {
2535 self.active_window_mut()
2536 .send_terminal_input(text.as_bytes());
2537 }
2538 }
2539 }
2540 Action::ShellCommand => {
2541 self.start_shell_command_prompt(false);
2543 }
2544 Action::ShellCommandReplace => {
2545 self.start_shell_command_prompt(true);
2547 }
2548 Action::OpenSettings => {
2549 self.open_settings();
2550 }
2551 Action::CloseSettings => {
2552 let has_changes = self
2554 .settings_state
2555 .as_ref()
2556 .is_some_and(|s| s.has_changes());
2557 if has_changes {
2558 if let Some(ref mut state) = self.settings_state {
2560 state.show_confirm_dialog();
2561 }
2562 } else {
2563 self.close_settings(false);
2564 }
2565 }
2566 Action::SettingsSave => {
2567 self.save_settings();
2568 }
2569 Action::SettingsReset => {
2570 if let Some(ref mut state) = self.settings_state {
2571 state.reset_current_to_default();
2572 }
2573 }
2574 Action::SettingsInherit => {
2575 if let Some(ref mut state) = self.settings_state {
2576 state.set_current_to_null();
2577 }
2578 }
2579 Action::SettingsToggleFocus => {
2580 if let Some(ref mut state) = self.settings_state {
2581 state.toggle_focus();
2582 }
2583 }
2584 Action::SettingsActivate => {
2585 self.settings_activate_current();
2586 }
2587 Action::SettingsSearch => {
2588 if let Some(ref mut state) = self.settings_state {
2589 state.start_search();
2590 }
2591 }
2592 Action::SettingsHelp => {
2593 if let Some(ref mut state) = self.settings_state {
2594 state.toggle_help();
2595 }
2596 }
2597 Action::SettingsIncrement => {
2598 self.settings_increment_current();
2599 }
2600 Action::SettingsDecrement => {
2601 self.settings_decrement_current();
2602 }
2603 Action::CalibrateInput => {
2604 self.open_calibration_wizard();
2605 }
2606 Action::EventDebug => {
2607 self.active_window_mut().open_event_debug();
2608 }
2609 Action::SuspendProcess => {
2610 self.request_suspend();
2611 }
2612 Action::OpenKeybindingEditor => {
2613 self.open_keybinding_editor();
2614 }
2615 Action::PromptConfirm => {
2616 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
2617 use super::prompt_actions::PromptResult;
2618 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
2619 PromptResult::ExecuteAction(action) => {
2620 return self.handle_action(action);
2621 }
2622 PromptResult::EarlyReturn => {
2623 return Ok(());
2624 }
2625 PromptResult::Done => {}
2626 }
2627 }
2628 }
2629 Action::PromptConfirmWithText(ref text) => {
2630 if let Some(ref mut prompt) = self.active_window_mut().prompt {
2632 prompt.set_input(text.clone());
2633 self.update_prompt_suggestions();
2634 }
2635 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
2636 use super::prompt_actions::PromptResult;
2637 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
2638 PromptResult::ExecuteAction(action) => {
2639 return self.handle_action(action);
2640 }
2641 PromptResult::EarlyReturn => {
2642 return Ok(());
2643 }
2644 PromptResult::Done => {}
2645 }
2646 }
2647 }
2648 Action::PopupConfirm => {
2649 use super::popup_actions::PopupConfirmResult;
2650 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
2651 return Ok(());
2652 }
2653 }
2654 Action::PopupCancel => {
2655 self.handle_popup_cancel();
2656 }
2657 Action::PopupFocus => {
2658 self.handle_popup_focus();
2659 }
2660 Action::CompletionAccept => {
2661 use super::popup_actions::PopupConfirmResult;
2662 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
2663 return Ok(());
2664 }
2665 }
2666 Action::CompletionDismiss => {
2667 self.handle_popup_cancel();
2668 }
2669 Action::InsertChar(c) => {
2670 if self.is_prompting() {
2671 return self.handle_insert_char_prompt(c);
2672 } else if self.active_window_mut().key_context == KeyContext::FileExplorer {
2673 self.active_window_mut().file_explorer_search_push_char(c);
2674 } else {
2675 self.handle_insert_char_editor(c)?;
2676 }
2677 }
2678 Action::PromptCopy => {
2680 if let Some(prompt) = &self.active_window_mut().prompt {
2681 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
2682 if !text.is_empty() {
2683 self.clipboard.copy(text);
2684 self.set_status_message(t!("clipboard.copied").to_string());
2685 }
2686 }
2687 }
2688 Action::PromptCut => {
2689 if let Some(prompt) = &self.active_window_mut().prompt {
2690 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
2691 if !text.is_empty() {
2692 self.clipboard.copy(text);
2693 }
2694 }
2695 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
2696 if prompt.has_selection() {
2697 prompt.delete_selection();
2698 } else {
2699 prompt.clear();
2700 }
2701 }
2702 self.set_status_message(t!("clipboard.cut").to_string());
2703 self.update_prompt_suggestions();
2704 }
2705 Action::PromptPaste => {
2706 if let Some(text) = self.clipboard.paste() {
2707 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
2708 prompt.insert_str(&text);
2709 }
2710 self.update_prompt_suggestions();
2711 }
2712 }
2713 _ => {
2714 self.apply_action_as_events(action)?;
2720 }
2721 }
2722
2723 Ok(())
2724 }
2725
2726 fn dispatch_floating_widget_key(
2737 &mut self,
2738 code: crossterm::event::KeyCode,
2739 modifiers: crossterm::event::KeyModifiers,
2740 ) -> bool {
2741 use crossterm::event::{KeyCode, KeyModifiers};
2742 let panel_id = match self.floating_widget_panel.as_ref() {
2743 Some(fwp) => fwp.panel_id,
2744 None => return false,
2745 };
2746 let key_name: Option<&str> = match code {
2747 KeyCode::Esc => {
2748 let mode_has_binding = self
2757 .active_window()
2758 .editor_mode
2759 .as_ref()
2760 .map(|mode_name| {
2761 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
2762 let mode_ctx =
2763 crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
2764 let keybindings = self.keybindings.read().unwrap();
2765 keybindings.has_explicit_binding(&key_event, &mode_ctx)
2766 })
2767 .unwrap_or(false);
2768 if mode_has_binding {
2769 return false;
2770 }
2771 let widget_key = self
2772 .widget_registry
2773 .get(panel_id)
2774 .map(|p| p.focus_key.clone())
2775 .unwrap_or_default();
2776 if self
2777 .plugin_manager
2778 .read()
2779 .unwrap()
2780 .has_hook_handlers("widget_event")
2781 {
2782 self.plugin_manager.read().unwrap().run_hook(
2783 "widget_event",
2784 crate::services::plugins::hooks::HookArgs::WidgetEvent {
2785 panel_id,
2786 widget_key,
2787 event_type: "cancel".to_string(),
2788 payload: serde_json::json!({}),
2789 },
2790 );
2791 }
2792 self.floating_widget_panel = None;
2793 let _ = self.widget_registry.unmount(panel_id);
2794 return true;
2795 }
2796 KeyCode::Tab => Some(if modifiers.contains(KeyModifiers::SHIFT) {
2797 "Shift+Tab"
2798 } else {
2799 "Tab"
2800 }),
2801 KeyCode::BackTab => Some("Shift+Tab"),
2802 KeyCode::Enter => Some("Enter"),
2803 KeyCode::Backspace => Some("Backspace"),
2804 KeyCode::Delete => Some("Delete"),
2805 KeyCode::Home => Some("Home"),
2806 KeyCode::End => Some("End"),
2807 KeyCode::Left => Some("Left"),
2808 KeyCode::Right => Some("Right"),
2809 KeyCode::Up => Some("Up"),
2810 KeyCode::Down => Some("Down"),
2811 KeyCode::PageUp => Some("PageUp"),
2812 KeyCode::PageDown => Some("PageDown"),
2813 _ => None,
2814 };
2815 if let Some(name) = key_name {
2816 let mode_has_binding = self
2835 .active_window()
2836 .editor_mode
2837 .as_ref()
2838 .map(|mode_name| {
2839 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
2840 let mode_ctx =
2841 crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
2842 let keybindings = self.keybindings.read().unwrap();
2843 keybindings.has_explicit_binding(&key_event, &mode_ctx)
2844 })
2845 .unwrap_or(false);
2846 if mode_has_binding {
2847 return false;
2848 }
2849 self.handle_widget_command(
2850 panel_id,
2851 fresh_core::api::WidgetAction::Key {
2852 key: name.to_string(),
2853 },
2854 );
2855 return true;
2856 }
2857 if let KeyCode::Char(c) = code {
2858 {
2869 let mode_has_binding = self
2870 .active_window()
2871 .editor_mode
2872 .as_ref()
2873 .map(|mode_name| {
2874 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
2875 let mode_ctx =
2876 crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
2877 let keybindings = self.keybindings.read().unwrap();
2878 keybindings.has_explicit_binding(&key_event, &mode_ctx)
2879 })
2880 .unwrap_or(false);
2881 if mode_has_binding {
2882 return false;
2883 }
2884 }
2885 if modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) {
2891 return true;
2892 }
2893 let ch = if modifiers.contains(KeyModifiers::SHIFT) {
2894 c.to_uppercase().next().unwrap_or(c)
2895 } else {
2896 c
2897 };
2898 if ch == ' ' {
2909 self.handle_widget_command(
2910 panel_id,
2911 fresh_core::api::WidgetAction::Key {
2912 key: "Space".to_string(),
2913 },
2914 );
2915 return true;
2916 }
2917 self.handle_widget_command(
2918 panel_id,
2919 fresh_core::api::WidgetAction::TextInputChar {
2920 text: ch.to_string(),
2921 },
2922 );
2923 return true;
2924 }
2925 true
2930 }
2931}