1use super::*;
2use crate::services::plugins::hooks::HookArgs;
3use anyhow::Result as AnyhowResult;
4use rust_i18n::t;
5impl Editor {
6 pub fn get_key_context(&self) -> crate::input::keybindings::KeyContext {
8 use crate::input::keybindings::KeyContext;
9
10 if self.settings_state.as_ref().is_some_and(|s| s.visible) {
12 KeyContext::Settings
13 } else if self.menu_state.active_menu.is_some() {
14 KeyContext::Menu
15 } else if self.is_prompting() {
16 KeyContext::Prompt
17 } else if self.active_state().popups.is_visible() {
18 KeyContext::Popup
19 } else {
20 self.key_context
22 }
23 }
24
25 pub fn handle_key(
28 &mut self,
29 code: crossterm::event::KeyCode,
30 modifiers: crossterm::event::KeyModifiers,
31 ) -> AnyhowResult<()> {
32 use crate::input::keybindings::Action;
33
34 let _t_total = std::time::Instant::now();
35
36 tracing::trace!(
37 "Editor.handle_key: code={:?}, modifiers={:?}",
38 code,
39 modifiers
40 );
41
42 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
44
45 if self.dispatch_terminal_input(&key_event).is_some() {
47 return Ok(());
48 }
49
50 let active_split = self.split_manager.active_split();
53 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
54 view_state.viewport.clear_skip_ensure_visible();
55 }
56
57 let mut context = self.get_key_context();
59
60 if matches!(context, crate::input::keybindings::KeyContext::Popup) {
63 let (is_transient_popup, has_selection) = {
65 let popup = self.active_state().popups.top();
66 (
67 popup.is_some_and(|p| p.transient),
68 popup.is_some_and(|p| p.has_selection()),
69 )
70 };
71
72 let is_copy_key = key_event.code == crossterm::event::KeyCode::Char('c')
74 && key_event
75 .modifiers
76 .contains(crossterm::event::KeyModifiers::CONTROL);
77
78 if is_transient_popup && !(has_selection && is_copy_key) {
79 self.hide_popup();
81 tracing::debug!("Dismissed transient popup on key press");
82 context = self.get_key_context();
84 }
85 }
86
87 if self.dispatch_modal_input(&key_event).is_some() {
89 return Ok(());
90 }
91
92 let should_check_mode_bindings = matches!(
95 context,
96 crate::input::keybindings::KeyContext::Normal
97 | crate::input::keybindings::KeyContext::FileExplorer
98 );
99
100 if should_check_mode_bindings {
101 if let Some(ref mode_name) = self.editor_mode {
103 if let Some(action_name) = self.mode_registry.resolve_chord_keybinding(
105 mode_name,
106 &self.chord_state,
107 code,
108 modifiers,
109 ) {
110 tracing::debug!("Mode chord resolved to action: {}", action_name);
111 self.chord_state.clear();
112 let action = Action::from_str(&action_name, &std::collections::HashMap::new())
113 .unwrap_or(Action::PluginAction(action_name));
114 return self.handle_action(action);
115 }
116
117 let is_potential_chord = self.mode_registry.is_chord_prefix(
119 mode_name,
120 &self.chord_state,
121 code,
122 modifiers,
123 );
124
125 if is_potential_chord {
126 tracing::debug!("Potential chord prefix in editor mode");
128 self.chord_state.push((code, modifiers));
129 return Ok(());
130 }
131
132 if !self.chord_state.is_empty() {
134 tracing::debug!("Chord sequence abandoned in mode, clearing state");
135 self.chord_state.clear();
136 }
137 }
138
139 if let Some(action_name) = self.resolve_mode_keybinding(code, modifiers) {
142 let action = Action::from_str(&action_name, &std::collections::HashMap::new())
143 .unwrap_or_else(|| Action::PluginAction(action_name.clone()));
144 return self.handle_action(action);
145 }
146
147 if let Some(ref mode_name) = self.editor_mode {
149 if self.mode_registry.is_read_only(mode_name) {
153 tracing::debug!(
154 "Ignoring unbound key in read-only mode {:?}",
155 self.editor_mode
156 );
157 return Ok(());
158 }
159 tracing::debug!(
161 "Mode {:?} is not read-only, allowing key through",
162 self.editor_mode
163 );
164 }
165 }
166
167 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
169 let chord_result = self
170 .keybindings
171 .resolve_chord(&self.chord_state, &key_event, context);
172
173 match chord_result {
174 crate::input::keybindings::ChordResolution::Complete(action) => {
175 tracing::debug!("Complete chord match -> Action: {:?}", action);
177 self.chord_state.clear();
178 return self.handle_action(action);
179 }
180 crate::input::keybindings::ChordResolution::Partial => {
181 tracing::debug!("Partial chord match - waiting for next key");
183 self.chord_state.push((code, modifiers));
184 return Ok(());
185 }
186 crate::input::keybindings::ChordResolution::NoMatch => {
187 if !self.chord_state.is_empty() {
189 tracing::debug!("Chord sequence abandoned, clearing state");
190 self.chord_state.clear();
191 }
192 }
193 }
194
195 let action = self.keybindings.resolve(&key_event, context);
197
198 tracing::trace!("Context: {:?} -> Action: {:?}", context, action);
199
200 match action {
203 Action::LspCompletion
204 | Action::LspGotoDefinition
205 | Action::LspReferences
206 | Action::LspHover
207 | Action::None => {
208 }
210 _ => {
211 self.cancel_pending_lsp_requests();
213 }
214 }
215
216 self.handle_action(action)
220 }
221
222 pub(super) fn handle_action(&mut self, action: Action) -> AnyhowResult<()> {
224 use crate::input::keybindings::Action;
225
226 self.record_macro_action(&action);
228
229 match action {
230 Action::Quit => self.quit(),
231 Action::ForceQuit => {
232 self.should_quit = true;
233 }
234 Action::Save => {
235 if self.active_state().buffer.file_path().is_none() {
237 self.start_prompt_with_initial_text(
238 t!("file.save_as_prompt").to_string(),
239 PromptType::SaveFileAs,
240 String::new(),
241 );
242 self.init_file_open_state();
243 } else if self.check_save_conflict().is_some() {
244 self.start_prompt(
246 t!("file.file_changed_prompt").to_string(),
247 PromptType::ConfirmSaveConflict,
248 );
249 } else {
250 self.save()?;
251 }
252 }
253 Action::SaveAs => {
254 let current_path = self
256 .active_state()
257 .buffer
258 .file_path()
259 .map(|p| {
260 p.strip_prefix(&self.working_dir)
262 .unwrap_or(p)
263 .to_string_lossy()
264 .to_string()
265 })
266 .unwrap_or_default();
267 self.start_prompt_with_initial_text(
268 t!("file.save_as_prompt").to_string(),
269 PromptType::SaveFileAs,
270 current_path,
271 );
272 self.init_file_open_state();
273 }
274 Action::Open => {
275 self.start_prompt(t!("file.open_prompt").to_string(), PromptType::OpenFile);
276 self.prefill_open_file_prompt();
277 self.init_file_open_state();
278 }
279 Action::SwitchProject => {
280 self.start_prompt(
281 t!("file.switch_project_prompt").to_string(),
282 PromptType::SwitchProject,
283 );
284 self.init_folder_open_state();
285 }
286 Action::GotoLine => self.start_prompt(
287 t!("file.goto_line_prompt").to_string(),
288 PromptType::GotoLine,
289 ),
290 Action::New => {
291 self.new_buffer();
292 }
293 Action::Close | Action::CloseTab => {
294 self.close_tab();
299 }
300 Action::Revert => {
301 if self.active_state().buffer.is_modified() {
303 let revert_key = t!("prompt.key.revert").to_string();
304 let cancel_key = t!("prompt.key.cancel").to_string();
305 self.start_prompt(
306 t!(
307 "prompt.revert_confirm",
308 revert_key = revert_key,
309 cancel_key = cancel_key
310 )
311 .to_string(),
312 PromptType::ConfirmRevert,
313 );
314 } else {
315 if let Err(e) = self.revert_file() {
317 self.set_status_message(
318 t!("error.failed_to_revert", error = e.to_string()).to_string(),
319 );
320 }
321 }
322 }
323 Action::ToggleAutoRevert => {
324 self.toggle_auto_revert();
325 }
326 Action::FormatBuffer => {
327 if let Err(e) = self.format_buffer() {
328 self.set_status_message(
329 t!("error.format_failed", error = e.to_string()).to_string(),
330 );
331 }
332 }
333 Action::TrimTrailingWhitespace => match self.trim_trailing_whitespace() {
334 Ok(true) => {
335 self.set_status_message(t!("whitespace.trimmed").to_string());
336 }
337 Ok(false) => {
338 self.set_status_message(t!("whitespace.no_trailing").to_string());
339 }
340 Err(e) => {
341 self.set_status_message(
342 t!("error.trim_whitespace_failed", error = e).to_string(),
343 );
344 }
345 },
346 Action::EnsureFinalNewline => match self.ensure_final_newline() {
347 Ok(true) => {
348 self.set_status_message(t!("whitespace.newline_added").to_string());
349 }
350 Ok(false) => {
351 self.set_status_message(t!("whitespace.already_has_newline").to_string());
352 }
353 Err(e) => {
354 self.set_status_message(
355 t!("error.ensure_newline_failed", error = e).to_string(),
356 );
357 }
358 },
359 Action::Copy => {
360 let state = self.active_state();
362 if let Some(popup) = state.popups.top() {
363 if popup.has_selection() {
364 if let Some(text) = popup.get_selected_text() {
365 self.clipboard.copy(text);
366 self.set_status_message(t!("clipboard.copied").to_string());
367 return Ok(());
368 }
369 }
370 }
371 let buffer_id = self.active_buffer();
373 if self.is_composite_buffer(buffer_id) {
374 if let Some(_handled) = self.handle_composite_action(buffer_id, &Action::Copy) {
375 return Ok(());
376 }
377 }
378 self.copy_selection()
379 }
380 Action::CopyWithTheme(theme) => self.copy_selection_with_theme(&theme),
381 Action::Cut => {
382 if self.is_editing_disabled() {
383 self.set_status_message(t!("buffer.editing_disabled").to_string());
384 return Ok(());
385 }
386 self.cut_selection()
387 }
388 Action::Paste => {
389 if self.is_editing_disabled() {
390 self.set_status_message(t!("buffer.editing_disabled").to_string());
391 return Ok(());
392 }
393 self.paste()
394 }
395 Action::YankWordForward => self.yank_word_forward(),
396 Action::YankWordBackward => self.yank_word_backward(),
397 Action::YankToLineEnd => self.yank_to_line_end(),
398 Action::YankToLineStart => self.yank_to_line_start(),
399 Action::Undo => {
400 self.handle_undo();
401 }
402 Action::Redo => {
403 self.handle_redo();
404 }
405 Action::ShowHelp => {
406 self.open_help_manual();
407 }
408 Action::ShowKeyboardShortcuts => {
409 self.open_keyboard_shortcuts();
410 }
411 Action::ShowWarnings => {
412 self.show_warnings_popup();
413 }
414 Action::ShowStatusLog => {
415 self.open_status_log();
416 }
417 Action::ShowLspStatus => {
418 self.show_lsp_status_popup();
419 }
420 Action::ClearWarnings => {
421 self.clear_warnings();
422 }
423 Action::CommandPalette => {
424 if let Some(prompt) = &self.prompt {
426 if prompt.prompt_type == PromptType::Command {
427 self.cancel_prompt();
428 return Ok(());
429 }
430 }
431
432 let active_buffer_mode = self
434 .buffer_metadata
435 .get(&self.active_buffer())
436 .and_then(|m| m.virtual_mode());
437 let suggestions = self.command_registry.read().unwrap().filter(
438 "",
439 self.key_context,
440 &self.keybindings,
441 self.has_active_selection(),
442 &self.active_custom_contexts,
443 active_buffer_mode,
444 );
445 self.start_prompt_with_suggestions(
446 t!("file.command_prompt").to_string(),
447 PromptType::Command,
448 suggestions,
449 );
450 }
451 Action::QuickOpen => {
452 if let Some(prompt) = &self.prompt {
454 if prompt.prompt_type == PromptType::QuickOpen {
455 self.cancel_prompt();
456 return Ok(());
457 }
458 }
459
460 self.start_quick_open();
462 }
463 Action::ToggleLineWrap => {
464 self.config.editor.line_wrap = !self.config.editor.line_wrap;
465
466 for view_state in self.split_view_states.values_mut() {
468 view_state.viewport.line_wrap_enabled = self.config.editor.line_wrap;
469 }
470
471 let state = if self.config.editor.line_wrap {
472 t!("view.state_enabled").to_string()
473 } else {
474 t!("view.state_disabled").to_string()
475 };
476 self.set_status_message(t!("view.line_wrap_state", state = state).to_string());
477 }
478 Action::ToggleComposeMode => {
479 self.handle_toggle_compose_mode();
480 }
481 Action::SetComposeWidth => {
482 let active_split = self.split_manager.active_split();
483 let current = self
484 .split_view_states
485 .get(&active_split)
486 .and_then(|v| v.compose_width.map(|w| w.to_string()))
487 .unwrap_or_default();
488 self.start_prompt_with_initial_text(
489 "Compose width (empty = viewport): ".to_string(),
490 PromptType::SetComposeWidth,
491 current,
492 );
493 }
494 Action::SetBackground => {
495 let default_path = self
496 .ansi_background_path
497 .as_ref()
498 .and_then(|p| {
499 p.strip_prefix(&self.working_dir)
500 .ok()
501 .map(|rel| rel.to_string_lossy().to_string())
502 })
503 .unwrap_or_else(|| DEFAULT_BACKGROUND_FILE.to_string());
504
505 self.start_prompt_with_initial_text(
506 "Background file: ".to_string(),
507 PromptType::SetBackgroundFile,
508 default_path,
509 );
510 }
511 Action::SetBackgroundBlend => {
512 let default_amount = format!("{:.2}", self.background_fade);
513 self.start_prompt_with_initial_text(
514 "Background blend (0-1): ".to_string(),
515 PromptType::SetBackgroundBlend,
516 default_amount,
517 );
518 }
519 Action::LspCompletion => {
520 self.request_completion()?;
521 }
522 Action::LspGotoDefinition => {
523 self.request_goto_definition()?;
524 }
525 Action::LspRename => {
526 self.start_rename()?;
527 }
528 Action::LspHover => {
529 self.request_hover()?;
530 }
531 Action::LspReferences => {
532 self.request_references()?;
533 }
534 Action::LspSignatureHelp => {
535 self.request_signature_help()?;
536 }
537 Action::LspCodeActions => {
538 self.request_code_actions()?;
539 }
540 Action::LspRestart => {
541 self.handle_lsp_restart();
542 }
543 Action::LspStop => {
544 self.handle_lsp_stop();
545 }
546 Action::ToggleInlayHints => {
547 self.toggle_inlay_hints();
548 }
549 Action::DumpConfig => {
550 self.dump_config();
551 }
552 Action::SelectTheme => {
553 self.start_select_theme_prompt();
554 }
555 Action::SelectKeybindingMap => {
556 self.start_select_keybinding_map_prompt();
557 }
558 Action::SelectCursorStyle => {
559 self.start_select_cursor_style_prompt();
560 }
561 Action::SelectLocale => {
562 self.start_select_locale_prompt();
563 }
564 Action::Search => {
565 let is_search_prompt = self.prompt.as_ref().is_some_and(|p| {
567 matches!(
568 p.prompt_type,
569 PromptType::Search
570 | PromptType::ReplaceSearch
571 | PromptType::QueryReplaceSearch
572 )
573 });
574
575 if is_search_prompt {
576 self.confirm_prompt();
577 } else {
578 self.start_search_prompt(
579 t!("file.search_prompt").to_string(),
580 PromptType::Search,
581 false,
582 );
583 }
584 }
585 Action::Replace => {
586 self.start_search_prompt(
588 t!("file.replace_prompt").to_string(),
589 PromptType::ReplaceSearch,
590 false,
591 );
592 }
593 Action::QueryReplace => {
594 self.search_confirm_each = true;
596 self.start_search_prompt(
597 "Query replace: ".to_string(),
598 PromptType::QueryReplaceSearch,
599 false,
600 );
601 }
602 Action::FindInSelection => {
603 self.start_search_prompt(
604 t!("file.search_prompt").to_string(),
605 PromptType::Search,
606 true,
607 );
608 }
609 Action::FindNext => {
610 self.find_next();
611 }
612 Action::FindPrevious => {
613 self.find_previous();
614 }
615 Action::FindSelectionNext => {
616 self.find_selection_next();
617 }
618 Action::FindSelectionPrevious => {
619 self.find_selection_previous();
620 }
621 Action::AddCursorNextMatch => self.add_cursor_at_next_match(),
622 Action::AddCursorAbove => self.add_cursor_above(),
623 Action::AddCursorBelow => self.add_cursor_below(),
624 Action::NextBuffer => self.next_buffer(),
625 Action::PrevBuffer => self.prev_buffer(),
626 Action::SwitchToPreviousTab => self.switch_to_previous_tab(),
627 Action::SwitchToTabByName => self.start_switch_to_tab_prompt(),
628
629 Action::ScrollTabsLeft => {
631 let active_split_id = self.split_manager.active_split();
632 if let Some(view_state) = self.split_view_states.get_mut(&active_split_id) {
633 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_sub(5);
634 self.set_status_message(t!("status.scrolled_tabs_left").to_string());
635 }
636 }
637 Action::ScrollTabsRight => {
638 let active_split_id = self.split_manager.active_split();
639 if let Some(view_state) = self.split_view_states.get_mut(&active_split_id) {
640 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_add(5);
641 self.set_status_message(t!("status.scrolled_tabs_right").to_string());
642 }
643 }
644 Action::NavigateBack => self.navigate_back(),
645 Action::NavigateForward => self.navigate_forward(),
646 Action::SplitHorizontal => self.split_pane_horizontal(),
647 Action::SplitVertical => self.split_pane_vertical(),
648 Action::CloseSplit => self.close_active_split(),
649 Action::NextSplit => self.next_split(),
650 Action::PrevSplit => self.prev_split(),
651 Action::IncreaseSplitSize => self.adjust_split_size(0.05),
652 Action::DecreaseSplitSize => self.adjust_split_size(-0.05),
653 Action::ToggleMaximizeSplit => self.toggle_maximize_split(),
654 Action::ToggleFileExplorer => self.toggle_file_explorer(),
655 Action::ToggleMenuBar => self.toggle_menu_bar(),
656 Action::ToggleTabBar => self.toggle_tab_bar(),
657 Action::ToggleLineNumbers => self.toggle_line_numbers(),
658 Action::ToggleMouseCapture => self.toggle_mouse_capture(),
659 Action::ToggleMouseHover => self.toggle_mouse_hover(),
660 Action::ToggleDebugHighlights => self.toggle_debug_highlights(),
661 Action::SetTabSize => {
663 let current = self
664 .buffers
665 .get(&self.active_buffer())
666 .map(|s| s.tab_size.to_string())
667 .unwrap_or_else(|| "4".to_string());
668 self.start_prompt_with_initial_text(
669 "Tab size: ".to_string(),
670 PromptType::SetTabSize,
671 current,
672 );
673 }
674 Action::SetLineEnding => {
675 self.start_set_line_ending_prompt();
676 }
677 Action::SetLanguage => {
678 self.start_set_language_prompt();
679 }
680 Action::ToggleIndentationStyle => {
681 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
682 state.use_tabs = !state.use_tabs;
683 let status = if state.use_tabs {
684 "Indentation: Tabs"
685 } else {
686 "Indentation: Spaces"
687 };
688 self.set_status_message(status.to_string());
689 }
690 }
691 Action::ToggleTabIndicators => {
692 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
693 state.show_whitespace_tabs = !state.show_whitespace_tabs;
694 let status = if state.show_whitespace_tabs {
695 "Tab indicators: Visible"
696 } else {
697 "Tab indicators: Hidden"
698 };
699 self.set_status_message(status.to_string());
700 }
701 }
702 Action::ResetBufferSettings => self.reset_buffer_settings(),
703 Action::FocusFileExplorer => self.focus_file_explorer(),
704 Action::FocusEditor => self.focus_editor(),
705 Action::FileExplorerUp => self.file_explorer_navigate_up(),
706 Action::FileExplorerDown => self.file_explorer_navigate_down(),
707 Action::FileExplorerPageUp => self.file_explorer_page_up(),
708 Action::FileExplorerPageDown => self.file_explorer_page_down(),
709 Action::FileExplorerExpand => self.file_explorer_toggle_expand(),
710 Action::FileExplorerCollapse => self.file_explorer_collapse(),
711 Action::FileExplorerOpen => self.file_explorer_open_file()?,
712 Action::FileExplorerRefresh => self.file_explorer_refresh(),
713 Action::FileExplorerNewFile => self.file_explorer_new_file(),
714 Action::FileExplorerNewDirectory => self.file_explorer_new_directory(),
715 Action::FileExplorerDelete => self.file_explorer_delete(),
716 Action::FileExplorerRename => self.file_explorer_rename(),
717 Action::FileExplorerToggleHidden => self.file_explorer_toggle_hidden(),
718 Action::FileExplorerToggleGitignored => self.file_explorer_toggle_gitignored(),
719 Action::RemoveSecondaryCursors => {
720 if let Some(events) = self.action_to_events(Action::RemoveSecondaryCursors) {
722 let batch = Event::Batch {
724 events: events.clone(),
725 description: "Remove secondary cursors".to_string(),
726 };
727 self.active_event_log_mut().append(batch.clone());
728 self.apply_event_to_active_buffer(&batch);
729
730 let active_split = self.split_manager.active_split();
732 let active_buffer = self.active_buffer();
733 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
734 let state = self.buffers.get_mut(&active_buffer).unwrap();
735 let primary = *state.cursors.primary();
736 view_state
737 .viewport
738 .ensure_visible(&mut state.buffer, &primary);
739 }
740 }
741 }
742
743 Action::MenuActivate => {
745 self.handle_menu_activate();
746 }
747 Action::MenuClose => {
748 self.handle_menu_close();
749 }
750 Action::MenuLeft => {
751 self.handle_menu_left();
752 }
753 Action::MenuRight => {
754 self.handle_menu_right();
755 }
756 Action::MenuUp => {
757 self.handle_menu_up();
758 }
759 Action::MenuDown => {
760 self.handle_menu_down();
761 }
762 Action::MenuExecute => {
763 if let Some(action) = self.handle_menu_execute() {
764 return self.handle_action(action);
765 }
766 }
767 Action::MenuOpen(menu_name) => {
768 self.handle_menu_open(&menu_name);
769 }
770
771 Action::SwitchKeybindingMap(map_name) => {
772 let is_builtin =
774 matches!(map_name.as_str(), "default" | "emacs" | "vscode" | "macos");
775 let is_user_defined = self.config.keybinding_maps.contains_key(&map_name);
776
777 if is_builtin || is_user_defined {
778 self.config.active_keybinding_map = map_name.clone().into();
780
781 self.keybindings =
783 crate::input::keybindings::KeybindingResolver::new(&self.config);
784
785 self.set_status_message(
786 t!("view.keybindings_switched", map = map_name).to_string(),
787 );
788 } else {
789 self.set_status_message(
790 t!("view.keybindings_unknown", map = map_name).to_string(),
791 );
792 }
793 }
794
795 Action::SmartHome => {
796 self.smart_home();
797 }
798 Action::ToggleComment => {
799 self.toggle_comment();
800 }
801 Action::GoToMatchingBracket => {
802 self.goto_matching_bracket();
803 }
804 Action::JumpToNextError => {
805 self.jump_to_next_error();
806 }
807 Action::JumpToPreviousError => {
808 self.jump_to_previous_error();
809 }
810 Action::SetBookmark(key) => {
811 self.set_bookmark(key);
812 }
813 Action::JumpToBookmark(key) => {
814 self.jump_to_bookmark(key);
815 }
816 Action::ClearBookmark(key) => {
817 self.clear_bookmark(key);
818 }
819 Action::ListBookmarks => {
820 self.list_bookmarks();
821 }
822 Action::ToggleSearchCaseSensitive => {
823 self.search_case_sensitive = !self.search_case_sensitive;
824 let state = if self.search_case_sensitive {
825 "enabled"
826 } else {
827 "disabled"
828 };
829 self.set_status_message(
830 t!("search.case_sensitive_state", state = state).to_string(),
831 );
832 if let Some(prompt) = &self.prompt {
835 if matches!(
836 prompt.prompt_type,
837 PromptType::Search
838 | PromptType::ReplaceSearch
839 | PromptType::QueryReplaceSearch
840 ) {
841 let query = prompt.input.clone();
842 self.update_search_highlights(&query);
843 }
844 } else if let Some(search_state) = &self.search_state {
845 let query = search_state.query.clone();
846 self.perform_search(&query);
847 }
848 }
849 Action::ToggleSearchWholeWord => {
850 self.search_whole_word = !self.search_whole_word;
851 let state = if self.search_whole_word {
852 "enabled"
853 } else {
854 "disabled"
855 };
856 self.set_status_message(t!("search.whole_word_state", state = state).to_string());
857 if let Some(prompt) = &self.prompt {
860 if matches!(
861 prompt.prompt_type,
862 PromptType::Search
863 | PromptType::ReplaceSearch
864 | PromptType::QueryReplaceSearch
865 ) {
866 let query = prompt.input.clone();
867 self.update_search_highlights(&query);
868 }
869 } else if let Some(search_state) = &self.search_state {
870 let query = search_state.query.clone();
871 self.perform_search(&query);
872 }
873 }
874 Action::ToggleSearchRegex => {
875 self.search_use_regex = !self.search_use_regex;
876 let state = if self.search_use_regex {
877 "enabled"
878 } else {
879 "disabled"
880 };
881 self.set_status_message(t!("search.regex_state", state = state).to_string());
882 if let Some(prompt) = &self.prompt {
885 if matches!(
886 prompt.prompt_type,
887 PromptType::Search
888 | PromptType::ReplaceSearch
889 | PromptType::QueryReplaceSearch
890 ) {
891 let query = prompt.input.clone();
892 self.update_search_highlights(&query);
893 }
894 } else if let Some(search_state) = &self.search_state {
895 let query = search_state.query.clone();
896 self.perform_search(&query);
897 }
898 }
899 Action::ToggleSearchConfirmEach => {
900 self.search_confirm_each = !self.search_confirm_each;
901 let state = if self.search_confirm_each {
902 "enabled"
903 } else {
904 "disabled"
905 };
906 self.set_status_message(t!("search.confirm_each_state", state = state).to_string());
907 }
908 Action::FileBrowserToggleHidden => {
909 self.file_open_toggle_hidden();
911 }
912 Action::StartMacroRecording => {
913 self.set_status_message(
915 "Use Ctrl+Shift+R to start recording (will prompt for register)".to_string(),
916 );
917 }
918 Action::StopMacroRecording => {
919 self.stop_macro_recording();
920 }
921 Action::PlayMacro(key) => {
922 self.play_macro(key);
923 }
924 Action::ToggleMacroRecording(key) => {
925 self.toggle_macro_recording(key);
926 }
927 Action::ShowMacro(key) => {
928 self.show_macro_in_buffer(key);
929 }
930 Action::ListMacros => {
931 self.list_macros_in_buffer();
932 }
933 Action::PromptRecordMacro => {
934 self.start_prompt("Record macro (0-9): ".to_string(), PromptType::RecordMacro);
935 }
936 Action::PromptPlayMacro => {
937 self.start_prompt("Play macro (0-9): ".to_string(), PromptType::PlayMacro);
938 }
939 Action::PlayLastMacro => {
940 if let Some(key) = self.last_macro_register {
941 self.play_macro(key);
942 } else {
943 self.set_status_message(t!("status.no_macro_recorded").to_string());
944 }
945 }
946 Action::PromptSetBookmark => {
947 self.start_prompt("Set bookmark (0-9): ".to_string(), PromptType::SetBookmark);
948 }
949 Action::PromptJumpToBookmark => {
950 self.start_prompt(
951 "Jump to bookmark (0-9): ".to_string(),
952 PromptType::JumpToBookmark,
953 );
954 }
955 Action::None => {}
956 Action::DeleteBackward => {
957 if self.is_editing_disabled() {
958 self.set_status_message(t!("buffer.editing_disabled").to_string());
959 return Ok(());
960 }
961 if let Some(events) = self.action_to_events(Action::DeleteBackward) {
963 if events.len() > 1 {
964 let description = "Delete backward".to_string();
966 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description)
967 {
968 self.active_event_log_mut().append(bulk_edit);
969 }
970 } else {
971 for event in events {
972 self.active_event_log_mut().append(event.clone());
973 self.apply_event_to_active_buffer(&event);
974 }
975 }
976 }
977 }
978 Action::PluginAction(action_name) => {
979 tracing::debug!("handle_action: PluginAction('{}')", action_name);
980 #[cfg(feature = "plugins")]
983 if let Some(result) = self.plugin_manager.execute_action_async(&action_name) {
984 match result {
985 Ok(receiver) => {
986 self.pending_plugin_actions
988 .push((action_name.clone(), receiver));
989 }
990 Err(e) => {
991 self.set_status_message(
992 t!("view.plugin_error", error = e.to_string()).to_string(),
993 );
994 tracing::error!("Plugin action error: {}", e);
995 }
996 }
997 } else {
998 self.set_status_message(t!("status.plugin_manager_unavailable").to_string());
999 }
1000 #[cfg(not(feature = "plugins"))]
1001 {
1002 let _ = action_name;
1003 self.set_status_message(
1004 "Plugins not available (compiled without plugin support)".to_string(),
1005 );
1006 }
1007 }
1008 Action::OpenTerminal => {
1009 self.open_terminal();
1010 }
1011 Action::CloseTerminal => {
1012 self.close_terminal();
1013 }
1014 Action::FocusTerminal => {
1015 if self.is_terminal_buffer(self.active_buffer()) {
1017 self.terminal_mode = true;
1018 self.key_context = KeyContext::Terminal;
1019 self.set_status_message(t!("status.terminal_mode_enabled").to_string());
1020 }
1021 }
1022 Action::TerminalEscape => {
1023 if self.terminal_mode {
1025 self.terminal_mode = false;
1026 self.key_context = KeyContext::Normal;
1027 self.set_status_message(t!("status.terminal_mode_disabled").to_string());
1028 }
1029 }
1030 Action::ToggleKeyboardCapture => {
1031 if self.terminal_mode {
1033 self.keyboard_capture = !self.keyboard_capture;
1034 if self.keyboard_capture {
1035 self.set_status_message(
1036 "Keyboard capture ON - all keys go to terminal (F9 to toggle)"
1037 .to_string(),
1038 );
1039 } else {
1040 self.set_status_message(
1041 "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
1042 );
1043 }
1044 }
1045 }
1046 Action::TerminalPaste => {
1047 if self.terminal_mode {
1049 if let Some(text) = self.clipboard.paste() {
1050 self.send_terminal_input(text.as_bytes());
1051 }
1052 }
1053 }
1054 Action::ShellCommand => {
1055 self.start_shell_command_prompt(false);
1057 }
1058 Action::ShellCommandReplace => {
1059 self.start_shell_command_prompt(true);
1061 }
1062 Action::OpenSettings => {
1063 self.open_settings();
1064 }
1065 Action::CloseSettings => {
1066 let has_changes = self
1068 .settings_state
1069 .as_ref()
1070 .is_some_and(|s| s.has_changes());
1071 if has_changes {
1072 if let Some(ref mut state) = self.settings_state {
1074 state.show_confirm_dialog();
1075 }
1076 } else {
1077 self.close_settings(false);
1078 }
1079 }
1080 Action::SettingsSave => {
1081 self.save_settings();
1082 }
1083 Action::SettingsReset => {
1084 if let Some(ref mut state) = self.settings_state {
1085 state.reset_current_to_default();
1086 }
1087 }
1088 Action::SettingsToggleFocus => {
1089 if let Some(ref mut state) = self.settings_state {
1090 state.toggle_focus();
1091 }
1092 }
1093 Action::SettingsActivate => {
1094 self.settings_activate_current();
1095 }
1096 Action::SettingsSearch => {
1097 if let Some(ref mut state) = self.settings_state {
1098 state.start_search();
1099 }
1100 }
1101 Action::SettingsHelp => {
1102 if let Some(ref mut state) = self.settings_state {
1103 state.toggle_help();
1104 }
1105 }
1106 Action::SettingsIncrement => {
1107 self.settings_increment_current();
1108 }
1109 Action::SettingsDecrement => {
1110 self.settings_decrement_current();
1111 }
1112 Action::CalibrateInput => {
1113 self.open_calibration_wizard();
1114 }
1115 Action::EventDebug => {
1116 self.open_event_debug();
1117 }
1118 Action::PromptConfirm => {
1119 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
1120 use super::prompt_actions::PromptResult;
1121 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
1122 PromptResult::ExecuteAction(action) => {
1123 return self.handle_action(action);
1124 }
1125 PromptResult::EarlyReturn => {
1126 return Ok(());
1127 }
1128 PromptResult::Done => {}
1129 }
1130 }
1131 }
1132 Action::PromptConfirmWithText(ref text) => {
1133 if let Some(ref mut prompt) = self.prompt {
1135 prompt.set_input(text.clone());
1136 self.update_prompt_suggestions();
1137 }
1138 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
1139 use super::prompt_actions::PromptResult;
1140 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
1141 PromptResult::ExecuteAction(action) => {
1142 return self.handle_action(action);
1143 }
1144 PromptResult::EarlyReturn => {
1145 return Ok(());
1146 }
1147 PromptResult::Done => {}
1148 }
1149 }
1150 }
1151 Action::PopupConfirm => {
1152 use super::popup_actions::PopupConfirmResult;
1153 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
1154 return Ok(());
1155 }
1156 }
1157 Action::PopupCancel => {
1158 self.handle_popup_cancel();
1159 }
1160 Action::InsertChar(c) => {
1161 if self.is_prompting() {
1162 return self.handle_insert_char_prompt(c);
1163 } else {
1164 self.handle_insert_char_editor(c)?;
1165 }
1166 }
1167 Action::PromptCopy => {
1169 if let Some(prompt) = &self.prompt {
1170 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
1171 if !text.is_empty() {
1172 self.clipboard.copy(text);
1173 self.set_status_message(t!("clipboard.copied").to_string());
1174 }
1175 }
1176 }
1177 Action::PromptCut => {
1178 if let Some(prompt) = &self.prompt {
1179 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
1180 if !text.is_empty() {
1181 self.clipboard.copy(text);
1182 }
1183 }
1184 if let Some(prompt) = self.prompt.as_mut() {
1185 if prompt.has_selection() {
1186 prompt.delete_selection();
1187 } else {
1188 prompt.clear();
1189 }
1190 }
1191 self.set_status_message(t!("clipboard.cut").to_string());
1192 self.update_prompt_suggestions();
1193 }
1194 Action::PromptPaste => {
1195 if let Some(text) = self.clipboard.paste() {
1196 if let Some(prompt) = self.prompt.as_mut() {
1197 prompt.insert_str(&text);
1198 }
1199 self.update_prompt_suggestions();
1200 }
1201 }
1202 _ => {
1203 self.apply_action_as_events(action)?;
1209 }
1210 }
1211
1212 Ok(())
1213 }
1214
1215 pub(super) fn handle_mouse_scroll(
1217 &mut self,
1218 col: u16,
1219 row: u16,
1220 delta: i32,
1221 ) -> AnyhowResult<()> {
1222 self.sync_editor_state_to_split_view_state();
1228
1229 if let Some(explorer_area) = self.cached_layout.file_explorer_area {
1231 if col >= explorer_area.x
1232 && col < explorer_area.x + explorer_area.width
1233 && row >= explorer_area.y
1234 && row < explorer_area.y + explorer_area.height
1235 {
1236 if let Some(explorer) = &mut self.file_explorer {
1238 let visible = explorer.tree().get_visible_nodes();
1239 if visible.is_empty() {
1240 return Ok(());
1241 }
1242
1243 let current_index = explorer.get_selected_index().unwrap_or(0);
1245
1246 let new_index = if delta < 0 {
1248 current_index.saturating_sub(delta.unsigned_abs() as usize)
1250 } else {
1251 (current_index + delta as usize).min(visible.len() - 1)
1253 };
1254
1255 if let Some(node_id) = explorer.get_node_at_index(new_index) {
1257 explorer.set_selected(Some(node_id));
1258 explorer.update_scroll_for_selection();
1259 }
1260 }
1261 return Ok(());
1262 }
1263 }
1264
1265 let active_split = self.split_manager.active_split();
1268 let buffer_id = self.active_buffer();
1269
1270 if self.is_composite_buffer(buffer_id) {
1272 let max_row = self
1273 .composite_buffers
1274 .get(&buffer_id)
1275 .map(|c| c.row_count().saturating_sub(1))
1276 .unwrap_or(0);
1277 if let Some(view_state) = self
1278 .composite_view_states
1279 .get_mut(&(active_split, buffer_id))
1280 {
1281 view_state.scroll(delta as isize, max_row);
1282 tracing::trace!(
1283 "handle_mouse_scroll (composite): delta={}, scroll_row={}",
1284 delta,
1285 view_state.scroll_row
1286 );
1287 }
1288 return Ok(());
1289 }
1290
1291 let view_transform_tokens = self
1293 .split_view_states
1294 .get(&active_split)
1295 .and_then(|vs| vs.view_transform.as_ref())
1296 .map(|vt| vt.tokens.clone());
1297
1298 let buffer = self.buffers.get_mut(&buffer_id).map(|s| &mut s.buffer);
1300 let view_state = self.split_view_states.get_mut(&active_split);
1301
1302 if let (Some(buffer), Some(view_state)) = (buffer, view_state) {
1303 let top_byte_before = view_state.viewport.top_byte;
1304 if let Some(tokens) = view_transform_tokens {
1305 use crate::view::ui::view_pipeline::ViewLineIterator;
1307 let tab_size = self.config.editor.tab_size;
1308 let view_lines: Vec<_> =
1309 ViewLineIterator::new(&tokens, false, false, tab_size).collect();
1310 view_state
1311 .viewport
1312 .scroll_view_lines(&view_lines, delta as isize);
1313 } else {
1314 if delta < 0 {
1316 let lines_to_scroll = delta.unsigned_abs() as usize;
1318 view_state.viewport.scroll_up(buffer, lines_to_scroll);
1319 } else {
1320 let lines_to_scroll = delta as usize;
1322 view_state.viewport.scroll_down(buffer, lines_to_scroll);
1323 }
1324 }
1325 view_state.viewport.set_skip_ensure_visible();
1327 tracing::trace!(
1328 "handle_mouse_scroll: delta={}, top_byte {} -> {}",
1329 delta,
1330 top_byte_before,
1331 view_state.viewport.top_byte
1332 );
1333 }
1334
1335 Ok(())
1336 }
1337
1338 pub(super) fn handle_scrollbar_drag_relative(
1340 &mut self,
1341 row: u16,
1342 split_id: SplitId,
1343 buffer_id: BufferId,
1344 scrollbar_rect: ratatui::layout::Rect,
1345 ) -> AnyhowResult<()> {
1346 let drag_start_row = match self.mouse_state.drag_start_row {
1347 Some(r) => r,
1348 None => return Ok(()), };
1350
1351 let drag_start_top_byte = match self.mouse_state.drag_start_top_byte {
1352 Some(b) => b,
1353 None => return Ok(()), };
1355
1356 let row_offset = (row as i32) - (drag_start_row as i32);
1358
1359 let viewport_height = self
1361 .split_view_states
1362 .get(&split_id)
1363 .map(|vs| vs.viewport.height as usize)
1364 .unwrap_or(10);
1365
1366 let line_start = if let Some(state) = self.buffers.get_mut(&buffer_id) {
1368 let scrollbar_height = scrollbar_rect.height as usize;
1369 if scrollbar_height == 0 {
1370 return Ok(());
1371 }
1372
1373 let buffer_len = state.buffer.len();
1374 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
1375
1376 let new_top_byte = if buffer_len <= large_file_threshold {
1379 let total_lines = if buffer_len > 0 {
1382 state.buffer.get_line_number(buffer_len.saturating_sub(1)) + 1
1383 } else {
1384 1
1385 };
1386
1387 let max_scroll_line = total_lines.saturating_sub(viewport_height);
1389
1390 if max_scroll_line == 0 {
1391 0
1393 } else {
1394 let relative_mouse_row = row.saturating_sub(scrollbar_rect.y) as usize;
1397 let scroll_ratio = if scrollbar_height > 1 {
1399 (relative_mouse_row as f64 / (scrollbar_height - 1) as f64).clamp(0.0, 1.0)
1400 } else {
1401 0.0
1402 };
1403
1404 let target_line = (scroll_ratio * max_scroll_line as f64).round() as usize;
1406 let target_line = target_line.min(max_scroll_line);
1407
1408 let mut iter = state.buffer.line_iterator(0, 80);
1412 let mut line_byte = 0;
1413
1414 for _ in 0..target_line {
1415 if let Some((pos, _content)) = iter.next_line() {
1416 line_byte = pos;
1417 } else {
1418 break;
1419 }
1420 }
1421
1422 if let Some((pos, _)) = iter.next_line() {
1424 pos
1425 } else {
1426 line_byte }
1428 }
1429 } else {
1430 let bytes_per_pixel = buffer_len as f64 / scrollbar_height as f64;
1432 let byte_offset = (row_offset as f64 * bytes_per_pixel) as i64;
1433
1434 let new_top_byte = if byte_offset >= 0 {
1435 drag_start_top_byte.saturating_add(byte_offset as usize)
1436 } else {
1437 drag_start_top_byte.saturating_sub((-byte_offset) as usize)
1438 };
1439
1440 new_top_byte.min(buffer_len.saturating_sub(1))
1442 };
1443
1444 let iter = state.buffer.line_iterator(new_top_byte, 80);
1446 iter.current_position()
1447 } else {
1448 return Ok(());
1449 };
1450
1451 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
1453 view_state.viewport.top_byte = line_start;
1454 view_state.viewport.set_skip_ensure_visible();
1456 }
1457
1458 self.move_cursor_to_visible_area(split_id, buffer_id);
1460
1461 Ok(())
1462 }
1463
1464 pub(super) fn handle_scrollbar_jump(
1466 &mut self,
1467 _col: u16,
1468 row: u16,
1469 split_id: SplitId,
1470 buffer_id: BufferId,
1471 scrollbar_rect: ratatui::layout::Rect,
1472 ) -> AnyhowResult<()> {
1473 let scrollbar_height = scrollbar_rect.height as usize;
1475 if scrollbar_height == 0 {
1476 return Ok(());
1477 }
1478
1479 let relative_row = row.saturating_sub(scrollbar_rect.y);
1482 let ratio = if scrollbar_height > 1 {
1483 ((relative_row as f64) / ((scrollbar_height - 1) as f64)).clamp(0.0, 1.0)
1484 } else {
1485 0.0
1486 };
1487
1488 let viewport_height = self
1490 .split_view_states
1491 .get(&split_id)
1492 .map(|vs| vs.viewport.height as usize)
1493 .unwrap_or(10);
1494
1495 let limited_line_start = if let Some(state) = self.buffers.get_mut(&buffer_id) {
1497 let buffer_len = state.buffer.len();
1498 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
1499
1500 let target_byte = if buffer_len <= large_file_threshold {
1503 let total_lines = if buffer_len > 0 {
1505 state.buffer.get_line_number(buffer_len.saturating_sub(1)) + 1
1506 } else {
1507 1
1508 };
1509
1510 let max_scroll_line = total_lines.saturating_sub(viewport_height);
1511
1512 if max_scroll_line == 0 {
1513 0
1515 } else {
1516 let target_line = (ratio * max_scroll_line as f64).round() as usize;
1518 let target_line = target_line.min(max_scroll_line);
1519
1520 let mut iter = state.buffer.line_iterator(0, 80);
1524 let mut line_byte = 0;
1525
1526 for _ in 0..target_line {
1527 if let Some((pos, _content)) = iter.next_line() {
1528 line_byte = pos;
1529 } else {
1530 break;
1531 }
1532 }
1533
1534 if let Some((pos, _)) = iter.next_line() {
1536 pos
1537 } else {
1538 line_byte }
1540 }
1541 } else {
1542 let target_byte = (buffer_len as f64 * ratio) as usize;
1544 target_byte.min(buffer_len.saturating_sub(1))
1545 };
1546
1547 let iter = state.buffer.line_iterator(target_byte, 80);
1549 let line_start = iter.current_position();
1550
1551 let max_top_byte = if buffer_len <= large_file_threshold {
1555 Self::calculate_max_scroll_position(&mut state.buffer, viewport_height)
1556 } else {
1557 buffer_len.saturating_sub(1)
1558 };
1559 line_start.min(max_top_byte)
1560 } else {
1561 return Ok(());
1562 };
1563
1564 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
1566 view_state.viewport.top_byte = limited_line_start;
1567 view_state.viewport.set_skip_ensure_visible();
1569 }
1570
1571 self.move_cursor_to_visible_area(split_id, buffer_id);
1573
1574 Ok(())
1575 }
1576
1577 pub(super) fn move_cursor_to_visible_area(&mut self, split_id: SplitId, buffer_id: BufferId) {
1580 let (top_byte, viewport_height) =
1582 if let Some(view_state) = self.split_view_states.get(&split_id) {
1583 (
1584 view_state.viewport.top_byte,
1585 view_state.viewport.height as usize,
1586 )
1587 } else {
1588 return;
1589 };
1590
1591 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1592 let buffer_len = state.buffer.len();
1593
1594 let mut iter = state.buffer.line_iterator(top_byte, 80);
1597 let mut bottom_byte = buffer_len;
1598
1599 for _ in 0..viewport_height {
1601 if let Some((pos, line)) = iter.next_line() {
1602 bottom_byte = pos + line.len();
1604 } else {
1605 bottom_byte = buffer_len;
1607 break;
1608 }
1609 }
1610
1611 let cursor_pos = state.cursors.primary().position;
1613 if cursor_pos < top_byte || cursor_pos > bottom_byte {
1614 let cursor = state.cursors.primary_mut();
1616 cursor.position = top_byte;
1617 }
1619 }
1620 }
1621
1622 pub(super) fn calculate_max_scroll_position(
1625 buffer: &mut crate::model::buffer::Buffer,
1626 viewport_height: usize,
1627 ) -> usize {
1628 if viewport_height == 0 {
1629 return 0;
1630 }
1631
1632 let buffer_len = buffer.len();
1633 if buffer_len == 0 {
1634 return 0;
1635 }
1636
1637 let mut line_count = 0;
1639 let mut iter = buffer.line_iterator(0, 80);
1640 while iter.next_line().is_some() {
1641 line_count += 1;
1642 }
1643
1644 if line_count <= viewport_height {
1646 return 0;
1647 }
1648
1649 let scrollable_lines = line_count.saturating_sub(viewport_height);
1652
1653 let mut iter = buffer.line_iterator(0, 80);
1655 let mut current_line = 0;
1656 let mut max_byte_pos = 0;
1657
1658 while current_line < scrollable_lines {
1659 if let Some((pos, _content)) = iter.next_line() {
1660 max_byte_pos = pos;
1661 current_line += 1;
1662 } else {
1663 break;
1664 }
1665 }
1666
1667 max_byte_pos
1668 }
1669
1670 pub(crate) fn screen_to_buffer_position(
1674 col: u16,
1675 row: u16,
1676 content_rect: ratatui::layout::Rect,
1677 gutter_width: u16,
1678 cached_mappings: &Option<Vec<crate::app::types::ViewLineMapping>>,
1679 fallback_position: usize,
1680 allow_gutter_click: bool,
1681 ) -> Option<usize> {
1682 let content_col = col.saturating_sub(content_rect.x);
1684 let content_row = row.saturating_sub(content_rect.y);
1685
1686 tracing::trace!(
1687 col,
1688 row,
1689 ?content_rect,
1690 gutter_width,
1691 content_col,
1692 content_row,
1693 num_mappings = cached_mappings.as_ref().map(|m| m.len()),
1694 "screen_to_buffer_position"
1695 );
1696
1697 let text_col = if content_col < gutter_width {
1699 if !allow_gutter_click {
1700 return None; }
1702 0 } else {
1704 content_col.saturating_sub(gutter_width) as usize
1705 };
1706
1707 let visual_row = content_row as usize;
1709
1710 let position_from_mapping =
1712 |line_mapping: &crate::app::types::ViewLineMapping, col: usize| -> usize {
1713 if col < line_mapping.visual_to_char.len() {
1714 if let Some(byte_pos) = line_mapping.source_byte_at_visual_col(col) {
1716 return byte_pos;
1717 }
1718 for c in (0..col).rev() {
1720 if let Some(byte_pos) = line_mapping.source_byte_at_visual_col(c) {
1721 return byte_pos;
1722 }
1723 }
1724 line_mapping.line_end_byte
1725 } else {
1726 if line_mapping.visual_to_char.len() <= 1 {
1730 if let Some(Some(first_byte)) = line_mapping.char_source_bytes.first() {
1732 return *first_byte;
1733 }
1734 }
1735 line_mapping.line_end_byte
1736 }
1737 };
1738
1739 let position = cached_mappings
1740 .as_ref()
1741 .and_then(|mappings| {
1742 if let Some(line_mapping) = mappings.get(visual_row) {
1743 Some(position_from_mapping(line_mapping, text_col))
1745 } else if !mappings.is_empty() {
1746 let last_mapping = mappings.last().unwrap();
1748 Some(position_from_mapping(last_mapping, text_col))
1749 } else {
1750 None
1751 }
1752 })
1753 .unwrap_or(fallback_position);
1754
1755 Some(position)
1756 }
1757
1758 pub(super) fn handle_editor_click(
1760 &mut self,
1761 col: u16,
1762 row: u16,
1763 split_id: crate::model::event::SplitId,
1764 buffer_id: BufferId,
1765 content_rect: ratatui::layout::Rect,
1766 modifiers: crossterm::event::KeyModifiers,
1767 ) -> AnyhowResult<()> {
1768 use crate::model::event::Event;
1769 use crossterm::event::KeyModifiers;
1770
1771 let modifiers_str = if modifiers.contains(KeyModifiers::SHIFT) {
1773 "shift".to_string()
1774 } else {
1775 String::new()
1776 };
1777
1778 if self.plugin_manager.has_hook_handlers("mouse_click") {
1781 self.plugin_manager.run_hook(
1782 "mouse_click",
1783 HookArgs::MouseClick {
1784 column: col,
1785 row,
1786 button: "left".to_string(),
1787 modifiers: modifiers_str,
1788 content_x: content_rect.x,
1789 content_y: content_rect.y,
1790 },
1791 );
1792 }
1793
1794 self.focus_split(split_id, buffer_id);
1796
1797 if self.is_composite_buffer(buffer_id) {
1799 return self.handle_composite_click(col, row, split_id, buffer_id, content_rect);
1800 }
1801
1802 if !self.is_terminal_buffer(buffer_id) {
1805 self.key_context = crate::input::keybindings::KeyContext::Normal;
1806 }
1807
1808 let cached_mappings = self
1810 .cached_layout
1811 .view_line_mappings
1812 .get(&split_id)
1813 .cloned();
1814
1815 let fallback = self
1817 .split_view_states
1818 .get(&split_id)
1819 .map(|vs| vs.viewport.top_byte)
1820 .unwrap_or(0);
1821
1822 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1824 let gutter_width = state.margins.left_total_width() as u16;
1825
1826 let Some(target_position) = Self::screen_to_buffer_position(
1827 col,
1828 row,
1829 content_rect,
1830 gutter_width,
1831 &cached_mappings,
1832 fallback,
1833 true, ) else {
1835 return Ok(());
1836 };
1837
1838 let onclick_action = state
1841 .text_properties
1842 .get_at(target_position)
1843 .iter()
1844 .find_map(|prop| {
1845 prop.get("onClick")
1846 .and_then(|v| v.as_str())
1847 .map(|s| s.to_string())
1848 });
1849
1850 if let Some(action_name) = onclick_action {
1851 tracing::debug!(
1853 "onClick triggered at position {}: action={}",
1854 target_position,
1855 action_name
1856 );
1857 let empty_args = std::collections::HashMap::new();
1858 if let Some(action) = Action::from_str(&action_name, &empty_args) {
1859 return self.handle_action(action);
1860 }
1861 return Ok(());
1862 }
1863
1864 let primary_cursor_id = state.cursors.primary_id();
1867 let primary_cursor = state.cursors.primary();
1868 let old_position = primary_cursor.position;
1869 let old_anchor = primary_cursor.anchor;
1870
1871 let extend_selection = modifiers.contains(KeyModifiers::SHIFT)
1874 || modifiers.contains(KeyModifiers::CONTROL);
1875 let new_anchor = if extend_selection {
1876 Some(old_anchor.unwrap_or(old_position))
1878 } else {
1879 None };
1881
1882 let event = Event::MoveCursor {
1883 cursor_id: primary_cursor_id,
1884 old_position,
1885 new_position: target_position,
1886 old_anchor,
1887 new_anchor,
1888 old_sticky_column: 0,
1889 new_sticky_column: 0, };
1891
1892 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
1894 event_log.append(event.clone());
1895 }
1896 state.apply(&event);
1897
1898 if !self.in_navigation {
1900 self.position_history
1901 .record_movement(buffer_id, target_position, None);
1902 }
1903
1904 self.mouse_state.dragging_text_selection = true;
1906 self.mouse_state.drag_selection_split = Some(split_id);
1907 self.mouse_state.drag_selection_anchor = Some(new_anchor.unwrap_or(target_position));
1909 }
1910
1911 Ok(())
1912 }
1913
1914 pub(super) fn handle_file_explorer_click(
1916 &mut self,
1917 col: u16,
1918 row: u16,
1919 explorer_area: ratatui::layout::Rect,
1920 ) -> AnyhowResult<()> {
1921 if row == explorer_area.y {
1923 let close_button_x = explorer_area.x + explorer_area.width.saturating_sub(3);
1926 if col >= close_button_x && col < explorer_area.x + explorer_area.width {
1927 self.toggle_file_explorer();
1928 return Ok(());
1929 }
1930 }
1931
1932 self.key_context = crate::input::keybindings::KeyContext::FileExplorer;
1934
1935 let relative_row = row.saturating_sub(explorer_area.y + 1); if let Some(ref mut explorer) = self.file_explorer {
1940 let display_nodes = explorer.get_display_nodes();
1941 let scroll_offset = explorer.get_scroll_offset();
1942 let clicked_index = (relative_row as usize) + scroll_offset;
1943
1944 if clicked_index < display_nodes.len() {
1945 let (node_id, _indent) = display_nodes[clicked_index];
1946
1947 explorer.set_selected(Some(node_id));
1949
1950 let node = explorer.tree().get_node(node_id);
1952 if let Some(node) = node {
1953 if node.is_dir() {
1954 self.file_explorer_toggle_expand();
1956 } else if node.is_file() {
1957 let path = node.entry.path.clone();
1960 let name = node.entry.name.clone();
1961 self.open_file(&path)?;
1962 self.set_status_message(
1963 rust_i18n::t!("explorer.opened_file", name = &name).to_string(),
1964 );
1965 }
1966 }
1967 }
1968 }
1969
1970 Ok(())
1971 }
1972
1973 fn start_set_line_ending_prompt(&mut self) {
1975 use crate::model::buffer::LineEnding;
1976
1977 let current_line_ending = self.active_state().buffer.line_ending();
1978
1979 let options = [
1980 (LineEnding::LF, "LF", "Unix/Linux/Mac"),
1981 (LineEnding::CRLF, "CRLF", "Windows"),
1982 (LineEnding::CR, "CR", "Classic Mac"),
1983 ];
1984
1985 let current_index = options
1986 .iter()
1987 .position(|(le, _, _)| *le == current_line_ending)
1988 .unwrap_or(0);
1989
1990 let suggestions: Vec<crate::input::commands::Suggestion> = options
1991 .iter()
1992 .map(|(le, name, desc)| {
1993 let is_current = *le == current_line_ending;
1994 crate::input::commands::Suggestion {
1995 text: format!("{} ({})", name, desc),
1996 description: if is_current {
1997 Some("current".to_string())
1998 } else {
1999 None
2000 },
2001 value: Some(name.to_string()),
2002 disabled: false,
2003 keybinding: None,
2004 source: None,
2005 }
2006 })
2007 .collect();
2008
2009 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
2010 "Line ending: ".to_string(),
2011 PromptType::SetLineEnding,
2012 suggestions,
2013 ));
2014
2015 if let Some(prompt) = self.prompt.as_mut() {
2016 if !prompt.suggestions.is_empty() {
2017 prompt.selected_suggestion = Some(current_index);
2018 let (_, name, desc) = options[current_index];
2019 prompt.input = format!("{} ({})", name, desc);
2020 prompt.cursor_pos = prompt.input.len();
2021 }
2022 }
2023 }
2024
2025 fn start_set_language_prompt(&mut self) {
2027 let current_language = self.active_state().language.clone();
2028
2029 let mut suggestions: Vec<crate::input::commands::Suggestion> = vec![
2031 crate::input::commands::Suggestion {
2033 text: "Plain Text".to_string(),
2034 description: if current_language == "Plain Text" || current_language == "text" {
2035 Some("current".to_string())
2036 } else {
2037 None
2038 },
2039 value: Some("Plain Text".to_string()),
2040 disabled: false,
2041 keybinding: None,
2042 source: None,
2043 },
2044 ];
2045
2046 let mut syntax_names: Vec<&str> = self.grammar_registry.available_syntaxes();
2048 syntax_names.sort_unstable_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
2050
2051 for syntax_name in syntax_names {
2052 if syntax_name == "Plain Text" {
2054 continue;
2055 }
2056 let is_current = syntax_name == current_language;
2057 suggestions.push(crate::input::commands::Suggestion {
2058 text: syntax_name.to_string(),
2059 description: if is_current {
2060 Some("current".to_string())
2061 } else {
2062 None
2063 },
2064 value: Some(syntax_name.to_string()),
2065 disabled: false,
2066 keybinding: None,
2067 source: None,
2068 });
2069 }
2070
2071 let current_index = suggestions
2073 .iter()
2074 .position(|s| s.value.as_deref() == Some(¤t_language))
2075 .unwrap_or(0);
2076
2077 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
2078 "Language: ".to_string(),
2079 PromptType::SetLanguage,
2080 suggestions,
2081 ));
2082
2083 if let Some(prompt) = self.prompt.as_mut() {
2084 if !prompt.suggestions.is_empty() {
2085 prompt.selected_suggestion = Some(current_index);
2086 }
2089 }
2090 }
2091
2092 fn start_select_theme_prompt(&mut self) {
2094 let available_themes = self.theme_registry.list();
2095 let current_theme_name = &self.theme.name;
2096
2097 let current_index = available_themes
2099 .iter()
2100 .position(|info| info.name == *current_theme_name)
2101 .unwrap_or(0);
2102
2103 let suggestions: Vec<crate::input::commands::Suggestion> = available_themes
2104 .iter()
2105 .map(|info| {
2106 let is_current = info.name == *current_theme_name;
2107 let description = match (is_current, info.pack.is_empty()) {
2108 (true, true) => Some("(current)".to_string()),
2109 (true, false) => Some(format!("{} (current)", info.pack)),
2110 (false, true) => None,
2111 (false, false) => Some(info.pack.clone()),
2112 };
2113 crate::input::commands::Suggestion {
2114 text: info.name.clone(),
2115 description,
2116 value: Some(info.name.clone()),
2117 disabled: false,
2118 keybinding: None,
2119 source: None,
2120 }
2121 })
2122 .collect();
2123
2124 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
2125 "Select theme: ".to_string(),
2126 PromptType::SelectTheme {
2127 original_theme: current_theme_name.clone(),
2128 },
2129 suggestions,
2130 ));
2131
2132 if let Some(prompt) = self.prompt.as_mut() {
2133 if !prompt.suggestions.is_empty() {
2134 prompt.selected_suggestion = Some(current_index);
2135 prompt.input = current_theme_name.to_string();
2137 prompt.cursor_pos = prompt.input.len();
2138 }
2139 }
2140 }
2141
2142 pub(super) fn apply_theme(&mut self, theme_name: &str) {
2144 if !theme_name.is_empty() {
2145 if let Some(theme) = self.theme_registry.get_cloned(theme_name) {
2146 self.theme = theme;
2147
2148 self.theme.set_terminal_cursor_color();
2150
2151 self.config.theme = self.theme.name.clone().into();
2153
2154 self.save_theme_to_config();
2156
2157 self.set_status_message(
2158 t!("view.theme_changed", theme = self.theme.name.clone()).to_string(),
2159 );
2160 } else {
2161 self.set_status_message(format!("Theme '{}' not found", theme_name));
2162 }
2163 }
2164 }
2165
2166 pub(super) fn preview_theme(&mut self, theme_name: &str) {
2169 if !theme_name.is_empty() && theme_name != self.theme.name {
2170 if let Some(theme) = self.theme_registry.get_cloned(theme_name) {
2171 self.theme = theme;
2172 self.theme.set_terminal_cursor_color();
2173 }
2174 }
2175 }
2176
2177 fn save_theme_to_config(&mut self) {
2179 if let Err(e) = self.filesystem.create_dir_all(&self.dir_context.config_dir) {
2181 tracing::warn!("Failed to create config directory: {}", e);
2182 return;
2183 }
2184
2185 let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
2187 if let Err(e) = resolver.save_to_layer(&self.config, ConfigLayer::User) {
2188 tracing::warn!("Failed to save theme to config: {}", e);
2189 }
2190 }
2191
2192 fn start_select_keybinding_map_prompt(&mut self) {
2194 let builtin_maps = vec!["default", "emacs", "vscode", "macos"];
2196
2197 let user_maps: Vec<&str> = self
2199 .config
2200 .keybinding_maps
2201 .keys()
2202 .map(|s| s.as_str())
2203 .collect();
2204
2205 let mut all_maps: Vec<&str> = builtin_maps;
2207 for map in &user_maps {
2208 if !all_maps.contains(map) {
2209 all_maps.push(map);
2210 }
2211 }
2212
2213 let current_map = &self.config.active_keybinding_map;
2214
2215 let current_index = all_maps
2217 .iter()
2218 .position(|name| *name == current_map)
2219 .unwrap_or(0);
2220
2221 let suggestions: Vec<crate::input::commands::Suggestion> = all_maps
2222 .iter()
2223 .map(|map_name| {
2224 let is_current = *map_name == current_map;
2225 crate::input::commands::Suggestion {
2226 text: map_name.to_string(),
2227 description: if is_current {
2228 Some("(current)".to_string())
2229 } else {
2230 None
2231 },
2232 value: Some(map_name.to_string()),
2233 disabled: false,
2234 keybinding: None,
2235 source: None,
2236 }
2237 })
2238 .collect();
2239
2240 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
2241 "Select keybinding map: ".to_string(),
2242 PromptType::SelectKeybindingMap,
2243 suggestions,
2244 ));
2245
2246 if let Some(prompt) = self.prompt.as_mut() {
2247 if !prompt.suggestions.is_empty() {
2248 prompt.selected_suggestion = Some(current_index);
2249 prompt.input = current_map.to_string();
2251 prompt.cursor_pos = prompt.input.len();
2252 }
2253 }
2254 }
2255
2256 pub(super) fn apply_keybinding_map(&mut self, map_name: &str) {
2258 if map_name.is_empty() {
2259 return;
2260 }
2261
2262 let is_builtin = matches!(map_name, "default" | "emacs" | "vscode" | "macos");
2264 let is_user_defined = self.config.keybinding_maps.contains_key(map_name);
2265
2266 if is_builtin || is_user_defined {
2267 self.config.active_keybinding_map = map_name.to_string().into();
2269
2270 self.keybindings = crate::input::keybindings::KeybindingResolver::new(&self.config);
2272
2273 self.save_keybinding_map_to_config();
2275
2276 self.set_status_message(t!("view.keybindings_switched", map = map_name).to_string());
2277 } else {
2278 self.set_status_message(t!("view.keybindings_unknown", map = map_name).to_string());
2279 }
2280 }
2281
2282 fn save_keybinding_map_to_config(&mut self) {
2284 if let Err(e) = self.filesystem.create_dir_all(&self.dir_context.config_dir) {
2286 tracing::warn!("Failed to create config directory: {}", e);
2287 return;
2288 }
2289
2290 let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
2292 if let Err(e) = resolver.save_to_layer(&self.config, ConfigLayer::User) {
2293 tracing::warn!("Failed to save keybinding map to config: {}", e);
2294 }
2295 }
2296
2297 fn start_select_cursor_style_prompt(&mut self) {
2299 use crate::config::CursorStyle;
2300
2301 let current_style = self.config.editor.cursor_style;
2302
2303 let suggestions: Vec<crate::input::commands::Suggestion> = CursorStyle::OPTIONS
2305 .iter()
2306 .zip(CursorStyle::DESCRIPTIONS.iter())
2307 .map(|(style_name, description)| {
2308 let is_current = *style_name == current_style.as_str();
2309 crate::input::commands::Suggestion {
2310 text: description.to_string(),
2311 description: if is_current {
2312 Some("(current)".to_string())
2313 } else {
2314 None
2315 },
2316 value: Some(style_name.to_string()),
2317 disabled: false,
2318 keybinding: None,
2319 source: None,
2320 }
2321 })
2322 .collect();
2323
2324 let current_index = CursorStyle::OPTIONS
2326 .iter()
2327 .position(|s| *s == current_style.as_str())
2328 .unwrap_or(0);
2329
2330 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
2331 "Select cursor style: ".to_string(),
2332 PromptType::SelectCursorStyle,
2333 suggestions,
2334 ));
2335
2336 if let Some(prompt) = self.prompt.as_mut() {
2337 if !prompt.suggestions.is_empty() {
2338 prompt.selected_suggestion = Some(current_index);
2339 prompt.input = CursorStyle::DESCRIPTIONS[current_index].to_string();
2340 prompt.cursor_pos = prompt.input.len();
2341 }
2342 }
2343 }
2344
2345 pub(super) fn apply_cursor_style(&mut self, style_name: &str) {
2347 use crate::config::CursorStyle;
2348
2349 if let Some(style) = CursorStyle::parse(style_name) {
2350 self.config.editor.cursor_style = style;
2352
2353 use std::io::stdout;
2355 let _ = crossterm::execute!(stdout(), style.to_crossterm_style());
2356
2357 self.save_cursor_style_to_config();
2359
2360 let description = CursorStyle::OPTIONS
2362 .iter()
2363 .zip(CursorStyle::DESCRIPTIONS.iter())
2364 .find(|(name, _)| **name == style_name)
2365 .map(|(_, desc)| *desc)
2366 .unwrap_or(style_name);
2367
2368 self.set_status_message(
2369 t!("view.cursor_style_changed", style = description).to_string(),
2370 );
2371 }
2372 }
2373
2374 fn save_cursor_style_to_config(&mut self) {
2376 if let Err(e) = self.filesystem.create_dir_all(&self.dir_context.config_dir) {
2378 tracing::warn!("Failed to create config directory: {}", e);
2379 return;
2380 }
2381
2382 let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
2384 if let Err(e) = resolver.save_to_layer(&self.config, ConfigLayer::User) {
2385 tracing::warn!("Failed to save cursor style to config: {}", e);
2386 }
2387 }
2388
2389 fn start_select_locale_prompt(&mut self) {
2391 let available_locales = crate::i18n::available_locales();
2392 let current_locale = crate::i18n::current_locale();
2393
2394 let current_index = available_locales
2396 .iter()
2397 .position(|name| *name == current_locale)
2398 .unwrap_or(0);
2399
2400 let suggestions: Vec<crate::input::commands::Suggestion> = available_locales
2401 .iter()
2402 .map(|locale_name| {
2403 let is_current = *locale_name == current_locale;
2404 let description = if let Some((english_name, native_name)) =
2405 crate::i18n::locale_display_name(locale_name)
2406 {
2407 if english_name == native_name {
2408 if is_current {
2410 format!("{} (current)", english_name)
2411 } else {
2412 english_name.to_string()
2413 }
2414 } else {
2415 if is_current {
2417 format!("{} / {} (current)", english_name, native_name)
2418 } else {
2419 format!("{} / {}", english_name, native_name)
2420 }
2421 }
2422 } else {
2423 if is_current {
2425 "(current)".to_string()
2426 } else {
2427 String::new()
2428 }
2429 };
2430 crate::input::commands::Suggestion {
2431 text: locale_name.to_string(),
2432 description: if description.is_empty() {
2433 None
2434 } else {
2435 Some(description)
2436 },
2437 value: Some(locale_name.to_string()),
2438 disabled: false,
2439 keybinding: None,
2440 source: None,
2441 }
2442 })
2443 .collect();
2444
2445 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
2446 t!("locale.select_prompt").to_string(),
2447 PromptType::SelectLocale,
2448 suggestions,
2449 ));
2450
2451 if let Some(prompt) = self.prompt.as_mut() {
2452 if !prompt.suggestions.is_empty() {
2453 prompt.selected_suggestion = Some(current_index);
2454 prompt.input = String::new();
2456 prompt.cursor_pos = 0;
2457 }
2458 }
2459 }
2460
2461 pub(super) fn apply_locale(&mut self, locale_name: &str) {
2463 if !locale_name.is_empty() {
2464 crate::i18n::set_locale(locale_name);
2466
2467 self.config.locale = crate::config::LocaleName(Some(locale_name.to_string()));
2469
2470 self.menus = crate::config::MenuConfig::translated();
2472
2473 if let Ok(mut registry) = self.command_registry.write() {
2475 registry.refresh_builtin_commands();
2476 }
2477
2478 self.save_locale_to_config();
2480
2481 self.set_status_message(t!("locale.changed", locale_name = locale_name).to_string());
2482 }
2483 }
2484
2485 fn save_locale_to_config(&mut self) {
2487 if let Err(e) = self.filesystem.create_dir_all(&self.dir_context.config_dir) {
2489 tracing::warn!("Failed to create config directory: {}", e);
2490 return;
2491 }
2492
2493 let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
2495 if let Err(e) = resolver.save_to_layer(&self.config, ConfigLayer::User) {
2496 tracing::warn!("Failed to save locale to config: {}", e);
2497 }
2498 }
2499
2500 fn switch_to_previous_tab(&mut self) {
2502 let active_split = self.split_manager.active_split();
2503 let previous_buffer = self
2504 .split_view_states
2505 .get(&active_split)
2506 .and_then(|vs| vs.previous_buffer());
2507
2508 if let Some(prev_id) = previous_buffer {
2509 let is_valid = self
2511 .split_view_states
2512 .get(&active_split)
2513 .is_some_and(|vs| vs.open_buffers.contains(&prev_id));
2514
2515 if is_valid && prev_id != self.active_buffer() {
2516 self.position_history.commit_pending_movement();
2518
2519 let current_state = self.active_state();
2520 let position = current_state.cursors.primary().position;
2521 let anchor = current_state.cursors.primary().anchor;
2522 self.position_history
2523 .record_movement(self.active_buffer(), position, anchor);
2524 self.position_history.commit_pending_movement();
2525
2526 self.set_active_buffer(prev_id);
2527 } else if !is_valid {
2528 self.set_status_message(t!("status.previous_tab_closed").to_string());
2529 }
2530 } else {
2531 self.set_status_message(t!("status.no_previous_tab").to_string());
2532 }
2533 }
2534
2535 fn start_switch_to_tab_prompt(&mut self) {
2537 let active_split = self.split_manager.active_split();
2538 let open_buffers = if let Some(view_state) = self.split_view_states.get(&active_split) {
2539 view_state.open_buffers.clone()
2540 } else {
2541 return;
2542 };
2543
2544 if open_buffers.is_empty() {
2545 self.set_status_message(t!("status.no_tabs_in_split").to_string());
2546 return;
2547 }
2548
2549 let current_index = open_buffers
2551 .iter()
2552 .position(|&id| id == self.active_buffer())
2553 .unwrap_or(0);
2554
2555 let suggestions: Vec<crate::input::commands::Suggestion> = open_buffers
2556 .iter()
2557 .map(|&buffer_id| {
2558 let display_name = self
2559 .buffer_metadata
2560 .get(&buffer_id)
2561 .map(|m| m.display_name.clone())
2562 .unwrap_or_else(|| format!("Buffer {:?}", buffer_id));
2563
2564 let is_current = buffer_id == self.active_buffer();
2565 let is_modified = self
2566 .buffers
2567 .get(&buffer_id)
2568 .is_some_and(|b| b.buffer.is_modified());
2569
2570 let description = match (is_current, is_modified) {
2571 (true, true) => Some("(current, modified)".to_string()),
2572 (true, false) => Some("(current)".to_string()),
2573 (false, true) => Some("(modified)".to_string()),
2574 (false, false) => None,
2575 };
2576
2577 crate::input::commands::Suggestion {
2578 text: display_name,
2579 description,
2580 value: Some(buffer_id.0.to_string()),
2581 disabled: false,
2582 keybinding: None,
2583 source: None,
2584 }
2585 })
2586 .collect();
2587
2588 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
2589 "Switch to tab: ".to_string(),
2590 PromptType::SwitchToTab,
2591 suggestions,
2592 ));
2593
2594 if let Some(prompt) = self.prompt.as_mut() {
2595 if !prompt.suggestions.is_empty() {
2596 prompt.selected_suggestion = Some(current_index);
2597 }
2598 }
2599 }
2600
2601 pub(crate) fn switch_to_tab(&mut self, buffer_id: BufferId) {
2603 let active_split = self.split_manager.active_split();
2605 let is_valid = self
2606 .split_view_states
2607 .get(&active_split)
2608 .is_some_and(|vs| vs.open_buffers.contains(&buffer_id));
2609
2610 if !is_valid {
2611 self.set_status_message(t!("status.tab_not_found").to_string());
2612 return;
2613 }
2614
2615 if buffer_id != self.active_buffer() {
2616 self.position_history.commit_pending_movement();
2618
2619 let current_state = self.active_state();
2620 let position = current_state.cursors.primary().position;
2621 let anchor = current_state.cursors.primary().anchor;
2622 self.position_history
2623 .record_movement(self.active_buffer(), position, anchor);
2624 self.position_history.commit_pending_movement();
2625
2626 self.set_active_buffer(buffer_id);
2627 }
2628 }
2629
2630 fn handle_insert_char_prompt(&mut self, c: char) -> AnyhowResult<()> {
2632 if let Some(ref prompt) = self.prompt {
2634 if prompt.prompt_type == PromptType::QueryReplaceConfirm {
2635 return self.handle_interactive_replace_key(c);
2636 }
2637 }
2638
2639 if let Some(ref prompt) = self.prompt {
2643 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
2644 if let Some(history) = self.prompt_histories.get_mut(&key) {
2645 history.reset_navigation();
2646 }
2647 }
2648 }
2649
2650 if let Some(prompt) = self.prompt_mut() {
2651 let s = c.to_string();
2653 prompt.insert_str(&s);
2654 }
2655 self.update_prompt_suggestions();
2656 Ok(())
2657 }
2658
2659 fn handle_insert_char_editor(&mut self, c: char) -> AnyhowResult<()> {
2661 if self.is_editing_disabled() {
2663 self.set_status_message(t!("buffer.editing_disabled").to_string());
2664 return Ok(());
2665 }
2666
2667 self.cancel_pending_lsp_requests();
2669
2670 if let Some(events) = self.action_to_events(Action::InsertChar(c)) {
2671 if events.len() > 1 {
2672 let description = format!("Insert '{}'", c);
2674 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description.clone())
2675 {
2676 self.active_event_log_mut().append(bulk_edit);
2677 }
2678 } else {
2679 for event in events {
2681 self.active_event_log_mut().append(event.clone());
2682 self.apply_event_to_active_buffer(&event);
2683 }
2684 }
2685 }
2686
2687 if c == '(' || c == ',' {
2689 let _ = self.request_signature_help();
2690 }
2691
2692 self.maybe_trigger_completion(c);
2694
2695 Ok(())
2696 }
2697
2698 fn apply_action_as_events(&mut self, action: Action) -> AnyhowResult<()> {
2704 let buffer_id = self.active_buffer();
2706 if self.is_composite_buffer(buffer_id) {
2707 if let Some(_handled) = self.handle_composite_action(buffer_id, &action) {
2708 return Ok(());
2709 }
2710 }
2711
2712 let action_description = format!("{:?}", action);
2714
2715 let is_editing_action = matches!(
2717 action,
2718 Action::InsertNewline
2719 | Action::InsertTab
2720 | Action::DeleteForward
2721 | Action::DeleteWordBackward
2722 | Action::DeleteWordForward
2723 | Action::DeleteLine
2724 | Action::DedentSelection
2725 | Action::ToggleComment
2726 );
2727
2728 if is_editing_action && self.is_editing_disabled() {
2729 self.set_status_message(t!("buffer.editing_disabled").to_string());
2730 return Ok(());
2731 }
2732
2733 if let Some(events) = self.action_to_events(action) {
2734 if events.len() > 1 {
2735 let has_buffer_mods = events
2737 .iter()
2738 .any(|e| matches!(e, Event::Insert { .. } | Event::Delete { .. }));
2739
2740 if has_buffer_mods {
2741 if let Some(bulk_edit) =
2743 self.apply_events_as_bulk_edit(events.clone(), action_description)
2744 {
2745 self.active_event_log_mut().append(bulk_edit);
2746 }
2747 } else {
2748 let batch = Event::Batch {
2750 events: events.clone(),
2751 description: action_description,
2752 };
2753 self.active_event_log_mut().append(batch.clone());
2754 self.apply_event_to_active_buffer(&batch);
2755 }
2756
2757 for event in &events {
2759 self.track_cursor_movement(event);
2760 }
2761 } else {
2762 for event in events {
2764 self.active_event_log_mut().append(event.clone());
2765 self.apply_event_to_active_buffer(&event);
2766 self.track_cursor_movement(&event);
2767 }
2768 }
2769 }
2770
2771 Ok(())
2772 }
2773
2774 fn track_cursor_movement(&mut self, event: &Event) {
2776 if self.in_navigation {
2777 return;
2778 }
2779
2780 if let Event::MoveCursor {
2781 new_position,
2782 new_anchor,
2783 ..
2784 } = event
2785 {
2786 self.position_history
2787 .record_movement(self.active_buffer(), *new_position, *new_anchor);
2788 }
2789 }
2790}