1use super::*;
2use anyhow::Result as AnyhowResult;
3use rust_i18n::t;
4
5fn key_event_to_payload(ev: &crossterm::event::KeyEvent) -> fresh_core::api::KeyEventPayload {
14 use crossterm::event::{KeyCode, KeyModifiers};
15 let key = match ev.code {
16 KeyCode::Char(c) => c.to_string(),
17 KeyCode::Esc => "escape".to_string(),
18 KeyCode::Enter => "enter".to_string(),
19 KeyCode::Tab => "tab".to_string(),
20 KeyCode::BackTab => "backtab".to_string(),
21 KeyCode::Backspace => "backspace".to_string(),
22 KeyCode::Delete => "delete".to_string(),
23 KeyCode::Left => "left".to_string(),
24 KeyCode::Right => "right".to_string(),
25 KeyCode::Up => "up".to_string(),
26 KeyCode::Down => "down".to_string(),
27 KeyCode::Home => "home".to_string(),
28 KeyCode::End => "end".to_string(),
29 KeyCode::PageUp => "pageup".to_string(),
30 KeyCode::PageDown => "pagedown".to_string(),
31 KeyCode::Insert => "insert".to_string(),
32 KeyCode::F(n) => format!("f{}", n),
33 _ => String::new(),
34 };
35 fresh_core::api::KeyEventPayload {
36 key,
37 ctrl: ev.modifiers.contains(KeyModifiers::CONTROL),
38 alt: ev.modifiers.contains(KeyModifiers::ALT),
39 shift: ev.modifiers.contains(KeyModifiers::SHIFT),
40 meta: ev.modifiers.contains(KeyModifiers::SUPER),
41 }
42}
43
44impl Editor {
45 fn try_resolve_next_key_callback(&mut self, key_event: &crossterm::event::KeyEvent) -> bool {
58 let payload = key_event_to_payload(key_event);
59 if let Some(callback_id) = self
60 .active_window_mut()
61 .pending_next_key_callbacks
62 .pop_front()
63 {
64 let json = serde_json::to_string(&payload).unwrap_or_else(|_| "null".to_string());
65 self.plugin_manager
66 .read()
67 .unwrap()
68 .resolve_callback(callback_id, json);
69 return true;
70 }
71 if self.active_window_mut().key_capture_active {
72 self.active_window_mut()
73 .pending_key_capture_buffer
74 .push_back(payload);
75 return true;
76 }
77 false
78 }
79}
80
81impl Editor {
82 pub(crate) fn popups_capture_keys(&self) -> bool {
101 use crate::input::keybindings::KeyContext;
102 use crate::view::popup::PopupResolver;
103 let trust_prompt_up = self
109 .global_popups
110 .top()
111 .is_some_and(|p| p.focused && matches!(p.resolver, PopupResolver::WorkspaceTrust));
112 if trust_prompt_up {
113 return true;
114 }
115 if matches!(self.active_window().key_context, KeyContext::FileExplorer) {
116 return false;
117 }
118 self.topmost_popup_focused()
119 }
120
121 pub(crate) fn topmost_popup_focused(&self) -> bool {
126 if let Some(popup) = self.global_popups.top() {
127 return popup.focused;
128 }
129 if let Some(popup) = self.active_state().popups.top() {
130 return popup.focused;
131 }
132 false
135 }
136
137 pub(crate) fn resolve_unfocused_popup_action(
147 &self,
148 event: &crossterm::event::KeyEvent,
149 ) -> Option<crate::input::keybindings::Action> {
150 use crate::input::keybindings::{Action, KeyContext};
151
152 let popup_visible =
153 self.global_popups.is_visible() || self.active_state().popups.is_visible();
154 if !popup_visible || self.topmost_popup_focused() {
155 return None;
156 }
157
158 let blocked_by_higher_modal = self
170 .overlay_layers()
171 .iter()
172 .take_while(|l| l.kind != crate::app::overlay::LayerKind::Popup)
173 .any(|l| l.owns_keyboard);
174 if blocked_by_higher_modal {
175 return None;
176 }
177
178 let kb = self.keybindings.read().ok()?;
179
180 let popup_focus_match = matches!(
189 kb.resolve_in_context_only(event, self.active_window().key_context.clone()),
190 Some(Action::PopupFocus),
191 );
192 if popup_focus_match {
193 return Some(Action::PopupFocus);
194 }
195
196 let resolved_popup = kb.resolve_in_context_only(event, KeyContext::Popup);
202 match resolved_popup {
203 Some(action @ (Action::PopupCancel | Action::PopupFocus)) => Some(action),
204 _ => None,
205 }
206 }
207
208 pub(crate) fn resolve_completion_popup_action(
214 &self,
215 event: &crossterm::event::KeyEvent,
216 ) -> Option<crate::input::keybindings::Action> {
217 use crate::input::keybindings::{Action, KeyContext};
218 use crate::view::popup::PopupKind;
219
220 let topmost_kind = if self.global_popups.is_visible() {
221 self.global_popups.top().map(|p| p.kind)
222 } else if self.active_state().popups.is_visible() {
223 self.active_state().popups.top().map(|p| p.kind)
224 } else {
225 None
226 };
227
228 if topmost_kind != Some(PopupKind::Completion) {
229 return None;
230 }
231
232 match self
233 .keybindings
234 .read()
235 .unwrap()
236 .resolve_in_context_only(event, KeyContext::Completion)
237 {
238 Some(action @ (Action::CompletionAccept | Action::CompletionDismiss)) => Some(action),
239 _ => None,
240 }
241 }
242
243 pub(crate) fn overlay_layers(&self) -> Vec<crate::app::overlay::Layer> {
254 use crate::app::overlay::{Layer, LayerKind};
255 use crate::input::keybindings::KeyContext;
256
257 let mut layers = Vec::new();
258
259 if self.active_window().is_event_debug_active() {
263 layers.push(Layer {
264 kind: LayerKind::EventDebug,
265 owns_keyboard: true,
266 key_context: None,
267 blocks_terminal_input: true,
268 });
269 }
270 if self.settings_state.as_ref().is_some_and(|s| s.visible) {
272 layers.push(Layer {
273 kind: LayerKind::Settings,
274 owns_keyboard: true,
275 key_context: Some(KeyContext::Settings),
276 blocks_terminal_input: true,
277 });
278 }
279 if self.keybinding_editor.is_some() {
285 layers.push(Layer {
286 kind: LayerKind::KeybindingEditor,
287 owns_keyboard: true,
288 key_context: None,
289 blocks_terminal_input: true,
290 });
291 }
292 if self.calibration_wizard.is_some() {
293 layers.push(Layer {
294 kind: LayerKind::CalibrationWizard,
295 owns_keyboard: true,
296 key_context: None,
297 blocks_terminal_input: true,
298 });
299 }
300 let trust_on_top = self.global_popups.top().is_some_and(|p| {
306 matches!(
307 p.resolver,
308 crate::view::popup::PopupResolver::WorkspaceTrust
309 )
310 });
311 if trust_on_top {
312 layers.push(Layer {
313 kind: LayerKind::WorkspaceTrust,
314 owns_keyboard: self.popups_capture_keys(),
315 key_context: Some(KeyContext::Popup),
316 blocks_terminal_input: true,
317 });
318 }
319 if self.menu_state.active_menu.is_some() {
320 layers.push(Layer {
321 kind: LayerKind::Menu,
322 owns_keyboard: true,
323 key_context: Some(KeyContext::Menu),
324 blocks_terminal_input: true,
325 });
326 }
327 if self.is_prompting() {
328 layers.push(Layer {
329 kind: LayerKind::Prompt,
330 owns_keyboard: true,
331 key_context: Some(KeyContext::Prompt),
332 blocks_terminal_input: true,
333 });
334 }
335 if !trust_on_top
340 && (self.global_popups.is_visible() || self.active_state().popups.is_visible())
341 {
342 layers.push(Layer {
343 kind: LayerKind::Popup,
344 owns_keyboard: self.popups_capture_keys(),
345 key_context: Some(KeyContext::Popup),
346 blocks_terminal_input: true,
347 });
348 }
349 if let Some(f) = self.floating_widget_panel.as_ref() {
356 layers.push(Layer {
357 kind: LayerKind::FloatingModal,
358 owns_keyboard: f.focused,
359 key_context: Some(KeyContext::Normal),
360 blocks_terminal_input: true,
361 });
362 }
363 if let Some(d) = self.dock.as_ref() {
368 layers.push(Layer {
369 kind: LayerKind::Dock,
370 owns_keyboard: d.focused,
371 key_context: Some(KeyContext::Dock),
372 blocks_terminal_input: d.focused,
373 });
374 }
375 let base_context = if self
377 .active_window()
378 .is_composite_buffer(self.active_buffer())
379 {
380 KeyContext::CompositeBuffer
381 } else {
382 self.active_window().key_context.clone()
383 };
384 layers.push(Layer {
385 kind: LayerKind::Editor,
386 owns_keyboard: true,
387 key_context: Some(base_context),
388 blocks_terminal_input: false,
389 });
390
391 layers
392 }
393
394 pub(crate) fn presents_blocking_overlay(&self) -> bool {
398 crate::app::overlay::any_layer_blocks_terminal_input(&self.overlay_layers())
399 }
400
401 pub fn get_key_context(&self) -> crate::input::keybindings::KeyContext {
406 crate::app::overlay::resolve_focus_context(&self.overlay_layers())
407 .expect("editor base layer always owns the keyboard")
408 }
409
410 pub fn handle_key(
413 &mut self,
414 code: crossterm::event::KeyCode,
415 modifiers: crossterm::event::KeyModifiers,
416 ) -> AnyhowResult<()> {
417 use crate::input::keybindings::Action;
418
419 let _t_total = std::time::Instant::now();
420
421 tracing::trace!(
422 "Editor.handle_key: code={:?}, modifiers={:?}",
423 code,
424 modifiers
425 );
426
427 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
429
430 if let Some(focused) = self.dock.as_ref().map(|d| d.focused) {
438 tracing::debug!(
439 target: "fresh::dock",
440 ?code,
441 dock_focused = focused,
442 key_context = ?self.active_window().key_context,
443 active_window = ?self.active_window_id(),
444 "handle_key: dock mounted (routing diagnostic)"
445 );
446 }
447
448 if self.active_window().is_event_debug_active() {
452 self.active_window_mut()
453 .handle_event_debug_input(&key_event);
454 return Ok(());
455 }
456
457 if self.dispatch_terminal_input(&key_event).is_some() {
462 return Ok(());
463 }
464
465 if self.try_resolve_next_key_callback(&key_event) {
472 return Ok(());
473 }
474
475 if self
484 .floating_widget_panel
485 .as_ref()
486 .is_some_and(|f| f.focused)
487 && self.dispatch_floating_widget_key(super::PanelSlot::Floating, code, modifiers)
488 {
489 return Ok(());
490 }
491 if self.dock.as_ref().is_some_and(|f| f.focused) {
499 let ctx = self.get_key_context();
500 let resolved = self
501 .keybindings
502 .read()
503 .ok()
504 .map(|kb| kb.resolve(&key_event, ctx));
505 if matches!(resolved, Some(Action::ToggleDockFocus)) {
506 self.handle_action(Action::ToggleDockFocus)?;
507 return Ok(());
508 }
509 }
510 if self.dock.as_ref().is_some_and(|f| f.focused)
511 && self.dispatch_floating_widget_key(super::PanelSlot::Dock, code, modifiers)
512 {
513 return Ok(());
514 }
515
516 let active_split = self.effective_active_split();
525 if let Some(view_state) = self
526 .windows
527 .get_mut(&self.active_window)
528 .and_then(|w| w.split_view_states_mut())
529 .expect("active window must have a populated split layout")
530 .get_mut(&active_split)
531 {
532 view_state.viewport.clear_skip_ensure_visible();
533 }
534
535 if self.active_window_mut().theme_info_popup.is_some() {
537 self.active_window_mut().theme_info_popup = None;
538 }
539
540 if self
541 .active_window_mut()
542 .file_explorer_context_menu
543 .is_some()
544 {
545 if let Some(result) = self.handle_file_explorer_context_menu_key(code, modifiers) {
546 return result;
547 }
548 }
549
550 let mut context = self.get_key_context();
552
553 let popup_visible_on_screen =
563 self.global_popups.is_visible() || self.active_state().popups.is_visible();
564 if popup_visible_on_screen {
565 let (is_transient_popup, has_selection) = {
569 let popup = self
570 .global_popups
571 .top()
572 .or_else(|| self.active_state().popups.top());
573 (
574 popup.is_some_and(|p| p.transient),
575 popup.is_some_and(|p| p.has_selection()),
576 )
577 };
578
579 let is_copy_key = key_event.code == crossterm::event::KeyCode::Char('c')
581 && key_event
582 .modifiers
583 .contains(crossterm::event::KeyModifiers::CONTROL);
584
585 let resolved_action = self
590 .keybindings
591 .read()
592 .ok()
593 .map(|kb| kb.resolve(&key_event, context.clone()));
594 let is_focus_popup_key = matches!(
595 resolved_action,
596 Some(crate::input::keybindings::Action::PopupFocus)
597 );
598
599 if is_transient_popup && !(has_selection && is_copy_key) && !is_focus_popup_key {
600 self.hide_popup();
602 tracing::debug!("Dismissed transient popup on key press");
603 context = self.get_key_context();
605 }
606 }
607
608 if let Some(action) = self.resolve_unfocused_popup_action(&key_event) {
614 self.handle_action(action)?;
615 return Ok(());
616 }
617
618 if self.dispatch_modal_input(&key_event).is_some() {
620 return Ok(());
621 }
622
623 if context != self.get_key_context() {
626 context = self.get_key_context();
627 }
628
629 let should_check_mode_bindings = matches!(
640 context,
641 crate::input::keybindings::KeyContext::Normal
642 | crate::input::keybindings::KeyContext::CompositeBuffer
643 );
644
645 if should_check_mode_bindings {
646 let effective_mode = self.effective_mode().map(|s| s.to_owned());
649
650 if let Some(ref mode_name) = effective_mode {
651 let mode_ctx = crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
652 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
653
654 let (chord_result, resolved_action) = {
656 let keybindings = self.keybindings.read().unwrap();
657 let chord_result = keybindings.resolve_chord(
658 &self.active_window().chord_state,
659 &key_event,
660 mode_ctx.clone(),
661 );
662 let resolved = keybindings.resolve(&key_event, mode_ctx);
663 (chord_result, resolved)
664 };
665 match chord_result {
666 crate::input::keybindings::ChordResolution::Complete(action) => {
667 tracing::debug!("Mode chord resolved to action: {:?}", action);
668 self.active_window_mut().chord_state.clear();
669 return self.handle_action(action);
670 }
671 crate::input::keybindings::ChordResolution::Partial => {
672 tracing::debug!("Potential chord prefix in mode '{}'", mode_name);
673 self.active_window_mut().chord_state.push((code, modifiers));
674 return Ok(());
675 }
676 crate::input::keybindings::ChordResolution::NoMatch => {
677 if !self.active_window_mut().chord_state.is_empty() {
678 tracing::debug!("Chord sequence abandoned in mode, clearing state");
679 self.active_window_mut().chord_state.clear();
680 }
681 }
682 }
683
684 if resolved_action != Action::None {
686 return self.handle_action(resolved_action);
687 }
688 }
689
690 if let Some(ref mode_name) = effective_mode {
702 if self.mode_registry.allows_text_input(mode_name) {
703 if let KeyCode::Char(c) = code {
704 let ch = if modifiers.contains(KeyModifiers::SHIFT) {
705 c.to_uppercase().next().unwrap_or(c)
706 } else {
707 c
708 };
709 if !modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) {
710 let action_name = format!("mode_text_input:{}", ch);
711 return self.handle_action(Action::PluginAction(action_name));
712 }
713 }
714 let normal_ctx = crate::input::keybindings::KeyContext::Normal;
723 let resolved = {
724 let keybindings = self.keybindings.read().unwrap();
725 keybindings.resolve(&key_event, normal_ctx)
726 };
727 match resolved {
728 Action::Paste | Action::Copy | Action::Cut | Action::SelectAll => {
729 return self.handle_action(resolved);
730 }
731 _ => {}
732 }
733 if modifiers.contains(KeyModifiers::SHIFT) {
742 let buffer_id = self.active_buffer();
743 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id)
744 {
745 let ctrl = modifiers.contains(KeyModifiers::CONTROL);
746 let handled = match code {
747 KeyCode::Left if ctrl => self
748 .with_focused_text_editor(panel_id, |e| {
749 e.move_word_left_selecting()
750 }),
751 KeyCode::Right if ctrl => self
752 .with_focused_text_editor(panel_id, |e| {
753 e.move_word_right_selecting()
754 }),
755 KeyCode::Left => self.with_focused_text_editor(panel_id, |e| {
756 e.move_left_selecting()
757 }),
758 KeyCode::Right => self.with_focused_text_editor(panel_id, |e| {
759 e.move_right_selecting()
760 }),
761 KeyCode::Up => self
762 .with_focused_text_editor(panel_id, |e| e.move_up_selecting()),
763 KeyCode::Down => self.with_focused_text_editor(panel_id, |e| {
764 e.move_down_selecting()
765 }),
766 KeyCode::Home => self.with_focused_text_editor(panel_id, |e| {
767 e.move_home_selecting()
768 }),
769 KeyCode::End => self
770 .with_focused_text_editor(panel_id, |e| e.move_end_selecting()),
771 _ => false,
772 };
773 if matches!(
779 code,
780 KeyCode::Left
781 | KeyCode::Right
782 | KeyCode::Up
783 | KeyCode::Down
784 | KeyCode::Home
785 | KeyCode::End
786 ) {
787 let _ = handled;
788 return Ok(());
789 }
790 }
791 }
792 tracing::debug!("Blocking unbound key in text-input mode '{}'", mode_name);
793 return Ok(());
794 }
795 }
796 if let Some(ref mode_name) = self.active_window().editor_mode {
797 if self.mode_registry.is_read_only(mode_name) {
798 tracing::debug!("Ignoring unbound key in read-only mode '{}'", mode_name);
799 return Ok(());
800 }
801 tracing::debug!(
802 "Mode '{}' is not read-only, allowing key through",
803 mode_name
804 );
805 }
806 }
807
808 {
815 let active_buf = self.active_buffer();
816 let active_split = self.effective_active_split();
817 if self.active_window().is_composite_buffer(active_buf) {
818 if let Some(handled) =
819 self.try_route_composite_key(active_split, active_buf, &key_event)
820 {
821 return handled;
822 }
823 }
824 }
825
826 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
828 let (chord_result, action) = {
829 let keybindings = self.keybindings.read().unwrap();
830 let chord_result = keybindings.resolve_chord(
831 &self.active_window().chord_state,
832 &key_event,
833 context.clone(),
834 );
835 let action = keybindings.resolve(&key_event, context.clone());
836 (chord_result, action)
837 };
838
839 match chord_result {
840 crate::input::keybindings::ChordResolution::Complete(action) => {
841 tracing::debug!("Complete chord match -> Action: {:?}", action);
843 self.active_window_mut().chord_state.clear();
844 return self.handle_action(action);
845 }
846 crate::input::keybindings::ChordResolution::Partial => {
847 tracing::debug!("Partial chord match - waiting for next key");
849 self.active_window_mut().chord_state.push((code, modifiers));
850 return Ok(());
851 }
852 crate::input::keybindings::ChordResolution::NoMatch => {
853 if !self.active_window_mut().chord_state.is_empty() {
855 tracing::debug!("Chord sequence abandoned, clearing state");
856 self.active_window_mut().chord_state.clear();
857 }
858 }
859 }
860
861 tracing::trace!("Context: {:?} -> Action: {:?}", context, action);
863
864 match action {
867 Action::LspCompletion
868 | Action::LspGotoDefinition
869 | Action::LspReferences
870 | Action::LspHover
871 | Action::None => {
872 }
874 _ => {
875 self.active_window_mut().cancel_pending_lsp_requests();
877 }
878 }
879
880 self.handle_action(action)
884 }
885
886 pub(crate) fn set_workspace_trust_level(
894 &mut self,
895 level: crate::services::workspace_trust::TrustLevel,
896 ) {
897 use crate::services::workspace_trust::TrustLevel;
898 let trust = &self.authority().workspace_trust;
911 trust.set_level(level);
912 let msg = match level {
913 TrustLevel::Trusted => t!("trust.now_trusted"),
914 TrustLevel::Restricted => t!("trust.now_restricted"),
915 TrustLevel::Blocked => t!("trust.now_blocked"),
916 }
917 .to_string();
918 self.active_window_mut().status_message = Some(msg);
919 }
920
921 pub(crate) fn handle_action(&mut self, action: Action) -> AnyhowResult<()> {
922 use crate::input::keybindings::Action;
923
924 self.record_macro_action(&action);
926
927 if !matches!(action, Action::DabbrevExpand) {
929 self.reset_dabbrev_state();
930 }
931
932 match action {
933 Action::Quit => self.quit(),
934 Action::ForceQuit => {
935 self.should_quit = true;
936 }
937 Action::Detach => {
938 self.should_detach = true;
939 }
940 Action::WorkspaceTrustTrust => {
941 self.set_workspace_trust_level(
942 crate::services::workspace_trust::TrustLevel::Trusted,
943 );
944 }
945 Action::WorkspaceTrustRestrict => {
946 self.set_workspace_trust_level(
947 crate::services::workspace_trust::TrustLevel::Restricted,
948 );
949 }
950 Action::WorkspaceTrustBlock => {
951 self.set_workspace_trust_level(
952 crate::services::workspace_trust::TrustLevel::Blocked,
953 );
954 }
955 Action::WorkspaceTrustPrompt => {
956 self.show_workspace_trust_popup(true);
958 }
959 Action::Save => {
960 if self.active_state().buffer.file_path().is_none() {
962 self.start_prompt_with_initial_text(
963 t!("file.save_as_prompt").to_string(),
964 PromptType::SaveFileAs,
965 String::new(),
966 );
967 self.init_file_open_state();
968 } else if self.check_save_conflict().is_some() {
969 self.start_prompt(
971 t!("file.file_changed_prompt").to_string(),
972 PromptType::ConfirmSaveConflict,
973 );
974 } else if let Err(e) = self.save() {
975 let msg = format!("{}", e);
976 self.active_window_mut().status_message =
977 Some(t!("file.save_failed", error = &msg).to_string());
978 }
979 }
980 Action::SaveAs => {
981 let current_path = self
983 .active_state()
984 .buffer
985 .file_path()
986 .map(|p| {
987 p.strip_prefix(self.working_dir())
989 .unwrap_or(p)
990 .to_string_lossy()
991 .to_string()
992 })
993 .unwrap_or_default();
994 self.start_prompt_with_initial_text(
995 t!("file.save_as_prompt").to_string(),
996 PromptType::SaveFileAs,
997 current_path,
998 );
999 self.init_file_open_state();
1000 }
1001 Action::Open => {
1002 self.start_prompt(t!("file.open_prompt").to_string(), PromptType::OpenFile);
1003 self.prefill_open_file_prompt();
1004 self.init_file_open_state();
1005 }
1006 Action::SwitchProject => {
1007 self.start_prompt(
1008 t!("file.switch_project_prompt").to_string(),
1009 PromptType::SwitchProject,
1010 );
1011 self.init_folder_open_state();
1012 }
1013 Action::GotoLine => {
1014 let has_line_index = self
1015 .buffers()
1016 .get(&self.active_buffer())
1017 .is_none_or(|s| s.buffer.line_count().is_some());
1018 if has_line_index {
1019 self.start_prompt(
1020 t!("file.goto_line_prompt").to_string(),
1021 PromptType::GotoLine,
1022 );
1023 } else {
1024 self.start_prompt(
1025 t!("goto.scan_confirm_prompt", yes = "y", no = "N").to_string(),
1026 PromptType::GotoLineScanConfirm,
1027 );
1028 }
1029 }
1030 Action::ScanLineIndex => {
1031 self.start_incremental_line_scan(false);
1032 }
1033 Action::New => {
1034 self.new_buffer();
1035 }
1036 Action::Close | Action::CloseTab => {
1037 self.close_tab();
1042 }
1043 Action::Revert => {
1044 if self.active_state().buffer.is_modified() {
1046 let revert_key = t!("prompt.key.revert").to_string();
1047 let cancel_key = t!("prompt.key.cancel").to_string();
1048 self.start_prompt(
1049 t!(
1050 "prompt.revert_confirm",
1051 revert_key = revert_key,
1052 cancel_key = cancel_key
1053 )
1054 .to_string(),
1055 PromptType::ConfirmRevert,
1056 );
1057 } else {
1058 if let Err(e) = self.revert_file() {
1060 self.set_status_message(
1061 t!("error.failed_to_revert", error = e.to_string()).to_string(),
1062 );
1063 }
1064 }
1065 }
1066 Action::ToggleAutoRevert => {
1067 self.toggle_auto_revert();
1068 }
1069 Action::FormatBuffer => {
1070 if let Err(e) = self.format_buffer() {
1071 self.set_status_message(
1072 t!("error.format_failed", error = e.to_string()).to_string(),
1073 );
1074 }
1075 }
1076 Action::TrimTrailingWhitespace => match self.trim_trailing_whitespace() {
1077 Ok(true) => {
1078 self.set_status_message(t!("whitespace.trimmed").to_string());
1079 }
1080 Ok(false) => {
1081 self.set_status_message(t!("whitespace.no_trailing").to_string());
1082 }
1083 Err(e) => {
1084 self.set_status_message(
1085 t!("error.trim_whitespace_failed", error = e).to_string(),
1086 );
1087 }
1088 },
1089 Action::EnsureFinalNewline => match self.ensure_final_newline() {
1090 Ok(true) => {
1091 self.set_status_message(t!("whitespace.newline_added").to_string());
1092 }
1093 Ok(false) => {
1094 self.set_status_message(t!("whitespace.already_has_newline").to_string());
1095 }
1096 Err(e) => {
1097 self.set_status_message(
1098 t!("error.ensure_newline_failed", error = e).to_string(),
1099 );
1100 }
1101 },
1102 Action::Copy => {
1103 let popup = self
1105 .global_popups
1106 .top()
1107 .or_else(|| self.active_state().popups.top());
1108 if let Some(popup) = popup {
1109 if popup.has_selection() {
1110 if let Some(text) = popup.get_selected_text() {
1111 self.clipboard.copy(text);
1112 self.set_status_message(t!("clipboard.copied").to_string());
1113 return Ok(());
1114 }
1115 }
1116 }
1117 if self.active_window_mut().key_context
1118 == crate::input::keybindings::KeyContext::FileExplorer
1119 {
1120 self.active_window_mut().file_explorer_copy();
1121 return Ok(());
1122 }
1123 let buffer_id = self.active_buffer();
1130 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
1131 if self.handle_widget_copy(panel_id) {
1132 self.set_status_message(t!("clipboard.copied").to_string());
1133 return Ok(());
1134 }
1135 }
1136 if self.active_window().is_composite_buffer(buffer_id) {
1138 if let Some(_handled) = self.handle_composite_action(buffer_id, &Action::Copy) {
1139 return Ok(());
1140 }
1141 }
1142 self.copy_selection()
1143 }
1144 Action::CopyWithTheme(theme) => self.copy_selection_with_theme(&theme),
1145 Action::CopyFilePath => self.copy_active_buffer_path(false),
1146 Action::CopyRelativeFilePath => self.copy_active_buffer_path(true),
1147 Action::Cut => {
1148 if self.active_window_mut().key_context
1149 == crate::input::keybindings::KeyContext::FileExplorer
1150 {
1151 self.active_window_mut().file_explorer_cut();
1152 return Ok(());
1153 }
1154 let buffer_id = self.active_buffer();
1158 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
1159 if self.handle_widget_cut(panel_id) {
1160 return Ok(());
1161 }
1162 }
1163 if self.active_window().is_editing_disabled() {
1164 self.set_status_message(t!("buffer.editing_disabled").to_string());
1165 return Ok(());
1166 }
1167 self.cut_selection()
1168 }
1169 Action::Paste => {
1170 if self.active_window_mut().key_context
1171 == crate::input::keybindings::KeyContext::FileExplorer
1172 {
1173 self.file_explorer_paste();
1174 return Ok(());
1175 }
1176 let buffer_id = self.active_buffer();
1182 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
1183 if let Some(text) = self.clipboard.paste() {
1184 let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
1185 self.handle_widget_insert_str(panel_id, &normalized);
1186 self.set_status_message(t!("clipboard.pasted").to_string());
1187 }
1188 return Ok(());
1189 }
1190 if self.active_window().is_editing_disabled() {
1191 self.set_status_message(t!("buffer.editing_disabled").to_string());
1192 return Ok(());
1193 }
1194 self.paste()
1195 }
1196 Action::SelectAll => {
1197 let buffer_id = self.active_buffer();
1202 if let Some(panel_id) = self.focused_text_widget_panel_for_buffer(buffer_id) {
1203 self.handle_widget_select_all(panel_id);
1204 return Ok(());
1205 }
1206 self.apply_action_as_events(Action::SelectAll)?;
1207 }
1208 Action::YankWordForward => self.yank_word_forward(),
1209 Action::YankWordBackward => self.yank_word_backward(),
1210 Action::YankToLineEnd => self.yank_to_line_end(),
1211 Action::YankToLineStart => self.yank_to_line_start(),
1212 Action::YankViWordEnd => self.yank_vi_word_end(),
1213 Action::Undo => {
1214 self.handle_undo();
1215 }
1216 Action::Redo => {
1217 self.handle_redo();
1218 }
1219 Action::ShowHelp => {
1220 self.ensure_help_panel_mode_registered();
1221 self.active_window_mut().open_help_manual();
1222 }
1223 Action::ShowKeyboardShortcuts => {
1224 self.ensure_help_panel_mode_registered();
1225 self.active_window_mut().open_keyboard_shortcuts();
1226 }
1227 Action::ShowWarnings => {
1228 self.show_warnings_popup();
1229 }
1230 Action::ShowStatusLog => {
1231 self.open_status_log();
1232 }
1233 Action::ShowLspStatus => {
1234 self.show_lsp_status_popup();
1235 }
1236 Action::ShowRemoteIndicatorMenu => {
1237 self.show_remote_indicator_popup();
1238 }
1239 Action::ClearWarnings => {
1240 self.active_window_mut().clear_warnings();
1241 }
1242 Action::CommandPalette => {
1243 if self.close_quick_open_if_open() {
1246 return Ok(());
1247 }
1248 self.start_quick_open();
1249 }
1250 Action::QuickOpen => {
1251 if self.close_quick_open_if_open() {
1252 return Ok(());
1253 }
1254 self.start_quick_open();
1255 }
1256 Action::QuickOpenBuffers => {
1257 if self.close_quick_open_if_open() {
1258 return Ok(());
1259 }
1260 self.start_quick_open_with_prefix("#");
1261 }
1262 Action::QuickOpenFiles => {
1263 if self.close_quick_open_if_open() {
1264 return Ok(());
1265 }
1266 self.start_quick_open_with_prefix("");
1267 }
1268 Action::OpenLiveGrep => {
1269 self.handle_action(Action::PluginAction("start_live_grep".to_string()))?;
1270 }
1271 Action::ResumeLiveGrep => {
1272 self.handle_action(Action::PluginAction("resume_live_grep".to_string()))?;
1273 }
1274 Action::ToggleUtilityDock => {
1275 use crate::view::split::SplitRole;
1276 if let Some(dock_leaf) = self
1277 .windows
1278 .get(&self.active_window)
1279 .and_then(|w| w.buffers.splits())
1280 .map(|(mgr, _)| mgr)
1281 .expect("active window must have a populated split layout")
1282 .find_leaf_by_role(SplitRole::UtilityDock)
1283 {
1284 let active = self
1285 .windows
1286 .get(&self.active_window)
1287 .and_then(|w| w.buffers.splits())
1288 .map(|(mgr, _)| mgr)
1289 .expect("active window must have a populated split layout")
1290 .active_split();
1291 if active == dock_leaf {
1292 self.next_split();
1297 } else {
1298 self.windows
1299 .get_mut(&self.active_window)
1300 .and_then(|w| w.split_manager_mut())
1301 .expect("active window must have a populated split layout")
1302 .set_active_split(dock_leaf);
1303 }
1304 } else {
1305 self.set_status_message(
1306 "No Utility Dock open — invoke a dock-aware utility (Diagnostics, Search/Replace, …)"
1307 .to_string(),
1308 );
1309 }
1310 }
1311 Action::CycleLiveGrepProvider => {
1312 let in_live_grep = self
1316 .active_window()
1317 .prompt
1318 .as_ref()
1319 .map(|p| match &p.prompt_type {
1320 PromptType::LiveGrep => true,
1321 PromptType::Plugin { custom_type } => custom_type == "live-grep",
1322 _ => false,
1323 })
1324 .unwrap_or(false);
1325 if !in_live_grep {
1326 self.set_status_message(
1327 "Cycle Live Grep provider only works inside Live Grep".to_string(),
1328 );
1329 return Ok(());
1330 }
1331 self.handle_action(Action::PluginAction("live_grep_cycle_provider".to_string()))?;
1332 }
1333 Action::OpenTerminalInDock => {
1334 self.handle_open_terminal_in_dock()?;
1335 }
1336 Action::ToggleLineWrap => {
1337 let new_value = !self.config.editor.line_wrap;
1338 self.config_mut().editor.line_wrap = new_value;
1339 self.sync_windows_config();
1347
1348 let leaf_ids: Vec<_> = self
1351 .windows
1352 .get(&self.active_window)
1353 .and_then(|w| w.buffers.splits())
1354 .map(|(_, vs)| vs)
1355 .expect("active window must have a populated split layout")
1356 .keys()
1357 .copied()
1358 .collect();
1359 for leaf_id in leaf_ids {
1360 let buffer_id = self
1361 .split_manager_mut()
1362 .get_buffer_id(leaf_id.into())
1363 .unwrap_or(BufferId(0));
1364 let effective_wrap =
1365 self.active_window().resolve_line_wrap_for_buffer(buffer_id);
1366 let wrap_column = self
1367 .active_window()
1368 .resolve_wrap_column_for_buffer(buffer_id);
1369 if let Some(view_state) = self
1370 .windows
1371 .get_mut(&self.active_window)
1372 .and_then(|w| w.split_view_states_mut())
1373 .expect("active window must have a populated split layout")
1374 .get_mut(&leaf_id)
1375 {
1376 view_state.viewport.line_wrap_enabled = effective_wrap;
1377 view_state.viewport.wrap_indent = self.config.editor.wrap_indent;
1378 view_state.viewport.wrap_column = wrap_column;
1379 }
1380 }
1381
1382 let state = if self.config.editor.line_wrap {
1383 t!("view.state_enabled").to_string()
1384 } else {
1385 t!("view.state_disabled").to_string()
1386 };
1387 self.set_status_message(t!("view.line_wrap_state", state = state).to_string());
1388 }
1389 Action::ToggleCurrentLineHighlight => {
1390 let new_value = !self.config.editor.highlight_current_line;
1391 self.config_mut().editor.highlight_current_line = new_value;
1392
1393 let leaf_ids: Vec<_> = self
1395 .windows
1396 .get(&self.active_window)
1397 .and_then(|w| w.buffers.splits())
1398 .map(|(_, vs)| vs)
1399 .expect("active window must have a populated split layout")
1400 .keys()
1401 .copied()
1402 .collect();
1403 for leaf_id in leaf_ids {
1404 if let Some(view_state) = self
1405 .windows
1406 .get_mut(&self.active_window)
1407 .and_then(|w| w.split_view_states_mut())
1408 .expect("active window must have a populated split layout")
1409 .get_mut(&leaf_id)
1410 {
1411 view_state.highlight_current_line =
1412 self.config.editor.highlight_current_line;
1413 }
1414 }
1415
1416 let state = if self.config.editor.highlight_current_line {
1417 t!("view.state_enabled").to_string()
1418 } else {
1419 t!("view.state_disabled").to_string()
1420 };
1421 self.set_status_message(
1422 t!("view.current_line_highlight_state", state = state).to_string(),
1423 );
1424 }
1425 Action::ToggleOccurrenceHighlight => {
1426 let new_value = !self.config.editor.highlight_occurrences;
1427 self.config_mut().editor.highlight_occurrences = new_value;
1428
1429 for window in self.windows.values_mut() {
1431 for (_, state) in &mut window.buffers {
1432 state.reference_highlight_overlay.enabled = new_value;
1433 if !new_value {
1434 state
1435 .reference_highlight_overlay
1436 .clear(&mut state.overlays, &mut state.marker_list);
1437 }
1438 }
1439 }
1440
1441 let state = if new_value {
1442 t!("view.state_enabled").to_string()
1443 } else {
1444 t!("view.state_disabled").to_string()
1445 };
1446 self.set_status_message(
1447 t!("view.occurrence_highlight_state", state = state).to_string(),
1448 );
1449 }
1450 Action::ToggleReadOnly => {
1451 let buffer_id = self.active_buffer();
1452 let is_now_read_only = self
1453 .active_window()
1454 .buffer_metadata
1455 .get(&buffer_id)
1456 .map(|m| !m.read_only)
1457 .unwrap_or(false);
1458 self.active_window_mut()
1459 .mark_buffer_read_only(buffer_id, is_now_read_only);
1460
1461 let state_str = if is_now_read_only {
1462 t!("view.state_enabled").to_string()
1463 } else {
1464 t!("view.state_disabled").to_string()
1465 };
1466 self.set_status_message(t!("view.read_only_state", state = state_str).to_string());
1467 }
1468 Action::TogglePageView => {
1469 self.active_window_mut().handle_toggle_page_view();
1470 }
1471 Action::SetPageWidth => {
1472 let active_split = self
1473 .windows
1474 .get(&self.active_window)
1475 .and_then(|w| w.buffers.splits())
1476 .map(|(mgr, _)| mgr)
1477 .expect("active window must have a populated split layout")
1478 .active_split();
1479 let current = self
1480 .windows
1481 .get(&self.active_window)
1482 .and_then(|w| w.buffers.splits())
1483 .map(|(_, vs)| vs)
1484 .expect("active window must have a populated split layout")
1485 .get(&active_split)
1486 .and_then(|v| v.compose_width.map(|w| w.to_string()))
1487 .unwrap_or_default();
1488 self.start_prompt_with_initial_text(
1489 "Page width (empty = viewport): ".to_string(),
1490 PromptType::SetPageWidth,
1491 current,
1492 );
1493 }
1494 Action::SetBackground => {
1495 let default_path = self
1496 .ansi_background_path
1497 .as_ref()
1498 .and_then(|p| {
1499 p.strip_prefix(self.working_dir())
1500 .ok()
1501 .map(|rel| rel.to_string_lossy().to_string())
1502 })
1503 .unwrap_or_else(|| DEFAULT_BACKGROUND_FILE.to_string());
1504
1505 self.start_prompt_with_initial_text(
1506 "Background file: ".to_string(),
1507 PromptType::SetBackgroundFile,
1508 default_path,
1509 );
1510 }
1511 Action::SetBackgroundBlend => {
1512 let default_amount = format!("{:.2}", self.background_fade);
1513 self.start_prompt_with_initial_text(
1514 "Background blend (0-1): ".to_string(),
1515 PromptType::SetBackgroundBlend,
1516 default_amount,
1517 );
1518 }
1519 Action::LspCompletion => {
1520 self.request_completion();
1521 }
1522 Action::DabbrevExpand => {
1523 self.dabbrev_expand();
1524 }
1525 Action::LspGotoDefinition => {
1526 self.request_goto_definition()?;
1527 }
1528 Action::LspRename => {
1529 self.start_rename()?;
1530 }
1531 Action::LspHover => {
1532 self.request_hover()?;
1533 }
1534 Action::LspReferences => {
1535 self.request_references()?;
1536 }
1537 Action::LspSignatureHelp => {
1538 self.request_signature_help();
1539 }
1540 Action::LspCodeActions => {
1541 self.request_code_actions()?;
1542 }
1543 Action::LspRestart => {
1544 self.handle_lsp_restart();
1545 }
1546 Action::LspStop => {
1547 self.handle_lsp_stop();
1548 }
1549 Action::LspToggleForBuffer => {
1550 self.handle_lsp_toggle_for_buffer();
1551 }
1552 Action::ToggleInlayHints => {
1553 self.toggle_inlay_hints();
1554 }
1555 Action::DumpConfig => {
1556 self.dump_config();
1557 }
1558 Action::RedrawScreen => {
1559 self.request_full_redraw();
1560 }
1561 Action::SelectTheme => {
1562 self.start_select_theme_prompt();
1563 }
1564 Action::InspectThemeAtCursor => {
1565 self.inspect_theme_at_cursor();
1566 }
1567 Action::SelectKeybindingMap => {
1568 self.start_select_keybinding_map_prompt();
1569 }
1570 Action::SelectCursorStyle => {
1571 self.start_select_cursor_style_prompt();
1572 }
1573 Action::SelectLocale => {
1574 self.start_select_locale_prompt();
1575 }
1576 Action::Search => {
1577 let is_search_prompt = self.active_window().prompt.as_ref().is_some_and(|p| {
1579 matches!(
1580 p.prompt_type,
1581 PromptType::Search
1582 | PromptType::ReplaceSearch
1583 | PromptType::QueryReplaceSearch
1584 )
1585 });
1586
1587 if is_search_prompt {
1588 self.confirm_prompt();
1589 } else {
1590 self.start_search_prompt(
1591 t!("file.search_prompt").to_string(),
1592 PromptType::Search,
1593 false,
1594 );
1595 }
1596 }
1597 Action::Replace => {
1598 self.start_search_prompt(
1600 t!("file.replace_prompt").to_string(),
1601 PromptType::ReplaceSearch,
1602 false,
1603 );
1604 }
1605 Action::QueryReplace => {
1606 self.active_window_mut().search_confirm_each = true;
1608 self.start_search_prompt(
1609 "Query replace: ".to_string(),
1610 PromptType::QueryReplaceSearch,
1611 false,
1612 );
1613 }
1614 Action::FindInSelection => {
1615 self.start_search_prompt(
1616 t!("file.search_prompt").to_string(),
1617 PromptType::Search,
1618 true,
1619 );
1620 }
1621 Action::FindNext => {
1622 self.find_next();
1623 }
1624 Action::FindPrevious => {
1625 self.find_previous();
1626 }
1627 Action::FindSelectionNext => {
1628 self.find_selection_next();
1629 }
1630 Action::FindSelectionPrevious => {
1631 self.find_selection_previous();
1632 }
1633 Action::ClearSearch => {
1634 self.active_window_mut().clear_search_highlights();
1635 }
1636 Action::AddCursorNextMatch => self.add_cursor_at_next_match(),
1637 Action::AddCursorAbove => self.add_cursor_above(),
1638 Action::AddCursorBelow => self.add_cursor_below(),
1639 Action::AddCursorsToLineEnds => self.add_cursors_to_line_ends(),
1640 Action::NextBuffer => self.next_buffer(),
1641 Action::PrevBuffer => self.prev_buffer(),
1642 Action::SwitchToPreviousTab => self.switch_to_previous_tab(),
1643 Action::SwitchToTabByName => self.start_switch_to_tab_prompt(),
1644
1645 Action::ScrollTabsLeft => {
1647 let active_split_id = self
1648 .windows
1649 .get(&self.active_window)
1650 .and_then(|w| w.buffers.splits())
1651 .map(|(mgr, _)| mgr)
1652 .expect("active window must have a populated split layout")
1653 .active_split();
1654 if let Some(view_state) = self
1655 .windows
1656 .get_mut(&self.active_window)
1657 .and_then(|w| w.split_view_states_mut())
1658 .expect("active window must have a populated split layout")
1659 .get_mut(&active_split_id)
1660 {
1661 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_sub(5);
1662 self.set_status_message(t!("status.scrolled_tabs_left").to_string());
1663 }
1664 }
1665 Action::ScrollTabsRight => {
1666 let active_split_id = self
1667 .windows
1668 .get(&self.active_window)
1669 .and_then(|w| w.buffers.splits())
1670 .map(|(mgr, _)| mgr)
1671 .expect("active window must have a populated split layout")
1672 .active_split();
1673 if let Some(view_state) = self
1674 .windows
1675 .get_mut(&self.active_window)
1676 .and_then(|w| w.split_view_states_mut())
1677 .expect("active window must have a populated split layout")
1678 .get_mut(&active_split_id)
1679 {
1680 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_add(5);
1681 self.set_status_message(t!("status.scrolled_tabs_right").to_string());
1682 }
1683 }
1684 Action::NavigateBack => self.navigate_back(),
1685 Action::NavigateForward => self.navigate_forward(),
1686 Action::SplitHorizontal => self.split_pane_horizontal(),
1687 Action::SplitVertical => self.split_pane_vertical(),
1688 Action::CloseSplit => self.close_active_split(),
1689 Action::NextSplit => self.next_split(),
1690 Action::PrevSplit => self.prev_split(),
1691 Action::NextWindow => self.next_window(),
1692 Action::PrevWindow => self.prev_window(),
1693 Action::IncreaseSplitSize => self.adjust_split_size(0.05),
1694 Action::DecreaseSplitSize => self.adjust_split_size(-0.05),
1695 Action::ToggleMaximizeSplit => self.toggle_maximize_split(),
1696 Action::ToggleFileExplorer => self.toggle_file_explorer(),
1697 Action::ToggleFileExplorerSide => self.toggle_file_explorer_side(),
1698 Action::ToggleMenuBar => self.toggle_menu_bar(),
1699 Action::ToggleTabBar => self.active_window_mut().toggle_tab_bar(),
1700 Action::ToggleStatusBar => self.active_window_mut().toggle_status_bar(),
1701 Action::TogglePromptLine => self.active_window_mut().toggle_prompt_line(),
1702 Action::ToggleVerticalScrollbar => self.toggle_vertical_scrollbar(),
1703 Action::ToggleHorizontalScrollbar => self.toggle_horizontal_scrollbar(),
1704 Action::ToggleLineNumbers => self.toggle_line_numbers(),
1705 Action::TriggerWaveAnimation => self.trigger_wave_animation(),
1706 Action::ToggleScrollSync => self.active_window_mut().toggle_scroll_sync(),
1707 Action::ToggleMouseCapture => self.toggle_mouse_capture(),
1708 Action::ToggleMouseHover => self.toggle_mouse_hover(),
1709 Action::ToggleDebugHighlights => self.active_window_mut().toggle_debug_highlights(),
1710 Action::AddRuler => {
1712 self.start_prompt(t!("rulers.add_prompt").to_string(), PromptType::AddRuler);
1713 }
1714 Action::RemoveRuler => {
1715 self.start_remove_ruler_prompt();
1716 }
1717 Action::SetTabSize => {
1719 let current = self
1720 .buffers()
1721 .get(&self.active_buffer())
1722 .map(|s| s.buffer_settings.tab_size.to_string())
1723 .unwrap_or_else(|| "4".to_string());
1724 self.start_prompt_with_initial_text(
1725 "Tab size: ".to_string(),
1726 PromptType::SetTabSize,
1727 current,
1728 );
1729 }
1730 Action::SetLineEnding => {
1731 self.start_set_line_ending_prompt();
1732 }
1733 Action::SetEncoding => {
1734 self.start_set_encoding_prompt();
1735 }
1736 Action::ReloadWithEncoding => {
1737 self.start_reload_with_encoding_prompt();
1738 }
1739 Action::SetLanguage => {
1740 self.start_set_language_prompt();
1741 }
1742 Action::ToggleIndentationStyle => {
1743 let __buffer_id = self.active_buffer();
1744 if let Some(state) = self
1745 .windows
1746 .get_mut(&self.active_window)
1747 .map(|w| &mut w.buffers)
1748 .expect("active window present")
1749 .get_mut(&__buffer_id)
1750 {
1751 state.buffer_settings.use_tabs = !state.buffer_settings.use_tabs;
1752 let status = if state.buffer_settings.use_tabs {
1753 "Indentation: Tabs"
1754 } else {
1755 "Indentation: Spaces"
1756 };
1757 self.set_status_message(status.to_string());
1758 }
1759 }
1760 Action::ToggleTabIndicators | Action::ToggleWhitespaceIndicators => {
1761 let __buffer_id = self.active_buffer();
1762 if let Some(state) = self
1763 .windows
1764 .get_mut(&self.active_window)
1765 .map(|w| &mut w.buffers)
1766 .expect("active window present")
1767 .get_mut(&__buffer_id)
1768 {
1769 state.buffer_settings.whitespace.toggle_all();
1770 let status = if state.buffer_settings.whitespace.any_visible() {
1771 t!("toggle.whitespace_indicators_shown")
1772 } else {
1773 t!("toggle.whitespace_indicators_hidden")
1774 };
1775 self.set_status_message(status.to_string());
1776 }
1777 }
1778 Action::ResetBufferSettings => self.reset_buffer_settings(),
1779 Action::FocusFileExplorer => self.focus_file_explorer(),
1780 Action::FocusEditor => self.active_window_mut().focus_editor(),
1781 Action::ToggleDockFocus => {
1782 match self.dock.as_ref().map(|d| d.focused) {
1788 Some(true) => self.blur_floating_panel(super::PanelSlot::Dock),
1789 Some(false) => self.refocus_floating_panel(super::PanelSlot::Dock),
1790 None => {
1793 return self.handle_action(Action::PluginAction(
1794 "orchestrator_dock_toggle".to_string(),
1795 ));
1796 }
1797 }
1798 }
1799 Action::FileExplorerUp => self.file_explorer_navigate_up(),
1800 Action::FileExplorerDown => self.file_explorer_navigate_down(),
1801 Action::FileExplorerPageUp => self.file_explorer_page_up(),
1802 Action::FileExplorerPageDown => self.file_explorer_page_down(),
1803 Action::FileExplorerExpand => self.file_explorer_toggle_expand(),
1804 Action::FileExplorerCollapse => self.file_explorer_collapse(),
1805 Action::FileExplorerOpen => self.file_explorer_open_file()?,
1806 Action::FileExplorerRefresh => self.file_explorer_refresh(),
1807 Action::FileExplorerNewFile => self.file_explorer_new_file(),
1808 Action::FileExplorerNewDirectory => self.file_explorer_new_directory(),
1809 Action::FileExplorerDelete => self.file_explorer_delete(),
1810 Action::FileExplorerRename => self.file_explorer_rename(),
1811 Action::FileExplorerToggleHidden => self.file_explorer_toggle_hidden(),
1812 Action::FileExplorerToggleGitignored => self.file_explorer_toggle_gitignored(),
1813 Action::FileExplorerSearchClear => {
1814 self.active_window_mut().file_explorer_search_clear()
1815 }
1816 Action::FileExplorerSearchBackspace => {
1817 self.active_window_mut().file_explorer_search_pop_char()
1818 }
1819 Action::FileExplorerCopy => self.active_window_mut().file_explorer_copy(),
1820 Action::FileExplorerCut => self.active_window_mut().file_explorer_cut(),
1821 Action::FileExplorerPaste => self.file_explorer_paste(),
1822 Action::FileExplorerDuplicate => self.file_explorer_duplicate(),
1823 Action::FileExplorerCopyFullPath => self.file_explorer_copy_path(false),
1824 Action::FileExplorerCopyRelativePath => self.file_explorer_copy_path(true),
1825 Action::FileExplorerExtendSelectionUp => {
1826 self.active_window_mut().file_explorer_extend_selection_up()
1827 }
1828 Action::FileExplorerExtendSelectionDown => self
1829 .active_window_mut()
1830 .file_explorer_extend_selection_down(),
1831 Action::FileExplorerToggleSelect => {
1832 self.active_window_mut().file_explorer_toggle_select()
1833 }
1834 Action::FileExplorerSelectAll => self.active_window_mut().file_explorer_select_all(),
1835 Action::RemoveSecondaryCursors => {
1836 if let Some(events) = self
1838 .active_window_mut()
1839 .action_to_events(Action::RemoveSecondaryCursors)
1840 {
1841 let batch = Event::Batch {
1843 events: events.clone(),
1844 description: "Remove secondary cursors".to_string(),
1845 };
1846 self.active_event_log_mut().append(batch.clone());
1847 self.apply_event_to_active_buffer(&batch);
1848
1849 let active_split = self
1851 .windows
1852 .get(&self.active_window)
1853 .and_then(|w| w.buffers.splits())
1854 .map(|(mgr, _)| mgr)
1855 .expect("active window must have a populated split layout")
1856 .active_split();
1857 let active_buffer = self.active_buffer();
1858 self.active_window_mut()
1859 .ensure_cursor_visible_for_split(active_buffer, active_split);
1860 }
1861 }
1862
1863 Action::MenuActivate => {
1865 self.handle_menu_activate();
1866 }
1867 Action::MenuClose => {
1868 self.handle_menu_close();
1869 }
1870 Action::MenuLeft => {
1871 self.handle_menu_left();
1872 }
1873 Action::MenuRight => {
1874 self.handle_menu_right();
1875 }
1876 Action::MenuUp => {
1877 self.handle_menu_up();
1878 }
1879 Action::MenuDown => {
1880 self.handle_menu_down();
1881 }
1882 Action::MenuExecute => {
1883 if let Some(action) = self.handle_menu_execute() {
1884 return self.handle_action(action);
1885 }
1886 }
1887 Action::MenuOpen(menu_name) => {
1888 if self.config.editor.menu_bar_mnemonics {
1889 self.handle_menu_open(&menu_name);
1890 }
1891 }
1892
1893 Action::SwitchKeybindingMap(map_name) => {
1894 let is_builtin =
1896 matches!(map_name.as_str(), "default" | "emacs" | "vscode" | "macos");
1897 let is_user_defined = self.config.keybinding_maps.contains_key(&map_name);
1898
1899 if is_builtin || is_user_defined {
1900 self.config_mut().active_keybinding_map = map_name.clone().into();
1902
1903 *self.keybindings.write().unwrap() =
1905 crate::input::keybindings::KeybindingResolver::new(&self.config);
1906
1907 self.set_status_message(
1908 t!("view.keybindings_switched", map = map_name).to_string(),
1909 );
1910 } else {
1911 self.set_status_message(
1912 t!("view.keybindings_unknown", map = map_name).to_string(),
1913 );
1914 }
1915 }
1916
1917 Action::SmartHome => {
1918 let buffer_id = self.active_buffer();
1920 if self.active_window().is_composite_buffer(buffer_id) {
1921 if let Some(_handled) =
1922 self.handle_composite_action(buffer_id, &Action::SmartHome)
1923 {
1924 return Ok(());
1925 }
1926 }
1927 self.smart_home();
1928 }
1929 Action::ToggleComment => {
1930 self.toggle_comment();
1931 }
1932 Action::ToggleFold => {
1933 self.active_window_mut().toggle_fold_at_cursor();
1934 }
1935 Action::GoToMatchingBracket => {
1936 self.goto_matching_bracket();
1937 }
1938 Action::JumpToNextError => {
1939 self.jump_to_next_error();
1940 }
1941 Action::JumpToPreviousError => {
1942 self.jump_to_previous_error();
1943 }
1944 Action::SetBookmark(key) => {
1945 self.active_window_mut().set_bookmark(key);
1946 }
1947 Action::JumpToBookmark(key) => {
1948 self.jump_to_bookmark(key);
1949 }
1950 Action::ClearBookmark(key) => {
1951 self.active_window_mut().clear_bookmark(key);
1952 }
1953 Action::ListBookmarks => {
1954 self.active_window_mut().list_bookmarks();
1955 }
1956 Action::ToggleSearchCaseSensitive => {
1957 self.active_window_mut().search_case_sensitive =
1958 !self.active_window().search_case_sensitive;
1959 let state = if self.active_window().search_case_sensitive {
1960 "enabled"
1961 } else {
1962 "disabled"
1963 };
1964 self.set_status_message(
1965 t!("search.case_sensitive_state", state = state).to_string(),
1966 );
1967 self.refresh_active_search();
1968 }
1969 Action::ToggleSearchWholeWord => {
1970 self.active_window_mut().search_whole_word =
1971 !self.active_window().search_whole_word;
1972 let state = if self.active_window().search_whole_word {
1973 "enabled"
1974 } else {
1975 "disabled"
1976 };
1977 self.set_status_message(t!("search.whole_word_state", state = state).to_string());
1978 self.refresh_active_search();
1979 }
1980 Action::ToggleSearchRegex => {
1981 self.active_window_mut().search_use_regex = !self.active_window().search_use_regex;
1982 let state = if self.active_window().search_use_regex {
1983 "enabled"
1984 } else {
1985 "disabled"
1986 };
1987 self.set_status_message(t!("search.regex_state", state = state).to_string());
1988 self.refresh_active_search();
1989 }
1990 Action::ToggleSearchConfirmEach => {
1991 self.active_window_mut().search_confirm_each =
1992 !self.active_window().search_confirm_each;
1993 let state = if self.active_window().search_confirm_each {
1994 "enabled"
1995 } else {
1996 "disabled"
1997 };
1998 self.set_status_message(t!("search.confirm_each_state", state = state).to_string());
1999 }
2000 Action::FileBrowserToggleHidden => {
2001 self.file_open_toggle_hidden();
2003 }
2004 Action::StartMacroRecording => {
2005 self.set_status_message(
2007 "Use Ctrl+Shift+R to start recording (will prompt for register)".to_string(),
2008 );
2009 }
2010 Action::StopMacroRecording => {
2011 self.stop_macro_recording();
2012 }
2013 Action::PlayMacro(key) => {
2014 self.play_macro(key);
2015 }
2016 Action::ToggleMacroRecording(key) => {
2017 self.toggle_macro_recording(key);
2018 }
2019 Action::ShowMacro(key) => {
2020 self.show_macro_in_buffer(key);
2021 }
2022 Action::ListMacros => {
2023 self.list_macros_in_buffer();
2024 }
2025 Action::PromptRecordMacro => {
2026 self.start_prompt("Record macro (0-9): ".to_string(), PromptType::RecordMacro);
2027 }
2028 Action::PromptPlayMacro => {
2029 self.start_prompt("Play macro (0-9): ".to_string(), PromptType::PlayMacro);
2030 }
2031 Action::PlayLastMacro => {
2032 if let Some(key) = self.active_window_mut().macros.last_register() {
2033 self.play_macro(key);
2034 } else {
2035 self.set_status_message(t!("status.no_macro_recorded").to_string());
2036 }
2037 }
2038 Action::PromptSetBookmark => {
2039 self.start_prompt("Set bookmark (0-9): ".to_string(), PromptType::SetBookmark);
2040 }
2041 Action::PromptJumpToBookmark => {
2042 self.start_prompt(
2043 "Jump to bookmark (0-9): ".to_string(),
2044 PromptType::JumpToBookmark,
2045 );
2046 }
2047 Action::CompositeNextHunk => {
2048 let buf = self.active_buffer();
2049 self.active_window_mut().composite_next_hunk_active(buf);
2050 }
2051 Action::CompositePrevHunk => {
2052 let buf = self.active_buffer();
2053 self.active_window_mut().composite_prev_hunk_active(buf);
2054 }
2055 Action::None => {}
2056 Action::DeleteBackward => {
2057 if self.active_window().is_editing_disabled() {
2058 self.set_status_message(t!("buffer.editing_disabled").to_string());
2059 return Ok(());
2060 }
2061 if let Some(events) = self
2063 .active_window_mut()
2064 .action_to_events(Action::DeleteBackward)
2065 {
2066 if events.len() > 1 {
2067 let description = "Delete backward".to_string();
2069 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description)
2070 {
2071 self.active_event_log_mut().append(bulk_edit);
2072 }
2073 } else {
2074 for event in events {
2075 self.active_event_log_mut().append(event.clone());
2076 self.apply_event_to_active_buffer(&event);
2077 }
2078 }
2079 }
2080 }
2081 Action::PluginAction(action_name) => {
2082 tracing::debug!("handle_action: PluginAction('{}')", action_name);
2083 #[cfg(feature = "plugins")]
2086 {
2087 let result = self
2088 .plugin_manager
2089 .read()
2090 .unwrap()
2091 .execute_action_async(&action_name);
2092 if let Some(result) = result {
2093 match result {
2094 Ok(receiver) => {
2095 self.pending_plugin_actions
2097 .push((action_name.clone(), receiver));
2098 }
2099 Err(e) => {
2100 self.set_status_message(
2101 t!("view.plugin_error", error = e.to_string()).to_string(),
2102 );
2103 tracing::error!("Plugin action error: {}", e);
2104 }
2105 }
2106 } else {
2107 self.set_status_message(
2108 t!("status.plugin_manager_unavailable").to_string(),
2109 );
2110 }
2111 }
2112 #[cfg(not(feature = "plugins"))]
2113 {
2114 let _ = action_name;
2115 self.set_status_message(
2116 "Plugins not available (compiled without plugin support)".to_string(),
2117 );
2118 }
2119 }
2120 Action::LoadPluginFromBuffer => {
2121 #[cfg(feature = "plugins")]
2122 {
2123 let buffer_id = self.active_buffer();
2124 let state = self.active_state();
2125 let buffer = &state.buffer;
2126 let total = buffer.total_bytes();
2127 let content =
2128 String::from_utf8_lossy(&buffer.slice_bytes(0..total)).to_string();
2129
2130 let is_ts = buffer
2132 .file_path()
2133 .and_then(|p| p.extension())
2134 .and_then(|e| e.to_str())
2135 .map(|e| e == "ts" || e == "tsx")
2136 .unwrap_or(true);
2137
2138 let name = buffer
2140 .file_path()
2141 .and_then(|p| p.file_name())
2142 .and_then(|s| s.to_str())
2143 .map(|s| s.to_string())
2144 .unwrap_or_else(|| "buffer-plugin".to_string());
2145
2146 let load_result = self
2147 .plugin_manager
2148 .read()
2149 .unwrap()
2150 .load_plugin_from_source(&content, &name, is_ts);
2151 match load_result {
2152 Ok(()) => {
2153 self.set_status_message(format!(
2154 "Plugin '{}' loaded from buffer",
2155 name
2156 ));
2157 }
2158 Err(e) => {
2159 self.set_status_message(format!("Failed to load plugin: {}", e));
2160 tracing::error!("LoadPluginFromBuffer error: {}", e);
2161 }
2162 }
2163
2164 self.setup_plugin_dev_lsp(buffer_id, &content);
2166 }
2167 #[cfg(not(feature = "plugins"))]
2168 {
2169 self.set_status_message(
2170 "Plugins not available (compiled without plugin support)".to_string(),
2171 );
2172 }
2173 }
2174 Action::InitReload => {
2175 self.load_init_script(true);
2180 self.fire_plugins_loaded_hook();
2183 }
2184 Action::InitEdit => {
2185 let config_dir = self.dir_context.config_dir.clone();
2188 match crate::init_script::ensure_starter(&config_dir) {
2189 Ok(path) => {
2190 let declarations =
2200 self.plugin_manager.read().unwrap().plugin_declarations();
2201 crate::init_script::write_plugin_declarations(&config_dir, &declarations);
2202 match self.open_file(&path) {
2203 Ok(_) => {
2204 self.set_status_message(format!("init.ts: {}", path.display()));
2205 }
2206 Err(e) => {
2207 self.set_status_message(format!("init.ts: open failed: {e}"));
2208 }
2209 }
2210 }
2211 Err(e) => {
2212 self.set_status_message(format!("init.ts: create failed: {e}"));
2213 }
2214 }
2215 }
2216 Action::InitCheck => {
2217 let report = crate::init_script::check(&self.dir_context.config_dir);
2220 if report.ok && report.diagnostics.is_empty() {
2221 self.set_status_message("init.ts: ok".into());
2222 } else if !report.ok {
2223 let first = report
2224 .diagnostics
2225 .first()
2226 .map(|d| format!("{}:{}: {}", d.line, d.column, d.message))
2227 .unwrap_or_else(|| "unknown error".into());
2228 self.set_status_message(format!(
2229 "init.ts: {} error(s) — first: {first}",
2230 report.diagnostics.len()
2231 ));
2232 } else {
2233 self.set_status_message(format!(
2234 "init.ts: {} warning(s)",
2235 report.diagnostics.len()
2236 ));
2237 }
2238 }
2239 Action::OpenTerminal => {
2240 self.open_terminal();
2241 }
2242 Action::CloseTerminal => {
2243 self.close_terminal();
2244 }
2245 Action::FocusTerminal => {
2246 if self
2248 .active_window()
2249 .is_terminal_buffer(self.active_buffer())
2250 {
2251 self.active_window_mut().terminal_mode = true;
2252 self.active_window_mut().key_context = KeyContext::Terminal;
2253 self.set_status_message(t!("status.terminal_mode_enabled").to_string());
2254 }
2255 }
2256 Action::TerminalEscape => {
2257 if self.active_window().terminal_mode {
2259 self.active_window_mut().terminal_mode = false;
2260 self.active_window_mut().key_context = KeyContext::Normal;
2261 self.set_status_message(t!("status.terminal_mode_disabled").to_string());
2262 }
2263 }
2264 Action::ToggleKeyboardCapture => {
2265 if self.active_window().terminal_mode {
2267 self.active_window_mut().keyboard_capture =
2268 !self.active_window_mut().keyboard_capture;
2269 if self.active_window_mut().keyboard_capture {
2270 self.set_status_message(
2271 "Keyboard capture ON - all keys go to terminal (F9 to toggle)"
2272 .to_string(),
2273 );
2274 } else {
2275 self.set_status_message(
2276 "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
2277 );
2278 }
2279 }
2280 }
2281 Action::TerminalPaste => {
2282 if self.active_window().terminal_mode {
2284 if let Some(text) = self.clipboard.paste() {
2285 self.active_window_mut()
2286 .send_terminal_input(text.as_bytes());
2287 }
2288 }
2289 }
2290 Action::SendSelectionToTerminal => {
2291 self.send_selection_to_terminal();
2292 }
2293 Action::ShellCommand => {
2294 self.start_shell_command_prompt(false);
2296 }
2297 Action::ShellCommandReplace => {
2298 self.start_shell_command_prompt(true);
2300 }
2301 Action::OpenSettings => {
2302 self.open_settings();
2303 }
2304 Action::CloseSettings => {
2305 let has_changes = self
2307 .settings_state
2308 .as_ref()
2309 .is_some_and(|s| s.has_changes());
2310 if has_changes {
2311 if let Some(ref mut state) = self.settings_state {
2313 state.show_confirm_dialog();
2314 }
2315 } else {
2316 self.close_settings(false);
2317 }
2318 }
2319 Action::SettingsSave => {
2320 self.save_settings();
2321 }
2322 Action::SettingsReset => {
2323 if let Some(ref mut state) = self.settings_state {
2324 state.reset_current_to_default();
2325 }
2326 }
2327 Action::SettingsInherit => {
2328 if let Some(ref mut state) = self.settings_state {
2329 state.set_current_to_null();
2330 }
2331 }
2332 Action::SettingsToggleFocus => {
2333 if let Some(ref mut state) = self.settings_state {
2334 state.toggle_focus();
2335 }
2336 }
2337 Action::SettingsActivate => {
2338 self.settings_activate_current();
2339 }
2340 Action::SettingsSearch => {
2341 if let Some(ref mut state) = self.settings_state {
2342 state.start_search();
2343 }
2344 }
2345 Action::SettingsHelp => {
2346 if let Some(ref mut state) = self.settings_state {
2347 state.toggle_help();
2348 }
2349 }
2350 Action::SettingsIncrement => {
2351 self.settings_increment_current();
2352 }
2353 Action::SettingsDecrement => {
2354 self.settings_decrement_current();
2355 }
2356 Action::CalibrateInput => {
2357 self.open_calibration_wizard();
2358 }
2359 Action::EventDebug => {
2360 self.active_window_mut().open_event_debug();
2361 }
2362 Action::SuspendProcess => {
2363 self.request_suspend();
2364 }
2365 Action::OpenKeybindingEditor => {
2366 self.open_keybinding_editor();
2367 }
2368 Action::PromptConfirm => {
2369 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
2370 use super::prompt_actions::PromptResult;
2371 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
2372 PromptResult::ExecuteAction(action) => {
2373 return self.handle_action(action);
2374 }
2375 PromptResult::EarlyReturn => {
2376 return Ok(());
2377 }
2378 PromptResult::Done => {}
2379 }
2380 }
2381 }
2382 Action::PromptConfirmWithText(ref text) => {
2383 if let Some(ref mut prompt) = self.active_window_mut().prompt {
2385 prompt.set_input(text.clone());
2386 self.update_prompt_suggestions();
2387 }
2388 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
2389 use super::prompt_actions::PromptResult;
2390 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
2391 PromptResult::ExecuteAction(action) => {
2392 return self.handle_action(action);
2393 }
2394 PromptResult::EarlyReturn => {
2395 return Ok(());
2396 }
2397 PromptResult::Done => {}
2398 }
2399 }
2400 }
2401 Action::PopupConfirm => {
2402 use super::popup_actions::PopupConfirmResult;
2403 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
2404 return Ok(());
2405 }
2406 }
2407 Action::PopupCancel => {
2408 self.handle_popup_cancel();
2409 }
2410 Action::PopupFocus => {
2411 self.handle_popup_focus();
2412 }
2413 Action::CompletionAccept => {
2414 use super::popup_actions::PopupConfirmResult;
2415 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
2416 return Ok(());
2417 }
2418 }
2419 Action::CompletionDismiss => {
2420 self.handle_popup_cancel();
2421 }
2422 Action::InsertChar(c) => {
2423 if self.is_prompting() {
2424 return self.handle_insert_char_prompt(c);
2425 } else if self.active_window_mut().key_context == KeyContext::FileExplorer {
2426 self.active_window_mut().file_explorer_search_push_char(c);
2427 } else {
2428 self.handle_insert_char_editor(c)?;
2429 }
2430 }
2431 Action::PromptCopy => {
2433 if let Some(prompt) = &self.active_window_mut().prompt {
2434 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
2435 if !text.is_empty() {
2436 self.clipboard.copy(text);
2437 self.set_status_message(t!("clipboard.copied").to_string());
2438 }
2439 }
2440 }
2441 Action::PromptCut => {
2442 if let Some(prompt) = &self.active_window_mut().prompt {
2443 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
2444 if !text.is_empty() {
2445 self.clipboard.copy(text);
2446 }
2447 }
2448 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
2449 if prompt.has_selection() {
2450 prompt.delete_selection();
2451 } else {
2452 prompt.clear();
2453 }
2454 }
2455 self.set_status_message(t!("clipboard.cut").to_string());
2456 self.update_prompt_suggestions();
2457 }
2458 Action::PromptPaste => {
2459 if let Some(text) = self.clipboard.paste() {
2460 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
2461 prompt.insert_str(&text);
2462 }
2463 self.update_prompt_suggestions();
2464 }
2465 }
2466 _ => {
2467 self.apply_action_as_events(action)?;
2473 }
2474 }
2475
2476 Ok(())
2477 }
2478
2479 fn fire_dock_widget_event(&self, panel_id: u64, event_type: &str) {
2484 if self
2485 .plugin_manager
2486 .read()
2487 .unwrap()
2488 .has_hook_handlers("widget_event")
2489 {
2490 self.plugin_manager.read().unwrap().run_hook(
2491 "widget_event",
2492 crate::services::plugins::hooks::HookArgs::WidgetEvent {
2493 panel_id,
2494 widget_key: "sessions".to_string(),
2495 event_type: event_type.to_string(),
2496 payload: serde_json::json!({}),
2497 },
2498 );
2499 }
2500 }
2501
2502 fn dispatch_floating_widget_key(
2513 &mut self,
2514 slot: super::PanelSlot,
2515 code: crossterm::event::KeyCode,
2516 modifiers: crossterm::event::KeyModifiers,
2517 ) -> bool {
2518 use crossterm::event::{KeyCode, KeyModifiers};
2519 let panel_id = match self.panel(slot) {
2520 Some(fwp) => fwp.panel_id,
2521 None => {
2522 tracing::debug!(
2523 target: "fresh::dock",
2524 ?slot,
2525 ?code,
2526 "dispatch_floating_widget_key: no panel mounted in slot — returning false"
2527 );
2528 return false;
2529 }
2530 };
2531 tracing::debug!(
2532 target: "fresh::dock",
2533 panel_id,
2534 ?slot,
2535 ?code,
2536 modifiers = ?modifiers,
2537 placement = ?self.panel(slot).map(|f| f.placement),
2538 focused = ?self.panel(slot).map(|f| f.focused),
2539 "dispatch_floating_widget_key: entry"
2540 );
2541 if matches!(
2549 self.panel(slot).map(|f| f.placement),
2550 Some(super::PanelPlacement::LeftDock { .. })
2551 ) {
2552 let on_filter = self
2553 .widget_registry
2554 .focus_key(panel_id)
2555 .map(|k| k == "filter")
2556 .unwrap_or(false);
2557 let on_project_menu = self
2566 .widget_registry
2567 .focus_key(panel_id)
2568 .map(|k| k.starts_with("project-pick:"))
2569 .unwrap_or(false);
2570 if on_project_menu {
2571 match code {
2572 KeyCode::Up => {
2573 self.fire_dock_widget_event(panel_id, "dock_menu_prev");
2574 return true;
2575 }
2576 KeyCode::Down => {
2577 self.fire_dock_widget_event(panel_id, "dock_menu_next");
2578 return true;
2579 }
2580 KeyCode::Tab if modifiers.contains(KeyModifiers::SHIFT) => {
2584 self.fire_dock_widget_event(panel_id, "dock_menu_prev");
2585 return true;
2586 }
2587 KeyCode::BackTab => {
2588 self.fire_dock_widget_event(panel_id, "dock_menu_prev");
2589 return true;
2590 }
2591 KeyCode::Tab => {
2592 self.fire_dock_widget_event(panel_id, "dock_menu_next");
2593 return true;
2594 }
2595 KeyCode::Enter | KeyCode::Char(' ') => {
2596 self.fire_dock_widget_event(panel_id, "dock_menu_accept");
2597 return true;
2598 }
2599 KeyCode::Esc => {
2600 self.fire_dock_widget_event(panel_id, "dock_menu_cancel");
2601 return true;
2602 }
2603 _ => {}
2604 }
2605 }
2606 match code {
2607 KeyCode::Esc => {
2608 if on_filter {
2609 self.set_panel_focus_and_notify(panel_id, "sessions".to_string());
2611 } else {
2612 self.blur_floating_panel(slot);
2614 }
2615 return true;
2616 }
2617 KeyCode::Enter => {
2618 if on_filter {
2619 self.set_panel_focus_and_notify(panel_id, "sessions".to_string());
2621 } else if self
2622 .widget_registry
2623 .focus_key(panel_id)
2624 .map(|k| k == "sessions" || k.is_empty())
2625 .unwrap_or(true)
2626 {
2627 self.fire_dock_widget_event(panel_id, "dock_activate");
2637 } else {
2638 self.handle_widget_command(
2648 panel_id,
2649 fresh_core::api::WidgetAction::Key {
2650 key: "Enter".to_string(),
2651 },
2652 );
2653 }
2654 return true;
2655 }
2656 KeyCode::Char('/') if modifiers.is_empty() => {
2657 self.set_panel_focus_and_notify(panel_id, "filter".to_string());
2658 return true;
2659 }
2660 KeyCode::Char('t' | 'T') if modifiers.contains(KeyModifiers::ALT) => {
2661 self.fire_dock_widget_event(panel_id, "dock_toggle_worktrees");
2668 return true;
2669 }
2670 KeyCode::Char('i' | 'I') if modifiers.contains(KeyModifiers::ALT) => {
2671 self.fire_dock_widget_event(panel_id, "dock_toggle_trivial");
2674 return true;
2675 }
2676 KeyCode::Char('p' | 'P') if modifiers.contains(KeyModifiers::ALT) => {
2677 self.fire_dock_widget_event(panel_id, "dock_toggle_scope");
2680 return true;
2681 }
2682 KeyCode::Char('n' | 'N') if modifiers.contains(KeyModifiers::ALT) => {
2683 if self
2689 .plugin_manager
2690 .read()
2691 .unwrap()
2692 .has_hook_handlers("widget_event")
2693 {
2694 self.plugin_manager.read().unwrap().run_hook(
2695 "widget_event",
2696 crate::services::plugins::hooks::HookArgs::WidgetEvent {
2697 panel_id,
2698 widget_key: "sessions".to_string(),
2699 event_type: "dock_new".to_string(),
2700 payload: serde_json::json!({}),
2701 },
2702 );
2703 }
2704 return true;
2705 }
2706 KeyCode::Char(' ') => {
2707 let has_handler = self
2710 .plugin_manager
2711 .read()
2712 .unwrap()
2713 .has_hook_handlers("widget_event");
2714 tracing::debug!(
2715 target: "fresh::dock",
2716 panel_id,
2717 has_handler,
2718 focus_key = ?self.widget_registry.focus_key(panel_id),
2719 "dispatch_floating_widget_key: Space on LeftDock — firing dock_space widget_event"
2720 );
2721 if has_handler {
2722 self.plugin_manager.read().unwrap().run_hook(
2723 "widget_event",
2724 crate::services::plugins::hooks::HookArgs::WidgetEvent {
2725 panel_id,
2726 widget_key: "sessions".to_string(),
2727 event_type: "dock_space".to_string(),
2728 payload: serde_json::json!({}),
2729 },
2730 );
2731 }
2732 return true;
2733 }
2734 _ => {}
2735 }
2736 }
2737 let key_name: Option<&str> = match code {
2738 KeyCode::Esc => {
2739 let mode_has_binding = self
2748 .active_window()
2749 .editor_mode
2750 .as_ref()
2751 .map(|mode_name| {
2752 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
2753 let mode_ctx =
2754 crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
2755 let keybindings = self.keybindings.read().unwrap();
2756 keybindings.has_explicit_binding(&key_event, &mode_ctx)
2757 })
2758 .unwrap_or(false);
2759 if mode_has_binding {
2760 return false;
2761 }
2762 let widget_key = self
2763 .widget_registry
2764 .get(panel_id)
2765 .map(|p| p.focus_key.clone())
2766 .unwrap_or_default();
2767 if self
2768 .plugin_manager
2769 .read()
2770 .unwrap()
2771 .has_hook_handlers("widget_event")
2772 {
2773 self.plugin_manager.read().unwrap().run_hook(
2774 "widget_event",
2775 crate::services::plugins::hooks::HookArgs::WidgetEvent {
2776 panel_id,
2777 widget_key,
2778 event_type: "cancel".to_string(),
2779 payload: serde_json::json!({}),
2780 },
2781 );
2782 }
2783 *self.panel_opt_mut(slot) = None;
2784 let _ = self.widget_registry.unmount(panel_id);
2785 return true;
2786 }
2787 KeyCode::Tab => Some(if modifiers.contains(KeyModifiers::SHIFT) {
2788 "Shift+Tab"
2789 } else {
2790 "Tab"
2791 }),
2792 KeyCode::BackTab => Some("Shift+Tab"),
2793 KeyCode::Enter => Some("Enter"),
2794 KeyCode::Backspace => Some("Backspace"),
2795 KeyCode::Delete => Some("Delete"),
2796 KeyCode::Home => Some("Home"),
2797 KeyCode::End => Some("End"),
2798 KeyCode::Left => Some("Left"),
2799 KeyCode::Right => Some("Right"),
2800 KeyCode::Up => Some("Up"),
2801 KeyCode::Down => Some("Down"),
2802 KeyCode::PageUp => Some("PageUp"),
2803 KeyCode::PageDown => Some("PageDown"),
2804 _ => None,
2805 };
2806 if let Some(name) = key_name {
2807 let mode_has_binding = self
2826 .active_window()
2827 .editor_mode
2828 .as_ref()
2829 .map(|mode_name| {
2830 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
2831 let mode_ctx =
2832 crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
2833 let keybindings = self.keybindings.read().unwrap();
2834 keybindings.has_explicit_binding(&key_event, &mode_ctx)
2835 })
2836 .unwrap_or(false);
2837 if mode_has_binding {
2838 return false;
2839 }
2840 self.handle_widget_command(
2841 panel_id,
2842 fresh_core::api::WidgetAction::Key {
2843 key: name.to_string(),
2844 },
2845 );
2846 return true;
2847 }
2848 if let KeyCode::Char(c) = code {
2849 {
2860 let mode_has_binding = self
2861 .active_window()
2862 .editor_mode
2863 .as_ref()
2864 .map(|mode_name| {
2865 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
2866 let mode_ctx =
2867 crate::input::keybindings::KeyContext::Mode(mode_name.to_string());
2868 let keybindings = self.keybindings.read().unwrap();
2869 keybindings.has_explicit_binding(&key_event, &mode_ctx)
2870 })
2871 .unwrap_or(false);
2872 if mode_has_binding {
2873 return false;
2874 }
2875 }
2876 if modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) {
2883 if matches!(
2884 self.panel(slot).map(|f| f.placement),
2885 Some(super::PanelPlacement::LeftDock { .. })
2886 ) {
2887 self.blur_floating_panel(slot);
2888 return false;
2889 }
2890 return true;
2891 }
2892 let ch = if modifiers.contains(KeyModifiers::SHIFT) {
2893 c.to_uppercase().next().unwrap_or(c)
2894 } else {
2895 c
2896 };
2897 if ch == ' ' {
2908 self.handle_widget_command(
2909 panel_id,
2910 fresh_core::api::WidgetAction::Key {
2911 key: "Space".to_string(),
2912 },
2913 );
2914 return true;
2915 }
2916 self.handle_widget_command(
2917 panel_id,
2918 fresh_core::api::WidgetAction::TextInputChar {
2919 text: ch.to_string(),
2920 },
2921 );
2922 return true;
2923 }
2924 true
2929 }
2930
2931 fn close_quick_open_if_open(&mut self) -> bool {
2935 if let Some(prompt) = &self.active_window_mut().prompt {
2936 if prompt.prompt_type == PromptType::QuickOpen {
2937 self.cancel_prompt();
2938 return true;
2939 }
2940 }
2941 false
2942 }
2943
2944 fn refresh_active_search(&mut self) {
2948 if let Some(prompt) = &self.active_window_mut().prompt {
2949 if matches!(
2950 prompt.prompt_type,
2951 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch
2952 ) {
2953 let query = prompt.input.clone();
2954 self.update_search_highlights(&query);
2955 }
2956 } else if let Some(search_state) = &self.active_window().search_state {
2957 let query = search_state.query.clone();
2958 self.perform_search(&query);
2959 }
2960 }
2961
2962 fn handle_open_terminal_in_dock(&mut self) -> AnyhowResult<()> {
2964 use crate::model::event::SplitDirection;
2965 use crate::view::split::SplitRole;
2966
2967 if let Some(dock_leaf) = self
2968 .windows
2969 .get(&self.active_window)
2970 .and_then(|w| w.buffers.splits())
2971 .map(|(mgr, _)| mgr)
2972 .expect("active window must have a populated split layout")
2973 .find_leaf_by_role(SplitRole::UtilityDock)
2974 {
2975 self.windows
2977 .get_mut(&self.active_window)
2978 .and_then(|w| w.split_manager_mut())
2979 .expect("active window must have a populated split layout")
2980 .set_active_split(dock_leaf);
2981 self.open_terminal();
2982 return Ok(());
2983 }
2984
2985 let Some(terminal_id) = self.spawn_terminal_session() else {
2989 return Ok(());
2990 };
2991 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
2992
2993 let new_leaf = self
2995 .windows
2996 .get_mut(&self.active_window)
2997 .and_then(|w| w.split_manager_mut())
2998 .expect("active window must have a populated split layout")
2999 .split_root_positioned(SplitDirection::Horizontal, buffer_id, 0.7, false)
3000 .map_err(|e| {
3001 self.set_status_message(format!("Failed to create dock for terminal: {}", e));
3002 });
3003 let Ok(new_leaf) = new_leaf else {
3004 return Ok(());
3005 };
3006
3007 let mut view_state = crate::view::split::SplitViewState::with_buffer(
3008 self.terminal_width,
3009 self.terminal_height,
3010 buffer_id,
3011 );
3012 view_state.apply_config_defaults(
3015 false,
3016 false,
3017 self.active_window().resolve_line_wrap_for_buffer(buffer_id),
3018 self.config.editor.wrap_indent,
3019 self.active_window()
3020 .resolve_wrap_column_for_buffer(buffer_id),
3021 self.config.editor.rulers.clone(),
3022 0,
3023 );
3024 view_state.viewport.line_wrap_enabled = false;
3026
3027 self.windows
3028 .get_mut(&self.active_window)
3029 .and_then(|w| w.split_view_states_mut())
3030 .expect("active window must have a populated split layout")
3031 .insert(new_leaf, view_state);
3032 self.windows
3033 .get_mut(&self.active_window)
3034 .and_then(|w| w.split_manager_mut())
3035 .expect("active window must have a populated split layout")
3036 .set_leaf_role(new_leaf, Some(SplitRole::UtilityDock));
3037 self.windows
3038 .get_mut(&self.active_window)
3039 .and_then(|w| w.split_manager_mut())
3040 .expect("active window must have a populated split layout")
3041 .set_active_split(new_leaf);
3042
3043 self.active_window_mut().terminal_mode = true;
3045 self.active_window_mut().key_context = crate::input::keybindings::KeyContext::Terminal;
3046 self.active_window_mut().resize_visible_terminals();
3047
3048 let exit_key = self
3049 .keybindings
3050 .read()
3051 .unwrap()
3052 .find_keybinding_for_action(
3053 "terminal_escape",
3054 crate::input::keybindings::KeyContext::Terminal,
3055 )
3056 .unwrap_or_else(|| "Ctrl+Space".to_string());
3057 self.set_status_message(
3058 rust_i18n::t!("terminal.opened", id = terminal_id.0, exit_key = exit_key).to_string(),
3059 );
3060 tracing::info!(
3061 "Opened terminal {:?} into new dock leaf {:?} (buffer {:?})",
3062 terminal_id,
3063 new_leaf,
3064 buffer_id
3065 );
3066 Ok(())
3067 }
3068}