1use super::*;
2use crate::model::event::LeafId;
3use crate::services::plugins::hooks::HookArgs;
4use anyhow::Result as AnyhowResult;
5use rust_i18n::t;
6impl Editor {
7 pub fn get_key_context(&self) -> crate::input::keybindings::KeyContext {
9 use crate::input::keybindings::KeyContext;
10
11 if self.settings_state.as_ref().is_some_and(|s| s.visible) {
13 KeyContext::Settings
14 } else if self.menu_state.active_menu.is_some() {
15 KeyContext::Menu
16 } else if self.is_prompting() {
17 KeyContext::Prompt
18 } else if self.active_state().popups.is_visible() {
19 KeyContext::Popup
20 } else {
21 self.key_context
23 }
24 }
25
26 pub fn handle_key(
29 &mut self,
30 code: crossterm::event::KeyCode,
31 modifiers: crossterm::event::KeyModifiers,
32 ) -> AnyhowResult<()> {
33 use crate::input::keybindings::Action;
34
35 let _t_total = std::time::Instant::now();
36
37 tracing::trace!(
38 "Editor.handle_key: code={:?}, modifiers={:?}",
39 code,
40 modifiers
41 );
42
43 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
45
46 if self.is_event_debug_active() {
50 self.handle_event_debug_input(&key_event);
51 return Ok(());
52 }
53
54 if self.dispatch_terminal_input(&key_event).is_some() {
56 return Ok(());
57 }
58
59 let active_split = self.split_manager.active_split();
62 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
63 view_state.viewport.clear_skip_ensure_visible();
64 }
65
66 if self.theme_info_popup.is_some() {
68 self.theme_info_popup = None;
69 }
70
71 let mut context = self.get_key_context();
73
74 if matches!(context, crate::input::keybindings::KeyContext::Popup) {
77 let (is_transient_popup, has_selection) = {
79 let popup = self.active_state().popups.top();
80 (
81 popup.is_some_and(|p| p.transient),
82 popup.is_some_and(|p| p.has_selection()),
83 )
84 };
85
86 let is_copy_key = key_event.code == crossterm::event::KeyCode::Char('c')
88 && key_event
89 .modifiers
90 .contains(crossterm::event::KeyModifiers::CONTROL);
91
92 if is_transient_popup && !(has_selection && is_copy_key) {
93 self.hide_popup();
95 tracing::debug!("Dismissed transient popup on key press");
96 context = self.get_key_context();
98 }
99 }
100
101 if self.dispatch_modal_input(&key_event).is_some() {
103 return Ok(());
104 }
105
106 if context != self.get_key_context() {
109 context = self.get_key_context();
110 }
111
112 let should_check_mode_bindings = matches!(
115 context,
116 crate::input::keybindings::KeyContext::Normal
117 | crate::input::keybindings::KeyContext::FileExplorer
118 );
119
120 if should_check_mode_bindings {
121 if let Some(ref mode_name) = self.editor_mode {
123 if let Some(action_name) = self.mode_registry.resolve_chord_keybinding(
125 mode_name,
126 &self.chord_state,
127 code,
128 modifiers,
129 ) {
130 tracing::debug!("Mode chord resolved to action: {}", action_name);
131 self.chord_state.clear();
132 let action = Action::from_str(&action_name, &std::collections::HashMap::new())
133 .unwrap_or(Action::PluginAction(action_name));
134 return self.handle_action(action);
135 }
136
137 let is_potential_chord = self.mode_registry.is_chord_prefix(
139 mode_name,
140 &self.chord_state,
141 code,
142 modifiers,
143 );
144
145 if is_potential_chord {
146 tracing::debug!("Potential chord prefix in editor mode");
148 self.chord_state.push((code, modifiers));
149 return Ok(());
150 }
151
152 if !self.chord_state.is_empty() {
154 tracing::debug!("Chord sequence abandoned in mode, clearing state");
155 self.chord_state.clear();
156 }
157 }
158
159 if let Some(action_name) = self.resolve_mode_keybinding(code, modifiers) {
162 let action = Action::from_str(&action_name, &std::collections::HashMap::new())
163 .unwrap_or_else(|| Action::PluginAction(action_name.clone()));
164 return self.handle_action(action);
165 }
166
167 if let Some(ref mode_name) = self.editor_mode {
169 if self.mode_registry.is_read_only(mode_name) {
173 tracing::debug!(
174 "Ignoring unbound key in read-only mode {:?}",
175 self.editor_mode
176 );
177 return Ok(());
178 }
179 tracing::debug!(
181 "Mode {:?} is not read-only, allowing key through",
182 self.editor_mode
183 );
184 }
185 }
186
187 let key_event = crossterm::event::KeyEvent::new(code, modifiers);
189 let chord_result = self
190 .keybindings
191 .resolve_chord(&self.chord_state, &key_event, context);
192
193 match chord_result {
194 crate::input::keybindings::ChordResolution::Complete(action) => {
195 tracing::debug!("Complete chord match -> Action: {:?}", action);
197 self.chord_state.clear();
198 return self.handle_action(action);
199 }
200 crate::input::keybindings::ChordResolution::Partial => {
201 tracing::debug!("Partial chord match - waiting for next key");
203 self.chord_state.push((code, modifiers));
204 return Ok(());
205 }
206 crate::input::keybindings::ChordResolution::NoMatch => {
207 if !self.chord_state.is_empty() {
209 tracing::debug!("Chord sequence abandoned, clearing state");
210 self.chord_state.clear();
211 }
212 }
213 }
214
215 let action = self.keybindings.resolve(&key_event, context);
217
218 tracing::trace!("Context: {:?} -> Action: {:?}", context, action);
219
220 match action {
223 Action::LspCompletion
224 | Action::LspGotoDefinition
225 | Action::LspReferences
226 | Action::LspHover
227 | Action::None => {
228 }
230 _ => {
231 self.cancel_pending_lsp_requests();
233 }
234 }
235
236 self.handle_action(action)
240 }
241
242 pub(crate) fn handle_action(&mut self, action: Action) -> AnyhowResult<()> {
245 use crate::input::keybindings::Action;
246
247 self.record_macro_action(&action);
249
250 match action {
251 Action::Quit => self.quit(),
252 Action::ForceQuit => {
253 self.should_quit = true;
254 }
255 Action::Detach => {
256 self.should_detach = true;
257 }
258 Action::Save => {
259 if self.active_state().buffer.file_path().is_none() {
261 self.start_prompt_with_initial_text(
262 t!("file.save_as_prompt").to_string(),
263 PromptType::SaveFileAs,
264 String::new(),
265 );
266 self.init_file_open_state();
267 } else if self.check_save_conflict().is_some() {
268 self.start_prompt(
270 t!("file.file_changed_prompt").to_string(),
271 PromptType::ConfirmSaveConflict,
272 );
273 } else {
274 self.save()?;
275 }
276 }
277 Action::SaveAs => {
278 let current_path = self
280 .active_state()
281 .buffer
282 .file_path()
283 .map(|p| {
284 p.strip_prefix(&self.working_dir)
286 .unwrap_or(p)
287 .to_string_lossy()
288 .to_string()
289 })
290 .unwrap_or_default();
291 self.start_prompt_with_initial_text(
292 t!("file.save_as_prompt").to_string(),
293 PromptType::SaveFileAs,
294 current_path,
295 );
296 self.init_file_open_state();
297 }
298 Action::Open => {
299 self.start_prompt(t!("file.open_prompt").to_string(), PromptType::OpenFile);
300 self.prefill_open_file_prompt();
301 self.init_file_open_state();
302 }
303 Action::SwitchProject => {
304 self.start_prompt(
305 t!("file.switch_project_prompt").to_string(),
306 PromptType::SwitchProject,
307 );
308 self.init_folder_open_state();
309 }
310 Action::GotoLine => {
311 let has_line_index = self
312 .buffers
313 .get(&self.active_buffer())
314 .is_none_or(|s| s.buffer.line_count().is_some());
315 if has_line_index {
316 self.start_prompt(
317 t!("file.goto_line_prompt").to_string(),
318 PromptType::GotoLine,
319 );
320 } else {
321 self.start_prompt(
322 t!("goto.scan_confirm_prompt", yes = "y", no = "N").to_string(),
323 PromptType::GotoLineScanConfirm,
324 );
325 }
326 }
327 Action::ScanLineIndex => {
328 self.start_incremental_line_scan(false);
329 }
330 Action::New => {
331 self.new_buffer();
332 }
333 Action::Close | Action::CloseTab => {
334 self.close_tab();
339 }
340 Action::Revert => {
341 if self.active_state().buffer.is_modified() {
343 let revert_key = t!("prompt.key.revert").to_string();
344 let cancel_key = t!("prompt.key.cancel").to_string();
345 self.start_prompt(
346 t!(
347 "prompt.revert_confirm",
348 revert_key = revert_key,
349 cancel_key = cancel_key
350 )
351 .to_string(),
352 PromptType::ConfirmRevert,
353 );
354 } else {
355 if let Err(e) = self.revert_file() {
357 self.set_status_message(
358 t!("error.failed_to_revert", error = e.to_string()).to_string(),
359 );
360 }
361 }
362 }
363 Action::ToggleAutoRevert => {
364 self.toggle_auto_revert();
365 }
366 Action::FormatBuffer => {
367 if let Err(e) = self.format_buffer() {
368 self.set_status_message(
369 t!("error.format_failed", error = e.to_string()).to_string(),
370 );
371 }
372 }
373 Action::TrimTrailingWhitespace => match self.trim_trailing_whitespace() {
374 Ok(true) => {
375 self.set_status_message(t!("whitespace.trimmed").to_string());
376 }
377 Ok(false) => {
378 self.set_status_message(t!("whitespace.no_trailing").to_string());
379 }
380 Err(e) => {
381 self.set_status_message(
382 t!("error.trim_whitespace_failed", error = e).to_string(),
383 );
384 }
385 },
386 Action::EnsureFinalNewline => match self.ensure_final_newline() {
387 Ok(true) => {
388 self.set_status_message(t!("whitespace.newline_added").to_string());
389 }
390 Ok(false) => {
391 self.set_status_message(t!("whitespace.already_has_newline").to_string());
392 }
393 Err(e) => {
394 self.set_status_message(
395 t!("error.ensure_newline_failed", error = e).to_string(),
396 );
397 }
398 },
399 Action::Copy => {
400 let state = self.active_state();
402 if let Some(popup) = state.popups.top() {
403 if popup.has_selection() {
404 if let Some(text) = popup.get_selected_text() {
405 self.clipboard.copy(text);
406 self.set_status_message(t!("clipboard.copied").to_string());
407 return Ok(());
408 }
409 }
410 }
411 let buffer_id = self.active_buffer();
413 if self.is_composite_buffer(buffer_id) {
414 if let Some(_handled) = self.handle_composite_action(buffer_id, &Action::Copy) {
415 return Ok(());
416 }
417 }
418 self.copy_selection()
419 }
420 Action::CopyWithTheme(theme) => self.copy_selection_with_theme(&theme),
421 Action::Cut => {
422 if self.is_editing_disabled() {
423 self.set_status_message(t!("buffer.editing_disabled").to_string());
424 return Ok(());
425 }
426 self.cut_selection()
427 }
428 Action::Paste => {
429 if self.is_editing_disabled() {
430 self.set_status_message(t!("buffer.editing_disabled").to_string());
431 return Ok(());
432 }
433 self.paste()
434 }
435 Action::YankWordForward => self.yank_word_forward(),
436 Action::YankWordBackward => self.yank_word_backward(),
437 Action::YankToLineEnd => self.yank_to_line_end(),
438 Action::YankToLineStart => self.yank_to_line_start(),
439 Action::Undo => {
440 self.handle_undo();
441 }
442 Action::Redo => {
443 self.handle_redo();
444 }
445 Action::ShowHelp => {
446 self.open_help_manual();
447 }
448 Action::ShowKeyboardShortcuts => {
449 self.open_keyboard_shortcuts();
450 }
451 Action::ShowWarnings => {
452 self.show_warnings_popup();
453 }
454 Action::ShowStatusLog => {
455 self.open_status_log();
456 }
457 Action::ShowLspStatus => {
458 self.show_lsp_status_popup();
459 }
460 Action::ClearWarnings => {
461 self.clear_warnings();
462 }
463 Action::CommandPalette => {
464 if let Some(prompt) = &self.prompt {
466 if prompt.prompt_type == PromptType::Command {
467 self.cancel_prompt();
468 return Ok(());
469 }
470 }
471
472 let active_buffer_mode = self
474 .buffer_metadata
475 .get(&self.active_buffer())
476 .and_then(|m| m.virtual_mode());
477 let has_lsp_config = {
478 let language = self
479 .buffers
480 .get(&self.active_buffer())
481 .map(|s| s.language.as_str());
482 language
483 .and_then(|lang| self.lsp.as_ref().and_then(|lsp| lsp.get_config(lang)))
484 .is_some()
485 };
486 let suggestions = self.command_registry.read().unwrap().filter(
487 "",
488 self.key_context,
489 &self.keybindings,
490 self.has_active_selection(),
491 &self.active_custom_contexts,
492 active_buffer_mode,
493 has_lsp_config,
494 );
495 self.start_prompt_with_suggestions(
496 t!("file.command_prompt").to_string(),
497 PromptType::Command,
498 suggestions,
499 );
500 }
501 Action::QuickOpen => {
502 if let Some(prompt) = &self.prompt {
504 if prompt.prompt_type == PromptType::QuickOpen {
505 self.cancel_prompt();
506 return Ok(());
507 }
508 }
509
510 self.start_quick_open();
512 }
513 Action::ToggleLineWrap => {
514 self.config.editor.line_wrap = !self.config.editor.line_wrap;
515
516 for view_state in self.split_view_states.values_mut() {
518 view_state.viewport.line_wrap_enabled = self.config.editor.line_wrap;
519 view_state.viewport.wrap_indent = self.config.editor.wrap_indent;
520 }
521
522 let state = if self.config.editor.line_wrap {
523 t!("view.state_enabled").to_string()
524 } else {
525 t!("view.state_disabled").to_string()
526 };
527 self.set_status_message(t!("view.line_wrap_state", state = state).to_string());
528 }
529 Action::ToggleReadOnly => {
530 let buffer_id = self.active_buffer();
531 let is_now_read_only = self
532 .buffer_metadata
533 .get(&buffer_id)
534 .map(|m| !m.read_only)
535 .unwrap_or(false);
536 self.mark_buffer_read_only(buffer_id, is_now_read_only);
537
538 let state_str = if is_now_read_only {
539 t!("view.state_enabled").to_string()
540 } else {
541 t!("view.state_disabled").to_string()
542 };
543 self.set_status_message(t!("view.read_only_state", state = state_str).to_string());
544 }
545 Action::ToggleComposeMode => {
546 self.handle_toggle_compose_mode();
547 }
548 Action::SetComposeWidth => {
549 let active_split = self.split_manager.active_split();
550 let current = self
551 .split_view_states
552 .get(&active_split)
553 .and_then(|v| v.compose_width.map(|w| w.to_string()))
554 .unwrap_or_default();
555 self.start_prompt_with_initial_text(
556 "Compose width (empty = viewport): ".to_string(),
557 PromptType::SetComposeWidth,
558 current,
559 );
560 }
561 Action::SetBackground => {
562 let default_path = self
563 .ansi_background_path
564 .as_ref()
565 .and_then(|p| {
566 p.strip_prefix(&self.working_dir)
567 .ok()
568 .map(|rel| rel.to_string_lossy().to_string())
569 })
570 .unwrap_or_else(|| DEFAULT_BACKGROUND_FILE.to_string());
571
572 self.start_prompt_with_initial_text(
573 "Background file: ".to_string(),
574 PromptType::SetBackgroundFile,
575 default_path,
576 );
577 }
578 Action::SetBackgroundBlend => {
579 let default_amount = format!("{:.2}", self.background_fade);
580 self.start_prompt_with_initial_text(
581 "Background blend (0-1): ".to_string(),
582 PromptType::SetBackgroundBlend,
583 default_amount,
584 );
585 }
586 Action::LspCompletion => {
587 self.request_completion();
588 }
589 Action::LspGotoDefinition => {
590 self.request_goto_definition()?;
591 }
592 Action::LspRename => {
593 self.start_rename()?;
594 }
595 Action::LspHover => {
596 self.request_hover()?;
597 }
598 Action::LspReferences => {
599 self.request_references()?;
600 }
601 Action::LspSignatureHelp => {
602 self.request_signature_help();
603 }
604 Action::LspCodeActions => {
605 self.request_code_actions()?;
606 }
607 Action::LspRestart => {
608 self.handle_lsp_restart();
609 }
610 Action::LspStop => {
611 self.handle_lsp_stop();
612 }
613 Action::LspToggleForBuffer => {
614 self.handle_lsp_toggle_for_buffer();
615 }
616 Action::ToggleInlayHints => {
617 self.toggle_inlay_hints();
618 }
619 Action::DumpConfig => {
620 self.dump_config();
621 }
622 Action::SelectTheme => {
623 self.start_select_theme_prompt();
624 }
625 Action::InspectThemeAtCursor => {
626 self.inspect_theme_at_cursor();
627 }
628 Action::SelectKeybindingMap => {
629 self.start_select_keybinding_map_prompt();
630 }
631 Action::SelectCursorStyle => {
632 self.start_select_cursor_style_prompt();
633 }
634 Action::SelectLocale => {
635 self.start_select_locale_prompt();
636 }
637 Action::Search => {
638 let is_search_prompt = self.prompt.as_ref().is_some_and(|p| {
640 matches!(
641 p.prompt_type,
642 PromptType::Search
643 | PromptType::ReplaceSearch
644 | PromptType::QueryReplaceSearch
645 )
646 });
647
648 if is_search_prompt {
649 self.confirm_prompt();
650 } else {
651 self.start_search_prompt(
652 t!("file.search_prompt").to_string(),
653 PromptType::Search,
654 false,
655 );
656 }
657 }
658 Action::Replace => {
659 self.start_search_prompt(
661 t!("file.replace_prompt").to_string(),
662 PromptType::ReplaceSearch,
663 false,
664 );
665 }
666 Action::QueryReplace => {
667 self.search_confirm_each = true;
669 self.start_search_prompt(
670 "Query replace: ".to_string(),
671 PromptType::QueryReplaceSearch,
672 false,
673 );
674 }
675 Action::FindInSelection => {
676 self.start_search_prompt(
677 t!("file.search_prompt").to_string(),
678 PromptType::Search,
679 true,
680 );
681 }
682 Action::FindNext => {
683 self.find_next();
684 }
685 Action::FindPrevious => {
686 self.find_previous();
687 }
688 Action::FindSelectionNext => {
689 self.find_selection_next();
690 }
691 Action::FindSelectionPrevious => {
692 self.find_selection_previous();
693 }
694 Action::AddCursorNextMatch => self.add_cursor_at_next_match(),
695 Action::AddCursorAbove => self.add_cursor_above(),
696 Action::AddCursorBelow => self.add_cursor_below(),
697 Action::NextBuffer => self.next_buffer(),
698 Action::PrevBuffer => self.prev_buffer(),
699 Action::SwitchToPreviousTab => self.switch_to_previous_tab(),
700 Action::SwitchToTabByName => self.start_switch_to_tab_prompt(),
701
702 Action::ScrollTabsLeft => {
704 let active_split_id = self.split_manager.active_split();
705 if let Some(view_state) = self.split_view_states.get_mut(&active_split_id) {
706 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_sub(5);
707 self.set_status_message(t!("status.scrolled_tabs_left").to_string());
708 }
709 }
710 Action::ScrollTabsRight => {
711 let active_split_id = self.split_manager.active_split();
712 if let Some(view_state) = self.split_view_states.get_mut(&active_split_id) {
713 view_state.tab_scroll_offset = view_state.tab_scroll_offset.saturating_add(5);
714 self.set_status_message(t!("status.scrolled_tabs_right").to_string());
715 }
716 }
717 Action::NavigateBack => self.navigate_back(),
718 Action::NavigateForward => self.navigate_forward(),
719 Action::SplitHorizontal => self.split_pane_horizontal(),
720 Action::SplitVertical => self.split_pane_vertical(),
721 Action::CloseSplit => self.close_active_split(),
722 Action::NextSplit => self.next_split(),
723 Action::PrevSplit => self.prev_split(),
724 Action::IncreaseSplitSize => self.adjust_split_size(0.05),
725 Action::DecreaseSplitSize => self.adjust_split_size(-0.05),
726 Action::ToggleMaximizeSplit => self.toggle_maximize_split(),
727 Action::ToggleFileExplorer => self.toggle_file_explorer(),
728 Action::ToggleMenuBar => self.toggle_menu_bar(),
729 Action::ToggleTabBar => self.toggle_tab_bar(),
730 Action::ToggleStatusBar => self.toggle_status_bar(),
731 Action::ToggleVerticalScrollbar => self.toggle_vertical_scrollbar(),
732 Action::ToggleHorizontalScrollbar => self.toggle_horizontal_scrollbar(),
733 Action::ToggleLineNumbers => self.toggle_line_numbers(),
734 Action::ToggleScrollSync => self.toggle_scroll_sync(),
735 Action::ToggleMouseCapture => self.toggle_mouse_capture(),
736 Action::ToggleMouseHover => self.toggle_mouse_hover(),
737 Action::ToggleDebugHighlights => self.toggle_debug_highlights(),
738 Action::AddRuler => {
740 self.start_prompt(t!("rulers.add_prompt").to_string(), PromptType::AddRuler);
741 }
742 Action::RemoveRuler => {
743 self.start_remove_ruler_prompt();
744 }
745 Action::SetTabSize => {
747 let current = self
748 .buffers
749 .get(&self.active_buffer())
750 .map(|s| s.buffer_settings.tab_size.to_string())
751 .unwrap_or_else(|| "4".to_string());
752 self.start_prompt_with_initial_text(
753 "Tab size: ".to_string(),
754 PromptType::SetTabSize,
755 current,
756 );
757 }
758 Action::SetLineEnding => {
759 self.start_set_line_ending_prompt();
760 }
761 Action::SetEncoding => {
762 self.start_set_encoding_prompt();
763 }
764 Action::ReloadWithEncoding => {
765 self.start_reload_with_encoding_prompt();
766 }
767 Action::SetLanguage => {
768 self.start_set_language_prompt();
769 }
770 Action::ToggleIndentationStyle => {
771 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
772 state.buffer_settings.use_tabs = !state.buffer_settings.use_tabs;
773 let status = if state.buffer_settings.use_tabs {
774 "Indentation: Tabs"
775 } else {
776 "Indentation: Spaces"
777 };
778 self.set_status_message(status.to_string());
779 }
780 }
781 Action::ToggleTabIndicators | Action::ToggleWhitespaceIndicators => {
782 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
783 state.buffer_settings.whitespace.toggle_all();
784 let status = if state.buffer_settings.whitespace.any_visible() {
785 t!("toggle.whitespace_indicators_shown")
786 } else {
787 t!("toggle.whitespace_indicators_hidden")
788 };
789 self.set_status_message(status.to_string());
790 }
791 }
792 Action::ResetBufferSettings => self.reset_buffer_settings(),
793 Action::FocusFileExplorer => self.focus_file_explorer(),
794 Action::FocusEditor => self.focus_editor(),
795 Action::FileExplorerUp => self.file_explorer_navigate_up(),
796 Action::FileExplorerDown => self.file_explorer_navigate_down(),
797 Action::FileExplorerPageUp => self.file_explorer_page_up(),
798 Action::FileExplorerPageDown => self.file_explorer_page_down(),
799 Action::FileExplorerExpand => self.file_explorer_toggle_expand(),
800 Action::FileExplorerCollapse => self.file_explorer_collapse(),
801 Action::FileExplorerOpen => self.file_explorer_open_file()?,
802 Action::FileExplorerRefresh => self.file_explorer_refresh(),
803 Action::FileExplorerNewFile => self.file_explorer_new_file(),
804 Action::FileExplorerNewDirectory => self.file_explorer_new_directory(),
805 Action::FileExplorerDelete => self.file_explorer_delete(),
806 Action::FileExplorerRename => self.file_explorer_rename(),
807 Action::FileExplorerToggleHidden => self.file_explorer_toggle_hidden(),
808 Action::FileExplorerToggleGitignored => self.file_explorer_toggle_gitignored(),
809 Action::FileExplorerSearchClear => self.file_explorer_search_clear(),
810 Action::FileExplorerSearchBackspace => self.file_explorer_search_pop_char(),
811 Action::RemoveSecondaryCursors => {
812 if let Some(events) = self.action_to_events(Action::RemoveSecondaryCursors) {
814 let batch = Event::Batch {
816 events: events.clone(),
817 description: "Remove secondary cursors".to_string(),
818 };
819 self.active_event_log_mut().append(batch.clone());
820 self.apply_event_to_active_buffer(&batch);
821
822 let active_split = self.split_manager.active_split();
824 let active_buffer = self.active_buffer();
825 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
826 let state = self.buffers.get_mut(&active_buffer).unwrap();
827 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
828 }
829 }
830 }
831
832 Action::MenuActivate => {
834 self.handle_menu_activate();
835 }
836 Action::MenuClose => {
837 self.handle_menu_close();
838 }
839 Action::MenuLeft => {
840 self.handle_menu_left();
841 }
842 Action::MenuRight => {
843 self.handle_menu_right();
844 }
845 Action::MenuUp => {
846 self.handle_menu_up();
847 }
848 Action::MenuDown => {
849 self.handle_menu_down();
850 }
851 Action::MenuExecute => {
852 if let Some(action) = self.handle_menu_execute() {
853 return self.handle_action(action);
854 }
855 }
856 Action::MenuOpen(menu_name) => {
857 self.handle_menu_open(&menu_name);
858 }
859
860 Action::SwitchKeybindingMap(map_name) => {
861 let is_builtin =
863 matches!(map_name.as_str(), "default" | "emacs" | "vscode" | "macos");
864 let is_user_defined = self.config.keybinding_maps.contains_key(&map_name);
865
866 if is_builtin || is_user_defined {
867 self.config.active_keybinding_map = map_name.clone().into();
869
870 self.keybindings =
872 crate::input::keybindings::KeybindingResolver::new(&self.config);
873
874 self.set_status_message(
875 t!("view.keybindings_switched", map = map_name).to_string(),
876 );
877 } else {
878 self.set_status_message(
879 t!("view.keybindings_unknown", map = map_name).to_string(),
880 );
881 }
882 }
883
884 Action::SmartHome => {
885 let buffer_id = self.active_buffer();
887 if self.is_composite_buffer(buffer_id) {
888 if let Some(_handled) =
889 self.handle_composite_action(buffer_id, &Action::SmartHome)
890 {
891 return Ok(());
892 }
893 }
894 self.smart_home();
895 }
896 Action::ToggleComment => {
897 self.toggle_comment();
898 }
899 Action::ToggleFold => {
900 self.toggle_fold_at_cursor();
901 }
902 Action::GoToMatchingBracket => {
903 self.goto_matching_bracket();
904 }
905 Action::JumpToNextError => {
906 self.jump_to_next_error();
907 }
908 Action::JumpToPreviousError => {
909 self.jump_to_previous_error();
910 }
911 Action::SetBookmark(key) => {
912 self.set_bookmark(key);
913 }
914 Action::JumpToBookmark(key) => {
915 self.jump_to_bookmark(key);
916 }
917 Action::ClearBookmark(key) => {
918 self.clear_bookmark(key);
919 }
920 Action::ListBookmarks => {
921 self.list_bookmarks();
922 }
923 Action::ToggleSearchCaseSensitive => {
924 self.search_case_sensitive = !self.search_case_sensitive;
925 let state = if self.search_case_sensitive {
926 "enabled"
927 } else {
928 "disabled"
929 };
930 self.set_status_message(
931 t!("search.case_sensitive_state", state = state).to_string(),
932 );
933 if let Some(prompt) = &self.prompt {
936 if matches!(
937 prompt.prompt_type,
938 PromptType::Search
939 | PromptType::ReplaceSearch
940 | PromptType::QueryReplaceSearch
941 ) {
942 let query = prompt.input.clone();
943 self.update_search_highlights(&query);
944 }
945 } else if let Some(search_state) = &self.search_state {
946 let query = search_state.query.clone();
947 self.perform_search(&query);
948 }
949 }
950 Action::ToggleSearchWholeWord => {
951 self.search_whole_word = !self.search_whole_word;
952 let state = if self.search_whole_word {
953 "enabled"
954 } else {
955 "disabled"
956 };
957 self.set_status_message(t!("search.whole_word_state", state = state).to_string());
958 if let Some(prompt) = &self.prompt {
961 if matches!(
962 prompt.prompt_type,
963 PromptType::Search
964 | PromptType::ReplaceSearch
965 | PromptType::QueryReplaceSearch
966 ) {
967 let query = prompt.input.clone();
968 self.update_search_highlights(&query);
969 }
970 } else if let Some(search_state) = &self.search_state {
971 let query = search_state.query.clone();
972 self.perform_search(&query);
973 }
974 }
975 Action::ToggleSearchRegex => {
976 self.search_use_regex = !self.search_use_regex;
977 let state = if self.search_use_regex {
978 "enabled"
979 } else {
980 "disabled"
981 };
982 self.set_status_message(t!("search.regex_state", state = state).to_string());
983 if let Some(prompt) = &self.prompt {
986 if matches!(
987 prompt.prompt_type,
988 PromptType::Search
989 | PromptType::ReplaceSearch
990 | PromptType::QueryReplaceSearch
991 ) {
992 let query = prompt.input.clone();
993 self.update_search_highlights(&query);
994 }
995 } else if let Some(search_state) = &self.search_state {
996 let query = search_state.query.clone();
997 self.perform_search(&query);
998 }
999 }
1000 Action::ToggleSearchConfirmEach => {
1001 self.search_confirm_each = !self.search_confirm_each;
1002 let state = if self.search_confirm_each {
1003 "enabled"
1004 } else {
1005 "disabled"
1006 };
1007 self.set_status_message(t!("search.confirm_each_state", state = state).to_string());
1008 }
1009 Action::FileBrowserToggleHidden => {
1010 self.file_open_toggle_hidden();
1012 }
1013 Action::StartMacroRecording => {
1014 self.set_status_message(
1016 "Use Ctrl+Shift+R to start recording (will prompt for register)".to_string(),
1017 );
1018 }
1019 Action::StopMacroRecording => {
1020 self.stop_macro_recording();
1021 }
1022 Action::PlayMacro(key) => {
1023 self.play_macro(key);
1024 }
1025 Action::ToggleMacroRecording(key) => {
1026 self.toggle_macro_recording(key);
1027 }
1028 Action::ShowMacro(key) => {
1029 self.show_macro_in_buffer(key);
1030 }
1031 Action::ListMacros => {
1032 self.list_macros_in_buffer();
1033 }
1034 Action::PromptRecordMacro => {
1035 self.start_prompt("Record macro (0-9): ".to_string(), PromptType::RecordMacro);
1036 }
1037 Action::PromptPlayMacro => {
1038 self.start_prompt("Play macro (0-9): ".to_string(), PromptType::PlayMacro);
1039 }
1040 Action::PlayLastMacro => {
1041 if let Some(key) = self.last_macro_register {
1042 self.play_macro(key);
1043 } else {
1044 self.set_status_message(t!("status.no_macro_recorded").to_string());
1045 }
1046 }
1047 Action::PromptSetBookmark => {
1048 self.start_prompt("Set bookmark (0-9): ".to_string(), PromptType::SetBookmark);
1049 }
1050 Action::PromptJumpToBookmark => {
1051 self.start_prompt(
1052 "Jump to bookmark (0-9): ".to_string(),
1053 PromptType::JumpToBookmark,
1054 );
1055 }
1056 Action::None => {}
1057 Action::DeleteBackward => {
1058 if self.is_editing_disabled() {
1059 self.set_status_message(t!("buffer.editing_disabled").to_string());
1060 return Ok(());
1061 }
1062 if let Some(events) = self.action_to_events(Action::DeleteBackward) {
1064 if events.len() > 1 {
1065 let description = "Delete backward".to_string();
1067 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description)
1068 {
1069 self.active_event_log_mut().append(bulk_edit);
1070 }
1071 } else {
1072 for event in events {
1073 self.active_event_log_mut().append(event.clone());
1074 self.apply_event_to_active_buffer(&event);
1075 }
1076 }
1077 }
1078 }
1079 Action::PluginAction(action_name) => {
1080 tracing::debug!("handle_action: PluginAction('{}')", action_name);
1081 #[cfg(feature = "plugins")]
1084 if let Some(result) = self.plugin_manager.execute_action_async(&action_name) {
1085 match result {
1086 Ok(receiver) => {
1087 self.pending_plugin_actions
1089 .push((action_name.clone(), receiver));
1090 }
1091 Err(e) => {
1092 self.set_status_message(
1093 t!("view.plugin_error", error = e.to_string()).to_string(),
1094 );
1095 tracing::error!("Plugin action error: {}", e);
1096 }
1097 }
1098 } else {
1099 self.set_status_message(t!("status.plugin_manager_unavailable").to_string());
1100 }
1101 #[cfg(not(feature = "plugins"))]
1102 {
1103 let _ = action_name;
1104 self.set_status_message(
1105 "Plugins not available (compiled without plugin support)".to_string(),
1106 );
1107 }
1108 }
1109 Action::LoadPluginFromBuffer => {
1110 #[cfg(feature = "plugins")]
1111 {
1112 let buffer_id = self.active_buffer();
1113 let state = self.active_state();
1114 let buffer = &state.buffer;
1115 let total = buffer.total_bytes();
1116 let content =
1117 String::from_utf8_lossy(&buffer.slice_bytes(0..total)).to_string();
1118
1119 let is_ts = buffer
1121 .file_path()
1122 .and_then(|p| p.extension())
1123 .and_then(|e| e.to_str())
1124 .map(|e| e == "ts" || e == "tsx")
1125 .unwrap_or(true);
1126
1127 let name = buffer
1129 .file_path()
1130 .and_then(|p| p.file_name())
1131 .and_then(|s| s.to_str())
1132 .map(|s| s.to_string())
1133 .unwrap_or_else(|| "buffer-plugin".to_string());
1134
1135 match self
1136 .plugin_manager
1137 .load_plugin_from_source(&content, &name, is_ts)
1138 {
1139 Ok(()) => {
1140 self.set_status_message(format!(
1141 "Plugin '{}' loaded from buffer",
1142 name
1143 ));
1144 }
1145 Err(e) => {
1146 self.set_status_message(format!("Failed to load plugin: {}", e));
1147 tracing::error!("LoadPluginFromBuffer error: {}", e);
1148 }
1149 }
1150
1151 self.setup_plugin_dev_lsp(buffer_id, &content);
1153 }
1154 #[cfg(not(feature = "plugins"))]
1155 {
1156 self.set_status_message(
1157 "Plugins not available (compiled without plugin support)".to_string(),
1158 );
1159 }
1160 }
1161 Action::OpenTerminal => {
1162 self.open_terminal();
1163 }
1164 Action::CloseTerminal => {
1165 self.close_terminal();
1166 }
1167 Action::FocusTerminal => {
1168 if self.is_terminal_buffer(self.active_buffer()) {
1170 self.terminal_mode = true;
1171 self.key_context = KeyContext::Terminal;
1172 self.set_status_message(t!("status.terminal_mode_enabled").to_string());
1173 }
1174 }
1175 Action::TerminalEscape => {
1176 if self.terminal_mode {
1178 self.terminal_mode = false;
1179 self.key_context = KeyContext::Normal;
1180 self.set_status_message(t!("status.terminal_mode_disabled").to_string());
1181 }
1182 }
1183 Action::ToggleKeyboardCapture => {
1184 if self.terminal_mode {
1186 self.keyboard_capture = !self.keyboard_capture;
1187 if self.keyboard_capture {
1188 self.set_status_message(
1189 "Keyboard capture ON - all keys go to terminal (F9 to toggle)"
1190 .to_string(),
1191 );
1192 } else {
1193 self.set_status_message(
1194 "Keyboard capture OFF - UI bindings active (F9 to toggle)".to_string(),
1195 );
1196 }
1197 }
1198 }
1199 Action::TerminalPaste => {
1200 if self.terminal_mode {
1202 if let Some(text) = self.clipboard.paste() {
1203 self.send_terminal_input(text.as_bytes());
1204 }
1205 }
1206 }
1207 Action::ShellCommand => {
1208 self.start_shell_command_prompt(false);
1210 }
1211 Action::ShellCommandReplace => {
1212 self.start_shell_command_prompt(true);
1214 }
1215 Action::OpenSettings => {
1216 self.open_settings();
1217 }
1218 Action::CloseSettings => {
1219 let has_changes = self
1221 .settings_state
1222 .as_ref()
1223 .is_some_and(|s| s.has_changes());
1224 if has_changes {
1225 if let Some(ref mut state) = self.settings_state {
1227 state.show_confirm_dialog();
1228 }
1229 } else {
1230 self.close_settings(false);
1231 }
1232 }
1233 Action::SettingsSave => {
1234 self.save_settings();
1235 }
1236 Action::SettingsReset => {
1237 if let Some(ref mut state) = self.settings_state {
1238 state.reset_current_to_default();
1239 }
1240 }
1241 Action::SettingsToggleFocus => {
1242 if let Some(ref mut state) = self.settings_state {
1243 state.toggle_focus();
1244 }
1245 }
1246 Action::SettingsActivate => {
1247 self.settings_activate_current();
1248 }
1249 Action::SettingsSearch => {
1250 if let Some(ref mut state) = self.settings_state {
1251 state.start_search();
1252 }
1253 }
1254 Action::SettingsHelp => {
1255 if let Some(ref mut state) = self.settings_state {
1256 state.toggle_help();
1257 }
1258 }
1259 Action::SettingsIncrement => {
1260 self.settings_increment_current();
1261 }
1262 Action::SettingsDecrement => {
1263 self.settings_decrement_current();
1264 }
1265 Action::CalibrateInput => {
1266 self.open_calibration_wizard();
1267 }
1268 Action::EventDebug => {
1269 self.open_event_debug();
1270 }
1271 Action::OpenKeybindingEditor => {
1272 self.open_keybinding_editor();
1273 }
1274 Action::PromptConfirm => {
1275 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
1276 use super::prompt_actions::PromptResult;
1277 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
1278 PromptResult::ExecuteAction(action) => {
1279 return self.handle_action(action);
1280 }
1281 PromptResult::EarlyReturn => {
1282 return Ok(());
1283 }
1284 PromptResult::Done => {}
1285 }
1286 }
1287 }
1288 Action::PromptConfirmWithText(ref text) => {
1289 if let Some(ref mut prompt) = self.prompt {
1291 prompt.set_input(text.clone());
1292 self.update_prompt_suggestions();
1293 }
1294 if let Some((input, prompt_type, selected_index)) = self.confirm_prompt() {
1295 use super::prompt_actions::PromptResult;
1296 match self.handle_prompt_confirm_input(input, prompt_type, selected_index) {
1297 PromptResult::ExecuteAction(action) => {
1298 return self.handle_action(action);
1299 }
1300 PromptResult::EarlyReturn => {
1301 return Ok(());
1302 }
1303 PromptResult::Done => {}
1304 }
1305 }
1306 }
1307 Action::PopupConfirm => {
1308 use super::popup_actions::PopupConfirmResult;
1309 if let PopupConfirmResult::EarlyReturn = self.handle_popup_confirm() {
1310 return Ok(());
1311 }
1312 }
1313 Action::PopupCancel => {
1314 self.handle_popup_cancel();
1315 }
1316 Action::InsertChar(c) => {
1317 if self.is_prompting() {
1318 return self.handle_insert_char_prompt(c);
1319 } else if self.key_context == KeyContext::FileExplorer {
1320 self.file_explorer_search_push_char(c);
1321 } else {
1322 self.handle_insert_char_editor(c)?;
1323 }
1324 }
1325 Action::PromptCopy => {
1327 if let Some(prompt) = &self.prompt {
1328 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
1329 if !text.is_empty() {
1330 self.clipboard.copy(text);
1331 self.set_status_message(t!("clipboard.copied").to_string());
1332 }
1333 }
1334 }
1335 Action::PromptCut => {
1336 if let Some(prompt) = &self.prompt {
1337 let text = prompt.selected_text().unwrap_or_else(|| prompt.get_text());
1338 if !text.is_empty() {
1339 self.clipboard.copy(text);
1340 }
1341 }
1342 if let Some(prompt) = self.prompt.as_mut() {
1343 if prompt.has_selection() {
1344 prompt.delete_selection();
1345 } else {
1346 prompt.clear();
1347 }
1348 }
1349 self.set_status_message(t!("clipboard.cut").to_string());
1350 self.update_prompt_suggestions();
1351 }
1352 Action::PromptPaste => {
1353 if let Some(text) = self.clipboard.paste() {
1354 if let Some(prompt) = self.prompt.as_mut() {
1355 prompt.insert_str(&text);
1356 }
1357 self.update_prompt_suggestions();
1358 }
1359 }
1360 _ => {
1361 self.apply_action_as_events(action)?;
1367 }
1368 }
1369
1370 Ok(())
1371 }
1372
1373 pub(super) fn handle_mouse_scroll(
1375 &mut self,
1376 col: u16,
1377 row: u16,
1378 delta: i32,
1379 ) -> AnyhowResult<()> {
1380 let buffer_id = self.active_buffer();
1382 self.plugin_manager.run_hook(
1383 "mouse_scroll",
1384 fresh_core::hooks::HookArgs::MouseScroll {
1385 buffer_id,
1386 delta,
1387 col,
1388 row,
1389 },
1390 );
1391
1392 if let Some(explorer_area) = self.cached_layout.file_explorer_area {
1394 if col >= explorer_area.x
1395 && col < explorer_area.x + explorer_area.width
1396 && row >= explorer_area.y
1397 && row < explorer_area.y + explorer_area.height
1398 {
1399 if let Some(explorer) = &mut self.file_explorer {
1401 let count = explorer.visible_count();
1402 if count == 0 {
1403 return Ok(());
1404 }
1405
1406 let current_index = explorer.get_selected_index().unwrap_or(0);
1408
1409 let new_index = if delta < 0 {
1411 current_index.saturating_sub(delta.unsigned_abs() as usize)
1413 } else {
1414 (current_index + delta as usize).min(count - 1)
1416 };
1417
1418 if let Some(node_id) = explorer.get_node_at_index(new_index) {
1420 explorer.set_selected(Some(node_id));
1421 explorer.update_scroll_for_selection();
1422 }
1423 }
1424 return Ok(());
1425 }
1426 }
1427
1428 let active_split = self.split_manager.active_split();
1431 let buffer_id = self.active_buffer();
1432
1433 if self.is_composite_buffer(buffer_id) {
1435 let max_row = self
1436 .composite_buffers
1437 .get(&buffer_id)
1438 .map(|c| c.row_count().saturating_sub(1))
1439 .unwrap_or(0);
1440 if let Some(view_state) = self
1441 .composite_view_states
1442 .get_mut(&(active_split, buffer_id))
1443 {
1444 view_state.scroll(delta as isize, max_row);
1445 tracing::trace!(
1446 "handle_mouse_scroll (composite): delta={}, scroll_row={}",
1447 delta,
1448 view_state.scroll_row
1449 );
1450 }
1451 return Ok(());
1452 }
1453
1454 let view_transform_tokens = self
1456 .split_view_states
1457 .get(&active_split)
1458 .and_then(|vs| vs.view_transform.as_ref())
1459 .map(|vt| vt.tokens.clone());
1460
1461 let state = self.buffers.get_mut(&buffer_id);
1463 let view_state = self.split_view_states.get_mut(&active_split);
1464
1465 if let (Some(state), Some(view_state)) = (state, view_state) {
1466 let buffer = &mut state.buffer;
1467 let top_byte_before = view_state.viewport.top_byte;
1468 if let Some(tokens) = view_transform_tokens {
1469 use crate::view::ui::view_pipeline::ViewLineIterator;
1471 let tab_size = self.config.editor.tab_size;
1472 let view_lines: Vec<_> =
1473 ViewLineIterator::new(&tokens, false, false, tab_size, false).collect();
1474 view_state
1475 .viewport
1476 .scroll_view_lines(&view_lines, delta as isize);
1477 } else {
1478 if delta < 0 {
1480 let lines_to_scroll = delta.unsigned_abs() as usize;
1482 view_state.viewport.scroll_up(buffer, lines_to_scroll);
1483 } else {
1484 let lines_to_scroll = delta as usize;
1486 view_state.viewport.scroll_down(buffer, lines_to_scroll);
1487 }
1488 }
1489 view_state.viewport.set_skip_ensure_visible();
1491
1492 if let Some(folds) = view_state.keyed_states.get(&buffer_id).map(|bs| &bs.folds) {
1493 if !folds.is_empty() {
1494 let top_line = buffer.get_line_number(view_state.viewport.top_byte);
1495 if let Some(range) = folds
1496 .resolved_ranges(buffer, &state.marker_list)
1497 .iter()
1498 .find(|r| top_line >= r.start_line && top_line <= r.end_line)
1499 {
1500 let target_line = if delta >= 0 {
1501 range.end_line.saturating_add(1)
1502 } else {
1503 range.header_line
1504 };
1505 let target_byte = buffer
1506 .line_start_offset(target_line)
1507 .unwrap_or_else(|| buffer.len());
1508 view_state.viewport.top_byte = target_byte;
1509 view_state.viewport.top_view_line_offset = 0;
1510 }
1511 }
1512 }
1513 tracing::trace!(
1514 "handle_mouse_scroll: delta={}, top_byte {} -> {}",
1515 delta,
1516 top_byte_before,
1517 view_state.viewport.top_byte
1518 );
1519 }
1520
1521 Ok(())
1522 }
1523
1524 pub(super) fn handle_horizontal_scroll(
1526 &mut self,
1527 _col: u16,
1528 _row: u16,
1529 delta: i32,
1530 ) -> AnyhowResult<()> {
1531 let active_split = self.split_manager.active_split();
1532
1533 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
1534 if view_state.viewport.line_wrap_enabled {
1536 return Ok(());
1537 }
1538
1539 let columns_to_scroll = delta.unsigned_abs() as usize;
1540 if delta < 0 {
1541 view_state.viewport.left_column = view_state
1543 .viewport
1544 .left_column
1545 .saturating_sub(columns_to_scroll);
1546 } else {
1547 let visible_width = view_state.viewport.width as usize;
1549 let max_scroll = view_state
1550 .viewport
1551 .max_line_length_seen
1552 .saturating_sub(visible_width);
1553 let new_left = view_state
1554 .viewport
1555 .left_column
1556 .saturating_add(columns_to_scroll);
1557 view_state.viewport.left_column = new_left.min(max_scroll);
1558 }
1559 view_state.viewport.set_skip_ensure_visible();
1561 }
1562
1563 Ok(())
1564 }
1565
1566 pub(super) fn handle_scrollbar_drag_relative(
1568 &mut self,
1569 row: u16,
1570 split_id: LeafId,
1571 buffer_id: BufferId,
1572 scrollbar_rect: ratatui::layout::Rect,
1573 ) -> AnyhowResult<()> {
1574 let drag_start_row = match self.mouse_state.drag_start_row {
1575 Some(r) => r,
1576 None => return Ok(()), };
1578
1579 if self.is_composite_buffer(buffer_id) {
1581 return self.handle_composite_scrollbar_drag_relative(
1582 row,
1583 drag_start_row,
1584 split_id,
1585 buffer_id,
1586 scrollbar_rect,
1587 );
1588 }
1589
1590 let drag_start_top_byte = match self.mouse_state.drag_start_top_byte {
1591 Some(b) => b,
1592 None => return Ok(()), };
1594
1595 let drag_start_view_line_offset = self.mouse_state.drag_start_view_line_offset.unwrap_or(0);
1596
1597 let row_offset = (row as i32) - (drag_start_row as i32);
1599
1600 let viewport_height = self
1602 .split_view_states
1603 .get(&split_id)
1604 .map(|vs| vs.viewport.height as usize)
1605 .unwrap_or(10);
1606
1607 let line_wrap_enabled = self
1609 .split_view_states
1610 .get(&split_id)
1611 .map(|vs| vs.viewport.line_wrap_enabled)
1612 .unwrap_or(false);
1613
1614 let viewport_width = self
1615 .split_view_states
1616 .get(&split_id)
1617 .map(|vs| vs.viewport.width as usize)
1618 .unwrap_or(80);
1619
1620 let scroll_position = if let Some(state) = self.buffers.get_mut(&buffer_id) {
1623 let scrollbar_height = scrollbar_rect.height as usize;
1624 if scrollbar_height == 0 {
1625 return Ok(());
1626 }
1627
1628 let buffer_len = state.buffer.len();
1629 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
1630
1631 if buffer_len <= large_file_threshold {
1633 if line_wrap_enabled {
1635 Self::calculate_scrollbar_drag_relative_visual(
1636 &mut state.buffer,
1637 row,
1638 scrollbar_rect.y,
1639 scrollbar_height,
1640 drag_start_row,
1641 drag_start_top_byte,
1642 drag_start_view_line_offset,
1643 viewport_height,
1644 viewport_width,
1645 )
1646 } else {
1647 let total_lines = if buffer_len > 0 {
1649 state.buffer.get_line_number(buffer_len.saturating_sub(1)) + 1
1650 } else {
1651 1
1652 };
1653
1654 let max_scroll_line = total_lines.saturating_sub(viewport_height);
1655
1656 if max_scroll_line == 0 || scrollbar_height <= 1 {
1657 (0, 0)
1659 } else {
1660 let start_line = state.buffer.get_line_number(drag_start_top_byte);
1662
1663 let thumb_size_raw = (viewport_height as f64 / total_lines as f64
1665 * scrollbar_height as f64)
1666 .ceil() as usize;
1667 let max_thumb_size = (scrollbar_height as f64 * 0.8).floor() as usize;
1668 let thumb_size = thumb_size_raw
1669 .max(1)
1670 .min(max_thumb_size)
1671 .min(scrollbar_height);
1672
1673 let max_thumb_start = scrollbar_height.saturating_sub(thumb_size);
1675
1676 if max_thumb_start == 0 {
1677 (0, 0)
1679 } else {
1680 let start_scroll_ratio =
1682 start_line.min(max_scroll_line) as f64 / max_scroll_line as f64;
1683 let thumb_row_at_start = scrollbar_rect.y as f64
1684 + start_scroll_ratio * max_thumb_start as f64;
1685
1686 let click_offset = drag_start_row as f64 - thumb_row_at_start;
1688
1689 let target_thumb_row = row as f64 - click_offset;
1691
1692 let target_scroll_ratio = ((target_thumb_row
1694 - scrollbar_rect.y as f64)
1695 / max_thumb_start as f64)
1696 .clamp(0.0, 1.0);
1697
1698 let target_line =
1700 (target_scroll_ratio * max_scroll_line as f64).round() as usize;
1701 let target_line = target_line.min(max_scroll_line);
1702
1703 let target_byte = state
1705 .buffer
1706 .line_start_offset(target_line)
1707 .unwrap_or(drag_start_top_byte);
1708
1709 (target_byte, 0)
1710 }
1711 }
1712 }
1713 } else {
1714 let bytes_per_pixel = buffer_len as f64 / scrollbar_height as f64;
1716 let byte_offset = (row_offset as f64 * bytes_per_pixel) as i64;
1717
1718 let new_top_byte = if byte_offset >= 0 {
1719 drag_start_top_byte.saturating_add(byte_offset as usize)
1720 } else {
1721 drag_start_top_byte.saturating_sub((-byte_offset) as usize)
1722 };
1723
1724 let new_top_byte = new_top_byte.min(buffer_len.saturating_sub(1));
1726
1727 let iter = state.buffer.line_iterator(new_top_byte, 80);
1729 (iter.current_position(), 0)
1730 }
1731 } else {
1732 return Ok(());
1733 };
1734
1735 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
1737 view_state.viewport.top_byte = scroll_position.0;
1738 view_state.viewport.top_view_line_offset = scroll_position.1;
1739 view_state.viewport.set_skip_ensure_visible();
1741 }
1742
1743 self.move_cursor_to_visible_area(split_id, buffer_id);
1745
1746 Ok(())
1747 }
1748
1749 pub(super) fn handle_scrollbar_jump(
1751 &mut self,
1752 _col: u16,
1753 row: u16,
1754 split_id: LeafId,
1755 buffer_id: BufferId,
1756 scrollbar_rect: ratatui::layout::Rect,
1757 ) -> AnyhowResult<()> {
1758 let scrollbar_height = scrollbar_rect.height as usize;
1760 if scrollbar_height == 0 {
1761 return Ok(());
1762 }
1763
1764 let relative_row = row.saturating_sub(scrollbar_rect.y);
1767 let ratio = if scrollbar_height > 1 {
1768 ((relative_row as f64) / ((scrollbar_height - 1) as f64)).clamp(0.0, 1.0)
1769 } else {
1770 0.0
1771 };
1772
1773 if self.is_composite_buffer(buffer_id) {
1775 return self.handle_composite_scrollbar_jump(
1776 ratio,
1777 split_id,
1778 buffer_id,
1779 scrollbar_rect,
1780 );
1781 }
1782
1783 let viewport_height = self
1785 .split_view_states
1786 .get(&split_id)
1787 .map(|vs| vs.viewport.height as usize)
1788 .unwrap_or(10);
1789
1790 let line_wrap_enabled = self
1792 .split_view_states
1793 .get(&split_id)
1794 .map(|vs| vs.viewport.line_wrap_enabled)
1795 .unwrap_or(false);
1796
1797 let viewport_width = self
1798 .split_view_states
1799 .get(&split_id)
1800 .map(|vs| vs.viewport.width as usize)
1801 .unwrap_or(80);
1802
1803 let scroll_position = if let Some(state) = self.buffers.get_mut(&buffer_id) {
1806 let buffer_len = state.buffer.len();
1807 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
1808
1809 if buffer_len <= large_file_threshold {
1812 if line_wrap_enabled {
1814 Self::calculate_scrollbar_jump_visual(
1817 &mut state.buffer,
1818 ratio,
1819 viewport_height,
1820 viewport_width,
1821 )
1822 } else {
1823 let total_lines = if buffer_len > 0 {
1825 state.buffer.get_line_number(buffer_len.saturating_sub(1)) + 1
1826 } else {
1827 1
1828 };
1829
1830 let max_scroll_line = total_lines.saturating_sub(viewport_height);
1831
1832 let target_byte = if max_scroll_line == 0 {
1833 0
1835 } else {
1836 let target_line = (ratio * max_scroll_line as f64).round() as usize;
1838 let target_line = target_line.min(max_scroll_line);
1839
1840 let mut iter = state.buffer.line_iterator(0, 80);
1844 let mut line_byte = 0;
1845
1846 for _ in 0..target_line {
1847 if let Some((pos, _content)) = iter.next_line() {
1848 line_byte = pos;
1849 } else {
1850 break;
1851 }
1852 }
1853
1854 if let Some((pos, _)) = iter.next_line() {
1856 pos
1857 } else {
1858 line_byte }
1860 };
1861
1862 let iter = state.buffer.line_iterator(target_byte, 80);
1864 let line_start = iter.current_position();
1865
1866 let max_top_byte =
1868 Self::calculate_max_scroll_position(&mut state.buffer, viewport_height);
1869 (line_start.min(max_top_byte), 0)
1870 }
1871 } else {
1872 let target_byte = (buffer_len as f64 * ratio) as usize;
1874 let target_byte = target_byte.min(buffer_len.saturating_sub(1));
1875
1876 let iter = state.buffer.line_iterator(target_byte, 80);
1878 let line_start = iter.current_position();
1879
1880 (line_start.min(buffer_len.saturating_sub(1)), 0)
1881 }
1882 } else {
1883 return Ok(());
1884 };
1885
1886 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
1888 view_state.viewport.top_byte = scroll_position.0;
1889 view_state.viewport.top_view_line_offset = scroll_position.1;
1890 view_state.viewport.set_skip_ensure_visible();
1892 }
1893
1894 self.move_cursor_to_visible_area(split_id, buffer_id);
1896
1897 Ok(())
1898 }
1899
1900 fn handle_composite_scrollbar_jump(
1903 &mut self,
1904 ratio: f64,
1905 split_id: LeafId,
1906 buffer_id: BufferId,
1907 scrollbar_rect: ratatui::layout::Rect,
1908 ) -> AnyhowResult<()> {
1909 let total_rows = self
1910 .composite_buffers
1911 .get(&buffer_id)
1912 .map(|c| c.row_count())
1913 .unwrap_or(0);
1914 let content_height = scrollbar_rect.height.saturating_sub(1) as usize;
1915 let max_scroll_row = total_rows.saturating_sub(content_height);
1916 let target_row = (ratio * max_scroll_row as f64).round() as usize;
1917 let target_row = target_row.min(max_scroll_row);
1918
1919 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id)) {
1920 view_state.set_scroll_row(target_row, max_scroll_row);
1921 }
1922 Ok(())
1923 }
1924
1925 fn handle_composite_scrollbar_drag_relative(
1928 &mut self,
1929 row: u16,
1930 drag_start_row: u16,
1931 split_id: LeafId,
1932 buffer_id: BufferId,
1933 scrollbar_rect: ratatui::layout::Rect,
1934 ) -> AnyhowResult<()> {
1935 let drag_start_scroll_row = match self.mouse_state.drag_start_composite_scroll_row {
1936 Some(r) => r,
1937 None => return Ok(()),
1938 };
1939
1940 let total_rows = self
1941 .composite_buffers
1942 .get(&buffer_id)
1943 .map(|c| c.row_count())
1944 .unwrap_or(0);
1945 let content_height = scrollbar_rect.height.saturating_sub(1) as usize;
1946 let max_scroll_row = total_rows.saturating_sub(content_height);
1947
1948 if max_scroll_row == 0 {
1949 return Ok(());
1950 }
1951
1952 let scrollbar_height = scrollbar_rect.height as usize;
1953 if scrollbar_height <= 1 {
1954 return Ok(());
1955 }
1956
1957 let thumb_size_raw =
1959 (content_height as f64 / total_rows as f64 * scrollbar_height as f64).ceil() as usize;
1960 let max_thumb_size = (scrollbar_height as f64 * 0.8).floor() as usize;
1961 let thumb_size = thumb_size_raw
1962 .max(1)
1963 .min(max_thumb_size)
1964 .min(scrollbar_height);
1965 let max_thumb_start = scrollbar_height.saturating_sub(thumb_size);
1966
1967 if max_thumb_start == 0 {
1968 return Ok(());
1969 }
1970
1971 let start_scroll_ratio =
1973 drag_start_scroll_row.min(max_scroll_row) as f64 / max_scroll_row as f64;
1974 let thumb_row_at_start =
1975 scrollbar_rect.y as f64 + start_scroll_ratio * max_thumb_start as f64;
1976
1977 let click_offset = drag_start_row as f64 - thumb_row_at_start;
1979
1980 let target_thumb_row = row as f64 - click_offset;
1982
1983 let target_scroll_ratio =
1985 ((target_thumb_row - scrollbar_rect.y as f64) / max_thumb_start as f64).clamp(0.0, 1.0);
1986
1987 let target_row = (target_scroll_ratio * max_scroll_row as f64).round() as usize;
1989 let target_row = target_row.min(max_scroll_row);
1990
1991 if let Some(view_state) = self.composite_view_states.get_mut(&(split_id, buffer_id)) {
1992 view_state.set_scroll_row(target_row, max_scroll_row);
1993 }
1994 Ok(())
1995 }
1996
1997 pub(super) fn move_cursor_to_visible_area(&mut self, split_id: LeafId, buffer_id: BufferId) {
2000 let (top_byte, viewport_height) =
2002 if let Some(view_state) = self.split_view_states.get(&split_id) {
2003 (
2004 view_state.viewport.top_byte,
2005 view_state.viewport.height as usize,
2006 )
2007 } else {
2008 return;
2009 };
2010
2011 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2012 let buffer_len = state.buffer.len();
2013
2014 let mut iter = state.buffer.line_iterator(top_byte, 80);
2017 let mut bottom_byte = buffer_len;
2018
2019 for _ in 0..viewport_height {
2021 if let Some((pos, line)) = iter.next_line() {
2022 bottom_byte = pos + line.len();
2024 } else {
2025 bottom_byte = buffer_len;
2027 break;
2028 }
2029 }
2030
2031 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2033 let cursor_pos = view_state.cursors.primary().position;
2034 if cursor_pos < top_byte || cursor_pos > bottom_byte {
2035 let cursor = view_state.cursors.primary_mut();
2037 cursor.position = top_byte;
2038 }
2040 }
2041 }
2042 }
2043
2044 pub(super) fn calculate_max_scroll_position(
2047 buffer: &mut crate::model::buffer::Buffer,
2048 viewport_height: usize,
2049 ) -> usize {
2050 if viewport_height == 0 {
2051 return 0;
2052 }
2053
2054 let buffer_len = buffer.len();
2055 if buffer_len == 0 {
2056 return 0;
2057 }
2058
2059 let mut line_count = 0;
2061 let mut iter = buffer.line_iterator(0, 80);
2062 while iter.next_line().is_some() {
2063 line_count += 1;
2064 }
2065
2066 if line_count <= viewport_height {
2068 return 0;
2069 }
2070
2071 let scrollable_lines = line_count.saturating_sub(viewport_height);
2074
2075 let mut iter = buffer.line_iterator(0, 80);
2077 let mut current_line = 0;
2078 let mut max_byte_pos = 0;
2079
2080 while current_line < scrollable_lines {
2081 if let Some((pos, _content)) = iter.next_line() {
2082 max_byte_pos = pos;
2083 current_line += 1;
2084 } else {
2085 break;
2086 }
2087 }
2088
2089 max_byte_pos
2090 }
2091
2092 fn calculate_scrollbar_jump_visual(
2097 buffer: &mut crate::model::buffer::Buffer,
2098 ratio: f64,
2099 viewport_height: usize,
2100 viewport_width: usize,
2101 ) -> (usize, usize) {
2102 use crate::primitives::line_wrapping::{wrap_line, WrapConfig};
2103
2104 let buffer_len = buffer.len();
2105 if buffer_len == 0 || viewport_height == 0 {
2106 return (0, 0);
2107 }
2108
2109 let line_count = buffer.line_count().unwrap_or(1);
2111 let digits = (line_count as f64).log10().floor() as usize + 1;
2112 let gutter_width = 1 + digits.max(4) + 3; let wrap_config = WrapConfig::new(viewport_width, gutter_width, true, true);
2115
2116 let mut total_visual_rows = 0;
2118 let mut visual_row_positions: Vec<(usize, usize)> = Vec::new(); let mut iter = buffer.line_iterator(0, 80);
2121 while let Some((line_start, content)) = iter.next_line() {
2122 let line_content = content.trim_end_matches(['\n', '\r']).to_string();
2123 let segments = wrap_line(&line_content, &wrap_config);
2124 let visual_rows_in_line = segments.len().max(1);
2125
2126 for offset in 0..visual_rows_in_line {
2127 visual_row_positions.push((line_start, offset));
2128 }
2129 total_visual_rows += visual_rows_in_line;
2130 }
2131
2132 if total_visual_rows == 0 {
2133 return (0, 0);
2134 }
2135
2136 let max_scroll_row = total_visual_rows.saturating_sub(viewport_height);
2138
2139 if max_scroll_row == 0 {
2140 return (0, 0);
2142 }
2143
2144 let target_row = (ratio * max_scroll_row as f64).round() as usize;
2146 let target_row = target_row.min(max_scroll_row);
2147
2148 if target_row < visual_row_positions.len() {
2150 visual_row_positions[target_row]
2151 } else {
2152 visual_row_positions.last().copied().unwrap_or((0, 0))
2154 }
2155 }
2156
2157 fn calculate_scrollbar_drag_relative_visual(
2161 buffer: &mut crate::model::buffer::Buffer,
2162 current_row: u16,
2163 scrollbar_y: u16,
2164 scrollbar_height: usize,
2165 drag_start_row: u16,
2166 drag_start_top_byte: usize,
2167 drag_start_view_line_offset: usize,
2168 viewport_height: usize,
2169 viewport_width: usize,
2170 ) -> (usize, usize) {
2171 use crate::primitives::line_wrapping::{wrap_line, WrapConfig};
2172
2173 let buffer_len = buffer.len();
2174 if buffer_len == 0 || viewport_height == 0 || scrollbar_height <= 1 {
2175 return (0, 0);
2176 }
2177
2178 let line_count = buffer.line_count().unwrap_or(1);
2180 let digits = (line_count as f64).log10().floor() as usize + 1;
2181 let gutter_width = 1 + digits.max(4) + 3; let wrap_config = WrapConfig::new(viewport_width, gutter_width, true, true);
2184
2185 let mut total_visual_rows = 0;
2187 let mut visual_row_positions: Vec<(usize, usize)> = Vec::new();
2188
2189 let mut iter = buffer.line_iterator(0, 80);
2190 while let Some((line_start, content)) = iter.next_line() {
2191 let line_content = content.trim_end_matches(['\n', '\r']).to_string();
2192 let segments = wrap_line(&line_content, &wrap_config);
2193 let visual_rows_in_line = segments.len().max(1);
2194
2195 for offset in 0..visual_rows_in_line {
2196 visual_row_positions.push((line_start, offset));
2197 }
2198 total_visual_rows += visual_rows_in_line;
2199 }
2200
2201 if total_visual_rows == 0 {
2202 return (0, 0);
2203 }
2204
2205 let max_scroll_row = total_visual_rows.saturating_sub(viewport_height);
2206 if max_scroll_row == 0 {
2207 return (0, 0);
2208 }
2209
2210 let line_start_visual_row = visual_row_positions
2213 .iter()
2214 .position(|(byte, _)| *byte >= drag_start_top_byte)
2215 .unwrap_or(0);
2216 let start_visual_row =
2217 (line_start_visual_row + drag_start_view_line_offset).min(max_scroll_row);
2218
2219 let thumb_size_raw = (viewport_height as f64 / total_visual_rows as f64
2221 * scrollbar_height as f64)
2222 .ceil() as usize;
2223 let max_thumb_size = (scrollbar_height as f64 * 0.8).floor() as usize;
2224 let thumb_size = thumb_size_raw
2225 .max(1)
2226 .min(max_thumb_size)
2227 .min(scrollbar_height);
2228
2229 let max_thumb_start = scrollbar_height.saturating_sub(thumb_size);
2231
2232 let start_scroll_ratio = start_visual_row as f64 / max_scroll_row as f64;
2235 let thumb_row_at_start = scrollbar_y as f64 + start_scroll_ratio * max_thumb_start as f64;
2236
2237 let click_offset = drag_start_row as f64 - thumb_row_at_start;
2239
2240 let target_thumb_row = current_row as f64 - click_offset;
2242
2243 let target_scroll_ratio = if max_thumb_start > 0 {
2245 ((target_thumb_row - scrollbar_y as f64) / max_thumb_start as f64).clamp(0.0, 1.0)
2246 } else {
2247 0.0
2248 };
2249
2250 let target_row = (target_scroll_ratio * max_scroll_row as f64).round() as usize;
2252 let target_row = target_row.min(max_scroll_row);
2253
2254 if target_row < visual_row_positions.len() {
2256 visual_row_positions[target_row]
2257 } else {
2258 visual_row_positions.last().copied().unwrap_or((0, 0))
2259 }
2260 }
2261
2262 pub(crate) fn screen_to_buffer_position(
2271 col: u16,
2272 row: u16,
2273 content_rect: ratatui::layout::Rect,
2274 gutter_width: u16,
2275 cached_mappings: &Option<Vec<crate::app::types::ViewLineMapping>>,
2276 fallback_position: usize,
2277 allow_gutter_click: bool,
2278 compose_width: Option<u16>,
2279 ) -> Option<usize> {
2280 let content_rect = Self::adjust_content_rect_for_compose(content_rect, compose_width);
2282
2283 let content_col = col.saturating_sub(content_rect.x);
2285 let content_row = row.saturating_sub(content_rect.y);
2286
2287 tracing::trace!(
2288 col,
2289 row,
2290 ?content_rect,
2291 gutter_width,
2292 content_col,
2293 content_row,
2294 num_mappings = cached_mappings.as_ref().map(|m| m.len()),
2295 "screen_to_buffer_position"
2296 );
2297
2298 let text_col = if content_col < gutter_width {
2300 if !allow_gutter_click {
2301 return None; }
2303 0 } else {
2305 content_col.saturating_sub(gutter_width) as usize
2306 };
2307
2308 let visual_row = content_row as usize;
2310
2311 let position_from_mapping =
2313 |line_mapping: &crate::app::types::ViewLineMapping, col: usize| -> usize {
2314 if col < line_mapping.visual_to_char.len() {
2315 if let Some(byte_pos) = line_mapping.source_byte_at_visual_col(col) {
2317 return byte_pos;
2318 }
2319 for c in (0..col).rev() {
2321 if let Some(byte_pos) = line_mapping.source_byte_at_visual_col(c) {
2322 return byte_pos;
2323 }
2324 }
2325 line_mapping.line_end_byte
2326 } else {
2327 if line_mapping.visual_to_char.len() <= 1 {
2331 if let Some(Some(first_byte)) = line_mapping.char_source_bytes.first() {
2333 return *first_byte;
2334 }
2335 }
2336 line_mapping.line_end_byte
2337 }
2338 };
2339
2340 let position = cached_mappings
2341 .as_ref()
2342 .and_then(|mappings| {
2343 if let Some(line_mapping) = mappings.get(visual_row) {
2344 Some(position_from_mapping(line_mapping, text_col))
2346 } else if !mappings.is_empty() {
2347 let last_mapping = mappings.last().unwrap();
2349 Some(position_from_mapping(last_mapping, text_col))
2350 } else {
2351 None
2352 }
2353 })
2354 .unwrap_or(fallback_position);
2355
2356 Some(position)
2357 }
2358
2359 pub(super) fn adjust_content_rect_for_compose(
2360 content_rect: ratatui::layout::Rect,
2361 compose_width: Option<u16>,
2362 ) -> ratatui::layout::Rect {
2363 if let Some(cw) = compose_width {
2364 let clamped = cw.min(content_rect.width).max(1);
2365 if clamped < content_rect.width {
2366 let pad_total = content_rect.width - clamped;
2367 let left_pad = pad_total / 2;
2368 ratatui::layout::Rect::new(
2369 content_rect.x + left_pad,
2370 content_rect.y,
2371 clamped,
2372 content_rect.height,
2373 )
2374 } else {
2375 content_rect
2376 }
2377 } else {
2378 content_rect
2379 }
2380 }
2381
2382 fn fold_toggle_byte_from_position(
2385 state: &crate::state::EditorState,
2386 collapsed_header_bytes: &std::collections::BTreeMap<usize, Option<String>>,
2387 target_position: usize,
2388 content_col: u16,
2389 gutter_width: u16,
2390 ) -> Option<usize> {
2391 if content_col >= gutter_width {
2392 return None;
2393 }
2394
2395 use crate::view::folding::indent_folding;
2396 let line_start = indent_folding::find_line_start_byte(&state.buffer, target_position);
2397
2398 if collapsed_header_bytes.contains_key(&line_start) {
2400 return Some(target_position);
2401 }
2402
2403 if !state.folding_ranges.is_empty() {
2405 let line = state.buffer.get_line_number(target_position);
2406 let has_lsp_fold = state.folding_ranges.iter().any(|range| {
2407 let start_line = range.start_line as usize;
2408 let end_line = range.end_line as usize;
2409 start_line == line && end_line > start_line
2410 });
2411 if has_lsp_fold {
2412 return Some(target_position);
2413 }
2414 }
2415
2416 if state.folding_ranges.is_empty() {
2418 let tab_size = state.buffer_settings.tab_size;
2419 let max_scan = crate::config::INDENT_FOLD_INDICATOR_MAX_SCAN;
2420 let max_bytes = max_scan * state.buffer.estimated_line_length();
2421 if indent_folding::indent_fold_end_byte(&state.buffer, line_start, tab_size, max_bytes)
2422 .is_some()
2423 {
2424 return Some(target_position);
2425 }
2426 }
2427
2428 None
2429 }
2430
2431 pub(super) fn fold_toggle_line_at_screen_position(
2432 &self,
2433 col: u16,
2434 row: u16,
2435 ) -> Option<(BufferId, usize)> {
2436 for (split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
2437 &self.cached_layout.split_areas
2438 {
2439 if col < content_rect.x
2440 || col >= content_rect.x + content_rect.width
2441 || row < content_rect.y
2442 || row >= content_rect.y + content_rect.height
2443 {
2444 continue;
2445 }
2446
2447 if self.is_terminal_buffer(*buffer_id) || self.is_composite_buffer(*buffer_id) {
2448 continue;
2449 }
2450
2451 let (gutter_width, collapsed_header_bytes) = {
2452 let state = self.buffers.get(buffer_id)?;
2453 let headers = self
2454 .split_view_states
2455 .get(split_id)
2456 .map(|vs| {
2457 vs.folds
2458 .collapsed_header_bytes(&state.buffer, &state.marker_list)
2459 })
2460 .unwrap_or_default();
2461 (state.margins.left_total_width() as u16, headers)
2462 };
2463
2464 let cached_mappings = self.cached_layout.view_line_mappings.get(split_id).cloned();
2465 let fallback = self
2466 .split_view_states
2467 .get(split_id)
2468 .map(|vs| vs.viewport.top_byte)
2469 .unwrap_or(0);
2470 let compose_width = self
2471 .split_view_states
2472 .get(split_id)
2473 .and_then(|vs| vs.compose_width);
2474
2475 let target_position = Self::screen_to_buffer_position(
2476 col,
2477 row,
2478 *content_rect,
2479 gutter_width,
2480 &cached_mappings,
2481 fallback,
2482 true,
2483 compose_width,
2484 )?;
2485
2486 let adjusted_rect = Self::adjust_content_rect_for_compose(*content_rect, compose_width);
2487 let content_col = col.saturating_sub(adjusted_rect.x);
2488 let state = self.buffers.get(buffer_id)?;
2489 if let Some(byte_pos) = Self::fold_toggle_byte_from_position(
2490 state,
2491 &collapsed_header_bytes,
2492 target_position,
2493 content_col,
2494 gutter_width,
2495 ) {
2496 return Some((*buffer_id, byte_pos));
2497 }
2498 }
2499
2500 None
2501 }
2502
2503 pub(super) fn handle_editor_click(
2505 &mut self,
2506 col: u16,
2507 row: u16,
2508 split_id: crate::model::event::LeafId,
2509 buffer_id: BufferId,
2510 content_rect: ratatui::layout::Rect,
2511 modifiers: crossterm::event::KeyModifiers,
2512 ) -> AnyhowResult<()> {
2513 use crate::model::event::{CursorId, Event};
2514 use crossterm::event::KeyModifiers;
2515 let modifiers_str = if modifiers.contains(KeyModifiers::SHIFT) {
2517 "shift".to_string()
2518 } else {
2519 String::new()
2520 };
2521
2522 if self.plugin_manager.has_hook_handlers("mouse_click") {
2525 self.plugin_manager.run_hook(
2526 "mouse_click",
2527 HookArgs::MouseClick {
2528 column: col,
2529 row,
2530 button: "left".to_string(),
2531 modifiers: modifiers_str,
2532 content_x: content_rect.x,
2533 content_y: content_rect.y,
2534 },
2535 );
2536 }
2537
2538 self.focus_split(split_id, buffer_id);
2540
2541 if self.is_composite_buffer(buffer_id) {
2543 return self.handle_composite_click(col, row, split_id, buffer_id, content_rect);
2544 }
2545
2546 if !self.is_terminal_buffer(buffer_id) {
2549 self.key_context = crate::input::keybindings::KeyContext::Normal;
2550 }
2551
2552 let cached_mappings = self
2554 .cached_layout
2555 .view_line_mappings
2556 .get(&split_id)
2557 .cloned();
2558
2559 let fallback = self
2561 .split_view_states
2562 .get(&split_id)
2563 .map(|vs| vs.viewport.top_byte)
2564 .unwrap_or(0);
2565
2566 let compose_width = self
2568 .split_view_states
2569 .get(&split_id)
2570 .and_then(|vs| vs.compose_width);
2571
2572 let (toggle_fold_byte, onclick_action, target_position, cursor_snapshot) =
2574 if let Some(state) = self.buffers.get(&buffer_id) {
2575 let gutter_width = state.margins.left_total_width() as u16;
2576
2577 let Some(target_position) = Self::screen_to_buffer_position(
2578 col,
2579 row,
2580 content_rect,
2581 gutter_width,
2582 &cached_mappings,
2583 fallback,
2584 true, compose_width,
2586 ) else {
2587 return Ok(());
2588 };
2589
2590 let adjusted_rect =
2592 Self::adjust_content_rect_for_compose(content_rect, compose_width);
2593 let content_col = col.saturating_sub(adjusted_rect.x);
2594 let collapsed_header_bytes = self
2595 .split_view_states
2596 .get(&split_id)
2597 .map(|vs| {
2598 vs.folds
2599 .collapsed_header_bytes(&state.buffer, &state.marker_list)
2600 })
2601 .unwrap_or_default();
2602 let toggle_fold_byte = Self::fold_toggle_byte_from_position(
2603 state,
2604 &collapsed_header_bytes,
2605 target_position,
2606 content_col,
2607 gutter_width,
2608 );
2609
2610 let cursor_snapshot = self
2611 .split_view_states
2612 .get(&split_id)
2613 .map(|vs| {
2614 let cursor = vs.cursors.primary();
2615 (
2616 vs.cursors.primary_id(),
2617 cursor.position,
2618 cursor.anchor,
2619 cursor.sticky_column,
2620 cursor.deselect_on_move,
2621 )
2622 })
2623 .unwrap_or((CursorId(0), 0, None, 0, true));
2624
2625 let onclick_action = state
2628 .text_properties
2629 .get_at(target_position)
2630 .iter()
2631 .find_map(|prop| {
2632 prop.get("onClick")
2633 .and_then(|v| v.as_str())
2634 .map(|s| s.to_string())
2635 });
2636
2637 (
2638 toggle_fold_byte,
2639 onclick_action,
2640 target_position,
2641 cursor_snapshot,
2642 )
2643 } else {
2644 return Ok(());
2645 };
2646
2647 if toggle_fold_byte.is_some() {
2648 self.toggle_fold_at_byte(buffer_id, target_position);
2649 return Ok(());
2650 }
2651
2652 let (primary_cursor_id, old_position, old_anchor, old_sticky_column, deselect_on_move) =
2653 cursor_snapshot;
2654
2655 if let Some(action_name) = onclick_action {
2656 tracing::debug!(
2658 "onClick triggered at position {}: action={}",
2659 target_position,
2660 action_name
2661 );
2662 let empty_args = std::collections::HashMap::new();
2663 if let Some(action) = Action::from_str(&action_name, &empty_args) {
2664 return self.handle_action(action);
2665 }
2666 return Ok(());
2667 }
2668
2669 let extend_selection =
2672 modifiers.contains(KeyModifiers::SHIFT) || modifiers.contains(KeyModifiers::CONTROL);
2673 let new_anchor = if extend_selection {
2674 Some(old_anchor.unwrap_or(old_position))
2675 } else if deselect_on_move {
2676 None
2677 } else {
2678 old_anchor
2679 };
2680
2681 let new_sticky_column = self
2682 .buffers
2683 .get(&buffer_id)
2684 .and_then(|state| state.buffer.offset_to_position(target_position))
2685 .map(|pos| pos.column)
2686 .unwrap_or(0);
2687
2688 let event = Event::MoveCursor {
2689 cursor_id: primary_cursor_id,
2690 old_position,
2691 new_position: target_position,
2692 old_anchor,
2693 new_anchor,
2694 old_sticky_column,
2695 new_sticky_column,
2696 };
2697
2698 self.active_event_log_mut().append(event.clone());
2699 self.apply_event_to_active_buffer(&event);
2700 self.track_cursor_movement(&event);
2701
2702 self.mouse_state.dragging_text_selection = true;
2704 self.mouse_state.drag_selection_split = Some(split_id);
2705 self.mouse_state.drag_selection_anchor = Some(new_anchor.unwrap_or(target_position));
2706
2707 Ok(())
2708 }
2709
2710 pub(super) fn handle_file_explorer_click(
2712 &mut self,
2713 col: u16,
2714 row: u16,
2715 explorer_area: ratatui::layout::Rect,
2716 ) -> AnyhowResult<()> {
2717 if row == explorer_area.y {
2719 let close_button_x = explorer_area.x + explorer_area.width.saturating_sub(3);
2722 if col >= close_button_x && col < explorer_area.x + explorer_area.width {
2723 self.toggle_file_explorer();
2724 return Ok(());
2725 }
2726 }
2727
2728 self.key_context = crate::input::keybindings::KeyContext::FileExplorer;
2730
2731 let relative_row = row.saturating_sub(explorer_area.y + 1); if let Some(ref mut explorer) = self.file_explorer {
2736 let display_nodes = explorer.get_display_nodes();
2737 let scroll_offset = explorer.get_scroll_offset();
2738 let clicked_index = (relative_row as usize) + scroll_offset;
2739
2740 if clicked_index < display_nodes.len() {
2741 let (node_id, _indent) = display_nodes[clicked_index];
2742
2743 explorer.set_selected(Some(node_id));
2745
2746 let node = explorer.tree().get_node(node_id);
2748 if let Some(node) = node {
2749 if node.is_dir() {
2750 self.file_explorer_toggle_expand();
2752 } else if node.is_file() {
2753 let path = node.entry.path.clone();
2756 let name = node.entry.name.clone();
2757 match self.open_file(&path) {
2758 Ok(_) => {
2759 self.set_status_message(
2760 rust_i18n::t!("explorer.opened_file", name = &name).to_string(),
2761 );
2762 }
2763 Err(e) => {
2764 if let Some(confirmation) = e.downcast_ref::<
2766 crate::model::buffer::LargeFileEncodingConfirmation,
2767 >() {
2768 self.start_large_file_encoding_confirmation(confirmation);
2769 } else {
2770 self.set_status_message(
2771 rust_i18n::t!("file.error_opening", error = e.to_string())
2772 .to_string(),
2773 );
2774 }
2775 }
2776 }
2777 }
2778 }
2779 }
2780 }
2781
2782 Ok(())
2783 }
2784
2785 fn start_set_line_ending_prompt(&mut self) {
2787 use crate::model::buffer::LineEnding;
2788
2789 let current_line_ending = self.active_state().buffer.line_ending();
2790
2791 let options = [
2792 (LineEnding::LF, "LF", "Unix/Linux/Mac"),
2793 (LineEnding::CRLF, "CRLF", "Windows"),
2794 (LineEnding::CR, "CR", "Classic Mac"),
2795 ];
2796
2797 let current_index = options
2798 .iter()
2799 .position(|(le, _, _)| *le == current_line_ending)
2800 .unwrap_or(0);
2801
2802 let suggestions: Vec<crate::input::commands::Suggestion> = options
2803 .iter()
2804 .map(|(le, name, desc)| {
2805 let is_current = *le == current_line_ending;
2806 crate::input::commands::Suggestion {
2807 text: format!("{} ({})", name, desc),
2808 description: if is_current {
2809 Some("current".to_string())
2810 } else {
2811 None
2812 },
2813 value: Some(name.to_string()),
2814 disabled: false,
2815 keybinding: None,
2816 source: None,
2817 }
2818 })
2819 .collect();
2820
2821 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
2822 "Line ending: ".to_string(),
2823 PromptType::SetLineEnding,
2824 suggestions,
2825 ));
2826
2827 if let Some(prompt) = self.prompt.as_mut() {
2828 if !prompt.suggestions.is_empty() {
2829 prompt.selected_suggestion = Some(current_index);
2830 let (_, name, desc) = options[current_index];
2831 prompt.input = format!("{} ({})", name, desc);
2832 prompt.cursor_pos = prompt.input.len();
2833 }
2834 }
2835 }
2836
2837 fn start_set_encoding_prompt(&mut self) {
2839 use crate::model::buffer::Encoding;
2840
2841 let current_encoding = self.active_state().buffer.encoding();
2842
2843 let suggestions: Vec<crate::input::commands::Suggestion> = Encoding::all()
2844 .iter()
2845 .map(|enc| {
2846 let is_current = *enc == current_encoding;
2847 crate::input::commands::Suggestion {
2848 text: format!("{} ({})", enc.display_name(), enc.description()),
2849 description: if is_current {
2850 Some("current".to_string())
2851 } else {
2852 None
2853 },
2854 value: Some(enc.display_name().to_string()),
2855 disabled: false,
2856 keybinding: None,
2857 source: None,
2858 }
2859 })
2860 .collect();
2861
2862 let current_index = Encoding::all()
2863 .iter()
2864 .position(|enc| *enc == current_encoding)
2865 .unwrap_or(0);
2866
2867 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
2868 "Encoding: ".to_string(),
2869 PromptType::SetEncoding,
2870 suggestions,
2871 ));
2872
2873 if let Some(prompt) = self.prompt.as_mut() {
2874 if !prompt.suggestions.is_empty() {
2875 prompt.selected_suggestion = Some(current_index);
2876 let enc = Encoding::all()[current_index];
2877 prompt.input = format!("{} ({})", enc.display_name(), enc.description());
2878 prompt.cursor_pos = prompt.input.len();
2879 prompt.selection_anchor = Some(0);
2881 }
2882 }
2883 }
2884
2885 fn start_reload_with_encoding_prompt(&mut self) {
2890 use crate::model::buffer::Encoding;
2891
2892 let has_file = self
2894 .buffers
2895 .get(&self.active_buffer())
2896 .and_then(|s| s.buffer.file_path())
2897 .is_some();
2898
2899 if !has_file {
2900 self.set_status_message("Cannot reload: buffer has no file".to_string());
2901 return;
2902 }
2903
2904 let is_modified = self
2906 .buffers
2907 .get(&self.active_buffer())
2908 .map(|s| s.buffer.is_modified())
2909 .unwrap_or(false);
2910
2911 if is_modified {
2912 self.set_status_message(
2913 "Cannot reload: buffer has unsaved modifications (save first)".to_string(),
2914 );
2915 return;
2916 }
2917
2918 let current_encoding = self.active_state().buffer.encoding();
2919
2920 let suggestions: Vec<crate::input::commands::Suggestion> = Encoding::all()
2921 .iter()
2922 .map(|enc| {
2923 let is_current = *enc == current_encoding;
2924 crate::input::commands::Suggestion {
2925 text: format!("{} ({})", enc.display_name(), enc.description()),
2926 description: if is_current {
2927 Some("current".to_string())
2928 } else {
2929 None
2930 },
2931 value: Some(enc.display_name().to_string()),
2932 disabled: false,
2933 keybinding: None,
2934 source: None,
2935 }
2936 })
2937 .collect();
2938
2939 let current_index = Encoding::all()
2940 .iter()
2941 .position(|enc| *enc == current_encoding)
2942 .unwrap_or(0);
2943
2944 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
2945 "Reload with encoding: ".to_string(),
2946 PromptType::ReloadWithEncoding,
2947 suggestions,
2948 ));
2949
2950 if let Some(prompt) = self.prompt.as_mut() {
2951 if !prompt.suggestions.is_empty() {
2952 prompt.selected_suggestion = Some(current_index);
2953 let enc = Encoding::all()[current_index];
2954 prompt.input = format!("{} ({})", enc.display_name(), enc.description());
2955 prompt.cursor_pos = prompt.input.len();
2956 }
2957 }
2958 }
2959
2960 fn start_set_language_prompt(&mut self) {
2962 let current_language = self.active_state().language.clone();
2963
2964 let mut suggestions: Vec<crate::input::commands::Suggestion> = vec![
2966 crate::input::commands::Suggestion {
2968 text: "Plain Text".to_string(),
2969 description: if current_language == "text" || current_language == "Plain Text" {
2970 Some("current".to_string())
2971 } else {
2972 None
2973 },
2974 value: Some("Plain Text".to_string()),
2975 disabled: false,
2976 keybinding: None,
2977 source: None,
2978 },
2979 ];
2980
2981 let mut syntax_names: Vec<&str> = self.grammar_registry.available_syntaxes();
2983 syntax_names.sort_unstable_by_key(|a| a.to_lowercase());
2985
2986 let mut current_index_found = None;
2987 for syntax_name in syntax_names {
2988 if syntax_name == "Plain Text" {
2990 continue;
2991 }
2992 let is_current = self
2996 .resolve_language_id(syntax_name)
2997 .is_some_and(|id| id == current_language);
2998 if is_current {
2999 current_index_found = Some(suggestions.len());
3000 }
3001 suggestions.push(crate::input::commands::Suggestion {
3002 text: syntax_name.to_string(),
3003 description: if is_current {
3004 Some("current".to_string())
3005 } else {
3006 None
3007 },
3008 value: Some(syntax_name.to_string()),
3009 disabled: false,
3010 keybinding: None,
3011 source: None,
3012 });
3013 }
3014
3015 let current_index = current_index_found.unwrap_or(0);
3017
3018 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
3019 "Language: ".to_string(),
3020 PromptType::SetLanguage,
3021 suggestions,
3022 ));
3023
3024 if let Some(prompt) = self.prompt.as_mut() {
3025 if !prompt.suggestions.is_empty() {
3026 prompt.selected_suggestion = Some(current_index);
3027 }
3030 }
3031 }
3032
3033 fn start_select_theme_prompt(&mut self) {
3035 let available_themes = self.theme_registry.list();
3036 let current_theme_name = &self.theme.name;
3037
3038 let current_index = available_themes
3040 .iter()
3041 .position(|info| info.name == *current_theme_name)
3042 .unwrap_or(0);
3043
3044 let suggestions: Vec<crate::input::commands::Suggestion> = available_themes
3045 .iter()
3046 .map(|info| {
3047 let is_current = info.name == *current_theme_name;
3048 let description = match (is_current, info.pack.is_empty()) {
3049 (true, true) => Some("(current)".to_string()),
3050 (true, false) => Some(format!("{} (current)", info.pack)),
3051 (false, true) => None,
3052 (false, false) => Some(info.pack.clone()),
3053 };
3054 crate::input::commands::Suggestion {
3055 text: info.name.clone(),
3056 description,
3057 value: Some(info.name.clone()),
3058 disabled: false,
3059 keybinding: None,
3060 source: None,
3061 }
3062 })
3063 .collect();
3064
3065 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
3066 "Select theme: ".to_string(),
3067 PromptType::SelectTheme {
3068 original_theme: current_theme_name.clone(),
3069 },
3070 suggestions,
3071 ));
3072
3073 if let Some(prompt) = self.prompt.as_mut() {
3074 if !prompt.suggestions.is_empty() {
3075 prompt.selected_suggestion = Some(current_index);
3076 prompt.input = current_theme_name.to_string();
3078 prompt.cursor_pos = prompt.input.len();
3079 }
3080 }
3081 }
3082
3083 pub(super) fn apply_theme(&mut self, theme_name: &str) {
3085 if !theme_name.is_empty() {
3086 if let Some(theme) = self.theme_registry.get_cloned(theme_name) {
3087 self.theme = theme;
3088
3089 self.theme.set_terminal_cursor_color();
3091
3092 let normalized = crate::view::theme::normalize_theme_name(theme_name);
3096 self.config.theme = normalized.into();
3097
3098 self.save_theme_to_config();
3100
3101 self.set_status_message(
3102 t!("view.theme_changed", theme = self.theme.name.clone()).to_string(),
3103 );
3104 } else {
3105 self.set_status_message(format!("Theme '{}' not found", theme_name));
3106 }
3107 }
3108 }
3109
3110 pub(super) fn preview_theme(&mut self, theme_name: &str) {
3113 if !theme_name.is_empty() && theme_name != self.theme.name {
3114 if let Some(theme) = self.theme_registry.get_cloned(theme_name) {
3115 self.theme = theme;
3116 self.theme.set_terminal_cursor_color();
3117 }
3118 }
3119 }
3120
3121 fn save_theme_to_config(&mut self) {
3123 if let Err(e) = self.filesystem.create_dir_all(&self.dir_context.config_dir) {
3125 tracing::warn!("Failed to create config directory: {}", e);
3126 return;
3127 }
3128
3129 let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
3133 let config_path = resolver.user_config_path();
3134 tracing::info!(
3135 "Saving theme '{}' to user config at {}",
3136 self.config.theme.0,
3137 config_path.display()
3138 );
3139
3140 let mut changes = std::collections::HashMap::new();
3141 changes.insert(
3142 "/theme".to_string(),
3143 serde_json::Value::String(self.config.theme.0.clone()),
3144 );
3145
3146 match resolver.save_changes_to_layer(
3147 &changes,
3148 &std::collections::HashSet::new(),
3149 ConfigLayer::User,
3150 ) {
3151 Ok(()) => {
3152 tracing::info!("Theme saved successfully to {}", config_path.display());
3153 }
3154 Err(e) => {
3155 tracing::warn!("Failed to save theme to config: {}", e);
3156 }
3157 }
3158 }
3159
3160 fn start_select_keybinding_map_prompt(&mut self) {
3162 let builtin_maps = vec!["default", "emacs", "vscode", "macos"];
3164
3165 let user_maps: Vec<&str> = self
3167 .config
3168 .keybinding_maps
3169 .keys()
3170 .map(|s| s.as_str())
3171 .collect();
3172
3173 let mut all_maps: Vec<&str> = builtin_maps;
3175 for map in &user_maps {
3176 if !all_maps.contains(map) {
3177 all_maps.push(map);
3178 }
3179 }
3180
3181 let current_map = &self.config.active_keybinding_map;
3182
3183 let current_index = all_maps
3185 .iter()
3186 .position(|name| *name == current_map)
3187 .unwrap_or(0);
3188
3189 let suggestions: Vec<crate::input::commands::Suggestion> = all_maps
3190 .iter()
3191 .map(|map_name| {
3192 let is_current = *map_name == current_map;
3193 crate::input::commands::Suggestion {
3194 text: map_name.to_string(),
3195 description: if is_current {
3196 Some("(current)".to_string())
3197 } else {
3198 None
3199 },
3200 value: Some(map_name.to_string()),
3201 disabled: false,
3202 keybinding: None,
3203 source: None,
3204 }
3205 })
3206 .collect();
3207
3208 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
3209 "Select keybinding map: ".to_string(),
3210 PromptType::SelectKeybindingMap,
3211 suggestions,
3212 ));
3213
3214 if let Some(prompt) = self.prompt.as_mut() {
3215 if !prompt.suggestions.is_empty() {
3216 prompt.selected_suggestion = Some(current_index);
3217 prompt.input = current_map.to_string();
3219 prompt.cursor_pos = prompt.input.len();
3220 }
3221 }
3222 }
3223
3224 pub(super) fn apply_keybinding_map(&mut self, map_name: &str) {
3226 if map_name.is_empty() {
3227 return;
3228 }
3229
3230 let is_builtin = matches!(map_name, "default" | "emacs" | "vscode" | "macos");
3232 let is_user_defined = self.config.keybinding_maps.contains_key(map_name);
3233
3234 if is_builtin || is_user_defined {
3235 self.config.active_keybinding_map = map_name.to_string().into();
3237
3238 self.keybindings = crate::input::keybindings::KeybindingResolver::new(&self.config);
3240
3241 self.save_keybinding_map_to_config();
3243
3244 self.set_status_message(t!("view.keybindings_switched", map = map_name).to_string());
3245 } else {
3246 self.set_status_message(t!("view.keybindings_unknown", map = map_name).to_string());
3247 }
3248 }
3249
3250 fn save_keybinding_map_to_config(&mut self) {
3252 if let Err(e) = self.filesystem.create_dir_all(&self.dir_context.config_dir) {
3254 tracing::warn!("Failed to create config directory: {}", e);
3255 return;
3256 }
3257
3258 let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
3260 if let Err(e) = resolver.save_to_layer(&self.config, ConfigLayer::User) {
3261 tracing::warn!("Failed to save keybinding map to config: {}", e);
3262 }
3263 }
3264
3265 fn start_select_cursor_style_prompt(&mut self) {
3267 use crate::config::CursorStyle;
3268
3269 let current_style = self.config.editor.cursor_style;
3270
3271 let suggestions: Vec<crate::input::commands::Suggestion> = CursorStyle::OPTIONS
3273 .iter()
3274 .zip(CursorStyle::DESCRIPTIONS.iter())
3275 .map(|(style_name, description)| {
3276 let is_current = *style_name == current_style.as_str();
3277 crate::input::commands::Suggestion {
3278 text: description.to_string(),
3279 description: if is_current {
3280 Some("(current)".to_string())
3281 } else {
3282 None
3283 },
3284 value: Some(style_name.to_string()),
3285 disabled: false,
3286 keybinding: None,
3287 source: None,
3288 }
3289 })
3290 .collect();
3291
3292 let current_index = CursorStyle::OPTIONS
3294 .iter()
3295 .position(|s| *s == current_style.as_str())
3296 .unwrap_or(0);
3297
3298 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
3299 "Select cursor style: ".to_string(),
3300 PromptType::SelectCursorStyle,
3301 suggestions,
3302 ));
3303
3304 if let Some(prompt) = self.prompt.as_mut() {
3305 if !prompt.suggestions.is_empty() {
3306 prompt.selected_suggestion = Some(current_index);
3307 prompt.input = CursorStyle::DESCRIPTIONS[current_index].to_string();
3308 prompt.cursor_pos = prompt.input.len();
3309 }
3310 }
3311 }
3312
3313 pub(super) fn apply_cursor_style(&mut self, style_name: &str) {
3315 use crate::config::CursorStyle;
3316
3317 if let Some(style) = CursorStyle::parse(style_name) {
3318 self.config.editor.cursor_style = style;
3320
3321 if self.session_mode {
3323 self.queue_escape_sequences(style.to_escape_sequence());
3325 } else {
3326 use std::io::stdout;
3328 #[allow(clippy::let_underscore_must_use)]
3330 let _ = crossterm::execute!(stdout(), style.to_crossterm_style());
3331 }
3332
3333 self.save_cursor_style_to_config();
3335
3336 let description = CursorStyle::OPTIONS
3338 .iter()
3339 .zip(CursorStyle::DESCRIPTIONS.iter())
3340 .find(|(name, _)| **name == style_name)
3341 .map(|(_, desc)| *desc)
3342 .unwrap_or(style_name);
3343
3344 self.set_status_message(
3345 t!("view.cursor_style_changed", style = description).to_string(),
3346 );
3347 }
3348 }
3349
3350 fn start_remove_ruler_prompt(&mut self) {
3352 let active_split = self.split_manager.active_split();
3353 let rulers = self
3354 .split_view_states
3355 .get(&active_split)
3356 .map(|vs| vs.rulers.clone())
3357 .unwrap_or_default();
3358
3359 if rulers.is_empty() {
3360 self.set_status_message(t!("rulers.none_configured").to_string());
3361 return;
3362 }
3363
3364 let suggestions: Vec<crate::input::commands::Suggestion> = rulers
3365 .iter()
3366 .map(|&col| crate::input::commands::Suggestion {
3367 text: format!("Column {}", col),
3368 description: None,
3369 value: Some(col.to_string()),
3370 disabled: false,
3371 keybinding: None,
3372 source: None,
3373 })
3374 .collect();
3375
3376 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
3377 t!("rulers.remove_prompt").to_string(),
3378 PromptType::RemoveRuler,
3379 suggestions,
3380 ));
3381 }
3382
3383 fn save_cursor_style_to_config(&mut self) {
3385 if let Err(e) = self.filesystem.create_dir_all(&self.dir_context.config_dir) {
3387 tracing::warn!("Failed to create config directory: {}", e);
3388 return;
3389 }
3390
3391 let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
3393 if let Err(e) = resolver.save_to_layer(&self.config, ConfigLayer::User) {
3394 tracing::warn!("Failed to save cursor style to config: {}", e);
3395 }
3396 }
3397
3398 fn start_select_locale_prompt(&mut self) {
3400 let available_locales = crate::i18n::available_locales();
3401 let current_locale = crate::i18n::current_locale();
3402
3403 let current_index = available_locales
3405 .iter()
3406 .position(|name| *name == current_locale)
3407 .unwrap_or(0);
3408
3409 let suggestions: Vec<crate::input::commands::Suggestion> = available_locales
3410 .iter()
3411 .map(|locale_name| {
3412 let is_current = *locale_name == current_locale;
3413 let description = if let Some((english_name, native_name)) =
3414 crate::i18n::locale_display_name(locale_name)
3415 {
3416 if english_name == native_name {
3417 if is_current {
3419 format!("{} (current)", english_name)
3420 } else {
3421 english_name.to_string()
3422 }
3423 } else {
3424 if is_current {
3426 format!("{} / {} (current)", english_name, native_name)
3427 } else {
3428 format!("{} / {}", english_name, native_name)
3429 }
3430 }
3431 } else {
3432 if is_current {
3434 "(current)".to_string()
3435 } else {
3436 String::new()
3437 }
3438 };
3439 crate::input::commands::Suggestion {
3440 text: locale_name.to_string(),
3441 description: if description.is_empty() {
3442 None
3443 } else {
3444 Some(description)
3445 },
3446 value: Some(locale_name.to_string()),
3447 disabled: false,
3448 keybinding: None,
3449 source: None,
3450 }
3451 })
3452 .collect();
3453
3454 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
3455 t!("locale.select_prompt").to_string(),
3456 PromptType::SelectLocale,
3457 suggestions,
3458 ));
3459
3460 if let Some(prompt) = self.prompt.as_mut() {
3461 if !prompt.suggestions.is_empty() {
3462 prompt.selected_suggestion = Some(current_index);
3463 prompt.input = String::new();
3465 prompt.cursor_pos = 0;
3466 }
3467 }
3468 }
3469
3470 pub(super) fn apply_locale(&mut self, locale_name: &str) {
3472 if !locale_name.is_empty() {
3473 crate::i18n::set_locale(locale_name);
3475
3476 self.config.locale = crate::config::LocaleName(Some(locale_name.to_string()));
3478
3479 self.menus = crate::config::MenuConfig::translated();
3481
3482 if let Ok(mut registry) = self.command_registry.write() {
3484 registry.refresh_builtin_commands();
3485 }
3486
3487 self.save_locale_to_config();
3489
3490 self.set_status_message(t!("locale.changed", locale_name = locale_name).to_string());
3491 }
3492 }
3493
3494 fn save_locale_to_config(&mut self) {
3496 if let Err(e) = self.filesystem.create_dir_all(&self.dir_context.config_dir) {
3498 tracing::warn!("Failed to create config directory: {}", e);
3499 return;
3500 }
3501
3502 let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
3504 if let Err(e) = resolver.save_to_layer(&self.config, ConfigLayer::User) {
3505 tracing::warn!("Failed to save locale to config: {}", e);
3506 }
3507 }
3508
3509 fn switch_to_previous_tab(&mut self) {
3511 let active_split = self.split_manager.active_split();
3512 let previous_buffer = self
3513 .split_view_states
3514 .get(&active_split)
3515 .and_then(|vs| vs.previous_buffer());
3516
3517 if let Some(prev_id) = previous_buffer {
3518 let is_valid = self
3520 .split_view_states
3521 .get(&active_split)
3522 .is_some_and(|vs| vs.open_buffers.contains(&prev_id));
3523
3524 if is_valid && prev_id != self.active_buffer() {
3525 self.position_history.commit_pending_movement();
3527
3528 let cursors = self.active_cursors();
3529 let position = cursors.primary().position;
3530 let anchor = cursors.primary().anchor;
3531 self.position_history
3532 .record_movement(self.active_buffer(), position, anchor);
3533 self.position_history.commit_pending_movement();
3534
3535 self.set_active_buffer(prev_id);
3536 } else if !is_valid {
3537 self.set_status_message(t!("status.previous_tab_closed").to_string());
3538 }
3539 } else {
3540 self.set_status_message(t!("status.no_previous_tab").to_string());
3541 }
3542 }
3543
3544 fn start_switch_to_tab_prompt(&mut self) {
3546 let active_split = self.split_manager.active_split();
3547 let open_buffers = if let Some(view_state) = self.split_view_states.get(&active_split) {
3548 view_state.open_buffers.clone()
3549 } else {
3550 return;
3551 };
3552
3553 if open_buffers.is_empty() {
3554 self.set_status_message(t!("status.no_tabs_in_split").to_string());
3555 return;
3556 }
3557
3558 let current_index = open_buffers
3560 .iter()
3561 .position(|&id| id == self.active_buffer())
3562 .unwrap_or(0);
3563
3564 let suggestions: Vec<crate::input::commands::Suggestion> = open_buffers
3565 .iter()
3566 .map(|&buffer_id| {
3567 let display_name = self
3568 .buffer_metadata
3569 .get(&buffer_id)
3570 .map(|m| m.display_name.clone())
3571 .unwrap_or_else(|| format!("Buffer {:?}", buffer_id));
3572
3573 let is_current = buffer_id == self.active_buffer();
3574 let is_modified = self
3575 .buffers
3576 .get(&buffer_id)
3577 .is_some_and(|b| b.buffer.is_modified());
3578
3579 let description = match (is_current, is_modified) {
3580 (true, true) => Some("(current, modified)".to_string()),
3581 (true, false) => Some("(current)".to_string()),
3582 (false, true) => Some("(modified)".to_string()),
3583 (false, false) => None,
3584 };
3585
3586 crate::input::commands::Suggestion {
3587 text: display_name,
3588 description,
3589 value: Some(buffer_id.0.to_string()),
3590 disabled: false,
3591 keybinding: None,
3592 source: None,
3593 }
3594 })
3595 .collect();
3596
3597 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
3598 "Switch to tab: ".to_string(),
3599 PromptType::SwitchToTab,
3600 suggestions,
3601 ));
3602
3603 if let Some(prompt) = self.prompt.as_mut() {
3604 if !prompt.suggestions.is_empty() {
3605 prompt.selected_suggestion = Some(current_index);
3606 }
3607 }
3608 }
3609
3610 pub(crate) fn switch_to_tab(&mut self, buffer_id: BufferId) {
3612 let active_split = self.split_manager.active_split();
3614 let is_valid = self
3615 .split_view_states
3616 .get(&active_split)
3617 .is_some_and(|vs| vs.open_buffers.contains(&buffer_id));
3618
3619 if !is_valid {
3620 self.set_status_message(t!("status.tab_not_found").to_string());
3621 return;
3622 }
3623
3624 if buffer_id != self.active_buffer() {
3625 self.position_history.commit_pending_movement();
3627
3628 let cursors = self.active_cursors();
3629 let position = cursors.primary().position;
3630 let anchor = cursors.primary().anchor;
3631 self.position_history
3632 .record_movement(self.active_buffer(), position, anchor);
3633 self.position_history.commit_pending_movement();
3634
3635 self.set_active_buffer(buffer_id);
3636 }
3637 }
3638
3639 fn handle_insert_char_prompt(&mut self, c: char) -> AnyhowResult<()> {
3641 if let Some(ref prompt) = self.prompt {
3643 if prompt.prompt_type == PromptType::QueryReplaceConfirm {
3644 return self.handle_interactive_replace_key(c);
3645 }
3646 }
3647
3648 if let Some(ref prompt) = self.prompt {
3652 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
3653 if let Some(history) = self.prompt_histories.get_mut(&key) {
3654 history.reset_navigation();
3655 }
3656 }
3657 }
3658
3659 if let Some(prompt) = self.prompt_mut() {
3660 let s = c.to_string();
3662 prompt.insert_str(&s);
3663 }
3664 self.update_prompt_suggestions();
3665 Ok(())
3666 }
3667
3668 fn handle_insert_char_editor(&mut self, c: char) -> AnyhowResult<()> {
3670 if self.is_editing_disabled() {
3672 self.set_status_message(t!("buffer.editing_disabled").to_string());
3673 return Ok(());
3674 }
3675
3676 self.cancel_pending_lsp_requests();
3678
3679 if let Some(events) = self.action_to_events(Action::InsertChar(c)) {
3680 if events.len() > 1 {
3681 let description = format!("Insert '{}'", c);
3683 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description.clone())
3684 {
3685 self.active_event_log_mut().append(bulk_edit);
3686 }
3687 } else {
3688 for event in events {
3690 self.active_event_log_mut().append(event.clone());
3691 self.apply_event_to_active_buffer(&event);
3692 }
3693 }
3694 }
3695
3696 if c == '(' || c == ',' {
3698 self.request_signature_help();
3699 }
3700
3701 self.maybe_trigger_completion(c);
3703
3704 Ok(())
3705 }
3706
3707 fn apply_action_as_events(&mut self, action: Action) -> AnyhowResult<()> {
3713 let buffer_id = self.active_buffer();
3715 if self.is_composite_buffer(buffer_id) {
3716 if let Some(_handled) = self.handle_composite_action(buffer_id, &action) {
3717 return Ok(());
3718 }
3719 }
3720
3721 let action_description = format!("{:?}", action);
3723
3724 let is_editing_action = matches!(
3726 action,
3727 Action::InsertNewline
3728 | Action::InsertTab
3729 | Action::DeleteForward
3730 | Action::DeleteWordBackward
3731 | Action::DeleteWordForward
3732 | Action::DeleteLine
3733 | Action::DuplicateLine
3734 | Action::MoveLineUp
3735 | Action::MoveLineDown
3736 | Action::DedentSelection
3737 | Action::ToggleComment
3738 );
3739
3740 if is_editing_action && self.is_editing_disabled() {
3741 self.set_status_message(t!("buffer.editing_disabled").to_string());
3742 return Ok(());
3743 }
3744
3745 if let Some(events) = self.action_to_events(action) {
3746 if events.len() > 1 {
3747 let has_buffer_mods = events
3749 .iter()
3750 .any(|e| matches!(e, Event::Insert { .. } | Event::Delete { .. }));
3751
3752 if has_buffer_mods {
3753 if let Some(bulk_edit) =
3755 self.apply_events_as_bulk_edit(events.clone(), action_description)
3756 {
3757 self.active_event_log_mut().append(bulk_edit);
3758 }
3759 } else {
3760 let batch = Event::Batch {
3762 events: events.clone(),
3763 description: action_description,
3764 };
3765 self.active_event_log_mut().append(batch.clone());
3766 self.apply_event_to_active_buffer(&batch);
3767 }
3768
3769 for event in &events {
3771 self.track_cursor_movement(event);
3772 }
3773 } else {
3774 for event in events {
3776 self.active_event_log_mut().append(event.clone());
3777 self.apply_event_to_active_buffer(&event);
3778 self.track_cursor_movement(&event);
3779 }
3780 }
3781 }
3782
3783 Ok(())
3784 }
3785
3786 pub(super) fn track_cursor_movement(&mut self, event: &Event) {
3788 if self.in_navigation {
3789 return;
3790 }
3791
3792 if let Event::MoveCursor {
3793 new_position,
3794 new_anchor,
3795 ..
3796 } = event
3797 {
3798 self.position_history
3799 .record_movement(self.active_buffer(), *new_position, *new_anchor);
3800 }
3801 }
3802}