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.clone());
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 let cached = self.active_window_mut().live_grep_last_state.clone();
1324 match cached {
1325 Some(state) if state.cached_results.as_ref().is_some_and(|r| !r.is_empty()) => {
1326 let results = state.cached_results.unwrap_or_default();
1327 let suggestions: Vec<crate::input::commands::Suggestion> = results
1332 .into_iter()
1333 .map(|m| {
1334 let label = format!("{}:{}", m.file, m.line);
1335 let value = format!("{}:{}:{}", m.file, m.line, m.column);
1336 let mut s = crate::input::commands::Suggestion::new(label);
1337 s.description = Some(m.content);
1338 s.value = Some(value);
1339 s
1340 })
1341 .collect();
1342 let mut prompt = crate::view::prompt::Prompt::with_suggestions(
1349 "Live grep: ".to_string(),
1350 PromptType::LiveGrep,
1351 suggestions,
1352 );
1353 prompt.input = state.query;
1354 prompt.cursor_pos = prompt.input.len();
1355 if let Some(idx) = state.selected_index {
1356 if idx < prompt.suggestions.len() {
1357 prompt.selected_suggestion = Some(idx);
1358 }
1359 }
1360 prompt.suggestions_set_for_input = Some(prompt.input.clone());
1361 prompt.overlay = true;
1363 self.active_window_mut().prompt = Some(prompt);
1364 }
1365 _ => {
1366 #[cfg(feature = "plugins")]
1368 {
1369 let result = self
1370 .plugin_manager
1371 .read()
1372 .unwrap()
1373 .execute_action_async("start_live_grep");
1374 if let Some(result) = result {
1375 match result {
1376 Ok(receiver) => {
1377 self.pending_plugin_actions
1378 .push(("start_live_grep".to_string(), receiver));
1379 }
1380 Err(e) => {
1381 self.set_status_message(format!(
1382 "Live Grep unavailable: {}",
1383 e
1384 ));
1385 }
1386 }
1387 }
1388 }
1389 }
1390 }
1391 }
1392 Action::LiveGrepExportQuickfix => {
1393 let is_grep = self
1398 .active_window()
1399 .prompt
1400 .as_ref()
1401 .map(|p| match &p.prompt_type {
1402 PromptType::LiveGrep => true,
1403 PromptType::Plugin { custom_type } => custom_type == "live-grep",
1404 _ => false,
1405 })
1406 .unwrap_or(false);
1407 if !is_grep {
1408 self.set_status_message(
1409 "Quickfix export is only available inside Live Grep".to_string(),
1410 );
1411 return Ok(());
1412 }
1413 let (query, matches) = {
1414 let prompt = self.active_window().prompt.as_ref().unwrap();
1415 (
1416 prompt.input.clone(),
1417 self.snapshot_prompt_results_for_grep(prompt),
1418 )
1419 };
1420 if matches.is_empty() {
1421 self.set_status_message("No Live Grep results to export".to_string());
1422 return Ok(());
1423 }
1424 self.cancel_prompt();
1426 self.install_quickfix_in_dock(query, matches);
1428 }
1429 Action::ToggleUtilityDock => {
1430 use crate::view::split::SplitRole;
1431 if let Some(dock_leaf) = self
1432 .windows
1433 .get(&self.active_window)
1434 .and_then(|w| w.buffers.splits())
1435 .map(|(mgr, _)| mgr)
1436 .expect("active window must have a populated split layout")
1437 .find_leaf_by_role(SplitRole::UtilityDock)
1438 {
1439 let active = self
1440 .windows
1441 .get(&self.active_window)
1442 .and_then(|w| w.buffers.splits())
1443 .map(|(mgr, _)| mgr)
1444 .expect("active window must have a populated split layout")
1445 .active_split();
1446 if active == dock_leaf {
1447 self.next_split();
1452 } else {
1453 self.windows
1454 .get_mut(&self.active_window)
1455 .and_then(|w| w.split_manager_mut())
1456 .expect("active window must have a populated split layout")
1457 .set_active_split(dock_leaf);
1458 }
1459 } else {
1460 self.set_status_message(
1461 "No Utility Dock open — invoke a dock-aware utility (Diagnostics, Search/Replace, …)"
1462 .to_string(),
1463 );
1464 }
1465 }
1466 Action::CycleLiveGrepProvider => {
1467 let in_live_grep = self
1473 .active_window()
1474 .prompt
1475 .as_ref()
1476 .map(|p| match &p.prompt_type {
1477 PromptType::LiveGrep => true,
1478 PromptType::Plugin { custom_type } => custom_type == "live-grep",
1479 _ => false,
1480 })
1481 .unwrap_or(false);
1482 if !in_live_grep {
1483 self.set_status_message(
1484 "Cycle Live Grep provider only works inside Live Grep".to_string(),
1485 );
1486 return Ok(());
1487 }
1488 #[cfg(feature = "plugins")]
1489 {
1490 let result = self
1491 .plugin_manager
1492 .read()
1493 .unwrap()
1494 .execute_action_async("live_grep_cycle_provider");
1495 if let Some(result) = result {
1496 match result {
1497 Ok(receiver) => {
1498 self.pending_plugin_actions
1499 .push(("live_grep_cycle_provider".to_string(), receiver));
1500 }
1501 Err(e) => {
1502 self.set_status_message(format!("Live Grep cycle failed: {}", e));
1503 }
1504 }
1505 } else {
1506 self.set_status_message("Live Grep plugin not loaded".to_string());
1507 }
1508 }
1509 #[cfg(not(feature = "plugins"))]
1510 {
1511 self.set_status_message(
1512 "Live Grep cycle requires the plugins feature".to_string(),
1513 );
1514 }
1515 }
1516 Action::OpenTerminalInDock => {
1517 use crate::model::event::SplitDirection;
1518 use crate::view::split::SplitRole;
1519 if let Some(dock_leaf) = self
1520 .windows
1521 .get(&self.active_window)
1522 .and_then(|w| w.buffers.splits())
1523 .map(|(mgr, _)| mgr)
1524 .expect("active window must have a populated split layout")
1525 .find_leaf_by_role(SplitRole::UtilityDock)
1526 {
1527 self.windows
1530 .get_mut(&self.active_window)
1531 .and_then(|w| w.split_manager_mut())
1532 .expect("active window must have a populated split layout")
1533 .set_active_split(dock_leaf);
1534 self.open_terminal();
1535 } else {
1536 let Some(terminal_id) = self.spawn_terminal_session() else {
1543 return Ok(());
1544 };
1545 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
1546 match self
1549 .windows
1550 .get_mut(&self.active_window)
1551 .and_then(|w| w.split_manager_mut())
1552 .expect("active window must have a populated split layout")
1553 .split_root_positioned(SplitDirection::Horizontal, buffer_id, 0.7, false)
1554 {
1555 Ok(new_leaf) => {
1556 let mut view_state = crate::view::split::SplitViewState::with_buffer(
1557 self.terminal_width,
1558 self.terminal_height,
1559 buffer_id,
1560 );
1561 view_state.apply_config_defaults(
1567 false,
1568 false,
1569 self.active_window().resolve_line_wrap_for_buffer(buffer_id),
1570 self.config.editor.wrap_indent,
1571 self.active_window()
1572 .resolve_wrap_column_for_buffer(buffer_id),
1573 self.config.editor.rulers.clone(),
1574 );
1575 view_state.viewport.line_wrap_enabled = false;
1579 self.windows
1580 .get_mut(&self.active_window)
1581 .and_then(|w| w.split_view_states_mut())
1582 .expect("active window must have a populated split layout")
1583 .insert(new_leaf, view_state);
1584 self.windows
1585 .get_mut(&self.active_window)
1586 .and_then(|w| w.split_manager_mut())
1587 .expect("active window must have a populated split layout")
1588 .set_leaf_role(new_leaf, Some(SplitRole::UtilityDock));
1589 self.windows
1590 .get_mut(&self.active_window)
1591 .and_then(|w| w.split_manager_mut())
1592 .expect("active window must have a populated split layout")
1593 .set_active_split(new_leaf);
1594 self.active_window_mut().terminal_mode = true;
1600 self.active_window_mut().key_context =
1601 crate::input::keybindings::KeyContext::Terminal;
1602 self.active_window_mut().resize_visible_terminals();
1603 let exit_key = self
1604 .keybindings
1605 .read()
1606 .unwrap()
1607 .find_keybinding_for_action(
1608 "terminal_escape",
1609 crate::input::keybindings::KeyContext::Terminal,
1610 )
1611 .unwrap_or_else(|| "Ctrl+Space".to_string());
1612 self.set_status_message(
1613 rust_i18n::t!(
1614 "terminal.opened",
1615 id = terminal_id.0,
1616 exit_key = exit_key
1617 )
1618 .to_string(),
1619 );
1620 tracing::info!(
1621 "Opened terminal {:?} into new dock leaf {:?} (buffer {:?})",
1622 terminal_id,
1623 new_leaf,
1624 buffer_id
1625 );
1626 }
1627 Err(e) => {
1628 self.set_status_message(format!(
1629 "Failed to create dock for terminal: {}",
1630 e
1631 ));
1632 return Ok(());
1633 }
1634 }
1635 }
1636 }
1637 Action::ToggleLineWrap => {
1638 let new_value = !self.config.editor.line_wrap;
1639 self.config_mut().editor.line_wrap = new_value;
1640 self.sync_windows_config();
1648
1649 let leaf_ids: Vec<_> = self
1652 .windows
1653 .get(&self.active_window)
1654 .and_then(|w| w.buffers.splits())
1655 .map(|(_, vs)| vs)
1656 .expect("active window must have a populated split layout")
1657 .keys()
1658 .copied()
1659 .collect();
1660 for leaf_id in leaf_ids {
1661 let buffer_id = self
1662 .split_manager_mut()
1663 .get_buffer_id(leaf_id.into())
1664 .unwrap_or(BufferId(0));
1665 let effective_wrap =
1666 self.active_window().resolve_line_wrap_for_buffer(buffer_id);
1667 let wrap_column = self
1668 .active_window()
1669 .resolve_wrap_column_for_buffer(buffer_id);
1670 if let Some(view_state) = self
1671 .windows
1672 .get_mut(&self.active_window)
1673 .and_then(|w| w.split_view_states_mut())
1674 .expect("active window must have a populated split layout")
1675 .get_mut(&leaf_id)
1676 {
1677 view_state.viewport.line_wrap_enabled = effective_wrap;
1678 view_state.viewport.wrap_indent = self.config.editor.wrap_indent;
1679 view_state.viewport.wrap_column = wrap_column;
1680 }
1681 }
1682
1683 let state = if self.config.editor.line_wrap {
1684 t!("view.state_enabled").to_string()
1685 } else {
1686 t!("view.state_disabled").to_string()
1687 };
1688 self.set_status_message(t!("view.line_wrap_state", state = state).to_string());
1689 }
1690 Action::ToggleCurrentLineHighlight => {
1691 let new_value = !self.config.editor.highlight_current_line;
1692 self.config_mut().editor.highlight_current_line = new_value;
1693
1694 let leaf_ids: Vec<_> = self
1696 .windows
1697 .get(&self.active_window)
1698 .and_then(|w| w.buffers.splits())
1699 .map(|(_, vs)| vs)
1700 .expect("active window must have a populated split layout")
1701 .keys()
1702 .copied()
1703 .collect();
1704 for leaf_id in leaf_ids {
1705 if let Some(view_state) = self
1706 .windows
1707 .get_mut(&self.active_window)
1708 .and_then(|w| w.split_view_states_mut())
1709 .expect("active window must have a populated split layout")
1710 .get_mut(&leaf_id)
1711 {
1712 view_state.highlight_current_line =
1713 self.config.editor.highlight_current_line;
1714 }
1715 }
1716
1717 let state = if self.config.editor.highlight_current_line {
1718 t!("view.state_enabled").to_string()
1719 } else {
1720 t!("view.state_disabled").to_string()
1721 };
1722 self.set_status_message(
1723 t!("view.current_line_highlight_state", state = state).to_string(),
1724 );
1725 }
1726 Action::ToggleReadOnly => {
1727 let buffer_id = self.active_buffer();
1728 let is_now_read_only = self
1729 .active_window()
1730 .buffer_metadata
1731 .get(&buffer_id)
1732 .map(|m| !m.read_only)
1733 .unwrap_or(false);
1734 self.active_window_mut()
1735 .mark_buffer_read_only(buffer_id, is_now_read_only);
1736
1737 let state_str = if is_now_read_only {
1738 t!("view.state_enabled").to_string()
1739 } else {
1740 t!("view.state_disabled").to_string()
1741 };
1742 self.set_status_message(t!("view.read_only_state", state = state_str).to_string());
1743 }
1744 Action::TogglePageView => {
1745 self.active_window_mut().handle_toggle_page_view();
1746 }
1747 Action::SetPageWidth => {
1748 let active_split = self
1749 .windows
1750 .get(&self.active_window)
1751 .and_then(|w| w.buffers.splits())
1752 .map(|(mgr, _)| mgr)
1753 .expect("active window must have a populated split layout")
1754 .active_split();
1755 let current = self
1756 .windows
1757 .get(&self.active_window)
1758 .and_then(|w| w.buffers.splits())
1759 .map(|(_, vs)| vs)
1760 .expect("active window must have a populated split layout")
1761 .get(&active_split)
1762 .and_then(|v| v.compose_width.map(|w| w.to_string()))
1763 .unwrap_or_default();
1764 self.start_prompt_with_initial_text(
1765 "Page width (empty = viewport): ".to_string(),
1766 PromptType::SetPageWidth,
1767 current,
1768 );
1769 }
1770 Action::SetBackground => {
1771 let default_path = self
1772 .ansi_background_path
1773 .as_ref()
1774 .and_then(|p| {
1775 p.strip_prefix(&self.working_dir)
1776 .ok()
1777 .map(|rel| rel.to_string_lossy().to_string())
1778 })
1779 .unwrap_or_else(|| DEFAULT_BACKGROUND_FILE.to_string());
1780
1781 self.start_prompt_with_initial_text(
1782 "Background file: ".to_string(),
1783 PromptType::SetBackgroundFile,
1784 default_path,
1785 );
1786 }
1787 Action::SetBackgroundBlend => {
1788 let default_amount = format!("{:.2}", self.background_fade);
1789 self.start_prompt_with_initial_text(
1790 "Background blend (0-1): ".to_string(),
1791 PromptType::SetBackgroundBlend,
1792 default_amount,
1793 );
1794 }
1795 Action::LspCompletion => {
1796 self.request_completion();
1797 }
1798 Action::DabbrevExpand => {
1799 self.dabbrev_expand();
1800 }
1801 Action::LspGotoDefinition => {
1802 self.request_goto_definition()?;
1803 }
1804 Action::LspRename => {
1805 self.start_rename()?;
1806 }
1807 Action::LspHover => {
1808 self.request_hover()?;
1809 }
1810 Action::LspReferences => {
1811 self.request_references()?;
1812 }
1813 Action::LspSignatureHelp => {
1814 self.request_signature_help();
1815 }
1816 Action::LspCodeActions => {
1817 self.request_code_actions()?;
1818 }
1819 Action::LspRestart => {
1820 self.handle_lsp_restart();
1821 }
1822 Action::LspStop => {
1823 self.handle_lsp_stop();
1824 }
1825 Action::LspToggleForBuffer => {
1826 self.handle_lsp_toggle_for_buffer();
1827 }
1828 Action::ToggleInlayHints => {
1829 self.toggle_inlay_hints();
1830 }
1831 Action::DumpConfig => {
1832 self.dump_config();
1833 }
1834 Action::RedrawScreen => {
1835 self.request_full_redraw();
1836 }
1837 Action::SelectTheme => {
1838 self.start_select_theme_prompt();
1839 }
1840 Action::InspectThemeAtCursor => {
1841 self.inspect_theme_at_cursor();
1842 }
1843 Action::SelectKeybindingMap => {
1844 self.start_select_keybinding_map_prompt();
1845 }
1846 Action::SelectCursorStyle => {
1847 self.start_select_cursor_style_prompt();
1848 }
1849 Action::SelectLocale => {
1850 self.start_select_locale_prompt();
1851 }
1852 Action::Search => {
1853 let is_search_prompt = self.active_window().prompt.as_ref().is_some_and(|p| {
1855 matches!(
1856 p.prompt_type,
1857 PromptType::Search
1858 | PromptType::ReplaceSearch
1859 | PromptType::QueryReplaceSearch
1860 )
1861 });
1862
1863 if is_search_prompt {
1864 self.confirm_prompt();
1865 } else {
1866 self.start_search_prompt(
1867 t!("file.search_prompt").to_string(),
1868 PromptType::Search,
1869 false,
1870 );
1871 }
1872 }
1873 Action::Replace => {
1874 self.start_search_prompt(
1876 t!("file.replace_prompt").to_string(),
1877 PromptType::ReplaceSearch,
1878 false,
1879 );
1880 }
1881 Action::QueryReplace => {
1882 self.active_window_mut().search_confirm_each = true;
1884 self.start_search_prompt(
1885 "Query replace: ".to_string(),
1886 PromptType::QueryReplaceSearch,
1887 false,
1888 );
1889 }
1890 Action::FindInSelection => {
1891 self.start_search_prompt(
1892 t!("file.search_prompt").to_string(),
1893 PromptType::Search,
1894 true,
1895 );
1896 }
1897 Action::FindNext => {
1898 self.find_next();
1899 }
1900 Action::FindPrevious => {
1901 self.find_previous();
1902 }
1903 Action::FindSelectionNext => {
1904 self.find_selection_next();
1905 }
1906 Action::FindSelectionPrevious => {
1907 self.find_selection_previous();
1908 }
1909 Action::AddCursorNextMatch => self.add_cursor_at_next_match(),
1910 Action::AddCursorAbove => self.add_cursor_above(),
1911 Action::AddCursorBelow => self.add_cursor_below(),
1912 Action::AddCursorsToLineEnds => self.add_cursors_to_line_ends(),
1913 Action::NextBuffer => self.next_buffer(),
1914 Action::PrevBuffer => self.prev_buffer(),
1915 Action::SwitchToPreviousTab => self.switch_to_previous_tab(),
1916 Action::SwitchToTabByName => self.start_switch_to_tab_prompt(),
1917
1918 Action::ScrollTabsLeft => {
1920 let active_split_id = self
1921 .windows
1922 .get(&self.active_window)
1923 .and_then(|w| w.buffers.splits())
1924 .map(|(mgr, _)| mgr)
1925 .expect("active window must have a populated split layout")
1926 .active_split();
1927 if let Some(view_state) = self
1928 .windows
1929 .get_mut(&self.active_window)
1930 .and_then(|w| w.split_view_states_mut())
1931 .expect("active window must have a populated split layout")
1932 .get_mut(&active_split_id)
1933 {
1934 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_sub(5);
1935 self.set_status_message(t!("status.scrolled_tabs_left").to_string());
1936 }
1937 }
1938 Action::ScrollTabsRight => {
1939 let active_split_id = self
1940 .windows
1941 .get(&self.active_window)
1942 .and_then(|w| w.buffers.splits())
1943 .map(|(mgr, _)| mgr)
1944 .expect("active window must have a populated split layout")
1945 .active_split();
1946 if let Some(view_state) = self
1947 .windows
1948 .get_mut(&self.active_window)
1949 .and_then(|w| w.split_view_states_mut())
1950 .expect("active window must have a populated split layout")
1951 .get_mut(&active_split_id)
1952 {
1953 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_add(5);
1954 self.set_status_message(t!("status.scrolled_tabs_right").to_string());
1955 }
1956 }
1957 Action::NavigateBack => self.navigate_back(),
1958 Action::NavigateForward => self.navigate_forward(),
1959 Action::SplitHorizontal => self.split_pane_horizontal(),
1960 Action::SplitVertical => self.split_pane_vertical(),
1961 Action::CloseSplit => self.close_active_split(),
1962 Action::NextSplit => self.next_split(),
1963 Action::PrevSplit => self.prev_split(),
1964 Action::NextWindow => self.next_window(),
1965 Action::PrevWindow => self.prev_window(),
1966 Action::IncreaseSplitSize => self.adjust_split_size(0.05),
1967 Action::DecreaseSplitSize => self.adjust_split_size(-0.05),
1968 Action::ToggleMaximizeSplit => self.toggle_maximize_split(),
1969 Action::ToggleFileExplorer => self.toggle_file_explorer(),
1970 Action::ToggleFileExplorerSide => self.toggle_file_explorer_side(),
1971 Action::ToggleMenuBar => self.toggle_menu_bar(),
1972 Action::ToggleTabBar => self.active_window_mut().toggle_tab_bar(),
1973 Action::ToggleStatusBar => self.active_window_mut().toggle_status_bar(),
1974 Action::TogglePromptLine => self.active_window_mut().toggle_prompt_line(),
1975 Action::ToggleVerticalScrollbar => self.toggle_vertical_scrollbar(),
1976 Action::ToggleHorizontalScrollbar => self.toggle_horizontal_scrollbar(),
1977 Action::ToggleLineNumbers => self.toggle_line_numbers(),
1978 Action::ToggleScrollSync => self.active_window_mut().toggle_scroll_sync(),
1979 Action::ToggleMouseCapture => self.toggle_mouse_capture(),
1980 Action::ToggleMouseHover => self.toggle_mouse_hover(),
1981 Action::ToggleDebugHighlights => self.active_window_mut().toggle_debug_highlights(),
1982 Action::AddRuler => {
1984 self.start_prompt(t!("rulers.add_prompt").to_string(), PromptType::AddRuler);
1985 }
1986 Action::RemoveRuler => {
1987 self.start_remove_ruler_prompt();
1988 }
1989 Action::SetTabSize => {
1991 let current = self
1992 .buffers()
1993 .get(&self.active_buffer())
1994 .map(|s| s.buffer_settings.tab_size.to_string())
1995 .unwrap_or_else(|| "4".to_string());
1996 self.start_prompt_with_initial_text(
1997 "Tab size: ".to_string(),
1998 PromptType::SetTabSize,
1999 current,
2000 );
2001 }
2002 Action::SetLineEnding => {
2003 self.start_set_line_ending_prompt();
2004 }
2005 Action::SetEncoding => {
2006 self.start_set_encoding_prompt();
2007 }
2008 Action::ReloadWithEncoding => {
2009 self.start_reload_with_encoding_prompt();
2010 }
2011 Action::SetLanguage => {
2012 self.start_set_language_prompt();
2013 }
2014 Action::ToggleIndentationStyle => {
2015 let __buffer_id = self.active_buffer();
2016 if let Some(state) = self
2017 .windows
2018 .get_mut(&self.active_window)
2019 .map(|w| &mut w.buffers)
2020 .expect("active window present")
2021 .get_mut(&__buffer_id)
2022 {
2023 state.buffer_settings.use_tabs = !state.buffer_settings.use_tabs;
2024 let status = if state.buffer_settings.use_tabs {
2025 "Indentation: Tabs"
2026 } else {
2027 "Indentation: Spaces"
2028 };
2029 self.set_status_message(status.to_string());
2030 }
2031 }
2032 Action::ToggleTabIndicators | Action::ToggleWhitespaceIndicators => {
2033 let __buffer_id = self.active_buffer();
2034 if let Some(state) = self
2035 .windows
2036 .get_mut(&self.active_window)
2037 .map(|w| &mut w.buffers)
2038 .expect("active window present")
2039 .get_mut(&__buffer_id)
2040 {
2041 state.buffer_settings.whitespace.toggle_all();
2042 let status = if state.buffer_settings.whitespace.any_visible() {
2043 t!("toggle.whitespace_indicators_shown")
2044 } else {
2045 t!("toggle.whitespace_indicators_hidden")
2046 };
2047 self.set_status_message(status.to_string());
2048 }
2049 }
2050 Action::ResetBufferSettings => self.reset_buffer_settings(),
2051 Action::FocusFileExplorer => self.focus_file_explorer(),
2052 Action::FocusEditor => self.active_window_mut().focus_editor(),
2053 Action::FileExplorerUp => self.file_explorer_navigate_up(),
2054 Action::FileExplorerDown => self.file_explorer_navigate_down(),
2055 Action::FileExplorerPageUp => self.file_explorer_page_up(),
2056 Action::FileExplorerPageDown => self.file_explorer_page_down(),
2057 Action::FileExplorerExpand => self.file_explorer_toggle_expand(),
2058 Action::FileExplorerCollapse => self.file_explorer_collapse(),
2059 Action::FileExplorerOpen => self.file_explorer_open_file()?,
2060 Action::FileExplorerRefresh => self.file_explorer_refresh(),
2061 Action::FileExplorerNewFile => self.file_explorer_new_file(),
2062 Action::FileExplorerNewDirectory => self.file_explorer_new_directory(),
2063 Action::FileExplorerDelete => self.file_explorer_delete(),
2064 Action::FileExplorerRename => self.file_explorer_rename(),
2065 Action::FileExplorerToggleHidden => self.file_explorer_toggle_hidden(),
2066 Action::FileExplorerToggleGitignored => self.file_explorer_toggle_gitignored(),
2067 Action::FileExplorerSearchClear => {
2068 self.active_window_mut().file_explorer_search_clear()
2069 }
2070 Action::FileExplorerSearchBackspace => {
2071 self.active_window_mut().file_explorer_search_pop_char()
2072 }
2073 Action::FileExplorerCopy => self.active_window_mut().file_explorer_copy(),
2074 Action::FileExplorerCut => self.active_window_mut().file_explorer_cut(),
2075 Action::FileExplorerPaste => self.file_explorer_paste(),
2076 Action::FileExplorerDuplicate => self.file_explorer_duplicate(),
2077 Action::FileExplorerCopyFullPath => self.file_explorer_copy_path(false),
2078 Action::FileExplorerCopyRelativePath => self.file_explorer_copy_path(true),
2079 Action::FileExplorerExtendSelectionUp => {
2080 self.active_window_mut().file_explorer_extend_selection_up()
2081 }
2082 Action::FileExplorerExtendSelectionDown => self
2083 .active_window_mut()
2084 .file_explorer_extend_selection_down(),
2085 Action::FileExplorerToggleSelect => {
2086 self.active_window_mut().file_explorer_toggle_select()
2087 }
2088 Action::FileExplorerSelectAll => self.active_window_mut().file_explorer_select_all(),
2089 Action::RemoveSecondaryCursors => {
2090 if let Some(events) = self
2092 .active_window_mut()
2093 .action_to_events(Action::RemoveSecondaryCursors)
2094 {
2095 let batch = Event::Batch {
2097 events: events.clone(),
2098 description: "Remove secondary cursors".to_string(),
2099 };
2100 self.active_event_log_mut().append(batch.clone());
2101 self.apply_event_to_active_buffer(&batch);
2102
2103 let active_split = self
2105 .windows
2106 .get(&self.active_window)
2107 .and_then(|w| w.buffers.splits())
2108 .map(|(mgr, _)| mgr)
2109 .expect("active window must have a populated split layout")
2110 .active_split();
2111 let active_buffer = self.active_buffer();
2112 self.active_window_mut()
2113 .ensure_cursor_visible_for_split(active_buffer, active_split);
2114 }
2115 }
2116
2117 Action::MenuActivate => {
2119 self.handle_menu_activate();
2120 }
2121 Action::MenuClose => {
2122 self.handle_menu_close();
2123 }
2124 Action::MenuLeft => {
2125 self.handle_menu_left();
2126 }
2127 Action::MenuRight => {
2128 self.handle_menu_right();
2129 }
2130 Action::MenuUp => {
2131 self.handle_menu_up();
2132 }
2133 Action::MenuDown => {
2134 self.handle_menu_down();
2135 }
2136 Action::MenuExecute => {
2137 if let Some(action) = self.handle_menu_execute() {
2138 return self.handle_action(action);
2139 }
2140 }
2141 Action::MenuOpen(menu_name) => {
2142 if self.config.editor.menu_bar_mnemonics {
2143 self.handle_menu_open(&menu_name);
2144 }
2145 }
2146
2147 Action::SwitchKeybindingMap(map_name) => {
2148 let is_builtin =
2150 matches!(map_name.as_str(), "default" | "emacs" | "vscode" | "macos");
2151 let is_user_defined = self.config.keybinding_maps.contains_key(&map_name);
2152
2153 if is_builtin || is_user_defined {
2154 self.config_mut().active_keybinding_map = map_name.clone().into();
2156
2157 *self.keybindings.write().unwrap() =
2159 crate::input::keybindings::KeybindingResolver::new(&self.config);
2160
2161 self.set_status_message(
2162 t!("view.keybindings_switched", map = map_name).to_string(),
2163 );
2164 } else {
2165 self.set_status_message(
2166 t!("view.keybindings_unknown", map = map_name).to_string(),
2167 );
2168 }
2169 }
2170
2171 Action::SmartHome => {
2172 let buffer_id = self.active_buffer();
2174 if self.active_window().is_composite_buffer(buffer_id) {
2175 if let Some(_handled) =
2176 self.handle_composite_action(buffer_id, &Action::SmartHome)
2177 {
2178 return Ok(());
2179 }
2180 }
2181 self.smart_home();
2182 }
2183 Action::ToggleComment => {
2184 self.toggle_comment();
2185 }
2186 Action::ToggleFold => {
2187 self.active_window_mut().toggle_fold_at_cursor();
2188 }
2189 Action::GoToMatchingBracket => {
2190 self.goto_matching_bracket();
2191 }
2192 Action::JumpToNextError => {
2193 self.jump_to_next_error();
2194 }
2195 Action::JumpToPreviousError => {
2196 self.jump_to_previous_error();
2197 }
2198 Action::SetBookmark(key) => {
2199 self.active_window_mut().set_bookmark(key);
2200 }
2201 Action::JumpToBookmark(key) => {
2202 self.jump_to_bookmark(key);
2203 }
2204 Action::ClearBookmark(key) => {
2205 self.active_window_mut().clear_bookmark(key);
2206 }
2207 Action::ListBookmarks => {
2208 self.active_window_mut().list_bookmarks();
2209 }
2210 Action::ToggleSearchCaseSensitive => {
2211 self.active_window_mut().search_case_sensitive =
2212 !self.active_window().search_case_sensitive;
2213 let state = if self.active_window().search_case_sensitive {
2214 "enabled"
2215 } else {
2216 "disabled"
2217 };
2218 self.set_status_message(
2219 t!("search.case_sensitive_state", state = state).to_string(),
2220 );
2221 if let Some(prompt) = &self.active_window_mut().prompt {
2224 if matches!(
2225 prompt.prompt_type,
2226 PromptType::Search
2227 | PromptType::ReplaceSearch
2228 | PromptType::QueryReplaceSearch
2229 ) {
2230 let query = prompt.input.clone();
2231 self.update_search_highlights(&query);
2232 }
2233 } else if let Some(search_state) = &self.active_window().search_state {
2234 let query = search_state.query.clone();
2235 self.perform_search(&query);
2236 }
2237 }
2238 Action::ToggleSearchWholeWord => {
2239 self.active_window_mut().search_whole_word =
2240 !self.active_window().search_whole_word;
2241 let state = if self.active_window().search_whole_word {
2242 "enabled"
2243 } else {
2244 "disabled"
2245 };
2246 self.set_status_message(t!("search.whole_word_state", state = state).to_string());
2247 if let Some(prompt) = &self.active_window_mut().prompt {
2250 if matches!(
2251 prompt.prompt_type,
2252 PromptType::Search
2253 | PromptType::ReplaceSearch
2254 | PromptType::QueryReplaceSearch
2255 ) {
2256 let query = prompt.input.clone();
2257 self.update_search_highlights(&query);
2258 }
2259 } else if let Some(search_state) = &self.active_window().search_state {
2260 let query = search_state.query.clone();
2261 self.perform_search(&query);
2262 }
2263 }
2264 Action::ToggleSearchRegex => {
2265 self.active_window_mut().search_use_regex = !self.active_window().search_use_regex;
2266 let state = if self.active_window().search_use_regex {
2267 "enabled"
2268 } else {
2269 "disabled"
2270 };
2271 self.set_status_message(t!("search.regex_state", state = state).to_string());
2272 if let Some(prompt) = &self.active_window_mut().prompt {
2275 if matches!(
2276 prompt.prompt_type,
2277 PromptType::Search
2278 | PromptType::ReplaceSearch
2279 | PromptType::QueryReplaceSearch
2280 ) {
2281 let query = prompt.input.clone();
2282 self.update_search_highlights(&query);
2283 }
2284 } else if let Some(search_state) = &self.active_window().search_state {
2285 let query = search_state.query.clone();
2286 self.perform_search(&query);
2287 }
2288 }
2289 Action::ToggleSearchConfirmEach => {
2290 self.active_window_mut().search_confirm_each =
2291 !self.active_window().search_confirm_each;
2292 let state = if self.active_window().search_confirm_each {
2293 "enabled"
2294 } else {
2295 "disabled"
2296 };
2297 self.set_status_message(t!("search.confirm_each_state", state = state).to_string());
2298 }
2299 Action::FileBrowserToggleHidden => {
2300 self.file_open_toggle_hidden();
2302 }
2303 Action::StartMacroRecording => {
2304 self.set_status_message(
2306 "Use Ctrl+Shift+R to start recording (will prompt for register)".to_string(),
2307 );
2308 }
2309 Action::StopMacroRecording => {
2310 self.stop_macro_recording();
2311 }
2312 Action::PlayMacro(key) => {
2313 self.play_macro(key);
2314 }
2315 Action::ToggleMacroRecording(key) => {
2316 self.toggle_macro_recording(key);
2317 }
2318 Action::ShowMacro(key) => {
2319 self.show_macro_in_buffer(key);
2320 }
2321 Action::ListMacros => {
2322 self.list_macros_in_buffer();
2323 }
2324 Action::PromptRecordMacro => {
2325 self.start_prompt("Record macro (0-9): ".to_string(), PromptType::RecordMacro);
2326 }
2327 Action::PromptPlayMacro => {
2328 self.start_prompt("Play macro (0-9): ".to_string(), PromptType::PlayMacro);
2329 }
2330 Action::PlayLastMacro => {
2331 if let Some(key) = self.active_window_mut().macros.last_register() {
2332 self.play_macro(key);
2333 } else {
2334 self.set_status_message(t!("status.no_macro_recorded").to_string());
2335 }
2336 }
2337 Action::PromptSetBookmark => {
2338 self.start_prompt("Set bookmark (0-9): ".to_string(), PromptType::SetBookmark);
2339 }
2340 Action::PromptJumpToBookmark => {
2341 self.start_prompt(
2342 "Jump to bookmark (0-9): ".to_string(),
2343 PromptType::JumpToBookmark,
2344 );
2345 }
2346 Action::CompositeNextHunk => {
2347 let buf = self.active_buffer();
2348 self.active_window_mut().composite_next_hunk_active(buf);
2349 }
2350 Action::CompositePrevHunk => {
2351 let buf = self.active_buffer();
2352 self.active_window_mut().composite_prev_hunk_active(buf);
2353 }
2354 Action::None => {}
2355 Action::DeleteBackward => {
2356 if self.active_window().is_editing_disabled() {
2357 self.set_status_message(t!("buffer.editing_disabled").to_string());
2358 return Ok(());
2359 }
2360 if let Some(events) = self
2362 .active_window_mut()
2363 .action_to_events(Action::DeleteBackward)
2364 {
2365 if events.len() > 1 {
2366 let description = "Delete backward".to_string();
2368 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description)
2369 {
2370 self.active_event_log_mut().append(bulk_edit);
2371 }
2372 } else {
2373 for event in events {
2374 self.active_event_log_mut().append(event.clone());
2375 self.apply_event_to_active_buffer(&event);
2376 }
2377 }
2378 }
2379 }
2380 Action::PluginAction(action_name) => {
2381 tracing::debug!("handle_action: PluginAction('{}')", action_name);
2382 #[cfg(feature = "plugins")]
2385 {
2386 let result = self
2387 .plugin_manager
2388 .read()
2389 .unwrap()
2390 .execute_action_async(&action_name);
2391 if let Some(result) = result {
2392 match result {
2393 Ok(receiver) => {
2394 self.pending_plugin_actions
2396 .push((action_name.clone(), receiver));
2397 }
2398 Err(e) => {
2399 self.set_status_message(
2400 t!("view.plugin_error", error = e.to_string()).to_string(),
2401 );
2402 tracing::error!("Plugin action error: {}", e);
2403 }
2404 }
2405 } else {
2406 self.set_status_message(
2407 t!("status.plugin_manager_unavailable").to_string(),
2408 );
2409 }
2410 }
2411 #[cfg(not(feature = "plugins"))]
2412 {
2413 let _ = action_name;
2414 self.set_status_message(
2415 "Plugins not available (compiled without plugin support)".to_string(),
2416 );
2417 }
2418 }
2419 Action::LoadPluginFromBuffer => {
2420 #[cfg(feature = "plugins")]
2421 {
2422 let buffer_id = self.active_buffer();
2423 let state = self.active_state();
2424 let buffer = &state.buffer;
2425 let total = buffer.total_bytes();
2426 let content =
2427 String::from_utf8_lossy(&buffer.slice_bytes(0..total)).to_string();
2428
2429 let is_ts = buffer
2431 .file_path()
2432 .and_then(|p| p.extension())
2433 .and_then(|e| e.to_str())
2434 .map(|e| e == "ts" || e == "tsx")
2435 .unwrap_or(true);
2436
2437 let name = buffer
2439 .file_path()
2440 .and_then(|p| p.file_name())
2441 .and_then(|s| s.to_str())
2442 .map(|s| s.to_string())
2443 .unwrap_or_else(|| "buffer-plugin".to_string());
2444
2445 let load_result = self
2446 .plugin_manager
2447 .read()
2448 .unwrap()
2449 .load_plugin_from_source(&content, &name, is_ts);
2450 match load_result {
2451 Ok(()) => {
2452 self.set_status_message(format!(
2453 "Plugin '{}' loaded from buffer",
2454 name
2455 ));
2456 }
2457 Err(e) => {
2458 self.set_status_message(format!("Failed to load plugin: {}", e));
2459 tracing::error!("LoadPluginFromBuffer error: {}", e);
2460 }
2461 }
2462
2463 self.setup_plugin_dev_lsp(buffer_id, &content);
2465 }
2466 #[cfg(not(feature = "plugins"))]
2467 {
2468 self.set_status_message(
2469 "Plugins not available (compiled without plugin support)".to_string(),
2470 );
2471 }
2472 }
2473 Action::InitReload => {
2474 self.load_init_script(true);
2479 self.fire_plugins_loaded_hook();
2482 }
2483 Action::InitEdit => {
2484 let config_dir = self.dir_context.config_dir.clone();
2487 match crate::init_script::ensure_starter(&config_dir) {
2488 Ok(path) => {
2489 let declarations =
2499 self.plugin_manager.read().unwrap().plugin_declarations();
2500 crate::init_script::write_plugin_declarations(&config_dir, &declarations);
2501 match self.open_file(&path) {
2502 Ok(_) => {
2503 self.set_status_message(format!("init.ts: {}", path.display()));
2504 }
2505 Err(e) => {
2506 self.set_status_message(format!("init.ts: open failed: {e}"));
2507 }
2508 }
2509 }
2510 Err(e) => {
2511 self.set_status_message(format!("init.ts: create failed: {e}"));
2512 }
2513 }
2514 }
2515 Action::InitCheck => {
2516 let report = crate::init_script::check(&self.dir_context.config_dir);
2519 if report.ok && report.diagnostics.is_empty() {
2520 self.set_status_message("init.ts: ok".into());
2521 } else if !report.ok {
2522 let first = report
2523 .diagnostics
2524 .first()
2525 .map(|d| format!("{}:{}: {}", d.line, d.column, d.message))
2526 .unwrap_or_else(|| "unknown error".into());
2527 self.set_status_message(format!(
2528 "init.ts: {} error(s) — first: {first}",
2529 report.diagnostics.len()
2530 ));
2531 } else {
2532 self.set_status_message(format!(
2533 "init.ts: {} warning(s)",
2534 report.diagnostics.len()
2535 ));
2536 }
2537 }
2538 Action::OpenTerminal => {
2539 self.open_terminal();
2540 }
2541 Action::CloseTerminal => {
2542 self.close_terminal();
2543 }
2544 Action::FocusTerminal => {
2545 if self
2547 .active_window()
2548 .is_terminal_buffer(self.active_buffer())
2549 {
2550 self.active_window_mut().terminal_mode = true;
2551 self.active_window_mut().key_context = KeyContext::Terminal;
2552 self.set_status_message(t!("status.terminal_mode_enabled").to_string());
2553 }
2554 }
2555 Action::TerminalEscape => {
2556 if self.active_window().terminal_mode {
2558 self.active_window_mut().terminal_mode = false;
2559 self.active_window_mut().key_context = KeyContext::Normal;
2560 self.set_status_message(t!("status.terminal_mode_disabled").to_string());
2561 }
2562 }
2563 Action::ToggleKeyboardCapture => {
2564 if self.active_window().terminal_mode {
2566 self.active_window_mut().keyboard_capture =
2567 !self.active_window_mut().keyboard_capture;
2568 if self.active_window_mut().keyboard_capture {
2569 self.set_status_message(
2570 "Keyboard capture ON - all keys go to terminal (F9 to toggle)"
2571 .to_string(),
2572 );
2573 } else {
2574 self.set_status_message(
2575 "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
2576 );
2577 }
2578 }
2579 }
2580 Action::TerminalPaste => {
2581 if self.active_window().terminal_mode {
2583 if let Some(text) = self.clipboard.paste() {
2584 self.active_window_mut()
2585 .send_terminal_input(text.as_bytes());
2586 }
2587 }
2588 }
2589 Action::ShellCommand => {
2590 self.start_shell_command_prompt(false);
2592 }
2593 Action::ShellCommandReplace => {
2594 self.start_shell_command_prompt(true);
2596 }
2597 Action::OpenSettings => {
2598 self.open_settings();
2599 }
2600 Action::CloseSettings => {
2601 let has_changes = self
2603 .settings_state
2604 .as_ref()
2605 .is_some_and(|s| s.has_changes());
2606 if has_changes {
2607 if let Some(ref mut state) = self.settings_state {
2609 state.show_confirm_dialog();
2610 }
2611 } else {
2612 self.close_settings(false);
2613 }
2614 }
2615 Action::SettingsSave => {
2616 self.save_settings();
2617 }
2618 Action::SettingsReset => {
2619 if let Some(ref mut state) = self.settings_state {
2620 state.reset_current_to_default();
2621 }
2622 }
2623 Action::SettingsInherit => {
2624 if let Some(ref mut state) = self.settings_state {
2625 state.set_current_to_null();
2626 }
2627 }
2628 Action::SettingsToggleFocus => {
2629 if let Some(ref mut state) = self.settings_state {
2630 state.toggle_focus();
2631 }
2632 }
2633 Action::SettingsActivate => {
2634 self.settings_activate_current();
2635 }
2636 Action::SettingsSearch => {
2637 if let Some(ref mut state) = self.settings_state {
2638 state.start_search();
2639 }
2640 }
2641 Action::SettingsHelp => {
2642 if let Some(ref mut state) = self.settings_state {
2643 state.toggle_help();
2644 }
2645 }
2646 Action::SettingsIncrement => {
2647 self.settings_increment_current();
2648 }
2649 Action::SettingsDecrement => {
2650 self.settings_decrement_current();
2651 }
2652 Action::CalibrateInput => {
2653 self.open_calibration_wizard();
2654 }
2655 Action::EventDebug => {
2656 self.active_window_mut().open_event_debug();
2657 }
2658 Action::SuspendProcess => {
2659 self.request_suspend();
2660 }
2661 Action::OpenKeybindingEditor => {
2662 self.open_keybinding_editor();
2663 }
2664 Action::PromptConfirm => {
2665 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
2666 use super::prompt_actions::PromptResult;
2667 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
2668 PromptResult::ExecuteAction(action) => {
2669 return self.handle_action(action);
2670 }
2671 PromptResult::EarlyReturn => {
2672 return Ok(());
2673 }
2674 PromptResult::Done => {}
2675 }
2676 }
2677 }
2678 Action::PromptConfirmWithText(ref text) => {
2679 if let Some(ref mut prompt) = self.active_window_mut().prompt {
2681 prompt.set_input(text.clone());
2682 self.update_prompt_suggestions();
2683 }
2684 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
2685 use super::prompt_actions::PromptResult;
2686 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
2687 PromptResult::ExecuteAction(action) => {
2688 return self.handle_action(action);
2689 }
2690 PromptResult::EarlyReturn => {
2691 return Ok(());
2692 }
2693 PromptResult::Done => {}
2694 }
2695 }
2696 }
2697 Action::PopupConfirm => {
2698 use super::popup_actions::PopupConfirmResult;
2699 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
2700 return Ok(());
2701 }
2702 }
2703 Action::PopupCancel => {
2704 self.handle_popup_cancel();
2705 }
2706 Action::PopupFocus => {
2707 self.handle_popup_focus();
2708 }
2709 Action::CompletionAccept => {
2710 use super::popup_actions::PopupConfirmResult;
2711 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
2712 return Ok(());
2713 }
2714 }
2715 Action::CompletionDismiss => {
2716 self.handle_popup_cancel();
2717 }
2718 Action::InsertChar(c) => {
2719 if self.is_prompting() {
2720 return self.handle_insert_char_prompt(c);
2721 } else if self.active_window_mut().key_context == KeyContext::FileExplorer {
2722 self.active_window_mut().file_explorer_search_push_char(c);
2723 } else {
2724 self.handle_insert_char_editor(c)?;
2725 }
2726 }
2727 Action::PromptCopy => {
2729 if let Some(prompt) = &self.active_window_mut().prompt {
2730 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
2731 if !text.is_empty() {
2732 self.clipboard.copy(text);
2733 self.set_status_message(t!("clipboard.copied").to_string());
2734 }
2735 }
2736 }
2737 Action::PromptCut => {
2738 if let Some(prompt) = &self.active_window_mut().prompt {
2739 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
2740 if !text.is_empty() {
2741 self.clipboard.copy(text);
2742 }
2743 }
2744 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
2745 if prompt.has_selection() {
2746 prompt.delete_selection();
2747 } else {
2748 prompt.clear();
2749 }
2750 }
2751 self.set_status_message(t!("clipboard.cut").to_string());
2752 self.update_prompt_suggestions();
2753 }
2754 Action::PromptPaste => {
2755 if let Some(text) = self.clipboard.paste() {
2756 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
2757 prompt.insert_str(&text);
2758 }
2759 self.update_prompt_suggestions();
2760 }
2761 }
2762 _ => {
2763 self.apply_action_as_events(action)?;
2769 }
2770 }
2771
2772 Ok(())
2773 }
2774
2775 fn dispatch_floating_widget_key(
2786 &mut self,
2787 code: crossterm::event::KeyCode,
2788 modifiers: crossterm::event::KeyModifiers,
2789 ) -> bool {
2790 use crossterm::event::{KeyCode, KeyModifiers};
2791 let panel_id = match self.floating_widget_panel.as_ref() {
2792 Some(fwp) => fwp.panel_id,
2793 None => return false,
2794 };
2795 let key_name: Option<&str> = match code {
2796 KeyCode::Esc => {
2797 let mode_has_binding = self
2806 .active_window()
2807 .editor_mode
2808 .as_ref()
2809 .map(|mode_name| {
2810 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
2811 let mode_ctx =
2812 crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
2813 let keybindings = self.keybindings.read().unwrap();
2814 keybindings.has_explicit_binding(&key_event, &mode_ctx)
2815 })
2816 .unwrap_or(false);
2817 if mode_has_binding {
2818 return false;
2819 }
2820 let widget_key = self
2821 .widget_registry
2822 .get(panel_id)
2823 .map(|p| p.focus_key.clone())
2824 .unwrap_or_default();
2825 if self
2826 .plugin_manager
2827 .read()
2828 .unwrap()
2829 .has_hook_handlers("widget_event")
2830 {
2831 self.plugin_manager.read().unwrap().run_hook(
2832 "widget_event",
2833 crate::services::plugins::hooks::HookArgs::WidgetEvent {
2834 panel_id,
2835 widget_key,
2836 event_type: "cancel".to_string(),
2837 payload: serde_json::json!({}),
2838 },
2839 );
2840 }
2841 self.floating_widget_panel = None;
2842 let _ = self.widget_registry.unmount(panel_id);
2843 return true;
2844 }
2845 KeyCode::Tab => Some(if modifiers.contains(KeyModifiers::SHIFT) {
2846 "Shift+Tab"
2847 } else {
2848 "Tab"
2849 }),
2850 KeyCode::BackTab => Some("Shift+Tab"),
2851 KeyCode::Enter => Some("Enter"),
2852 KeyCode::Backspace => Some("Backspace"),
2853 KeyCode::Delete => Some("Delete"),
2854 KeyCode::Home => Some("Home"),
2855 KeyCode::End => Some("End"),
2856 KeyCode::Left => Some("Left"),
2857 KeyCode::Right => Some("Right"),
2858 KeyCode::Up => Some("Up"),
2859 KeyCode::Down => Some("Down"),
2860 KeyCode::PageUp => Some("PageUp"),
2861 KeyCode::PageDown => Some("PageDown"),
2862 _ => None,
2863 };
2864 if let Some(name) = key_name {
2865 let mode_has_binding = self
2884 .active_window()
2885 .editor_mode
2886 .as_ref()
2887 .map(|mode_name| {
2888 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
2889 let mode_ctx =
2890 crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
2891 let keybindings = self.keybindings.read().unwrap();
2892 keybindings.has_explicit_binding(&key_event, &mode_ctx)
2893 })
2894 .unwrap_or(false);
2895 if mode_has_binding {
2896 return false;
2897 }
2898 self.handle_widget_command(
2899 panel_id,
2900 fresh_core::api::WidgetAction::Key {
2901 key: name.to_string(),
2902 },
2903 );
2904 return true;
2905 }
2906 if let KeyCode::Char(c) = code {
2907 {
2918 let mode_has_binding = self
2919 .active_window()
2920 .editor_mode
2921 .as_ref()
2922 .map(|mode_name| {
2923 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
2924 let mode_ctx =
2925 crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
2926 let keybindings = self.keybindings.read().unwrap();
2927 keybindings.has_explicit_binding(&key_event, &mode_ctx)
2928 })
2929 .unwrap_or(false);
2930 if mode_has_binding {
2931 return false;
2932 }
2933 }
2934 if modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) {
2940 return true;
2941 }
2942 let ch = if modifiers.contains(KeyModifiers::SHIFT) {
2943 c.to_uppercase().next().unwrap_or(c)
2944 } else {
2945 c
2946 };
2947 if ch == ' ' {
2958 self.handle_widget_command(
2959 panel_id,
2960 fresh_core::api::WidgetAction::Key {
2961 key: "Space".to_string(),
2962 },
2963 );
2964 return true;
2965 }
2966 self.handle_widget_command(
2967 panel_id,
2968 fresh_core::api::WidgetAction::TextInputChar {
2969 text: ch.to_string(),
2970 },
2971 );
2972 return true;
2973 }
2974 true
2979 }
2980}