1mod async_messages;
2mod buffer_management;
3mod calibration_actions;
4pub mod calibration_wizard;
5mod clipboard;
6mod composite_buffer_actions;
7mod file_explorer;
8pub mod file_open;
9mod file_open_input;
10mod file_operations;
11mod help;
12mod input;
13mod input_dispatch;
14mod lsp_actions;
15mod lsp_requests;
16mod menu_actions;
17mod menu_context;
18mod mouse_input;
19mod on_save_actions;
20mod plugin_commands;
21mod popup_actions;
22mod prompt_actions;
23mod recovery_actions;
24mod render;
25pub mod session;
26mod settings_actions;
27mod shell_command;
28mod split_actions;
29mod tab_drag;
30mod terminal;
31mod terminal_input;
32mod toggle_actions;
33pub mod types;
34mod undo_actions;
35mod view_actions;
36pub mod warning_domains;
37
38use anyhow::Result as AnyhowResult;
39use rust_i18n::t;
40use std::path::Component;
41
42pub(crate) fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
45 let mut components = Vec::new();
46
47 for component in path.components() {
48 match component {
49 Component::CurDir => {
50 }
52 Component::ParentDir => {
53 if let Some(Component::Normal(_)) = components.last() {
55 components.pop();
56 } else {
57 components.push(component);
59 }
60 }
61 _ => {
62 components.push(component);
63 }
64 }
65 }
66
67 if components.is_empty() {
68 std::path::PathBuf::from(".")
69 } else {
70 components.iter().collect()
71 }
72}
73
74use self::types::{
75 Bookmark, CachedLayout, EventLineInfo, InteractiveReplaceState, LspMessageEntry,
76 LspProgressInfo, MacroRecordingState, MouseState, SearchState, TabContextMenu,
77 DEFAULT_BACKGROUND_FILE,
78};
79use crate::config::Config;
80use crate::config_io::{ConfigLayer, ConfigResolver, DirectoryContext};
81use crate::input::actions::action_to_events as convert_action_to_events;
82use crate::input::buffer_mode::ModeRegistry;
83use crate::input::command_registry::CommandRegistry;
84use crate::input::commands::Suggestion;
85use crate::input::keybindings::{Action, KeyContext, KeybindingResolver};
86use crate::input::position_history::PositionHistory;
87use crate::model::event::{Event, EventLog, SplitDirection, SplitId};
88use crate::services::async_bridge::{AsyncBridge, AsyncMessage};
89use crate::services::fs::{FsBackend, FsManager, LocalFsBackend};
90use crate::services::lsp::manager::{detect_language, LspManager};
91use crate::services::plugins::PluginManager;
92use crate::services::recovery::{RecoveryConfig, RecoveryService};
93use crate::services::time_source::{RealTimeSource, SharedTimeSource};
94use crate::state::EditorState;
95use crate::types::LspServerConfig;
96use crate::view::file_tree::{FileTree, FileTreeView};
97use crate::view::prompt::{Prompt, PromptType};
98use crate::view::scroll_sync::ScrollSyncManager;
99use crate::view::split::{SplitManager, SplitViewState};
100use crate::view::ui::{
101 FileExplorerRenderer, SplitRenderer, StatusBarRenderer, SuggestionsRenderer,
102};
103use crossterm::event::{KeyCode, KeyModifiers};
104#[cfg(feature = "plugins")]
105use fresh_core::api::BufferSavedDiff;
106use fresh_core::api::PluginCommand;
107use lsp_types::{Position, Range as LspRange, TextDocumentContentChangeEvent};
108use ratatui::{
109 layout::{Constraint, Direction, Layout},
110 Frame,
111};
112use std::collections::{HashMap, HashSet};
113use std::ops::Range;
114use std::path::{Path, PathBuf};
115use std::sync::{Arc, RwLock};
116use std::time::Instant;
117
118pub use self::types::{BufferKind, BufferMetadata, HoverTarget};
120pub use self::warning_domains::{
121 GeneralWarningDomain, LspWarningDomain, WarningAction, WarningActionId, WarningDomain,
122 WarningDomainRegistry, WarningLevel, WarningPopupContent,
123};
124pub use crate::model::event::BufferId;
125
126fn uri_to_path(uri: &lsp_types::Uri) -> Result<PathBuf, String> {
128 url::Url::parse(uri.as_str())
130 .map_err(|e| format!("Failed to parse URI: {}", e))?
131 .to_file_path()
132 .map_err(|_| "URI is not a file path".to_string())
133}
134
135#[derive(Clone, Debug)]
137struct SemanticTokenRangeRequest {
138 buffer_id: BufferId,
139 version: u64,
140 range: Range<usize>,
141 start_line: usize,
142 end_line: usize,
143}
144
145#[derive(Clone, Copy, Debug)]
146enum SemanticTokensFullRequestKind {
147 Full,
148 FullDelta,
149}
150
151#[derive(Clone, Debug)]
152struct SemanticTokenFullRequest {
153 buffer_id: BufferId,
154 version: u64,
155 kind: SemanticTokensFullRequestKind,
156}
157
158pub struct Editor {
160 buffers: HashMap<BufferId, EditorState>,
162
163 event_logs: HashMap<BufferId, EventLog>,
168
169 next_buffer_id: usize,
171
172 config: Config,
174
175 dir_context: DirectoryContext,
177
178 grammar_registry: std::sync::Arc<crate::primitives::grammar::GrammarRegistry>,
180
181 theme: crate::view::theme::Theme,
183
184 ansi_background: Option<crate::primitives::ansi_background::AnsiBackground>,
186
187 ansi_background_path: Option<PathBuf>,
189
190 background_fade: f32,
192
193 keybindings: KeybindingResolver,
195
196 clipboard: crate::services::clipboard::Clipboard,
198
199 should_quit: bool,
201
202 restart_with_dir: Option<PathBuf>,
205
206 status_message: Option<String>,
208
209 plugin_status_message: Option<String>,
211
212 plugin_errors: Vec<String>,
215
216 prompt: Option<Prompt>,
218
219 terminal_width: u16,
221 terminal_height: u16,
222
223 lsp: Option<LspManager>,
225
226 buffer_metadata: HashMap<BufferId, BufferMetadata>,
228
229 mode_registry: ModeRegistry,
231
232 tokio_runtime: Option<tokio::runtime::Runtime>,
234
235 async_bridge: Option<AsyncBridge>,
237
238 split_manager: SplitManager,
240
241 split_view_states: HashMap<SplitId, SplitViewState>,
245
246 previous_viewports: HashMap<SplitId, (usize, u16, u16)>,
250
251 scroll_sync_manager: ScrollSyncManager,
254
255 file_explorer: Option<FileTreeView>,
257
258 fs_manager: Arc<FsManager>,
260
261 file_explorer_visible: bool,
263
264 file_explorer_sync_in_progress: bool,
267
268 file_explorer_width_percent: f32,
271
272 pending_file_explorer_show_hidden: Option<bool>,
274
275 pending_file_explorer_show_gitignored: Option<bool>,
277
278 file_explorer_decorations: HashMap<String, Vec<crate::view::file_tree::FileExplorerDecoration>>,
280
281 file_explorer_decoration_cache: crate::view::file_tree::FileExplorerDecorationCache,
283
284 menu_bar_visible: bool,
286
287 menu_bar_auto_shown: bool,
290
291 tab_bar_visible: bool,
293
294 mouse_enabled: bool,
296
297 mouse_cursor_position: Option<(u16, u16)>,
301
302 gpm_active: bool,
304
305 key_context: KeyContext,
307
308 menu_state: crate::view::ui::MenuState,
310
311 menus: crate::config::MenuConfig,
313
314 working_dir: PathBuf,
316
317 pub position_history: PositionHistory,
319
320 in_navigation: bool,
322
323 next_lsp_request_id: u64,
325
326 pending_completion_request: Option<u64>,
328
329 completion_items: Option<Vec<lsp_types::CompletionItem>>,
332
333 pending_goto_definition_request: Option<u64>,
335
336 pending_hover_request: Option<u64>,
338
339 pending_references_request: Option<u64>,
341
342 pending_references_symbol: String,
344
345 pending_signature_help_request: Option<u64>,
347
348 pending_code_actions_request: Option<u64>,
350
351 pending_inlay_hints_request: Option<u64>,
353
354 pending_semantic_token_requests: HashMap<u64, SemanticTokenFullRequest>,
356
357 semantic_tokens_in_flight: HashMap<BufferId, (u64, u64, SemanticTokensFullRequestKind)>,
359
360 pending_semantic_token_range_requests: HashMap<u64, SemanticTokenRangeRequest>,
362
363 semantic_tokens_range_in_flight: HashMap<BufferId, (u64, usize, usize, u64)>,
365
366 semantic_tokens_range_last_request: HashMap<BufferId, (usize, usize, u64, Instant)>,
368
369 semantic_tokens_range_applied: HashMap<BufferId, (usize, usize, u64)>,
371
372 semantic_tokens_full_debounce: HashMap<BufferId, Instant>,
374
375 hover_symbol_range: Option<(usize, usize)>,
378
379 hover_symbol_overlay: Option<crate::view::overlay::OverlayHandle>,
381
382 mouse_hover_screen_position: Option<(u16, u16)>,
385
386 search_state: Option<SearchState>,
388
389 search_namespace: crate::view::overlay::OverlayNamespace,
391
392 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace,
394
395 pending_search_range: Option<Range<usize>>,
397
398 interactive_replace_state: Option<InteractiveReplaceState>,
400
401 lsp_status: String,
403
404 mouse_state: MouseState,
406
407 tab_context_menu: Option<TabContextMenu>,
409
410 pub(crate) cached_layout: CachedLayout,
412
413 command_registry: Arc<RwLock<CommandRegistry>>,
415
416 plugin_manager: PluginManager,
418
419 seen_byte_ranges: HashMap<BufferId, std::collections::HashSet<(usize, usize)>>,
423
424 panel_ids: HashMap<String, BufferId>,
427
428 background_process_handles: HashMap<u64, tokio::task::AbortHandle>,
431
432 prompt_histories: HashMap<String, crate::input::input_history::InputHistory>,
435
436 pending_async_prompt_callback: Option<fresh_core::api::JsCallbackId>,
440
441 lsp_progress: std::collections::HashMap<String, LspProgressInfo>,
443
444 lsp_server_statuses:
446 std::collections::HashMap<String, crate::services::async_bridge::LspServerStatus>,
447
448 lsp_window_messages: Vec<LspMessageEntry>,
450
451 lsp_log_messages: Vec<LspMessageEntry>,
453
454 diagnostic_result_ids: HashMap<String, String>,
457
458 stored_diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
461
462 event_broadcaster: crate::model::control_event::EventBroadcaster,
464
465 bookmarks: HashMap<char, Bookmark>,
467
468 search_case_sensitive: bool,
470 search_whole_word: bool,
471 search_use_regex: bool,
472 search_confirm_each: bool,
474
475 macros: HashMap<char, Vec<Action>>,
477
478 macro_recording: Option<MacroRecordingState>,
480
481 last_macro_register: Option<char>,
483
484 macro_playing: bool,
486
487 #[cfg(feature = "plugins")]
489 pending_plugin_actions: Vec<(
490 String,
491 crate::services::plugins::thread::oneshot::Receiver<anyhow::Result<()>>,
492 )>,
493
494 #[cfg(feature = "plugins")]
496 plugin_render_requested: bool,
497
498 chord_state: Vec<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)>,
501
502 pending_lsp_confirmation: Option<String>,
505
506 pending_close_buffer: Option<BufferId>,
509
510 auto_revert_enabled: bool,
512
513 last_auto_revert_poll: std::time::Instant,
515
516 last_file_tree_poll: std::time::Instant,
518
519 file_mod_times: HashMap<PathBuf, std::time::SystemTime>,
522
523 dir_mod_times: HashMap<PathBuf, std::time::SystemTime>,
526
527 file_rapid_change_counts: HashMap<PathBuf, (std::time::Instant, u32)>,
530
531 file_open_state: Option<file_open::FileOpenState>,
533
534 file_browser_layout: Option<crate::view::ui::FileBrowserLayout>,
536
537 recovery_service: RecoveryService,
539
540 full_redraw_requested: bool,
542
543 time_source: SharedTimeSource,
545
546 last_auto_save: std::time::Instant,
548
549 active_custom_contexts: HashSet<String>,
552
553 editor_mode: Option<String>,
556
557 warning_log: Option<(std::sync::mpsc::Receiver<()>, PathBuf)>,
559
560 warning_domains: WarningDomainRegistry,
563
564 update_checker: Option<crate::services::release_checker::PeriodicUpdateChecker>,
566
567 terminal_manager: crate::services::terminal::TerminalManager,
569
570 terminal_buffers: HashMap<BufferId, crate::services::terminal::TerminalId>,
572
573 terminal_backing_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
575
576 terminal_log_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
578
579 terminal_mode: bool,
581
582 keyboard_capture: bool,
586
587 terminal_mode_resume: std::collections::HashSet<BufferId>,
591
592 previous_click_time: Option<std::time::Instant>,
594
595 previous_click_position: Option<(u16, u16)>,
598
599 pub(crate) settings_state: Option<crate::view::settings::SettingsState>,
601
602 pub(crate) calibration_wizard: Option<calibration_wizard::CalibrationWizard>,
604
605 pub(crate) key_translator: crate::input::key_translator::KeyTranslator,
607
608 color_capability: crate::view::color_support::ColorCapability,
610
611 review_hunks: Vec<fresh_core::api::ReviewHunk>,
613
614 active_action_popup: Option<(String, Vec<(String, String)>)>,
617
618 composite_buffers: HashMap<BufferId, crate::model::composite_buffer::CompositeBuffer>,
621
622 composite_view_states:
625 HashMap<(SplitId, BufferId), crate::view::composite_view::CompositeViewState>,
626
627 stdin_streaming: Option<StdinStreamingState>,
629}
630
631pub struct StdinStreamingState {
633 pub temp_path: PathBuf,
635 pub buffer_id: BufferId,
637 pub last_known_size: usize,
639 pub complete: bool,
641 pub thread_handle: Option<std::thread::JoinHandle<anyhow::Result<()>>>,
643}
644
645impl Editor {
646 pub fn new(
649 config: Config,
650 width: u16,
651 height: u16,
652 dir_context: DirectoryContext,
653 color_capability: crate::view::color_support::ColorCapability,
654 ) -> AnyhowResult<Self> {
655 Self::with_working_dir(
656 config,
657 width,
658 height,
659 None,
660 dir_context,
661 true,
662 color_capability,
663 )
664 }
665
666 pub fn with_working_dir(
669 config: Config,
670 width: u16,
671 height: u16,
672 working_dir: Option<PathBuf>,
673 dir_context: DirectoryContext,
674 plugins_enabled: bool,
675 color_capability: crate::view::color_support::ColorCapability,
676 ) -> AnyhowResult<Self> {
677 Self::with_options(
678 config,
679 width,
680 height,
681 working_dir,
682 None,
683 plugins_enabled,
684 dir_context,
685 None,
686 color_capability,
687 crate::primitives::grammar::GrammarRegistry::for_editor(),
688 )
689 }
690
691 #[allow(clippy::too_many_arguments)]
694 pub fn for_test(
695 config: Config,
696 width: u16,
697 height: u16,
698 working_dir: Option<PathBuf>,
699 dir_context: DirectoryContext,
700 color_capability: crate::view::color_support::ColorCapability,
701 fs_backend: Option<Arc<dyn FsBackend>>,
702 time_source: Option<SharedTimeSource>,
703 ) -> AnyhowResult<Self> {
704 Self::with_options(
705 config,
706 width,
707 height,
708 working_dir,
709 fs_backend,
710 true,
711 dir_context,
712 time_source,
713 color_capability,
714 crate::primitives::grammar::GrammarRegistry::empty(),
715 )
716 }
717
718 #[allow(clippy::too_many_arguments)]
722 fn with_options(
723 mut config: Config,
724 width: u16,
725 height: u16,
726 working_dir: Option<PathBuf>,
727 fs_backend: Option<Arc<dyn FsBackend>>,
728 enable_plugins: bool,
729 dir_context: DirectoryContext,
730 time_source: Option<SharedTimeSource>,
731 color_capability: crate::view::color_support::ColorCapability,
732 grammar_registry: Arc<crate::primitives::grammar::GrammarRegistry>,
733 ) -> AnyhowResult<Self> {
734 let time_source = time_source.unwrap_or_else(RealTimeSource::shared);
736 tracing::info!("Editor::new called with width={}, height={}", width, height);
737
738 let working_dir = working_dir
740 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
741
742 let working_dir = working_dir.canonicalize().unwrap_or(working_dir);
745
746 let theme_loader = crate::view::theme::LocalThemeLoader::new();
748 let theme = crate::view::theme::Theme::load(&config.theme, &theme_loader)
749 .ok_or_else(|| anyhow::anyhow!("Theme '{:?}' not found", config.theme))?;
750
751 theme.set_terminal_cursor_color();
753
754 tracing::info!(
755 "Grammar registry has {} syntaxes",
756 grammar_registry.available_syntaxes().len()
757 );
758
759 let keybindings = KeybindingResolver::new(&config);
760
761 let mut buffers = HashMap::new();
763 let mut event_logs = HashMap::new();
764
765 let buffer_id = BufferId(0);
766 let mut state = EditorState::new(
767 width,
768 height,
769 config.editor.large_file_threshold_bytes as usize,
770 );
771 state.margins.set_line_numbers(config.editor.line_numbers);
773 tracing::info!("EditorState created for buffer {:?}", buffer_id);
775 buffers.insert(buffer_id, state);
776 event_logs.insert(buffer_id, EventLog::new());
777
778 let mut buffer_metadata = HashMap::new();
780 buffer_metadata.insert(buffer_id, BufferMetadata::new());
781
782 let root_uri = url::Url::from_file_path(&working_dir)
784 .ok()
785 .and_then(|u| u.as_str().parse::<lsp_types::Uri>().ok());
786
787 let tokio_runtime = tokio::runtime::Builder::new_multi_thread()
789 .worker_threads(2) .thread_name("editor-async")
791 .enable_all()
792 .build()
793 .ok();
794
795 let async_bridge = AsyncBridge::new();
797
798 if tokio_runtime.is_none() {
799 tracing::warn!("Failed to create Tokio runtime - async features disabled");
800 }
801
802 let mut lsp = LspManager::new(root_uri);
804
805 if let Some(ref runtime) = tokio_runtime {
807 lsp.set_runtime(runtime.handle().clone(), async_bridge.clone());
808 }
809
810 for (language, lsp_config) in &config.lsp {
812 lsp.set_language_config(language.clone(), lsp_config.clone());
813 }
814
815 let split_manager = SplitManager::new(buffer_id);
817
818 let mut split_view_states = HashMap::new();
820 let initial_split_id = split_manager.active_split();
821 let mut initial_view_state = SplitViewState::with_buffer(width, height, buffer_id);
822 initial_view_state.viewport.line_wrap_enabled = config.editor.line_wrap;
823 split_view_states.insert(initial_split_id, initial_view_state);
824
825 let fs_backend = fs_backend.unwrap_or_else(|| Arc::new(LocalFsBackend::new()));
828 let fs_manager = Arc::new(FsManager::new(fs_backend));
829
830 let command_registry = Arc::new(RwLock::new(CommandRegistry::new()));
832
833 let plugin_manager = PluginManager::new(
835 enable_plugins,
836 Arc::clone(&command_registry),
837 dir_context.clone(),
838 );
839
840 #[cfg(feature = "plugins")]
843 if let Some(snapshot_handle) = plugin_manager.state_snapshot_handle() {
844 let mut snapshot = snapshot_handle.write().unwrap();
845 snapshot.working_dir = working_dir.clone();
846 }
847
848 if plugin_manager.is_active() {
853 let mut plugin_dirs: Vec<std::path::PathBuf> = vec![];
854
855 if let Ok(exe_path) = std::env::current_exe() {
857 if let Some(exe_dir) = exe_path.parent() {
858 let exe_plugin_dir = exe_dir.join("plugins");
859 if exe_plugin_dir.exists() {
860 plugin_dirs.push(exe_plugin_dir);
861 }
862 }
863 }
864
865 let working_plugin_dir = working_dir.join("plugins");
867 if working_plugin_dir.exists() && !plugin_dirs.contains(&working_plugin_dir) {
868 plugin_dirs.push(working_plugin_dir);
869 }
870
871 #[cfg(feature = "embed-plugins")]
873 if plugin_dirs.is_empty() {
874 if let Some(embedded_dir) =
875 crate::services::plugins::embedded::get_embedded_plugins_dir()
876 {
877 tracing::info!("Using embedded plugins from: {:?}", embedded_dir);
878 plugin_dirs.push(embedded_dir.clone());
879 }
880 }
881
882 if plugin_dirs.is_empty() {
883 tracing::debug!(
884 "No plugins directory found next to executable or in working dir: {:?}",
885 working_dir
886 );
887 }
888
889 for plugin_dir in plugin_dirs {
891 tracing::info!("Loading TypeScript plugins from: {:?}", plugin_dir);
892 let (errors, discovered_plugins) =
893 plugin_manager.load_plugins_from_dir_with_config(&plugin_dir, &config.plugins);
894
895 for (name, plugin_config) in discovered_plugins {
898 config.plugins.insert(name, plugin_config);
899 }
900
901 if !errors.is_empty() {
902 for err in &errors {
903 tracing::error!("TypeScript plugin load error: {}", err);
904 }
905 #[cfg(debug_assertions)]
907 panic!(
908 "TypeScript plugin loading failed with {} error(s): {}",
909 errors.len(),
910 errors.join("; ")
911 );
912 }
913 }
914 }
915
916 let file_explorer_width = config.file_explorer.width;
918 let recovery_enabled = config.editor.recovery_enabled;
919 let auto_save_interval_secs = config.editor.auto_save_interval_secs;
920 let check_for_updates = config.check_for_updates;
921 let show_menu_bar = config.editor.show_menu_bar;
922 let show_tab_bar = config.editor.show_tab_bar;
923
924 let update_checker = if check_for_updates {
926 tracing::debug!("Update checking enabled, starting periodic checker");
927 Some(
928 crate::services::release_checker::start_periodic_update_check(
929 crate::services::release_checker::DEFAULT_RELEASES_URL,
930 time_source.clone(),
931 dir_context.data_dir.clone(),
932 ),
933 )
934 } else {
935 tracing::debug!("Update checking disabled by config");
936 None
937 };
938
939 let mut editor = Editor {
940 buffers,
941 event_logs,
942 next_buffer_id: 1,
943 config,
944 dir_context: dir_context.clone(),
945 grammar_registry,
946 theme,
947 ansi_background: None,
948 ansi_background_path: None,
949 background_fade: crate::primitives::ansi_background::DEFAULT_BACKGROUND_FADE,
950 keybindings,
951 clipboard: crate::services::clipboard::Clipboard::new(),
952 should_quit: false,
953 restart_with_dir: None,
954 status_message: None,
955 plugin_status_message: None,
956 plugin_errors: Vec::new(),
957 prompt: None,
958 terminal_width: width,
959 terminal_height: height,
960 lsp: Some(lsp),
961 buffer_metadata,
962 mode_registry: ModeRegistry::new(),
963 tokio_runtime,
964 async_bridge: Some(async_bridge),
965 split_manager,
966 split_view_states,
967 previous_viewports: HashMap::new(),
968 scroll_sync_manager: ScrollSyncManager::new(),
969 file_explorer: None,
970 fs_manager,
971 file_explorer_visible: false,
972 file_explorer_sync_in_progress: false,
973 file_explorer_width_percent: file_explorer_width,
974 pending_file_explorer_show_hidden: None,
975 pending_file_explorer_show_gitignored: None,
976 menu_bar_visible: show_menu_bar,
977 file_explorer_decorations: HashMap::new(),
978 file_explorer_decoration_cache:
979 crate::view::file_tree::FileExplorerDecorationCache::default(),
980 menu_bar_auto_shown: false,
981 tab_bar_visible: show_tab_bar,
982 mouse_enabled: true,
983 mouse_cursor_position: None,
984 gpm_active: false,
985 key_context: KeyContext::Normal,
986 menu_state: crate::view::ui::MenuState::new(),
987 menus: crate::config::MenuConfig::translated(),
988 working_dir,
989 position_history: PositionHistory::new(),
990 in_navigation: false,
991 next_lsp_request_id: 0,
992 pending_completion_request: None,
993 completion_items: None,
994 pending_goto_definition_request: None,
995 pending_hover_request: None,
996 pending_references_request: None,
997 pending_references_symbol: String::new(),
998 pending_signature_help_request: None,
999 pending_code_actions_request: None,
1000 pending_inlay_hints_request: None,
1001 pending_semantic_token_requests: HashMap::new(),
1002 semantic_tokens_in_flight: HashMap::new(),
1003 pending_semantic_token_range_requests: HashMap::new(),
1004 semantic_tokens_range_in_flight: HashMap::new(),
1005 semantic_tokens_range_last_request: HashMap::new(),
1006 semantic_tokens_range_applied: HashMap::new(),
1007 semantic_tokens_full_debounce: HashMap::new(),
1008 hover_symbol_range: None,
1009 hover_symbol_overlay: None,
1010 mouse_hover_screen_position: None,
1011 search_state: None,
1012 search_namespace: crate::view::overlay::OverlayNamespace::from_string(
1013 "search".to_string(),
1014 ),
1015 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace::from_string(
1016 "lsp-diagnostic".to_string(),
1017 ),
1018 pending_search_range: None,
1019 interactive_replace_state: None,
1020 lsp_status: String::new(),
1021 mouse_state: MouseState::default(),
1022 tab_context_menu: None,
1023 cached_layout: CachedLayout::default(),
1024 command_registry,
1025 plugin_manager,
1026 seen_byte_ranges: HashMap::new(),
1027 panel_ids: HashMap::new(),
1028 background_process_handles: HashMap::new(),
1029 prompt_histories: {
1030 let mut histories = HashMap::new();
1032 for history_name in ["search", "replace", "goto_line"] {
1033 let path = dir_context.prompt_history_path(history_name);
1034 let history = crate::input::input_history::InputHistory::load_from_file(&path)
1035 .unwrap_or_else(|e| {
1036 tracing::warn!("Failed to load {} history: {}", history_name, e);
1037 crate::input::input_history::InputHistory::new()
1038 });
1039 histories.insert(history_name.to_string(), history);
1040 }
1041 histories
1042 },
1043 pending_async_prompt_callback: None,
1044 lsp_progress: std::collections::HashMap::new(),
1045 lsp_server_statuses: std::collections::HashMap::new(),
1046 lsp_window_messages: Vec::new(),
1047 lsp_log_messages: Vec::new(),
1048 diagnostic_result_ids: HashMap::new(),
1049 stored_diagnostics: HashMap::new(),
1050 event_broadcaster: crate::model::control_event::EventBroadcaster::default(),
1051 bookmarks: HashMap::new(),
1052 search_case_sensitive: true,
1053 search_whole_word: false,
1054 search_use_regex: false,
1055 search_confirm_each: false,
1056 macros: HashMap::new(),
1057 macro_recording: None,
1058 last_macro_register: None,
1059 macro_playing: false,
1060 #[cfg(feature = "plugins")]
1061 pending_plugin_actions: Vec::new(),
1062 #[cfg(feature = "plugins")]
1063 plugin_render_requested: false,
1064 chord_state: Vec::new(),
1065 pending_lsp_confirmation: None,
1066 pending_close_buffer: None,
1067 auto_revert_enabled: true,
1068 last_auto_revert_poll: time_source.now(),
1069 last_file_tree_poll: time_source.now(),
1070 file_mod_times: HashMap::new(),
1071 dir_mod_times: HashMap::new(),
1072 file_rapid_change_counts: HashMap::new(),
1073 file_open_state: None,
1074 file_browser_layout: None,
1075 recovery_service: {
1076 let recovery_config = RecoveryConfig {
1077 enabled: recovery_enabled,
1078 auto_save_interval_secs,
1079 ..RecoveryConfig::default()
1080 };
1081 RecoveryService::with_config_and_dir(recovery_config, dir_context.recovery_dir())
1082 },
1083 full_redraw_requested: false,
1084 time_source: time_source.clone(),
1085 last_auto_save: time_source.now(),
1086 active_custom_contexts: HashSet::new(),
1087 editor_mode: None,
1088 warning_log: None,
1089 warning_domains: WarningDomainRegistry::new(),
1090 update_checker,
1091 terminal_manager: crate::services::terminal::TerminalManager::new(),
1092 terminal_buffers: HashMap::new(),
1093 terminal_backing_files: HashMap::new(),
1094 terminal_log_files: HashMap::new(),
1095 terminal_mode: false,
1096 keyboard_capture: false,
1097 terminal_mode_resume: std::collections::HashSet::new(),
1098 previous_click_time: None,
1099 previous_click_position: None,
1100 settings_state: None,
1101 calibration_wizard: None,
1102 key_translator: crate::input::key_translator::KeyTranslator::load_default()
1103 .unwrap_or_default(),
1104 color_capability,
1105 stdin_streaming: None,
1106 review_hunks: Vec::new(),
1107 active_action_popup: None,
1108 composite_buffers: HashMap::new(),
1109 composite_view_states: HashMap::new(),
1110 };
1111
1112 #[cfg(feature = "plugins")]
1113 {
1114 editor.update_plugin_state_snapshot();
1115 if editor.plugin_manager.is_active() {
1116 editor.plugin_manager.run_hook(
1117 "editor_initialized",
1118 crate::services::plugins::hooks::HookArgs::EditorInitialized,
1119 );
1120 }
1121 }
1122
1123 Ok(editor)
1124 }
1125
1126 pub fn event_broadcaster(&self) -> &crate::model::control_event::EventBroadcaster {
1128 &self.event_broadcaster
1129 }
1130
1131 pub fn async_bridge(&self) -> Option<&AsyncBridge> {
1133 self.async_bridge.as_ref()
1134 }
1135
1136 pub fn config(&self) -> &Config {
1138 &self.config
1139 }
1140
1141 pub fn key_translator(&self) -> &crate::input::key_translator::KeyTranslator {
1143 &self.key_translator
1144 }
1145
1146 pub fn time_source(&self) -> &SharedTimeSource {
1148 &self.time_source
1149 }
1150
1151 pub fn emit_event(&self, name: impl Into<String>, data: serde_json::Value) {
1153 self.event_broadcaster.emit_named(name, data);
1154 }
1155
1156 fn send_plugin_response(&self, response: fresh_core::api::PluginResponse) {
1158 self.plugin_manager.deliver_response(response);
1159 }
1160
1161 fn take_pending_semantic_token_request(
1163 &mut self,
1164 request_id: u64,
1165 ) -> Option<SemanticTokenFullRequest> {
1166 if let Some(request) = self.pending_semantic_token_requests.remove(&request_id) {
1167 self.semantic_tokens_in_flight.remove(&request.buffer_id);
1168 Some(request)
1169 } else {
1170 None
1171 }
1172 }
1173
1174 fn take_pending_semantic_token_range_request(
1176 &mut self,
1177 request_id: u64,
1178 ) -> Option<SemanticTokenRangeRequest> {
1179 if let Some(request) = self
1180 .pending_semantic_token_range_requests
1181 .remove(&request_id)
1182 {
1183 self.semantic_tokens_range_in_flight
1184 .remove(&request.buffer_id);
1185 Some(request)
1186 } else {
1187 None
1188 }
1189 }
1190
1191 pub fn get_all_keybindings(&self) -> Vec<(String, String)> {
1193 self.keybindings.get_all_bindings()
1194 }
1195
1196 pub fn get_keybinding_for_action(&self, action_name: &str) -> Option<String> {
1199 self.keybindings
1200 .find_keybinding_for_action(action_name, self.key_context)
1201 }
1202
1203 pub fn mode_registry_mut(&mut self) -> &mut ModeRegistry {
1205 &mut self.mode_registry
1206 }
1207
1208 pub fn mode_registry(&self) -> &ModeRegistry {
1210 &self.mode_registry
1211 }
1212
1213 #[inline]
1218 pub fn active_buffer(&self) -> BufferId {
1219 self.split_manager
1220 .active_buffer_id()
1221 .expect("Editor always has at least one buffer")
1222 }
1223
1224 pub fn active_buffer_mode(&self) -> Option<&str> {
1226 self.buffer_metadata
1227 .get(&self.active_buffer())
1228 .and_then(|meta| meta.virtual_mode())
1229 }
1230
1231 pub fn is_active_buffer_read_only(&self) -> bool {
1233 if let Some(metadata) = self.buffer_metadata.get(&self.active_buffer()) {
1234 if metadata.read_only {
1235 return true;
1236 }
1237 if let Some(mode_name) = metadata.virtual_mode() {
1239 return self.mode_registry.is_read_only(mode_name);
1240 }
1241 }
1242 false
1243 }
1244
1245 pub fn is_editing_disabled(&self) -> bool {
1248 self.active_state().editing_disabled
1249 }
1250
1251 pub fn resolve_mode_keybinding(
1258 &self,
1259 code: KeyCode,
1260 modifiers: KeyModifiers,
1261 ) -> Option<String> {
1262 if let Some(ref global_mode) = self.editor_mode {
1264 if let Some(binding) =
1265 self.mode_registry
1266 .resolve_keybinding(global_mode, code, modifiers)
1267 {
1268 return Some(binding);
1269 }
1270 }
1271
1272 let mode_name = self.active_buffer_mode()?;
1274 self.mode_registry
1275 .resolve_keybinding(mode_name, code, modifiers)
1276 }
1277
1278 pub fn has_active_lsp_progress(&self) -> bool {
1280 !self.lsp_progress.is_empty()
1281 }
1282
1283 pub fn get_lsp_progress(&self) -> Vec<(String, String, Option<String>)> {
1285 self.lsp_progress
1286 .iter()
1287 .map(|(token, info)| (token.clone(), info.title.clone(), info.message.clone()))
1288 .collect()
1289 }
1290
1291 pub fn is_lsp_server_ready(&self, language: &str) -> bool {
1293 use crate::services::async_bridge::LspServerStatus;
1294 self.lsp_server_statuses
1295 .get(language)
1296 .map(|status| matches!(status, LspServerStatus::Running))
1297 .unwrap_or(false)
1298 }
1299
1300 pub fn get_lsp_status(&self) -> &str {
1302 &self.lsp_status
1303 }
1304
1305 pub fn get_stored_diagnostics(&self) -> &HashMap<String, Vec<lsp_types::Diagnostic>> {
1308 &self.stored_diagnostics
1309 }
1310
1311 pub fn is_update_available(&self) -> bool {
1313 self.update_checker
1314 .as_ref()
1315 .map(|c| c.is_update_available())
1316 .unwrap_or(false)
1317 }
1318
1319 pub fn latest_version(&self) -> Option<&str> {
1321 self.update_checker
1322 .as_ref()
1323 .and_then(|c| c.latest_version())
1324 }
1325
1326 pub fn get_update_result(
1328 &self,
1329 ) -> Option<&crate::services::release_checker::ReleaseCheckResult> {
1330 self.update_checker
1331 .as_ref()
1332 .and_then(|c| c.get_cached_result())
1333 }
1334
1335 #[doc(hidden)]
1340 pub fn set_update_checker(
1341 &mut self,
1342 checker: crate::services::release_checker::PeriodicUpdateChecker,
1343 ) {
1344 self.update_checker = Some(checker);
1345 }
1346
1347 pub fn set_lsp_config(&mut self, language: String, config: LspServerConfig) {
1349 if let Some(ref mut lsp) = self.lsp {
1350 lsp.set_language_config(language, config);
1351 }
1352 }
1353
1354 pub fn running_lsp_servers(&self) -> Vec<String> {
1356 self.lsp
1357 .as_ref()
1358 .map(|lsp| lsp.running_servers())
1359 .unwrap_or_default()
1360 }
1361
1362 pub fn shutdown_lsp_server(&mut self, language: &str) -> bool {
1366 if let Some(ref mut lsp) = self.lsp {
1367 lsp.shutdown_server(language)
1368 } else {
1369 false
1370 }
1371 }
1372
1373 pub fn enable_event_streaming<P: AsRef<Path>>(&mut self, path: P) -> AnyhowResult<()> {
1375 for event_log in self.event_logs.values_mut() {
1377 event_log.enable_streaming(&path)?;
1378 }
1379 Ok(())
1380 }
1381
1382 pub fn log_keystroke(&mut self, key_code: &str, modifiers: &str) {
1384 if let Some(event_log) = self.event_logs.get_mut(&self.active_buffer()) {
1385 event_log.log_keystroke(key_code, modifiers);
1386 }
1387 }
1388
1389 pub fn set_warning_log(&mut self, receiver: std::sync::mpsc::Receiver<()>, path: PathBuf) {
1394 self.warning_log = Some((receiver, path));
1395 }
1396
1397 pub fn check_warning_log(&mut self) -> bool {
1402 let Some((receiver, path)) = &self.warning_log else {
1403 return false;
1404 };
1405
1406 let mut new_warning_count = 0usize;
1408 while receiver.try_recv().is_ok() {
1409 new_warning_count += 1;
1410 }
1411
1412 if new_warning_count > 0 {
1413 self.warning_domains.general.add_warnings(new_warning_count);
1415 self.warning_domains.general.set_log_path(path.clone());
1416 }
1417
1418 new_warning_count > 0
1419 }
1420
1421 pub fn get_warning_domains(&self) -> &WarningDomainRegistry {
1423 &self.warning_domains
1424 }
1425
1426 pub fn get_warning_log_path(&self) -> Option<&PathBuf> {
1428 self.warning_domains.general.log_path.as_ref()
1429 }
1430
1431 pub fn open_warning_log(&mut self) {
1433 if let Some(path) = self.warning_domains.general.log_path.clone() {
1434 if let Err(e) = self.open_file(&path) {
1435 tracing::error!("Failed to open warning log: {}", e);
1436 }
1437 }
1438 }
1439
1440 pub fn clear_warning_indicator(&mut self) {
1442 self.warning_domains.general.clear();
1443 }
1444
1445 pub fn clear_warnings(&mut self) {
1447 self.warning_domains.general.clear();
1448 self.warning_domains.lsp.clear();
1449 self.status_message = Some("Warnings cleared".to_string());
1450 }
1451
1452 pub fn has_lsp_error(&self) -> bool {
1454 self.warning_domains.lsp.level() == WarningLevel::Error
1455 }
1456
1457 pub fn get_effective_warning_level(&self) -> WarningLevel {
1460 self.warning_domains.lsp.level()
1461 }
1462
1463 pub fn get_general_warning_level(&self) -> WarningLevel {
1465 self.warning_domains.general.level()
1466 }
1467
1468 pub fn get_general_warning_count(&self) -> usize {
1470 self.warning_domains.general.count
1471 }
1472
1473 pub fn update_lsp_warning_domain(&mut self) {
1475 self.warning_domains
1476 .lsp
1477 .update_from_statuses(&self.lsp_server_statuses);
1478 }
1479
1480 pub fn check_mouse_hover_timer(&mut self) -> bool {
1486 if !self.config.editor.mouse_hover_enabled {
1488 return false;
1489 }
1490
1491 let hover_delay = std::time::Duration::from_millis(self.config.editor.mouse_hover_delay_ms);
1492
1493 let hover_info = match self.mouse_state.lsp_hover_state {
1495 Some((byte_pos, start_time, screen_x, screen_y)) => {
1496 if self.mouse_state.lsp_hover_request_sent {
1497 return false; }
1499 if start_time.elapsed() < hover_delay {
1500 return false; }
1502 Some((byte_pos, screen_x, screen_y))
1503 }
1504 None => return false,
1505 };
1506
1507 let Some((byte_pos, screen_x, screen_y)) = hover_info else {
1508 return false;
1509 };
1510
1511 self.mouse_state.lsp_hover_request_sent = true;
1513
1514 self.mouse_hover_screen_position = Some((screen_x, screen_y));
1516
1517 if let Err(e) = self.request_hover_at_position(byte_pos) {
1519 tracing::debug!("Failed to request hover: {}", e);
1520 return false;
1521 }
1522
1523 true
1524 }
1525
1526 pub fn check_semantic_highlight_timer(&self) -> bool {
1531 for state in self.buffers.values() {
1533 if let Some(remaining) = state.reference_highlight_overlay.needs_redraw() {
1534 if remaining.is_zero() {
1535 return true;
1536 }
1537 }
1538 }
1539 false
1540 }
1541
1542 fn load_ansi_background(&mut self, input: &str) -> AnyhowResult<()> {
1544 let trimmed = input.trim();
1545
1546 if trimmed.is_empty() {
1547 self.ansi_background = None;
1548 self.ansi_background_path = None;
1549 self.set_status_message(t!("status.background_cleared").to_string());
1550 return Ok(());
1551 }
1552
1553 let input_path = Path::new(trimmed);
1554 let resolved = if input_path.is_absolute() {
1555 input_path.to_path_buf()
1556 } else {
1557 self.working_dir.join(input_path)
1558 };
1559
1560 let canonical = resolved.canonicalize().unwrap_or_else(|_| resolved.clone());
1561
1562 let parsed = crate::primitives::ansi_background::AnsiBackground::from_file(&canonical)?;
1563
1564 self.ansi_background = Some(parsed);
1565 self.ansi_background_path = Some(canonical.clone());
1566 self.set_status_message(
1567 t!(
1568 "view.background_set",
1569 path = canonical.display().to_string()
1570 )
1571 .to_string(),
1572 );
1573
1574 Ok(())
1575 }
1576
1577 fn effective_tabs_width(&self) -> u16 {
1582 if self.file_explorer_visible && self.file_explorer.is_some() {
1583 let editor_percent = 1.0 - self.file_explorer_width_percent;
1585 (self.terminal_width as f32 * editor_percent) as u16
1586 } else {
1587 self.terminal_width
1588 }
1589 }
1590
1591 fn set_active_buffer(&mut self, buffer_id: BufferId) {
1601 if self.active_buffer() == buffer_id {
1602 return; }
1604
1605 self.on_editor_focus_lost();
1607
1608 self.cancel_search_prompt_if_active();
1611
1612 let previous = self.active_buffer();
1614
1615 if self.terminal_mode && self.is_terminal_buffer(previous) {
1617 self.terminal_mode_resume.insert(previous);
1618 self.terminal_mode = false;
1619 self.key_context = crate::input::keybindings::KeyContext::Normal;
1620 }
1621
1622 self.split_manager.set_active_buffer_id(buffer_id);
1624
1625 if self.terminal_mode_resume.contains(&buffer_id) && self.is_terminal_buffer(buffer_id) {
1627 self.terminal_mode = true;
1628 self.key_context = crate::input::keybindings::KeyContext::Terminal;
1629 } else if self.is_terminal_buffer(buffer_id) {
1630 self.sync_terminal_to_buffer(buffer_id);
1633 }
1634
1635 let active_split = self.split_manager.active_split();
1637 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
1638 view_state.add_buffer(buffer_id);
1639 view_state.previous_buffer = Some(previous);
1641 }
1642
1643 self.ensure_active_tab_visible(active_split, buffer_id, self.effective_tabs_width());
1646
1647 self.plugin_manager.run_hook(
1652 "buffer_activated",
1653 crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
1654 );
1655 }
1656
1657 pub(super) fn focus_split(
1668 &mut self,
1669 split_id: crate::model::event::SplitId,
1670 buffer_id: BufferId,
1671 ) {
1672 let previous_split = self.split_manager.active_split();
1673 let previous_buffer = self.active_buffer(); let split_changed = previous_split != split_id;
1675
1676 if split_changed {
1677 if self.terminal_mode && self.is_terminal_buffer(previous_buffer) {
1679 self.terminal_mode = false;
1680 self.key_context = crate::input::keybindings::KeyContext::Normal;
1681 }
1682
1683 self.split_manager.set_active_split(split_id);
1685
1686 self.split_manager.set_active_buffer_id(buffer_id);
1688
1689 if self.is_terminal_buffer(buffer_id) {
1691 self.terminal_mode = true;
1692 self.key_context = crate::input::keybindings::KeyContext::Terminal;
1693 } else {
1694 self.key_context = crate::input::keybindings::KeyContext::Normal;
1697 }
1698
1699 if previous_buffer != buffer_id {
1701 self.position_history.commit_pending_movement();
1702 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
1703 view_state.add_buffer(buffer_id);
1704 view_state.previous_buffer = Some(previous_buffer);
1705 }
1706 }
1709 } else {
1710 self.set_active_buffer(buffer_id);
1712 }
1713 }
1714
1715 pub fn active_state(&self) -> &EditorState {
1717 self.buffers.get(&self.active_buffer()).unwrap()
1718 }
1719
1720 pub fn active_state_mut(&mut self) -> &mut EditorState {
1722 self.buffers.get_mut(&self.active_buffer()).unwrap()
1723 }
1724
1725 pub fn set_completion_items(&mut self, items: Vec<lsp_types::CompletionItem>) {
1727 self.completion_items = Some(items);
1728 }
1729
1730 pub fn active_viewport(&self) -> &crate::view::viewport::Viewport {
1732 let active_split = self.split_manager.active_split();
1733 &self.split_view_states.get(&active_split).unwrap().viewport
1734 }
1735
1736 pub fn active_viewport_mut(&mut self) -> &mut crate::view::viewport::Viewport {
1738 let active_split = self.split_manager.active_split();
1739 &mut self
1740 .split_view_states
1741 .get_mut(&active_split)
1742 .unwrap()
1743 .viewport
1744 }
1745
1746 pub fn get_buffer_display_name(&self, buffer_id: BufferId) -> String {
1748 if let Some(composite) = self.composite_buffers.get(&buffer_id) {
1750 return composite.name.clone();
1751 }
1752
1753 self.buffer_metadata
1754 .get(&buffer_id)
1755 .map(|m| m.display_name.clone())
1756 .or_else(|| {
1757 self.buffers.get(&buffer_id).and_then(|state| {
1758 state
1759 .buffer
1760 .file_path()
1761 .and_then(|p| p.file_name())
1762 .and_then(|n| n.to_str())
1763 .map(|s| s.to_string())
1764 })
1765 })
1766 .unwrap_or_else(|| "[No Name]".to_string())
1767 }
1768
1769 pub fn apply_event_to_active_buffer(&mut self, event: &Event) {
1778 match event {
1781 Event::Scroll { line_offset } => {
1782 self.handle_scroll_event(*line_offset);
1783 return;
1784 }
1785 Event::SetViewport { top_line } => {
1786 self.handle_set_viewport_event(*top_line);
1787 return;
1788 }
1789 Event::Recenter => {
1790 self.handle_recenter_event();
1791 return;
1792 }
1793 _ => {}
1794 }
1795
1796 let lsp_changes = self.collect_lsp_changes(event);
1800
1801 let line_info = self.calculate_event_line_info(event);
1803
1804 self.active_state_mut().apply(event);
1806
1807 self.sync_editor_state_to_split_view_state();
1810
1811 match event {
1814 Event::Insert { .. } | Event::Delete { .. } | Event::BulkEdit { .. } => {
1815 self.invalidate_layouts_for_buffer(self.active_buffer());
1816 self.schedule_semantic_tokens_full_refresh(self.active_buffer());
1817 }
1818 Event::Batch { events, .. } => {
1819 let has_edits = events
1820 .iter()
1821 .any(|e| matches!(e, Event::Insert { .. } | Event::Delete { .. }));
1822 if has_edits {
1823 self.invalidate_layouts_for_buffer(self.active_buffer());
1824 self.schedule_semantic_tokens_full_refresh(self.active_buffer());
1825 }
1826 }
1827 _ => {}
1828 }
1829
1830 self.adjust_other_split_cursors_for_event(event);
1832
1833 let in_interactive_replace = self.interactive_replace_state.is_some();
1837
1838 let _ = in_interactive_replace; self.trigger_plugin_hooks_for_event(event, line_info);
1847
1848 self.send_lsp_changes_for_buffer(self.active_buffer(), lsp_changes);
1850 }
1851
1852 pub fn apply_events_as_bulk_edit(
1866 &mut self,
1867 events: Vec<Event>,
1868 description: String,
1869 ) -> Option<Event> {
1870 use crate::model::event::CursorId;
1871
1872 let has_buffer_mods = events
1874 .iter()
1875 .any(|e| matches!(e, Event::Insert { .. } | Event::Delete { .. }));
1876
1877 if !has_buffer_mods {
1878 return None;
1880 }
1881
1882 let state = self.active_state_mut();
1883
1884 let old_cursors: Vec<(CursorId, usize, Option<usize>)> = state
1886 .cursors
1887 .iter()
1888 .map(|(id, c)| (id, c.position, c.anchor))
1889 .collect();
1890
1891 let old_tree = state.buffer.snapshot_piece_tree();
1893
1894 let mut edits: Vec<(usize, usize, String)> = Vec::new();
1898
1899 for event in &events {
1900 match event {
1901 Event::Insert { position, text, .. } => {
1902 edits.push((*position, 0, text.clone()));
1903 }
1904 Event::Delete { range, .. } => {
1905 edits.push((range.start, range.len(), String::new()));
1906 }
1907 _ => {}
1908 }
1909 }
1910
1911 edits.sort_by(|a, b| b.0.cmp(&a.0));
1913
1914 let edit_refs: Vec<(usize, usize, &str)> = edits
1916 .iter()
1917 .map(|(pos, del, text)| (*pos, *del, text.as_str()))
1918 .collect();
1919
1920 let _delta = state.buffer.apply_bulk_edits(&edit_refs);
1922
1923 let new_tree = state.buffer.snapshot_piece_tree();
1925
1926 let mut new_cursors: Vec<(CursorId, usize, Option<usize>)> = old_cursors.clone();
1929
1930 let mut position_deltas: Vec<(usize, isize)> = Vec::new();
1933 for (pos, del_len, text) in &edits {
1934 let delta = text.len() as isize - *del_len as isize;
1935 position_deltas.push((*pos, delta));
1936 }
1937 position_deltas.sort_by_key(|(pos, _)| *pos);
1938
1939 let calc_shift = |original_pos: usize| -> isize {
1941 let mut shift: isize = 0;
1942 for (edit_pos, delta) in &position_deltas {
1943 if *edit_pos < original_pos {
1944 shift += delta;
1945 }
1946 }
1947 shift
1948 };
1949
1950 for (cursor_id, ref mut pos, ref mut anchor) in &mut new_cursors {
1954 let mut found_move_cursor = false;
1955 let original_pos = *pos;
1957
1958 let insert_at_cursor_pos = events.iter().any(|e| {
1962 matches!(e, Event::Insert { position, cursor_id: c, .. }
1963 if *c == *cursor_id && *position == original_pos)
1964 });
1965
1966 for event in &events {
1968 if let Event::MoveCursor {
1969 cursor_id: event_cursor,
1970 new_position,
1971 new_anchor,
1972 ..
1973 } = event
1974 {
1975 if event_cursor == cursor_id {
1976 let shift = if insert_at_cursor_pos {
1980 calc_shift(original_pos)
1981 } else {
1982 0
1983 };
1984 *pos = (*new_position as isize + shift) as usize;
1985 *anchor = *new_anchor;
1986 found_move_cursor = true;
1987 }
1988 }
1989 }
1990
1991 if !found_move_cursor {
1993 for event in &events {
1994 match event {
1995 Event::Insert {
1996 position,
1997 text,
1998 cursor_id: event_cursor,
1999 } if event_cursor == cursor_id => {
2000 let shift = calc_shift(*position);
2003 let adjusted_pos = (*position as isize + shift) as usize;
2004 *pos = adjusted_pos + text.len();
2005 *anchor = None;
2006 }
2007 Event::Delete {
2008 range,
2009 cursor_id: event_cursor,
2010 ..
2011 } if event_cursor == cursor_id => {
2012 let shift = calc_shift(range.start);
2015 *pos = (range.start as isize + shift) as usize;
2016 *anchor = None;
2017 }
2018 _ => {}
2019 }
2020 }
2021 }
2022 }
2023
2024 for (cursor_id, position, anchor) in &new_cursors {
2026 if let Some(cursor) = state.cursors.get_mut(*cursor_id) {
2027 cursor.position = *position;
2028 cursor.anchor = *anchor;
2029 }
2030 }
2031
2032 state.highlighter.invalidate_all();
2034
2035 let bulk_edit = Event::BulkEdit {
2037 old_tree: Some(old_tree),
2038 new_tree: Some(new_tree),
2039 old_cursors,
2040 new_cursors,
2041 description,
2042 };
2043
2044 self.sync_editor_state_to_split_view_state();
2046 self.invalidate_layouts_for_buffer(self.active_buffer());
2047 self.adjust_other_split_cursors_for_event(&bulk_edit);
2048 Some(bulk_edit)
2051 }
2052
2053 fn trigger_plugin_hooks_for_event(&mut self, event: &Event, line_info: EventLineInfo) {
2056 let buffer_id = self.active_buffer();
2057
2058 let hook_args = match event {
2060 Event::Insert { position, text, .. } => {
2061 let insert_position = *position;
2062 let insert_len = text.len();
2063
2064 if let Some(seen) = self.seen_byte_ranges.get_mut(&buffer_id) {
2066 let adjusted: std::collections::HashSet<(usize, usize)> = seen
2071 .iter()
2072 .filter_map(|&(start, end)| {
2073 if end <= insert_position {
2074 Some((start, end))
2076 } else if start >= insert_position {
2077 Some((start + insert_len, end + insert_len))
2079 } else {
2080 None
2082 }
2083 })
2084 .collect();
2085 *seen = adjusted;
2086 }
2087
2088 Some((
2089 "after_insert",
2090 crate::services::plugins::hooks::HookArgs::AfterInsert {
2091 buffer_id,
2092 position: *position,
2093 text: text.clone(),
2094 affected_start: insert_position,
2096 affected_end: insert_position + insert_len,
2097 start_line: line_info.start_line,
2099 end_line: line_info.end_line,
2100 lines_added: line_info.line_delta.max(0) as usize,
2101 },
2102 ))
2103 }
2104 Event::Delete {
2105 range,
2106 deleted_text,
2107 ..
2108 } => {
2109 let delete_start = range.start;
2110
2111 let delete_end = range.end;
2113 let delete_len = delete_end - delete_start;
2114 if let Some(seen) = self.seen_byte_ranges.get_mut(&buffer_id) {
2115 let adjusted: std::collections::HashSet<(usize, usize)> = seen
2120 .iter()
2121 .filter_map(|&(start, end)| {
2122 if end <= delete_start {
2123 Some((start, end))
2125 } else if start >= delete_end {
2126 Some((start - delete_len, end - delete_len))
2128 } else {
2129 None
2131 }
2132 })
2133 .collect();
2134 *seen = adjusted;
2135 }
2136
2137 Some((
2138 "after_delete",
2139 crate::services::plugins::hooks::HookArgs::AfterDelete {
2140 buffer_id,
2141 range: range.clone(),
2142 deleted_text: deleted_text.clone(),
2143 affected_start: delete_start,
2145 deleted_len: deleted_text.len(),
2146 start_line: line_info.start_line,
2148 end_line: line_info.end_line,
2149 lines_removed: (-line_info.line_delta).max(0) as usize,
2150 },
2151 ))
2152 }
2153 Event::Batch { events, .. } => {
2154 for e in events {
2158 let sub_line_info = self.calculate_event_line_info(e);
2161 self.trigger_plugin_hooks_for_event(e, sub_line_info);
2162 }
2163 None
2164 }
2165 Event::MoveCursor {
2166 cursor_id,
2167 old_position,
2168 new_position,
2169 ..
2170 } => {
2171 let line = self.active_state().buffer.get_line_number(*new_position) + 1;
2173 Some((
2174 "cursor_moved",
2175 crate::services::plugins::hooks::HookArgs::CursorMoved {
2176 buffer_id,
2177 cursor_id: *cursor_id,
2178 old_position: *old_position,
2179 new_position: *new_position,
2180 line,
2181 },
2182 ))
2183 }
2184 _ => None,
2185 };
2186
2187 if let Some((hook_name, args)) = hook_args {
2189 #[cfg(feature = "plugins")]
2193 self.update_plugin_state_snapshot();
2194
2195 self.plugin_manager.run_hook(hook_name, args);
2196 }
2197 }
2198
2199 fn handle_scroll_event(&mut self, line_offset: isize) {
2205 use crate::view::ui::view_pipeline::ViewLineIterator;
2206
2207 let active_split = self.split_manager.active_split();
2208
2209 if let Some(group) = self.scroll_sync_manager.find_group_for_split(active_split) {
2213 let left = group.left_split;
2214 let right = group.right_split;
2215 if let Some(vs) = self.split_view_states.get_mut(&left) {
2216 vs.viewport.set_skip_ensure_visible();
2217 }
2218 if let Some(vs) = self.split_view_states.get_mut(&right) {
2219 vs.viewport.set_skip_ensure_visible();
2220 }
2221 }
2223
2224 let sync_group = self
2226 .split_view_states
2227 .get(&active_split)
2228 .and_then(|vs| vs.sync_group);
2229 let splits_to_scroll = if let Some(group_id) = sync_group {
2230 self.split_manager
2231 .get_splits_in_group(group_id, &self.split_view_states)
2232 } else {
2233 vec![active_split]
2234 };
2235
2236 for split_id in splits_to_scroll {
2237 let buffer_id = if let Some(id) = self.split_manager.buffer_for_split(split_id) {
2238 id
2239 } else {
2240 continue;
2241 };
2242 let tab_size = self.config.editor.tab_size;
2243
2244 let view_transform_tokens = self
2246 .split_view_states
2247 .get(&split_id)
2248 .and_then(|vs| vs.view_transform.as_ref())
2249 .map(|vt| vt.tokens.clone());
2250
2251 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2253 let buffer = &mut state.buffer;
2254 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2255 if let Some(tokens) = view_transform_tokens {
2256 let view_lines: Vec<_> =
2258 ViewLineIterator::new(&tokens, false, false, tab_size).collect();
2259 view_state
2260 .viewport
2261 .scroll_view_lines(&view_lines, line_offset);
2262 } else {
2263 if line_offset > 0 {
2265 view_state
2266 .viewport
2267 .scroll_down(buffer, line_offset as usize);
2268 } else {
2269 view_state
2270 .viewport
2271 .scroll_up(buffer, line_offset.unsigned_abs());
2272 }
2273 }
2274 view_state.viewport.set_skip_ensure_visible();
2276 }
2277 }
2278 }
2279 }
2280
2281 fn handle_set_viewport_event(&mut self, top_line: usize) {
2283 let active_split = self.split_manager.active_split();
2284
2285 if self.scroll_sync_manager.is_split_synced(active_split) {
2288 if let Some(group) = self
2289 .scroll_sync_manager
2290 .find_group_for_split_mut(active_split)
2291 {
2292 let scroll_line = if group.is_left_split(active_split) {
2294 top_line
2295 } else {
2296 group.right_to_left_line(top_line)
2297 };
2298 group.set_scroll_line(scroll_line);
2299 }
2300
2301 if let Some(group) = self.scroll_sync_manager.find_group_for_split(active_split) {
2303 let left = group.left_split;
2304 let right = group.right_split;
2305 if let Some(vs) = self.split_view_states.get_mut(&left) {
2306 vs.viewport.set_skip_ensure_visible();
2307 }
2308 if let Some(vs) = self.split_view_states.get_mut(&right) {
2309 vs.viewport.set_skip_ensure_visible();
2310 }
2311 }
2312 return;
2313 }
2314
2315 let sync_group = self
2317 .split_view_states
2318 .get(&active_split)
2319 .and_then(|vs| vs.sync_group);
2320 let splits_to_scroll = if let Some(group_id) = sync_group {
2321 self.split_manager
2322 .get_splits_in_group(group_id, &self.split_view_states)
2323 } else {
2324 vec![active_split]
2325 };
2326
2327 for split_id in splits_to_scroll {
2328 let buffer_id = if let Some(id) = self.split_manager.buffer_for_split(split_id) {
2329 id
2330 } else {
2331 continue;
2332 };
2333
2334 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2335 let buffer = &mut state.buffer;
2336 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2337 view_state.viewport.scroll_to(buffer, top_line);
2338 view_state.viewport.set_skip_ensure_visible();
2340 }
2341 }
2342 }
2343 }
2344
2345 fn handle_recenter_event(&mut self) {
2347 let active_split = self.split_manager.active_split();
2348
2349 let sync_group = self
2351 .split_view_states
2352 .get(&active_split)
2353 .and_then(|vs| vs.sync_group);
2354 let splits_to_recenter = if let Some(group_id) = sync_group {
2355 self.split_manager
2356 .get_splits_in_group(group_id, &self.split_view_states)
2357 } else {
2358 vec![active_split]
2359 };
2360
2361 for split_id in splits_to_recenter {
2362 let buffer_id = if let Some(id) = self.split_manager.buffer_for_split(split_id) {
2363 id
2364 } else {
2365 continue;
2366 };
2367
2368 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2369 let buffer = &mut state.buffer;
2370 let view_state = self.split_view_states.get_mut(&split_id);
2371
2372 if let Some(view_state) = view_state {
2373 let cursor = *view_state.cursors.primary();
2375 let viewport_height = view_state.viewport.visible_line_count();
2376 let target_rows_from_top = viewport_height / 2;
2377
2378 let mut iter = buffer.line_iterator(cursor.position, 80);
2380 for _ in 0..target_rows_from_top {
2381 if iter.prev().is_none() {
2382 break;
2383 }
2384 }
2385 let new_top_byte = iter.current_position();
2386 view_state.viewport.top_byte = new_top_byte;
2387 view_state.viewport.set_skip_ensure_visible();
2389 }
2390 }
2391 }
2392 }
2393
2394 fn invalidate_layouts_for_buffer(&mut self, buffer_id: BufferId) {
2399 let splits_for_buffer = self.split_manager.splits_for_buffer(buffer_id);
2401
2402 for split_id in splits_for_buffer {
2404 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2405 view_state.invalidate_layout();
2406 }
2407 }
2408 }
2409
2410 pub fn active_event_log(&self) -> &EventLog {
2412 self.event_logs.get(&self.active_buffer()).unwrap()
2413 }
2414
2415 pub fn active_event_log_mut(&mut self) -> &mut EventLog {
2417 self.event_logs.get_mut(&self.active_buffer()).unwrap()
2418 }
2419
2420 pub(super) fn update_modified_from_event_log(&mut self) {
2424 let is_at_saved = self
2425 .event_logs
2426 .get(&self.active_buffer())
2427 .map(|log| log.is_at_saved_position())
2428 .unwrap_or(false);
2429
2430 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
2431 state.buffer.set_modified(!is_at_saved);
2432 }
2433 }
2434
2435 pub fn should_quit(&self) -> bool {
2437 self.should_quit
2438 }
2439
2440 pub fn should_restart(&self) -> bool {
2442 self.restart_with_dir.is_some()
2443 }
2444
2445 pub fn take_restart_dir(&mut self) -> Option<PathBuf> {
2448 self.restart_with_dir.take()
2449 }
2450
2451 pub fn request_full_redraw(&mut self) {
2456 self.full_redraw_requested = true;
2457 }
2458
2459 pub fn take_full_redraw_request(&mut self) -> bool {
2461 let requested = self.full_redraw_requested;
2462 self.full_redraw_requested = false;
2463 requested
2464 }
2465
2466 pub fn request_restart(&mut self, new_working_dir: PathBuf) {
2467 tracing::info!(
2468 "Restart requested with new working directory: {}",
2469 new_working_dir.display()
2470 );
2471 self.restart_with_dir = Some(new_working_dir);
2472 self.should_quit = true;
2474 }
2475
2476 pub fn theme(&self) -> &crate::view::theme::Theme {
2478 &self.theme
2479 }
2480
2481 pub fn is_settings_open(&self) -> bool {
2483 self.settings_state.as_ref().is_some_and(|s| s.visible)
2484 }
2485
2486 pub fn quit(&mut self) {
2488 let modified_count = self.count_modified_buffers();
2490 if modified_count > 0 {
2491 let discard_key = t!("prompt.key.discard").to_string();
2493 let cancel_key = t!("prompt.key.cancel").to_string();
2494 let msg = if modified_count == 1 {
2495 t!(
2496 "prompt.quit_modified_one",
2497 discard_key = discard_key,
2498 cancel_key = cancel_key
2499 )
2500 .to_string()
2501 } else {
2502 t!(
2503 "prompt.quit_modified_many",
2504 count = modified_count,
2505 discard_key = discard_key,
2506 cancel_key = cancel_key
2507 )
2508 .to_string()
2509 };
2510 self.start_prompt(msg, PromptType::ConfirmQuitWithModified);
2511 } else {
2512 self.should_quit = true;
2513 }
2514 }
2515
2516 fn count_modified_buffers(&self) -> usize {
2518 self.buffers
2519 .values()
2520 .filter(|state| state.buffer.is_modified())
2521 .count()
2522 }
2523
2524 pub fn resize(&mut self, width: u16, height: u16) {
2526 self.terminal_width = width;
2528 self.terminal_height = height;
2529
2530 for view_state in self.split_view_states.values_mut() {
2532 view_state.viewport.resize(width, height);
2533 }
2534
2535 self.resize_visible_terminals();
2537 }
2538
2539 pub fn start_prompt(&mut self, message: String, prompt_type: PromptType) {
2543 self.start_prompt_with_suggestions(message, prompt_type, Vec::new());
2544 }
2545
2546 fn start_search_prompt(
2551 &mut self,
2552 message: String,
2553 prompt_type: PromptType,
2554 use_selection_range: bool,
2555 ) {
2556 self.pending_search_range = None;
2558
2559 let selection_range = {
2560 let state = self.active_state();
2561 state.cursors.primary().selection_range()
2562 };
2563
2564 let selected_text = if let Some(range) = selection_range.clone() {
2565 let state = self.active_state_mut();
2566 let text = state.get_text_range(range.start, range.end);
2567 if !text.contains('\n') && !text.is_empty() {
2568 Some(text)
2569 } else {
2570 None
2571 }
2572 } else {
2573 None
2574 };
2575
2576 if use_selection_range {
2577 self.pending_search_range = selection_range;
2578 }
2579
2580 let from_history = selected_text.is_none();
2582 let default_text = selected_text.or_else(|| {
2583 self.get_prompt_history("search")
2584 .and_then(|h| h.last().map(|s| s.to_string()))
2585 });
2586
2587 self.start_prompt(message, prompt_type);
2589
2590 if let Some(text) = default_text {
2592 if let Some(ref mut prompt) = self.prompt {
2593 prompt.set_input(text.clone());
2594 prompt.selection_anchor = Some(0);
2595 prompt.cursor_pos = text.len();
2596 }
2597 if from_history {
2598 self.get_or_create_prompt_history("search").init_at_last();
2599 }
2600 self.update_search_highlights(&text);
2601 }
2602 }
2603
2604 pub fn start_prompt_with_suggestions(
2606 &mut self,
2607 message: String,
2608 prompt_type: PromptType,
2609 suggestions: Vec<Suggestion>,
2610 ) {
2611 self.on_editor_focus_lost();
2613
2614 match prompt_type {
2617 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
2618 self.clear_search_highlights();
2619 }
2620 _ => {}
2621 }
2622
2623 let needs_suggestions = matches!(
2625 prompt_type,
2626 PromptType::OpenFile
2627 | PromptType::SwitchProject
2628 | PromptType::SaveFileAs
2629 | PromptType::Command
2630 );
2631
2632 self.prompt = Some(Prompt::with_suggestions(message, prompt_type, suggestions));
2633
2634 if needs_suggestions {
2636 self.update_prompt_suggestions();
2637 }
2638 }
2639
2640 pub fn start_prompt_with_initial_text(
2642 &mut self,
2643 message: String,
2644 prompt_type: PromptType,
2645 initial_text: String,
2646 ) {
2647 self.on_editor_focus_lost();
2649
2650 self.prompt = Some(Prompt::with_initial_text(
2651 message,
2652 prompt_type,
2653 initial_text,
2654 ));
2655 }
2656
2657 fn cancel_search_prompt_if_active(&mut self) {
2660 if let Some(ref prompt) = self.prompt {
2661 if matches!(
2662 prompt.prompt_type,
2663 PromptType::Search
2664 | PromptType::ReplaceSearch
2665 | PromptType::Replace { .. }
2666 | PromptType::QueryReplaceSearch
2667 | PromptType::QueryReplace { .. }
2668 | PromptType::QueryReplaceConfirm
2669 ) {
2670 self.prompt = None;
2671 self.interactive_replace_state = None;
2673 let ns = self.search_namespace.clone();
2675 let state = self.active_state_mut();
2676 state.overlays.clear_namespace(&ns, &mut state.marker_list);
2677 }
2678 }
2679 }
2680
2681 fn prefill_open_file_prompt(&mut self) {
2683 if let Some(prompt) = self.prompt.as_mut() {
2687 if prompt.prompt_type == PromptType::OpenFile {
2688 prompt.input.clear();
2689 prompt.cursor_pos = 0;
2690 prompt.selection_anchor = None;
2691 }
2692 }
2693 }
2694
2695 fn init_file_open_state(&mut self) {
2701 let buffer_id = self.active_buffer();
2703
2704 let initial_dir = if self.is_terminal_buffer(buffer_id) {
2707 self.get_terminal_id(buffer_id)
2708 .and_then(|tid| self.terminal_manager.get(tid))
2709 .and_then(|handle| handle.cwd())
2710 .unwrap_or_else(|| self.working_dir.clone())
2711 } else {
2712 self.active_state()
2713 .buffer
2714 .file_path()
2715 .and_then(|path| path.parent())
2716 .map(|p| p.to_path_buf())
2717 .unwrap_or_else(|| self.working_dir.clone())
2718 };
2719
2720 let show_hidden = self.config.file_browser.show_hidden;
2722 self.file_open_state = Some(file_open::FileOpenState::new(
2723 initial_dir.clone(),
2724 show_hidden,
2725 ));
2726
2727 self.load_file_open_directory(initial_dir);
2729 }
2730
2731 fn init_folder_open_state(&mut self) {
2736 let initial_dir = self.working_dir.clone();
2738
2739 let show_hidden = self.config.file_browser.show_hidden;
2741 self.file_open_state = Some(file_open::FileOpenState::new(
2742 initial_dir.clone(),
2743 show_hidden,
2744 ));
2745
2746 self.load_file_open_directory(initial_dir);
2748 }
2749
2750 pub fn change_working_dir(&mut self, new_path: PathBuf) {
2760 let new_path = new_path.canonicalize().unwrap_or(new_path);
2762
2763 self.request_restart(new_path);
2766 }
2767
2768 fn load_file_open_directory(&mut self, path: PathBuf) {
2770 if let Some(state) = &mut self.file_open_state {
2772 state.current_dir = path.clone();
2773 state.loading = true;
2774 state.error = None;
2775 state.update_shortcuts();
2776 }
2777
2778 if let Some(ref runtime) = self.tokio_runtime {
2780 let fs_manager = self.fs_manager.clone();
2781 let sender = self.async_bridge.as_ref().map(|b| b.sender());
2782
2783 runtime.spawn(async move {
2784 let result = fs_manager.list_dir_with_metadata(path).await;
2785 if let Some(sender) = sender {
2786 let _ = sender.send(AsyncMessage::FileOpenDirectoryLoaded(result));
2787 }
2788 });
2789 } else {
2790 if let Some(state) = &mut self.file_open_state {
2792 state.set_error("Async runtime not available".to_string());
2793 }
2794 }
2795 }
2796
2797 pub(super) fn handle_file_open_directory_loaded(
2799 &mut self,
2800 result: std::io::Result<Vec<crate::services::fs::FsEntry>>,
2801 ) {
2802 match result {
2803 Ok(entries) => {
2804 if let Some(state) = &mut self.file_open_state {
2805 state.set_entries(entries);
2806 }
2807 let filter = self
2809 .prompt
2810 .as_ref()
2811 .map(|p| p.input.clone())
2812 .unwrap_or_default();
2813 if !filter.is_empty() {
2814 if let Some(state) = &mut self.file_open_state {
2815 state.apply_filter(&filter);
2816 }
2817 }
2818 }
2819 Err(e) => {
2820 if let Some(state) = &mut self.file_open_state {
2821 state.set_error(e.to_string());
2822 }
2823 }
2824 }
2825 }
2826
2827 pub fn cancel_prompt(&mut self) {
2829 let theme_to_restore = if let Some(ref prompt) = self.prompt {
2831 if let PromptType::SelectTheme { original_theme } = &prompt.prompt_type {
2832 Some(original_theme.clone())
2833 } else {
2834 None
2835 }
2836 } else {
2837 None
2838 };
2839
2840 if let Some(ref prompt) = self.prompt {
2842 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
2844 if let Some(history) = self.prompt_histories.get_mut(&key) {
2845 history.reset_navigation();
2846 }
2847 }
2848 match &prompt.prompt_type {
2849 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
2850 self.clear_search_highlights();
2851 }
2852 PromptType::Plugin { custom_type } => {
2853 use crate::services::plugins::hooks::HookArgs;
2855 self.plugin_manager.run_hook(
2856 "prompt_cancelled",
2857 HookArgs::PromptCancelled {
2858 prompt_type: custom_type.clone(),
2859 input: prompt.input.clone(),
2860 },
2861 );
2862 }
2863 PromptType::LspRename { overlay_handle, .. } => {
2864 let remove_overlay_event = crate::model::event::Event::RemoveOverlay {
2866 handle: overlay_handle.clone(),
2867 };
2868 self.apply_event_to_active_buffer(&remove_overlay_event);
2869 }
2870 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs => {
2871 self.file_open_state = None;
2873 self.file_browser_layout = None;
2874 }
2875 PromptType::AsyncPrompt => {
2876 if let Some(callback_id) = self.pending_async_prompt_callback.take() {
2878 self.plugin_manager
2879 .resolve_callback(callback_id, "null".to_string());
2880 }
2881 }
2882 _ => {}
2883 }
2884 }
2885
2886 self.prompt = None;
2887 self.pending_search_range = None;
2888 self.status_message = Some(t!("search.cancelled").to_string());
2889
2890 if let Some(original_theme) = theme_to_restore {
2892 self.preview_theme(&original_theme);
2893 }
2894 }
2895
2896 pub fn confirm_prompt(&mut self) -> Option<(String, PromptType, Option<usize>)> {
2901 if let Some(prompt) = self.prompt.take() {
2902 let selected_index = prompt.selected_suggestion;
2903 let final_input = if matches!(
2905 prompt.prompt_type,
2906 PromptType::Command
2907 | PromptType::OpenFile
2908 | PromptType::SwitchProject
2909 | PromptType::SaveFileAs
2910 | PromptType::StopLspServer
2911 | PromptType::SelectTheme { .. }
2912 | PromptType::SelectLocale
2913 | PromptType::SwitchToTab
2914 | PromptType::Plugin { .. }
2915 ) {
2916 if let Some(selected_idx) = prompt.selected_suggestion {
2918 if let Some(suggestion) = prompt.suggestions.get(selected_idx) {
2919 if suggestion.disabled {
2921 if matches!(prompt.prompt_type, PromptType::Command) {
2923 self.command_registry
2924 .write()
2925 .unwrap()
2926 .record_usage(&suggestion.text);
2927 }
2928 self.set_status_message(
2929 t!(
2930 "error.command_not_available",
2931 command = suggestion.text.clone()
2932 )
2933 .to_string(),
2934 );
2935 return None;
2936 }
2937 suggestion.get_value().to_string()
2939 } else {
2940 prompt.input.clone()
2941 }
2942 } else {
2943 prompt.input.clone()
2944 }
2945 } else {
2946 prompt.input.clone()
2947 };
2948
2949 if matches!(prompt.prompt_type, PromptType::StopLspServer) {
2951 let is_valid = prompt
2952 .suggestions
2953 .iter()
2954 .any(|s| s.text == final_input || s.get_value() == final_input);
2955 if !is_valid {
2956 self.prompt = Some(prompt);
2958 self.set_status_message(
2959 t!("error.no_lsp_match", input = final_input.clone()).to_string(),
2960 );
2961 return None;
2962 }
2963 }
2964
2965 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
2967 let history = self.get_or_create_prompt_history(&key);
2968 history.push(final_input.clone());
2969 history.reset_navigation();
2970 }
2971
2972 Some((final_input, prompt.prompt_type, selected_index))
2973 } else {
2974 None
2975 }
2976 }
2977
2978 pub fn is_prompting(&self) -> bool {
2980 self.prompt.is_some()
2981 }
2982
2983 fn get_or_create_prompt_history(
2985 &mut self,
2986 key: &str,
2987 ) -> &mut crate::input::input_history::InputHistory {
2988 self.prompt_histories.entry(key.to_string()).or_default()
2989 }
2990
2991 fn get_prompt_history(&self, key: &str) -> Option<&crate::input::input_history::InputHistory> {
2993 self.prompt_histories.get(key)
2994 }
2995
2996 fn prompt_type_to_history_key(prompt_type: &crate::view::prompt::PromptType) -> Option<String> {
2998 use crate::view::prompt::PromptType;
2999 match prompt_type {
3000 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
3001 Some("search".to_string())
3002 }
3003 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
3004 Some("replace".to_string())
3005 }
3006 PromptType::GotoLine => Some("goto_line".to_string()),
3007 PromptType::Plugin { custom_type } => Some(format!("plugin:{}", custom_type)),
3008 _ => None,
3009 }
3010 }
3011
3012 pub fn editor_mode(&self) -> Option<String> {
3015 self.editor_mode.clone()
3016 }
3017
3018 pub fn command_registry(&self) -> &Arc<RwLock<CommandRegistry>> {
3020 &self.command_registry
3021 }
3022
3023 pub fn plugin_manager(&self) -> &PluginManager {
3025 &self.plugin_manager
3026 }
3027
3028 pub fn plugin_manager_mut(&mut self) -> &mut PluginManager {
3030 &mut self.plugin_manager
3031 }
3032
3033 pub fn file_explorer_is_focused(&self) -> bool {
3035 self.key_context == KeyContext::FileExplorer
3036 }
3037
3038 pub fn prompt_input(&self) -> Option<&str> {
3040 self.prompt.as_ref().map(|p| p.input.as_str())
3041 }
3042
3043 pub fn has_active_selection(&self) -> bool {
3045 self.active_state()
3046 .cursors
3047 .primary()
3048 .selection_range()
3049 .is_some()
3050 }
3051
3052 pub fn prompt_mut(&mut self) -> Option<&mut Prompt> {
3054 self.prompt.as_mut()
3055 }
3056
3057 pub fn set_status_message(&mut self, message: String) {
3059 self.plugin_status_message = None;
3060 self.status_message = Some(message);
3061 }
3062
3063 pub fn get_status_message(&self) -> Option<&String> {
3065 self.plugin_status_message
3066 .as_ref()
3067 .or(self.status_message.as_ref())
3068 }
3069
3070 pub fn get_plugin_errors(&self) -> &[String] {
3073 &self.plugin_errors
3074 }
3075
3076 pub fn clear_plugin_errors(&mut self) {
3078 self.plugin_errors.clear();
3079 }
3080
3081 pub fn update_prompt_suggestions(&mut self) {
3083 let (prompt_type, input) = if let Some(prompt) = &self.prompt {
3085 (prompt.prompt_type.clone(), prompt.input.clone())
3086 } else {
3087 return;
3088 };
3089
3090 match prompt_type {
3091 PromptType::Command => {
3092 let selection_active = self.has_active_selection();
3093 let active_buffer_mode = self
3094 .buffer_metadata
3095 .get(&self.active_buffer())
3096 .and_then(|m| m.virtual_mode());
3097 if let Some(prompt) = &mut self.prompt {
3098 prompt.suggestions = self.command_registry.read().unwrap().filter(
3100 &input,
3101 self.key_context,
3102 &self.keybindings,
3103 selection_active,
3104 &self.active_custom_contexts,
3105 active_buffer_mode,
3106 );
3107 prompt.selected_suggestion = if prompt.suggestions.is_empty() {
3108 None
3109 } else {
3110 Some(0)
3111 };
3112 }
3113 }
3114 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
3115 self.update_search_highlights(&input);
3117 if let Some(history) = self.prompt_histories.get_mut("search") {
3119 history.reset_navigation();
3120 }
3121 }
3122 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
3123 if let Some(history) = self.prompt_histories.get_mut("replace") {
3125 history.reset_navigation();
3126 }
3127 }
3128 PromptType::GotoLine => {
3129 if let Some(history) = self.prompt_histories.get_mut("goto_line") {
3131 history.reset_navigation();
3132 }
3133 }
3134 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs => {
3135 self.update_file_open_filter();
3137 }
3138 PromptType::Plugin { custom_type } => {
3139 let key = format!("plugin:{}", custom_type);
3141 if let Some(history) = self.prompt_histories.get_mut(&key) {
3142 history.reset_navigation();
3143 }
3144 use crate::services::plugins::hooks::HookArgs;
3146 self.plugin_manager.run_hook(
3147 "prompt_changed",
3148 HookArgs::PromptChanged {
3149 prompt_type: custom_type,
3150 input,
3151 },
3152 );
3153 if let Some(prompt) = &mut self.prompt {
3158 prompt.filter_suggestions(false);
3159 }
3160 }
3161 PromptType::SwitchToTab
3162 | PromptType::SelectTheme { .. }
3163 | PromptType::StopLspServer => {
3164 if let Some(prompt) = &mut self.prompt {
3165 prompt.filter_suggestions(false);
3166 }
3167 }
3168 PromptType::SelectLocale => {
3169 if let Some(prompt) = &mut self.prompt {
3171 prompt.filter_suggestions(true);
3172 }
3173 }
3174 _ => {}
3175 }
3176 }
3177
3178 pub fn process_async_messages(&mut self) -> bool {
3186 self.plugin_manager.check_thread_health();
3189
3190 let Some(bridge) = &self.async_bridge else {
3191 return false;
3192 };
3193
3194 let messages = bridge.try_recv_all();
3195 let needs_render = !messages.is_empty();
3196
3197 for message in messages {
3198 match message {
3199 AsyncMessage::LspDiagnostics { uri, diagnostics } => {
3200 self.handle_lsp_diagnostics(uri, diagnostics);
3201 }
3202 AsyncMessage::LspInitialized {
3203 language,
3204 completion_trigger_characters,
3205 semantic_tokens_legend,
3206 semantic_tokens_full,
3207 semantic_tokens_full_delta,
3208 semantic_tokens_range,
3209 } => {
3210 tracing::info!("LSP server initialized for language: {}", language);
3211 tracing::debug!(
3212 "LSP completion trigger characters for {}: {:?}",
3213 language,
3214 completion_trigger_characters
3215 );
3216 self.status_message = Some(format!("LSP ({}) ready", language));
3217
3218 if let Some(lsp) = &mut self.lsp {
3220 lsp.set_completion_trigger_characters(
3221 &language,
3222 completion_trigger_characters,
3223 );
3224 lsp.set_semantic_tokens_capabilities(
3225 &language,
3226 semantic_tokens_legend,
3227 semantic_tokens_full,
3228 semantic_tokens_full_delta,
3229 semantic_tokens_range,
3230 );
3231 }
3232
3233 self.resend_did_open_for_language(&language);
3235 self.request_semantic_tokens_for_language(&language);
3236 }
3237 AsyncMessage::LspError {
3238 language,
3239 error,
3240 stderr_log_path,
3241 } => {
3242 tracing::error!("LSP error for {}: {}", language, error);
3243 self.status_message = Some(format!("LSP error ({}): {}", language, error));
3244
3245 let server_command = self
3247 .config
3248 .lsp
3249 .get(&language)
3250 .map(|c| c.command.clone())
3251 .unwrap_or_else(|| "unknown".to_string());
3252
3253 let error_type = if error.contains("not found") || error.contains("NotFound") {
3255 "not_found"
3256 } else if error.contains("permission") || error.contains("PermissionDenied") {
3257 "spawn_failed"
3258 } else if error.contains("timeout") {
3259 "timeout"
3260 } else {
3261 "spawn_failed"
3262 }
3263 .to_string();
3264
3265 self.plugin_manager.run_hook(
3267 "lsp_server_error",
3268 crate::services::plugins::hooks::HookArgs::LspServerError {
3269 language: language.clone(),
3270 server_command,
3271 error_type,
3272 message: error.clone(),
3273 },
3274 );
3275
3276 if let Some(log_path) = stderr_log_path {
3279 let has_content = log_path.metadata().map(|m| m.len() > 0).unwrap_or(false);
3280 if has_content {
3281 tracing::info!("Opening LSP stderr log in background: {:?}", log_path);
3282 match self.open_file_no_focus(&log_path) {
3283 Ok(buffer_id) => {
3284 if let Some(state) = self.buffers.get_mut(&buffer_id) {
3286 state.editing_disabled = true;
3287 }
3288 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id)
3289 {
3290 metadata.read_only = true;
3291 }
3292 self.status_message = Some(format!(
3293 "LSP error ({}): {} - See stderr log",
3294 language, error
3295 ));
3296 }
3297 Err(e) => {
3298 tracing::error!("Failed to open LSP stderr log: {}", e);
3299 }
3300 }
3301 }
3302 }
3303 }
3304 AsyncMessage::LspCompletion { request_id, items } => {
3305 if let Err(e) = self.handle_completion_response(request_id, items) {
3306 tracing::error!("Error handling completion response: {}", e);
3307 }
3308 }
3309 AsyncMessage::LspGotoDefinition {
3310 request_id,
3311 locations,
3312 } => {
3313 if let Err(e) = self.handle_goto_definition_response(request_id, locations) {
3314 tracing::error!("Error handling goto definition response: {}", e);
3315 }
3316 }
3317 AsyncMessage::LspRename { request_id, result } => {
3318 if let Err(e) = self.handle_rename_response(request_id, result) {
3319 tracing::error!("Error handling rename response: {}", e);
3320 }
3321 }
3322 AsyncMessage::LspHover {
3323 request_id,
3324 contents,
3325 is_markdown,
3326 range,
3327 } => {
3328 self.handle_hover_response(request_id, contents, is_markdown, range);
3329 }
3330 AsyncMessage::LspReferences {
3331 request_id,
3332 locations,
3333 } => {
3334 if let Err(e) = self.handle_references_response(request_id, locations) {
3335 tracing::error!("Error handling references response: {}", e);
3336 }
3337 }
3338 AsyncMessage::LspSignatureHelp {
3339 request_id,
3340 signature_help,
3341 } => {
3342 self.handle_signature_help_response(request_id, signature_help);
3343 }
3344 AsyncMessage::LspCodeActions {
3345 request_id,
3346 actions,
3347 } => {
3348 self.handle_code_actions_response(request_id, actions);
3349 }
3350 AsyncMessage::LspPulledDiagnostics {
3351 request_id: _,
3352 uri,
3353 result_id,
3354 diagnostics,
3355 unchanged,
3356 } => {
3357 self.handle_lsp_pulled_diagnostics(uri, result_id, diagnostics, unchanged);
3358 }
3359 AsyncMessage::LspInlayHints {
3360 request_id,
3361 uri,
3362 hints,
3363 } => {
3364 self.handle_lsp_inlay_hints(request_id, uri, hints);
3365 }
3366 AsyncMessage::LspSemanticTokens {
3367 request_id,
3368 uri,
3369 response,
3370 } => {
3371 self.handle_lsp_semantic_tokens(request_id, uri, response);
3372 }
3373 AsyncMessage::LspServerQuiescent { language } => {
3374 self.handle_lsp_server_quiescent(language);
3375 }
3376 AsyncMessage::FileChanged { path } => {
3377 self.handle_async_file_changed(path);
3378 }
3379 AsyncMessage::GitStatusChanged { status } => {
3380 tracing::info!("Git status changed: {}", status);
3381 }
3383 AsyncMessage::FileExplorerInitialized(view) => {
3384 self.handle_file_explorer_initialized(view);
3385 }
3386 AsyncMessage::FileExplorerToggleNode(node_id) => {
3387 self.handle_file_explorer_toggle_node(node_id);
3388 }
3389 AsyncMessage::FileExplorerRefreshNode(node_id) => {
3390 self.handle_file_explorer_refresh_node(node_id);
3391 }
3392 AsyncMessage::FileExplorerExpandedToPath(view) => {
3393 self.handle_file_explorer_expanded_to_path(view);
3394 }
3395 AsyncMessage::Plugin(plugin_msg) => {
3396 use fresh_core::api::{JsCallbackId, PluginAsyncMessage};
3397 match plugin_msg {
3398 PluginAsyncMessage::ProcessOutput {
3399 process_id,
3400 stdout,
3401 stderr,
3402 exit_code,
3403 } => {
3404 self.handle_plugin_process_output(
3405 JsCallbackId::from(process_id),
3406 stdout,
3407 stderr,
3408 exit_code,
3409 );
3410 }
3411 PluginAsyncMessage::DelayComplete { callback_id } => {
3412 self.plugin_manager.resolve_callback(
3413 JsCallbackId::from(callback_id),
3414 "null".to_string(),
3415 );
3416 }
3417 PluginAsyncMessage::ProcessStdout { process_id, data } => {
3418 self.plugin_manager.run_hook(
3419 "onProcessStdout",
3420 crate::services::plugins::hooks::HookArgs::ProcessOutput {
3421 process_id,
3422 data,
3423 },
3424 );
3425 }
3426 PluginAsyncMessage::ProcessStderr { process_id, data } => {
3427 self.plugin_manager.run_hook(
3428 "onProcessStderr",
3429 crate::services::plugins::hooks::HookArgs::ProcessOutput {
3430 process_id,
3431 data,
3432 },
3433 );
3434 }
3435 PluginAsyncMessage::ProcessExit {
3436 process_id,
3437 callback_id,
3438 exit_code,
3439 } => {
3440 self.background_process_handles.remove(&process_id);
3441 let result = fresh_core::api::BackgroundProcessResult {
3442 process_id,
3443 exit_code,
3444 };
3445 self.plugin_manager.resolve_callback(
3446 JsCallbackId::from(callback_id),
3447 serde_json::to_string(&result).unwrap(),
3448 );
3449 }
3450 PluginAsyncMessage::LspResponse {
3451 language: _,
3452 request_id,
3453 result,
3454 } => {
3455 self.handle_plugin_lsp_response(request_id, result);
3456 }
3457 PluginAsyncMessage::PluginResponse(response) => {
3458 self.handle_plugin_response(response);
3459 }
3460 }
3461 }
3462 AsyncMessage::LspProgress {
3463 language,
3464 token,
3465 value,
3466 } => {
3467 self.handle_lsp_progress(language, token, value);
3468 }
3469 AsyncMessage::LspWindowMessage {
3470 language,
3471 message_type,
3472 message,
3473 } => {
3474 self.handle_lsp_window_message(language, message_type, message);
3475 }
3476 AsyncMessage::LspLogMessage {
3477 language,
3478 message_type,
3479 message,
3480 } => {
3481 self.handle_lsp_log_message(language, message_type, message);
3482 }
3483 AsyncMessage::LspStatusUpdate {
3484 language,
3485 status,
3486 message: _,
3487 } => {
3488 self.handle_lsp_status_update(language, status);
3489 }
3490 AsyncMessage::FileOpenDirectoryLoaded(result) => {
3491 self.handle_file_open_directory_loaded(result);
3492 }
3493 AsyncMessage::TerminalOutput { terminal_id } => {
3494 tracing::trace!("Terminal output received for {:?}", terminal_id);
3496
3497 if self.config.terminal.jump_to_end_on_output && !self.terminal_mode {
3500 if let Some(&active_terminal_id) =
3502 self.terminal_buffers.get(&self.active_buffer())
3503 {
3504 if active_terminal_id == terminal_id {
3505 self.enter_terminal_mode();
3506 }
3507 }
3508 }
3509
3510 if self.terminal_mode {
3512 if let Some(handle) = self.terminal_manager.get(terminal_id) {
3513 if let Ok(mut state) = handle.state.lock() {
3514 state.scroll_to_bottom();
3515 }
3516 }
3517 }
3518 }
3519 AsyncMessage::TerminalExited { terminal_id } => {
3520 tracing::info!("Terminal {:?} exited", terminal_id);
3521 if let Some((&buffer_id, _)) = self
3523 .terminal_buffers
3524 .iter()
3525 .find(|(_, &tid)| tid == terminal_id)
3526 {
3527 if self.active_buffer() == buffer_id && self.terminal_mode {
3529 self.terminal_mode = false;
3530 self.key_context = crate::input::keybindings::KeyContext::Normal;
3531 }
3532
3533 self.sync_terminal_to_buffer(buffer_id);
3535
3536 let exit_msg = "\n[Terminal process exited]\n";
3538
3539 if let Some(backing_path) =
3540 self.terminal_backing_files.get(&terminal_id).cloned()
3541 {
3542 if let Ok(mut file) =
3543 std::fs::OpenOptions::new().append(true).open(&backing_path)
3544 {
3545 use std::io::Write;
3546 let _ = file.write_all(exit_msg.as_bytes());
3547 }
3548
3549 let _ = self.revert_buffer_by_id(buffer_id, &backing_path);
3551 }
3552
3553 if let Some(state) = self.buffers.get_mut(&buffer_id) {
3555 state.editing_disabled = true;
3556 state.margins.set_line_numbers(false);
3557 state.buffer.set_modified(false);
3558 }
3559
3560 self.terminal_buffers.remove(&buffer_id);
3562
3563 self.set_status_message(
3564 t!("terminal.exited", id = terminal_id.0).to_string(),
3565 );
3566 }
3567 self.terminal_manager.close(terminal_id);
3568 }
3569
3570 AsyncMessage::LspServerRequest {
3571 language,
3572 server_command,
3573 method,
3574 params,
3575 } => {
3576 self.handle_lsp_server_request(language, server_command, method, params);
3577 }
3578 AsyncMessage::PluginLspResponse {
3579 language: _,
3580 request_id,
3581 result,
3582 } => {
3583 self.handle_plugin_lsp_response(request_id, result);
3584 }
3585 AsyncMessage::PluginProcessOutput {
3586 process_id,
3587 stdout,
3588 stderr,
3589 exit_code,
3590 } => {
3591 self.handle_plugin_process_output(
3592 fresh_core::api::JsCallbackId::from(process_id),
3593 stdout,
3594 stderr,
3595 exit_code,
3596 );
3597 }
3598 }
3599 }
3600
3601 #[cfg(feature = "plugins")]
3604 self.update_plugin_state_snapshot();
3605
3606 let processed_any_commands = self.process_plugin_commands();
3608
3609 #[cfg(feature = "plugins")]
3611 self.process_pending_plugin_actions();
3612
3613 self.process_pending_lsp_restarts();
3615
3616 #[cfg(feature = "plugins")]
3618 let plugin_render = {
3619 let render = self.plugin_render_requested;
3620 self.plugin_render_requested = false;
3621 render
3622 };
3623 #[cfg(not(feature = "plugins"))]
3624 let plugin_render = false;
3625
3626 if let Some(ref mut checker) = self.update_checker {
3628 let _ = checker.poll_result();
3630 }
3631
3632 let file_changes = self.poll_file_changes();
3634 let tree_changes = self.poll_file_tree_changes();
3635
3636 needs_render || processed_any_commands || plugin_render || file_changes || tree_changes
3638 }
3639
3640 fn update_lsp_status_from_progress(&mut self) {
3642 if self.lsp_progress.is_empty() {
3643 self.update_lsp_status_from_server_statuses();
3645 return;
3646 }
3647
3648 if let Some((_, info)) = self.lsp_progress.iter().next() {
3650 let mut status = format!("LSP ({}): {}", info.language, info.title);
3651 if let Some(ref msg) = info.message {
3652 status.push_str(&format!(" - {}", msg));
3653 }
3654 if let Some(pct) = info.percentage {
3655 status.push_str(&format!(" ({}%)", pct));
3656 }
3657 self.lsp_status = status;
3658 }
3659 }
3660
3661 fn update_lsp_status_from_server_statuses(&mut self) {
3663 use crate::services::async_bridge::LspServerStatus;
3664
3665 let mut statuses: Vec<(String, LspServerStatus)> = self
3667 .lsp_server_statuses
3668 .iter()
3669 .map(|(lang, status)| (lang.clone(), *status))
3670 .collect();
3671
3672 if statuses.is_empty() {
3673 self.lsp_status = String::new();
3674 return;
3675 }
3676
3677 statuses.sort_by(|a, b| a.0.cmp(&b.0));
3679
3680 let status_parts: Vec<String> = statuses
3682 .iter()
3683 .map(|(lang, status)| {
3684 let status_str = match status {
3685 LspServerStatus::Starting => "starting",
3686 LspServerStatus::Initializing => "initializing",
3687 LspServerStatus::Running => "ready",
3688 LspServerStatus::Error => "error",
3689 LspServerStatus::Shutdown => "shutdown",
3690 };
3691 format!("{}: {}", lang, status_str)
3692 })
3693 .collect();
3694
3695 self.lsp_status = format!("LSP [{}]", status_parts.join(", "));
3696 }
3697
3698 #[cfg(feature = "plugins")]
3700 fn update_plugin_state_snapshot(&mut self) {
3701 if let Some(snapshot_handle) = self.plugin_manager.state_snapshot_handle() {
3703 use fresh_core::api::{BufferInfo, CursorInfo, ViewportInfo};
3704 let mut snapshot = snapshot_handle.write().unwrap();
3705
3706 snapshot.active_buffer_id = self.active_buffer();
3708
3709 snapshot.active_split_id = self.split_manager.active_split().0;
3711
3712 snapshot.buffers.clear();
3714 snapshot.buffer_saved_diffs.clear();
3715 snapshot.buffer_cursor_positions.clear();
3716 snapshot.buffer_text_properties.clear();
3717
3718 for (buffer_id, state) in &self.buffers {
3719 let buffer_info = BufferInfo {
3720 id: *buffer_id,
3721 path: state.buffer.file_path().map(|p| p.to_path_buf()),
3722 modified: state.buffer.is_modified(),
3723 length: state.buffer.len(),
3724 };
3725 snapshot.buffers.insert(*buffer_id, buffer_info);
3726
3727 let is_large_file = state.buffer.line_count().is_none();
3730 let diff = if is_large_file {
3731 BufferSavedDiff {
3732 equal: !state.buffer.is_modified(),
3733 byte_ranges: vec![],
3734 line_ranges: None,
3735 }
3736 } else {
3737 let diff = state.buffer.diff_since_saved();
3738 BufferSavedDiff {
3739 equal: diff.equal,
3740 byte_ranges: diff.byte_ranges.clone(),
3741 line_ranges: diff.line_ranges.clone(),
3742 }
3743 };
3744 snapshot.buffer_saved_diffs.insert(*buffer_id, diff);
3745
3746 let cursor_pos = state.cursors.primary().position;
3748 snapshot
3749 .buffer_cursor_positions
3750 .insert(*buffer_id, cursor_pos);
3751
3752 if !state.text_properties.is_empty() {
3754 snapshot
3755 .buffer_text_properties
3756 .insert(*buffer_id, state.text_properties.all().to_vec());
3757 }
3758 }
3759
3760 if let Some(active_state) = self.buffers.get_mut(&self.active_buffer()) {
3762 let primary = active_state.cursors.primary();
3764 let primary_position = primary.position;
3765 let primary_selection = primary.selection_range();
3766
3767 snapshot.primary_cursor = Some(CursorInfo {
3768 position: primary_position,
3769 selection: primary_selection.clone(),
3770 });
3771
3772 snapshot.selected_text = primary_selection
3774 .map(|range| active_state.get_text_range(range.start, range.end));
3775
3776 snapshot.all_cursors = active_state
3778 .cursors
3779 .iter()
3780 .map(|(_, cursor)| CursorInfo {
3781 position: cursor.position,
3782 selection: cursor.selection_range(),
3783 })
3784 .collect();
3785
3786 let active_split = self.split_manager.active_split();
3788 if let Some(view_state) = self.split_view_states.get(&active_split) {
3789 snapshot.viewport = Some(ViewportInfo {
3790 top_byte: view_state.viewport.top_byte,
3791 left_column: view_state.viewport.left_column,
3792 width: view_state.viewport.width,
3793 height: view_state.viewport.height,
3794 });
3795 } else {
3796 snapshot.viewport = None;
3797 }
3798 } else {
3799 snapshot.primary_cursor = None;
3800 snapshot.all_cursors.clear();
3801 snapshot.viewport = None;
3802 snapshot.selected_text = None;
3803 }
3804
3805 snapshot.clipboard = self.clipboard.get_internal().to_string();
3807
3808 snapshot.working_dir = self.working_dir.clone();
3810
3811 snapshot.diagnostics = self.stored_diagnostics.clone();
3813
3814 snapshot.config = serde_json::to_value(&self.config).unwrap_or(serde_json::Value::Null);
3816
3817 snapshot.user_config = Config::read_user_config_raw(&self.working_dir);
3820
3821 snapshot.editor_mode = self.editor_mode.clone();
3823 }
3824 }
3825
3826 pub fn handle_plugin_command(&mut self, command: PluginCommand) -> AnyhowResult<()> {
3828 match command {
3829 PluginCommand::InsertText {
3831 buffer_id,
3832 position,
3833 text,
3834 } => {
3835 self.handle_insert_text(buffer_id, position, text);
3836 }
3837 PluginCommand::DeleteRange { buffer_id, range } => {
3838 self.handle_delete_range(buffer_id, range);
3839 }
3840 PluginCommand::InsertAtCursor { text } => {
3841 self.handle_insert_at_cursor(text);
3842 }
3843 PluginCommand::DeleteSelection => {
3844 self.handle_delete_selection();
3845 }
3846
3847 PluginCommand::AddOverlay {
3849 buffer_id,
3850 namespace,
3851 range,
3852 color,
3853 bg_color,
3854 underline,
3855 bold,
3856 italic,
3857 extend_to_line_end,
3858 } => {
3859 self.handle_add_overlay(
3860 buffer_id,
3861 namespace,
3862 range,
3863 color,
3864 bg_color,
3865 underline,
3866 bold,
3867 italic,
3868 extend_to_line_end,
3869 );
3870 }
3871 PluginCommand::RemoveOverlay { buffer_id, handle } => {
3872 self.handle_remove_overlay(buffer_id, handle);
3873 }
3874 PluginCommand::ClearAllOverlays { buffer_id } => {
3875 self.handle_clear_all_overlays(buffer_id);
3876 }
3877 PluginCommand::ClearNamespace {
3878 buffer_id,
3879 namespace,
3880 } => {
3881 self.handle_clear_namespace(buffer_id, namespace);
3882 }
3883 PluginCommand::ClearOverlaysInRange {
3884 buffer_id,
3885 start,
3886 end,
3887 } => {
3888 self.handle_clear_overlays_in_range(buffer_id, start, end);
3889 }
3890
3891 PluginCommand::AddVirtualText {
3893 buffer_id,
3894 virtual_text_id,
3895 position,
3896 text,
3897 color,
3898 use_bg,
3899 before,
3900 } => {
3901 self.handle_add_virtual_text(
3902 buffer_id,
3903 virtual_text_id,
3904 position,
3905 text,
3906 color,
3907 use_bg,
3908 before,
3909 );
3910 }
3911 PluginCommand::RemoveVirtualText {
3912 buffer_id,
3913 virtual_text_id,
3914 } => {
3915 self.handle_remove_virtual_text(buffer_id, virtual_text_id);
3916 }
3917 PluginCommand::RemoveVirtualTextsByPrefix { buffer_id, prefix } => {
3918 self.handle_remove_virtual_texts_by_prefix(buffer_id, prefix);
3919 }
3920 PluginCommand::ClearVirtualTexts { buffer_id } => {
3921 self.handle_clear_virtual_texts(buffer_id);
3922 }
3923 PluginCommand::AddVirtualLine {
3924 buffer_id,
3925 position,
3926 text,
3927 fg_color,
3928 bg_color,
3929 above,
3930 namespace,
3931 priority,
3932 } => {
3933 self.handle_add_virtual_line(
3934 buffer_id, position, text, fg_color, bg_color, above, namespace, priority,
3935 );
3936 }
3937 PluginCommand::ClearVirtualTextNamespace {
3938 buffer_id,
3939 namespace,
3940 } => {
3941 self.handle_clear_virtual_text_namespace(buffer_id, namespace);
3942 }
3943
3944 PluginCommand::AddMenuItem {
3946 menu_label,
3947 item,
3948 position,
3949 } => {
3950 self.handle_add_menu_item(menu_label, item, position);
3951 }
3952 PluginCommand::AddMenu { menu, position } => {
3953 self.handle_add_menu(menu, position);
3954 }
3955 PluginCommand::RemoveMenuItem {
3956 menu_label,
3957 item_label,
3958 } => {
3959 self.handle_remove_menu_item(menu_label, item_label);
3960 }
3961 PluginCommand::RemoveMenu { menu_label } => {
3962 self.handle_remove_menu(menu_label);
3963 }
3964
3965 PluginCommand::FocusSplit { split_id } => {
3967 self.handle_focus_split(split_id);
3968 }
3969 PluginCommand::SetSplitBuffer {
3970 split_id,
3971 buffer_id,
3972 } => {
3973 self.handle_set_split_buffer(split_id, buffer_id);
3974 }
3975 PluginCommand::SetSplitScroll { split_id, top_byte } => {
3976 self.handle_set_split_scroll(split_id, top_byte);
3977 }
3978 PluginCommand::RequestHighlights {
3979 buffer_id,
3980 range,
3981 request_id,
3982 } => {
3983 self.handle_request_highlights(buffer_id, range, request_id);
3984 }
3985 PluginCommand::CloseSplit { split_id } => {
3986 self.handle_close_split(split_id);
3987 }
3988 PluginCommand::SetSplitRatio { split_id, ratio } => {
3989 self.handle_set_split_ratio(split_id, ratio);
3990 }
3991 PluginCommand::DistributeSplitsEvenly { split_ids: _ } => {
3992 self.handle_distribute_splits_evenly();
3993 }
3994 PluginCommand::SetBufferCursor {
3995 buffer_id,
3996 position,
3997 } => {
3998 self.handle_set_buffer_cursor(buffer_id, position);
3999 }
4000
4001 PluginCommand::SetLayoutHints {
4003 buffer_id,
4004 split_id,
4005 range: _,
4006 hints,
4007 } => {
4008 self.handle_set_layout_hints(buffer_id, split_id, hints);
4009 }
4010 PluginCommand::SetLineNumbers { buffer_id, enabled } => {
4011 self.handle_set_line_numbers(buffer_id, enabled);
4012 }
4013 PluginCommand::SubmitViewTransform {
4014 buffer_id,
4015 split_id,
4016 payload,
4017 } => {
4018 self.handle_submit_view_transform(buffer_id, split_id, payload);
4019 }
4020 PluginCommand::ClearViewTransform {
4021 buffer_id: _,
4022 split_id,
4023 } => {
4024 self.handle_clear_view_transform(split_id);
4025 }
4026 PluginCommand::RefreshLines { buffer_id } => {
4027 self.handle_refresh_lines(buffer_id);
4028 }
4029 PluginCommand::SetLineIndicator {
4030 buffer_id,
4031 line,
4032 namespace,
4033 symbol,
4034 color,
4035 priority,
4036 } => {
4037 self.handle_set_line_indicator(buffer_id, line, namespace, symbol, color, priority);
4038 }
4039 PluginCommand::ClearLineIndicators {
4040 buffer_id,
4041 namespace,
4042 } => {
4043 self.handle_clear_line_indicators(buffer_id, namespace);
4044 }
4045 PluginCommand::SetFileExplorerDecorations {
4046 namespace,
4047 decorations,
4048 } => {
4049 self.handle_set_file_explorer_decorations(namespace, decorations);
4050 }
4051 PluginCommand::ClearFileExplorerDecorations { namespace } => {
4052 self.handle_clear_file_explorer_decorations(&namespace);
4053 }
4054
4055 PluginCommand::SetStatus { message } => {
4057 self.handle_set_status(message);
4058 }
4059 PluginCommand::ApplyTheme { theme_name } => {
4060 self.apply_theme(&theme_name);
4061 }
4062 PluginCommand::ReloadConfig => {
4063 self.reload_config();
4064 }
4065 PluginCommand::StartPrompt { label, prompt_type } => {
4066 self.handle_start_prompt(label, prompt_type);
4067 }
4068 PluginCommand::StartPromptWithInitial {
4069 label,
4070 prompt_type,
4071 initial_value,
4072 } => {
4073 self.handle_start_prompt_with_initial(label, prompt_type, initial_value);
4074 }
4075 PluginCommand::StartPromptAsync {
4076 label,
4077 initial_value,
4078 callback_id,
4079 } => {
4080 self.handle_start_prompt_async(label, initial_value, callback_id);
4081 }
4082 PluginCommand::SetPromptSuggestions { suggestions } => {
4083 self.handle_set_prompt_suggestions(suggestions);
4084 }
4085
4086 PluginCommand::RegisterCommand { command } => {
4088 self.handle_register_command(command);
4089 }
4090 PluginCommand::UnregisterCommand { name } => {
4091 self.handle_unregister_command(name);
4092 }
4093 PluginCommand::DefineMode {
4094 name,
4095 parent,
4096 bindings,
4097 read_only,
4098 } => {
4099 self.handle_define_mode(name, parent, bindings, read_only);
4100 }
4101
4102 PluginCommand::OpenFileInBackground { path } => {
4104 self.handle_open_file_in_background(path);
4105 }
4106 PluginCommand::OpenFileAtLocation { path, line, column } => {
4107 return self.handle_open_file_at_location(path, line, column);
4108 }
4109 PluginCommand::OpenFileInSplit {
4110 split_id,
4111 path,
4112 line,
4113 column,
4114 } => {
4115 return self.handle_open_file_in_split(split_id, path, line, column);
4116 }
4117 PluginCommand::ShowBuffer { buffer_id } => {
4118 self.handle_show_buffer(buffer_id);
4119 }
4120 PluginCommand::CloseBuffer { buffer_id } => {
4121 self.handle_close_buffer(buffer_id);
4122 }
4123
4124 PluginCommand::SendLspRequest {
4126 language,
4127 method,
4128 params,
4129 request_id,
4130 } => {
4131 self.handle_send_lsp_request(language, method, params, request_id);
4132 }
4133
4134 PluginCommand::SetClipboard { text } => {
4136 self.handle_set_clipboard(text);
4137 }
4138
4139 PluginCommand::SpawnProcess {
4141 command,
4142 args,
4143 cwd,
4144 callback_id,
4145 } => {
4146 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
4148 let effective_cwd = cwd.unwrap_or_else(|| {
4149 std::env::current_dir()
4150 .map(|p| p.to_string_lossy().to_string())
4151 .unwrap_or_else(|_| ".".to_string())
4152 });
4153 let sender = bridge.sender();
4154 runtime.spawn(async move {
4155 let output = tokio::process::Command::new(&command)
4156 .args(&args)
4157 .current_dir(&effective_cwd)
4158 .output()
4159 .await;
4160
4161 match output {
4162 Ok(output) => {
4163 let _ = sender.send(AsyncMessage::PluginProcessOutput {
4164 process_id: callback_id.as_u64(),
4165 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
4166 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
4167 exit_code: output.status.code().unwrap_or(-1),
4168 });
4169 }
4170 Err(e) => {
4171 let _ = sender.send(AsyncMessage::PluginProcessOutput {
4172 process_id: callback_id.as_u64(),
4173 stdout: String::new(),
4174 stderr: e.to_string(),
4175 exit_code: -1,
4176 });
4177 }
4178 }
4179 });
4180 } else {
4181 let effective_cwd = cwd.unwrap_or_else(|| ".".to_string());
4183 match std::process::Command::new(&command)
4184 .args(&args)
4185 .current_dir(&effective_cwd)
4186 .output()
4187 {
4188 Ok(output) => {
4189 let result = fresh_core::api::SpawnResult {
4191 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
4192 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
4193 exit_code: output.status.code().unwrap_or(-1),
4194 };
4195 self.plugin_manager.resolve_callback(
4196 callback_id,
4197 serde_json::to_string(&result).unwrap(),
4198 );
4199 }
4200 Err(e) => {
4201 self.plugin_manager
4202 .reject_callback(callback_id, e.to_string());
4203 }
4204 }
4205 }
4206 }
4207
4208 PluginCommand::SpawnProcessWait {
4209 process_id,
4210 callback_id,
4211 } => {
4212 tracing::warn!(
4215 "SpawnProcessWait not fully implemented - process_id={}",
4216 process_id
4217 );
4218 self.plugin_manager.reject_callback(
4219 callback_id,
4220 format!(
4221 "SpawnProcessWait not yet fully implemented for process_id={}",
4222 process_id
4223 ),
4224 );
4225 }
4226
4227 PluginCommand::Delay {
4228 callback_id,
4229 duration_ms,
4230 } => {
4231 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
4233 let sender = bridge.sender();
4234 let callback_id_u64 = callback_id.as_u64();
4235 runtime.spawn(async move {
4236 tokio::time::sleep(tokio::time::Duration::from_millis(duration_ms)).await;
4237 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
4238 fresh_core::api::PluginAsyncMessage::DelayComplete {
4239 callback_id: callback_id_u64,
4240 },
4241 ));
4242 });
4243 } else {
4244 std::thread::sleep(std::time::Duration::from_millis(duration_ms));
4246 self.plugin_manager
4247 .resolve_callback(callback_id, "null".to_string());
4248 }
4249 }
4250
4251 PluginCommand::SpawnBackgroundProcess {
4252 process_id,
4253 command,
4254 args,
4255 cwd,
4256 callback_id,
4257 } => {
4258 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
4260 use tokio::io::{AsyncBufReadExt, BufReader};
4261 use tokio::process::Command as TokioCommand;
4262
4263 let effective_cwd = cwd.unwrap_or_else(|| {
4264 std::env::current_dir()
4265 .map(|p| p.to_string_lossy().to_string())
4266 .unwrap_or_else(|_| ".".to_string())
4267 });
4268
4269 let sender = bridge.sender();
4270 let sender_stdout = sender.clone();
4271 let sender_stderr = sender.clone();
4272 let callback_id_u64 = callback_id.as_u64();
4273
4274 let handle = runtime.spawn(async move {
4275 let mut child = match TokioCommand::new(&command)
4276 .args(&args)
4277 .current_dir(&effective_cwd)
4278 .stdout(std::process::Stdio::piped())
4279 .stderr(std::process::Stdio::piped())
4280 .spawn()
4281 {
4282 Ok(child) => child,
4283 Err(e) => {
4284 let _ = sender.send(
4285 crate::services::async_bridge::AsyncMessage::Plugin(
4286 fresh_core::api::PluginAsyncMessage::ProcessExit {
4287 process_id,
4288 callback_id: callback_id_u64,
4289 exit_code: -1,
4290 },
4291 ),
4292 );
4293 tracing::error!("Failed to spawn background process: {}", e);
4294 return;
4295 }
4296 };
4297
4298 let stdout = child.stdout.take();
4300 let stderr = child.stderr.take();
4301 let pid = process_id;
4302
4303 if let Some(stdout) = stdout {
4305 let sender = sender_stdout;
4306 tokio::spawn(async move {
4307 let reader = BufReader::new(stdout);
4308 let mut lines = reader.lines();
4309 while let Ok(Some(line)) = lines.next_line().await {
4310 let _ = sender.send(
4311 crate::services::async_bridge::AsyncMessage::Plugin(
4312 fresh_core::api::PluginAsyncMessage::ProcessStdout {
4313 process_id: pid,
4314 data: line + "\n",
4315 },
4316 ),
4317 );
4318 }
4319 });
4320 }
4321
4322 if let Some(stderr) = stderr {
4324 let sender = sender_stderr;
4325 tokio::spawn(async move {
4326 let reader = BufReader::new(stderr);
4327 let mut lines = reader.lines();
4328 while let Ok(Some(line)) = lines.next_line().await {
4329 let _ = sender.send(
4330 crate::services::async_bridge::AsyncMessage::Plugin(
4331 fresh_core::api::PluginAsyncMessage::ProcessStderr {
4332 process_id: pid,
4333 data: line + "\n",
4334 },
4335 ),
4336 );
4337 }
4338 });
4339 }
4340
4341 let exit_code = match child.wait().await {
4343 Ok(status) => status.code().unwrap_or(-1),
4344 Err(_) => -1,
4345 };
4346
4347 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
4348 fresh_core::api::PluginAsyncMessage::ProcessExit {
4349 process_id,
4350 callback_id: callback_id_u64,
4351 exit_code,
4352 },
4353 ));
4354 });
4355
4356 self.background_process_handles
4358 .insert(process_id, handle.abort_handle());
4359 } else {
4360 self.plugin_manager
4362 .reject_callback(callback_id, "Async runtime not available".to_string());
4363 }
4364 }
4365
4366 PluginCommand::KillBackgroundProcess { process_id } => {
4367 if let Some(handle) = self.background_process_handles.remove(&process_id) {
4368 handle.abort();
4369 tracing::debug!("Killed background process {}", process_id);
4370 }
4371 }
4372
4373 PluginCommand::CreateVirtualBuffer {
4375 name,
4376 mode,
4377 read_only,
4378 } => {
4379 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
4380 tracing::info!(
4381 "Created virtual buffer '{}' with mode '{}' (id={:?})",
4382 name,
4383 mode,
4384 buffer_id
4385 );
4386 }
4388 PluginCommand::CreateVirtualBufferWithContent {
4389 name,
4390 mode,
4391 read_only,
4392 entries,
4393 show_line_numbers,
4394 show_cursors,
4395 editing_disabled,
4396 hidden_from_tabs,
4397 request_id,
4398 } => {
4399 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
4400 tracing::info!(
4401 "Created virtual buffer '{}' with mode '{}' (id={:?})",
4402 name,
4403 mode,
4404 buffer_id
4405 );
4406
4407 if let Some(state) = self.buffers.get_mut(&buffer_id) {
4409 state.margins.set_line_numbers(show_line_numbers);
4410 state.show_cursors = show_cursors;
4411 state.editing_disabled = editing_disabled;
4412 tracing::debug!(
4413 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
4414 buffer_id,
4415 show_line_numbers,
4416 show_cursors,
4417 editing_disabled
4418 );
4419 }
4420
4421 if hidden_from_tabs {
4423 if let Some(meta) = self.buffer_metadata.get_mut(&buffer_id) {
4424 meta.hidden_from_tabs = true;
4425 }
4426 }
4427
4428 match self.set_virtual_buffer_content(buffer_id, entries) {
4430 Ok(()) => {
4431 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
4432 self.set_active_buffer(buffer_id);
4434 tracing::debug!("Switched to virtual buffer {:?}", buffer_id);
4435
4436 if let Some(req_id) = request_id {
4438 tracing::info!(
4439 "CreateVirtualBufferWithContent: resolving callback for request_id={}, buffer_id={:?}",
4440 req_id,
4441 buffer_id
4442 );
4443 let result = fresh_core::api::VirtualBufferResult {
4445 buffer_id: buffer_id.0 as u64,
4446 split_id: None,
4447 };
4448 self.plugin_manager.resolve_callback(
4449 fresh_core::api::JsCallbackId::from(req_id),
4450 serde_json::to_string(&result).unwrap_or_default(),
4451 );
4452 tracing::info!("CreateVirtualBufferWithContent: resolve_callback sent for request_id={}", req_id);
4453 }
4454 }
4455 Err(e) => {
4456 tracing::error!("Failed to set virtual buffer content: {}", e);
4457 }
4458 }
4459 }
4460 PluginCommand::CreateVirtualBufferInSplit {
4461 name,
4462 mode,
4463 read_only,
4464 entries,
4465 ratio,
4466 direction,
4467 panel_id,
4468 show_line_numbers,
4469 show_cursors,
4470 editing_disabled,
4471 line_wrap,
4472 request_id,
4473 } => {
4474 if let Some(pid) = &panel_id {
4476 if let Some(&existing_buffer_id) = self.panel_ids.get(pid) {
4477 if self.buffers.contains_key(&existing_buffer_id) {
4479 if let Err(e) =
4481 self.set_virtual_buffer_content(existing_buffer_id, entries)
4482 {
4483 tracing::error!("Failed to update panel content: {}", e);
4484 } else {
4485 tracing::info!("Updated existing panel '{}' content", pid);
4486 }
4487
4488 let splits = self.split_manager.splits_for_buffer(existing_buffer_id);
4490 if let Some(&split_id) = splits.first() {
4491 self.split_manager.set_active_split(split_id);
4492 self.split_manager.set_active_buffer_id(existing_buffer_id);
4495 tracing::debug!(
4496 "Focused split {:?} containing panel buffer",
4497 split_id
4498 );
4499 }
4500
4501 if let Some(req_id) = request_id {
4503 let result = fresh_core::api::VirtualBufferResult {
4504 buffer_id: existing_buffer_id.0 as u64,
4505 split_id: splits.first().map(|s| s.0 as u64),
4506 };
4507 self.plugin_manager.resolve_callback(
4508 fresh_core::api::JsCallbackId::from(req_id),
4509 serde_json::to_string(&result).unwrap_or_default(),
4510 );
4511 }
4512 return Ok(());
4513 } else {
4514 tracing::warn!(
4516 "Removing stale panel_id '{}' pointing to non-existent buffer {:?}",
4517 pid,
4518 existing_buffer_id
4519 );
4520 self.panel_ids.remove(pid);
4521 }
4523 }
4524 }
4525
4526 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
4528 tracing::info!(
4529 "Created virtual buffer '{}' with mode '{}' in split (id={:?})",
4530 name,
4531 mode,
4532 buffer_id
4533 );
4534
4535 if let Some(state) = self.buffers.get_mut(&buffer_id) {
4537 state.margins.set_line_numbers(show_line_numbers);
4538 state.show_cursors = show_cursors;
4539 state.editing_disabled = editing_disabled;
4540 tracing::debug!(
4541 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
4542 buffer_id,
4543 show_line_numbers,
4544 show_cursors,
4545 editing_disabled
4546 );
4547 }
4548
4549 if let Some(pid) = panel_id {
4551 self.panel_ids.insert(pid, buffer_id);
4552 }
4553
4554 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
4556 tracing::error!("Failed to set virtual buffer content: {}", e);
4557 return Ok(());
4558 }
4559
4560 self.save_current_split_view_state();
4562
4563 let split_dir = match direction.as_deref() {
4565 Some("vertical") => crate::model::event::SplitDirection::Vertical,
4566 _ => crate::model::event::SplitDirection::Horizontal,
4567 };
4568
4569 let created_split_id =
4571 match self.split_manager.split_active(split_dir, buffer_id, ratio) {
4572 Ok(new_split_id) => {
4573 let mut view_state = SplitViewState::with_buffer(
4575 self.terminal_width,
4576 self.terminal_height,
4577 buffer_id,
4578 );
4579 view_state.viewport.line_wrap_enabled =
4580 line_wrap.unwrap_or(self.config.editor.line_wrap);
4581 self.split_view_states.insert(new_split_id, view_state);
4582
4583 self.split_manager.set_active_split(new_split_id);
4585 tracing::info!(
4588 "Created {:?} split with virtual buffer {:?}",
4589 split_dir,
4590 buffer_id
4591 );
4592 Some(new_split_id)
4593 }
4594 Err(e) => {
4595 tracing::error!("Failed to create split: {}", e);
4596 self.set_active_buffer(buffer_id);
4598 None
4599 }
4600 };
4601
4602 if let Some(req_id) = request_id {
4605 tracing::trace!("CreateVirtualBufferInSplit: resolving callback for request_id={}, buffer_id={:?}, split_id={:?}", req_id, buffer_id, created_split_id);
4606 let result = fresh_core::api::VirtualBufferResult {
4607 buffer_id: buffer_id.0 as u64,
4608 split_id: created_split_id.map(|s| s.0 as u64),
4609 };
4610 self.plugin_manager.resolve_callback(
4611 fresh_core::api::JsCallbackId::from(req_id),
4612 serde_json::to_string(&result).unwrap_or_default(),
4613 );
4614 }
4615 }
4616 PluginCommand::SetVirtualBufferContent { buffer_id, entries } => {
4617 match self.set_virtual_buffer_content(buffer_id, entries) {
4618 Ok(()) => {
4619 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
4620 }
4621 Err(e) => {
4622 tracing::error!("Failed to set virtual buffer content: {}", e);
4623 }
4624 }
4625 }
4626 PluginCommand::GetTextPropertiesAtCursor { buffer_id } => {
4627 if let Some(state) = self.buffers.get(&buffer_id) {
4629 let cursor_pos = state.cursors.primary().position;
4630 let properties = state.text_properties.get_at(cursor_pos);
4631 tracing::debug!(
4632 "Text properties at cursor in {:?}: {} properties found",
4633 buffer_id,
4634 properties.len()
4635 );
4636 }
4638 }
4639 PluginCommand::CreateVirtualBufferInExistingSplit {
4640 name,
4641 mode,
4642 read_only,
4643 entries,
4644 split_id,
4645 show_line_numbers,
4646 show_cursors,
4647 editing_disabled,
4648 line_wrap,
4649 request_id,
4650 } => {
4651 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
4653 tracing::info!(
4654 "Created virtual buffer '{}' with mode '{}' for existing split {:?} (id={:?})",
4655 name,
4656 mode,
4657 split_id,
4658 buffer_id
4659 );
4660
4661 if let Some(state) = self.buffers.get_mut(&buffer_id) {
4663 state.margins.set_line_numbers(show_line_numbers);
4664 state.show_cursors = show_cursors;
4665 state.editing_disabled = editing_disabled;
4666 }
4667
4668 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
4670 tracing::error!("Failed to set virtual buffer content: {}", e);
4671 return Ok(());
4672 }
4673
4674 if let Err(e) = self.split_manager.set_split_buffer(split_id, buffer_id) {
4676 tracing::error!("Failed to set buffer in split {:?}: {}", split_id, e);
4677 self.set_active_buffer(buffer_id);
4679 } else {
4680 self.split_manager.set_active_split(split_id);
4682 self.split_manager.set_active_buffer_id(buffer_id);
4683
4684 if let Some(wrap) = line_wrap {
4686 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
4687 view_state.viewport.line_wrap_enabled = wrap;
4688 }
4689 }
4690
4691 tracing::info!(
4692 "Displayed virtual buffer {:?} in split {:?}",
4693 buffer_id,
4694 split_id
4695 );
4696 }
4697
4698 if let Some(req_id) = request_id {
4700 let result = fresh_core::api::VirtualBufferResult {
4701 buffer_id: buffer_id.0 as u64,
4702 split_id: Some(split_id.0 as u64),
4703 };
4704 self.plugin_manager.resolve_callback(
4705 fresh_core::api::JsCallbackId::from(req_id),
4706 serde_json::to_string(&result).unwrap_or_default(),
4707 );
4708 }
4709 }
4710
4711 PluginCommand::SetContext { name, active } => {
4713 if active {
4714 self.active_custom_contexts.insert(name.clone());
4715 tracing::debug!("Set custom context: {}", name);
4716 } else {
4717 self.active_custom_contexts.remove(&name);
4718 tracing::debug!("Unset custom context: {}", name);
4719 }
4720 }
4721
4722 PluginCommand::SetReviewDiffHunks { hunks } => {
4724 self.review_hunks = hunks;
4725 tracing::debug!("Set {} review hunks", self.review_hunks.len());
4726 }
4727
4728 PluginCommand::ExecuteAction { action_name } => {
4730 self.handle_execute_action(action_name);
4731 }
4732 PluginCommand::ExecuteActions { actions } => {
4733 self.handle_execute_actions(actions);
4734 }
4735 PluginCommand::GetBufferText {
4736 buffer_id,
4737 start,
4738 end,
4739 request_id,
4740 } => {
4741 self.handle_get_buffer_text(buffer_id, start, end, request_id);
4742 }
4743 PluginCommand::GetLineStartPosition {
4744 buffer_id,
4745 line,
4746 request_id,
4747 } => {
4748 self.handle_get_line_start_position(buffer_id, line, request_id);
4749 }
4750 PluginCommand::SetEditorMode { mode } => {
4751 self.handle_set_editor_mode(mode);
4752 }
4753
4754 PluginCommand::ShowActionPopup {
4756 popup_id,
4757 title,
4758 message,
4759 actions,
4760 } => {
4761 tracing::info!(
4762 "Action popup requested: id={}, title={}, actions={}",
4763 popup_id,
4764 title,
4765 actions.len()
4766 );
4767
4768 let items: Vec<crate::model::event::PopupListItemData> = actions
4770 .iter()
4771 .map(|action| crate::model::event::PopupListItemData {
4772 text: action.label.clone(),
4773 detail: None,
4774 icon: None,
4775 data: Some(action.id.clone()),
4776 })
4777 .collect();
4778
4779 let action_ids: Vec<(String, String)> =
4781 actions.into_iter().map(|a| (a.id, a.label)).collect();
4782 self.active_action_popup = Some((popup_id.clone(), action_ids));
4783
4784 let popup = crate::model::event::PopupData {
4786 title: Some(title),
4787 description: Some(message),
4788 transient: false,
4789 content: crate::model::event::PopupContentData::List { items, selected: 0 },
4790 position: crate::model::event::PopupPositionData::BottomRight,
4791 width: 60,
4792 max_height: 15,
4793 bordered: true,
4794 };
4795
4796 self.show_popup(popup);
4797 tracing::info!(
4798 "Action popup shown: id={}, active_action_popup={:?}",
4799 popup_id,
4800 self.active_action_popup.as_ref().map(|(id, _)| id)
4801 );
4802 }
4803
4804 PluginCommand::DisableLspForLanguage { language } => {
4805 tracing::info!("Disabling LSP for language: {}", language);
4806
4807 if let Some(ref mut lsp) = self.lsp {
4809 lsp.shutdown_server(&language);
4810 tracing::info!("Stopped LSP server for {}", language);
4811 }
4812
4813 if let Some(lsp_config) = self.config.lsp.get_mut(&language) {
4815 lsp_config.enabled = false;
4816 lsp_config.auto_start = false;
4817 tracing::info!("Disabled LSP config for {}", language);
4818 }
4819
4820 if let Err(e) = self.save_config() {
4822 tracing::error!("Failed to save config: {}", e);
4823 self.status_message = Some(format!(
4824 "LSP disabled for {} (config save failed)",
4825 language
4826 ));
4827 } else {
4828 self.status_message = Some(format!("LSP disabled for {}", language));
4829 }
4830
4831 self.warning_domains.lsp.clear();
4833 }
4834
4835 PluginCommand::CreateScrollSyncGroup {
4837 group_id,
4838 left_split,
4839 right_split,
4840 } => {
4841 let success = self.scroll_sync_manager.create_group_with_id(
4842 group_id,
4843 left_split,
4844 right_split,
4845 );
4846 if success {
4847 tracing::debug!(
4848 "Created scroll sync group {} for splits {:?} and {:?}",
4849 group_id,
4850 left_split,
4851 right_split
4852 );
4853 } else {
4854 tracing::warn!(
4855 "Failed to create scroll sync group {} (ID already exists)",
4856 group_id
4857 );
4858 }
4859 }
4860 PluginCommand::SetScrollSyncAnchors { group_id, anchors } => {
4861 use crate::view::scroll_sync::SyncAnchor;
4862 let anchor_count = anchors.len();
4863 let sync_anchors: Vec<SyncAnchor> = anchors
4864 .into_iter()
4865 .map(|(left_line, right_line)| SyncAnchor {
4866 left_line,
4867 right_line,
4868 })
4869 .collect();
4870 self.scroll_sync_manager.set_anchors(group_id, sync_anchors);
4871 tracing::debug!(
4872 "Set {} anchors for scroll sync group {}",
4873 anchor_count,
4874 group_id
4875 );
4876 }
4877 PluginCommand::RemoveScrollSyncGroup { group_id } => {
4878 if self.scroll_sync_manager.remove_group(group_id) {
4879 tracing::debug!("Removed scroll sync group {}", group_id);
4880 } else {
4881 tracing::warn!("Scroll sync group {} not found", group_id);
4882 }
4883 }
4884
4885 PluginCommand::CreateCompositeBuffer {
4887 name,
4888 mode,
4889 layout,
4890 sources,
4891 hunks,
4892 request_id,
4893 } => {
4894 self.handle_create_composite_buffer(name, mode, layout, sources, hunks, request_id);
4895 }
4896 PluginCommand::UpdateCompositeAlignment { buffer_id, hunks } => {
4897 self.handle_update_composite_alignment(buffer_id, hunks);
4898 }
4899 PluginCommand::CloseCompositeBuffer { buffer_id } => {
4900 self.close_composite_buffer(buffer_id);
4901 }
4902
4903 PluginCommand::SaveBufferToPath { buffer_id, path } => {
4905 self.handle_save_buffer_to_path(buffer_id, path);
4906 }
4907 }
4908 Ok(())
4909 }
4910
4911 fn handle_save_buffer_to_path(&mut self, buffer_id: BufferId, path: std::path::PathBuf) {
4913 if let Some(state) = self.buffers.get_mut(&buffer_id) {
4914 match state.buffer.save_to_file(&path) {
4916 Ok(()) => {
4917 state.buffer.set_file_path(path.clone());
4919 let _ = self.finalize_save(Some(path));
4921 tracing::debug!("Saved buffer {:?} to path", buffer_id);
4922 }
4923 Err(e) => {
4924 self.handle_set_status(format!("Error saving: {}", e));
4925 tracing::error!("Failed to save buffer to path: {}", e);
4926 }
4927 }
4928 } else {
4929 self.handle_set_status(format!("Buffer {:?} not found", buffer_id));
4930 tracing::warn!("SaveBufferToPath: buffer {:?} not found", buffer_id);
4931 }
4932 }
4933
4934 fn handle_execute_action(&mut self, action_name: String) {
4936 use crate::input::keybindings::Action;
4937 use std::collections::HashMap;
4938
4939 if let Some(action) = Action::from_str(&action_name, &HashMap::new()) {
4941 if let Err(e) = self.handle_action(action) {
4943 tracing::warn!("Failed to execute action '{}': {}", action_name, e);
4944 } else {
4945 tracing::debug!("Executed action: {}", action_name);
4946 }
4947 } else {
4948 tracing::warn!("Unknown action: {}", action_name);
4949 }
4950 }
4951
4952 fn handle_execute_actions(&mut self, actions: Vec<fresh_core::api::ActionSpec>) {
4955 use crate::input::keybindings::Action;
4956 use std::collections::HashMap;
4957
4958 for action_spec in actions {
4959 if let Some(action) = Action::from_str(&action_spec.action, &HashMap::new()) {
4960 for _ in 0..action_spec.count {
4962 if let Err(e) = self.handle_action(action.clone()) {
4963 tracing::warn!("Failed to execute action '{}': {}", action_spec.action, e);
4964 return; }
4966 }
4967 tracing::debug!(
4968 "Executed action '{}' {} time(s)",
4969 action_spec.action,
4970 action_spec.count
4971 );
4972 } else {
4973 tracing::warn!("Unknown action: {}", action_spec.action);
4974 return; }
4976 }
4977 }
4978
4979 fn handle_get_buffer_text(
4981 &mut self,
4982 buffer_id: BufferId,
4983 start: usize,
4984 end: usize,
4985 request_id: u64,
4986 ) {
4987 let result = if let Some(state) = self.buffers.get_mut(&buffer_id) {
4988 let len = state.buffer.len();
4990 if start <= end && end <= len {
4991 Ok(state.get_text_range(start, end))
4992 } else {
4993 Err(format!(
4994 "Invalid range {}..{} for buffer of length {}",
4995 start, end, len
4996 ))
4997 }
4998 } else {
4999 Err(format!("Buffer {:?} not found", buffer_id))
5000 };
5001
5002 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
5004 match result {
5005 Ok(text) => {
5006 let json = serde_json::to_string(&text).unwrap_or_else(|_| "null".to_string());
5008 self.plugin_manager.resolve_callback(callback_id, json);
5009 }
5010 Err(error) => {
5011 self.plugin_manager.reject_callback(callback_id, error);
5012 }
5013 }
5014 }
5015
5016 fn handle_set_editor_mode(&mut self, mode: Option<String>) {
5018 self.editor_mode = mode.clone();
5019 tracing::debug!("Set editor mode: {:?}", mode);
5020 }
5021
5022 fn handle_get_line_start_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
5024 let actual_buffer_id = if buffer_id.0 == 0 {
5026 self.active_buffer_id()
5027 } else {
5028 buffer_id
5029 };
5030
5031 let result = if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
5032 let line_number = line as usize;
5034 let buffer_len = state.buffer.len();
5035
5036 if line_number == 0 {
5037 Some(0)
5039 } else {
5040 let mut current_line = 0;
5042 let mut line_start = None;
5043
5044 let content = state.get_text_range(0, buffer_len);
5046 for (byte_idx, c) in content.char_indices() {
5047 if c == '\n' {
5048 current_line += 1;
5049 if current_line == line_number {
5050 line_start = Some(byte_idx + 1);
5052 break;
5053 }
5054 }
5055 }
5056 line_start
5057 }
5058 } else {
5059 None
5060 };
5061
5062 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
5064 let json = serde_json::to_string(&result).unwrap_or_else(|_| "null".to_string());
5066 self.plugin_manager.resolve_callback(callback_id, json);
5067 }
5068}
5069
5070fn parse_key_string(key_str: &str) -> Option<(KeyCode, KeyModifiers)> {
5079 use crossterm::event::{KeyCode, KeyModifiers};
5080
5081 let mut modifiers = KeyModifiers::NONE;
5082 let mut remaining = key_str;
5083
5084 loop {
5086 if remaining.starts_with("C-") {
5087 modifiers |= KeyModifiers::CONTROL;
5088 remaining = &remaining[2..];
5089 } else if remaining.starts_with("M-") {
5090 modifiers |= KeyModifiers::ALT;
5091 remaining = &remaining[2..];
5092 } else if remaining.starts_with("S-") {
5093 modifiers |= KeyModifiers::SHIFT;
5094 remaining = &remaining[2..];
5095 } else {
5096 break;
5097 }
5098 }
5099
5100 let upper = remaining.to_uppercase();
5103 let code = match upper.as_str() {
5104 "RET" | "RETURN" | "ENTER" => KeyCode::Enter,
5105 "TAB" => KeyCode::Tab,
5106 "ESC" | "ESCAPE" => KeyCode::Esc,
5107 "SPC" | "SPACE" => KeyCode::Char(' '),
5108 "DEL" | "DELETE" => KeyCode::Delete,
5109 "BS" | "BACKSPACE" => KeyCode::Backspace,
5110 "UP" => KeyCode::Up,
5111 "DOWN" => KeyCode::Down,
5112 "LEFT" => KeyCode::Left,
5113 "RIGHT" => KeyCode::Right,
5114 "HOME" => KeyCode::Home,
5115 "END" => KeyCode::End,
5116 "PAGEUP" | "PGUP" => KeyCode::PageUp,
5117 "PAGEDOWN" | "PGDN" => KeyCode::PageDown,
5118 s if s.starts_with('F') && s.len() > 1 => {
5119 if let Ok(n) = s[1..].parse::<u8>() {
5121 KeyCode::F(n)
5122 } else {
5123 return None;
5124 }
5125 }
5126 _ if remaining.len() == 1 => {
5127 let c = remaining.chars().next()?;
5130 if c.is_ascii_uppercase() {
5131 modifiers |= KeyModifiers::SHIFT;
5132 }
5133 KeyCode::Char(c.to_ascii_lowercase())
5134 }
5135 _ => return None,
5136 };
5137
5138 Some((code, modifiers))
5139}
5140
5141#[cfg(test)]
5142mod tests {
5143 use super::*;
5144 use tempfile::TempDir;
5145
5146 fn test_dir_context() -> (DirectoryContext, TempDir) {
5148 let temp_dir = TempDir::new().unwrap();
5149 let dir_context = DirectoryContext::for_testing(temp_dir.path());
5150 (dir_context, temp_dir)
5151 }
5152
5153 #[test]
5154 fn test_editor_new() {
5155 let config = Config::default();
5156 let (dir_context, _temp) = test_dir_context();
5157 let editor = Editor::new(
5158 config,
5159 80,
5160 24,
5161 dir_context,
5162 crate::view::color_support::ColorCapability::TrueColor,
5163 )
5164 .unwrap();
5165
5166 assert_eq!(editor.buffers.len(), 1);
5167 assert!(!editor.should_quit());
5168 }
5169
5170 #[test]
5171 fn test_new_buffer() {
5172 let config = Config::default();
5173 let (dir_context, _temp) = test_dir_context();
5174 let mut editor = Editor::new(
5175 config,
5176 80,
5177 24,
5178 dir_context,
5179 crate::view::color_support::ColorCapability::TrueColor,
5180 )
5181 .unwrap();
5182
5183 let id = editor.new_buffer();
5184 assert_eq!(editor.buffers.len(), 2);
5185 assert_eq!(editor.active_buffer(), id);
5186 }
5187
5188 #[test]
5189 #[ignore]
5190 fn test_clipboard() {
5191 let config = Config::default();
5192 let (dir_context, _temp) = test_dir_context();
5193 let mut editor = Editor::new(
5194 config,
5195 80,
5196 24,
5197 dir_context,
5198 crate::view::color_support::ColorCapability::TrueColor,
5199 )
5200 .unwrap();
5201
5202 editor.clipboard.set_internal("test".to_string());
5204
5205 editor.paste();
5207
5208 let content = editor.active_state().buffer.to_string().unwrap();
5209 assert_eq!(content, "test");
5210 }
5211
5212 #[test]
5213 fn test_action_to_events_insert_char() {
5214 let config = Config::default();
5215 let (dir_context, _temp) = test_dir_context();
5216 let mut editor = Editor::new(
5217 config,
5218 80,
5219 24,
5220 dir_context,
5221 crate::view::color_support::ColorCapability::TrueColor,
5222 )
5223 .unwrap();
5224
5225 let events = editor.action_to_events(Action::InsertChar('a'));
5226 assert!(events.is_some());
5227
5228 let events = events.unwrap();
5229 assert_eq!(events.len(), 1);
5230
5231 match &events[0] {
5232 Event::Insert { position, text, .. } => {
5233 assert_eq!(*position, 0);
5234 assert_eq!(text, "a");
5235 }
5236 _ => panic!("Expected Insert event"),
5237 }
5238 }
5239
5240 #[test]
5241 fn test_action_to_events_move_right() {
5242 let config = Config::default();
5243 let (dir_context, _temp) = test_dir_context();
5244 let mut editor = Editor::new(
5245 config,
5246 80,
5247 24,
5248 dir_context,
5249 crate::view::color_support::ColorCapability::TrueColor,
5250 )
5251 .unwrap();
5252
5253 let state = editor.active_state_mut();
5255 state.apply(&Event::Insert {
5256 position: 0,
5257 text: "hello".to_string(),
5258 cursor_id: state.cursors.primary_id(),
5259 });
5260
5261 let events = editor.action_to_events(Action::MoveRight);
5262 assert!(events.is_some());
5263
5264 let events = events.unwrap();
5265 assert_eq!(events.len(), 1);
5266
5267 match &events[0] {
5268 Event::MoveCursor {
5269 new_position,
5270 new_anchor,
5271 ..
5272 } => {
5273 assert_eq!(*new_position, 5);
5275 assert_eq!(*new_anchor, None); }
5277 _ => panic!("Expected MoveCursor event"),
5278 }
5279 }
5280
5281 #[test]
5282 fn test_action_to_events_move_up_down() {
5283 let config = Config::default();
5284 let (dir_context, _temp) = test_dir_context();
5285 let mut editor = Editor::new(
5286 config,
5287 80,
5288 24,
5289 dir_context,
5290 crate::view::color_support::ColorCapability::TrueColor,
5291 )
5292 .unwrap();
5293
5294 let state = editor.active_state_mut();
5296 state.apply(&Event::Insert {
5297 position: 0,
5298 text: "line1\nline2\nline3".to_string(),
5299 cursor_id: state.cursors.primary_id(),
5300 });
5301
5302 state.apply(&Event::MoveCursor {
5304 cursor_id: state.cursors.primary_id(),
5305 old_position: 0, new_position: 6,
5307 old_anchor: None, new_anchor: None,
5309 old_sticky_column: 0,
5310 new_sticky_column: 0,
5311 });
5312
5313 let events = editor.action_to_events(Action::MoveUp);
5315 assert!(events.is_some());
5316 let events = events.unwrap();
5317 assert_eq!(events.len(), 1);
5318
5319 match &events[0] {
5320 Event::MoveCursor { new_position, .. } => {
5321 assert_eq!(*new_position, 0); }
5323 _ => panic!("Expected MoveCursor event"),
5324 }
5325 }
5326
5327 #[test]
5328 fn test_action_to_events_insert_newline() {
5329 let config = Config::default();
5330 let (dir_context, _temp) = test_dir_context();
5331 let mut editor = Editor::new(
5332 config,
5333 80,
5334 24,
5335 dir_context,
5336 crate::view::color_support::ColorCapability::TrueColor,
5337 )
5338 .unwrap();
5339
5340 let events = editor.action_to_events(Action::InsertNewline);
5341 assert!(events.is_some());
5342
5343 let events = events.unwrap();
5344 assert_eq!(events.len(), 1);
5345
5346 match &events[0] {
5347 Event::Insert { text, .. } => {
5348 assert_eq!(text, "\n");
5349 }
5350 _ => panic!("Expected Insert event"),
5351 }
5352 }
5353
5354 #[test]
5355 fn test_action_to_events_unimplemented() {
5356 let config = Config::default();
5357 let (dir_context, _temp) = test_dir_context();
5358 let mut editor = Editor::new(
5359 config,
5360 80,
5361 24,
5362 dir_context,
5363 crate::view::color_support::ColorCapability::TrueColor,
5364 )
5365 .unwrap();
5366
5367 assert!(editor.action_to_events(Action::Save).is_none());
5369 assert!(editor.action_to_events(Action::Quit).is_none());
5370 assert!(editor.action_to_events(Action::Undo).is_none());
5371 }
5372
5373 #[test]
5374 fn test_action_to_events_delete_backward() {
5375 let config = Config::default();
5376 let (dir_context, _temp) = test_dir_context();
5377 let mut editor = Editor::new(
5378 config,
5379 80,
5380 24,
5381 dir_context,
5382 crate::view::color_support::ColorCapability::TrueColor,
5383 )
5384 .unwrap();
5385
5386 let state = editor.active_state_mut();
5388 state.apply(&Event::Insert {
5389 position: 0,
5390 text: "hello".to_string(),
5391 cursor_id: state.cursors.primary_id(),
5392 });
5393
5394 let events = editor.action_to_events(Action::DeleteBackward);
5395 assert!(events.is_some());
5396
5397 let events = events.unwrap();
5398 assert_eq!(events.len(), 1);
5399
5400 match &events[0] {
5401 Event::Delete {
5402 range,
5403 deleted_text,
5404 ..
5405 } => {
5406 assert_eq!(range.clone(), 4..5); assert_eq!(deleted_text, "o");
5408 }
5409 _ => panic!("Expected Delete event"),
5410 }
5411 }
5412
5413 #[test]
5414 fn test_action_to_events_delete_forward() {
5415 let config = Config::default();
5416 let (dir_context, _temp) = test_dir_context();
5417 let mut editor = Editor::new(
5418 config,
5419 80,
5420 24,
5421 dir_context,
5422 crate::view::color_support::ColorCapability::TrueColor,
5423 )
5424 .unwrap();
5425
5426 let state = editor.active_state_mut();
5428 state.apply(&Event::Insert {
5429 position: 0,
5430 text: "hello".to_string(),
5431 cursor_id: state.cursors.primary_id(),
5432 });
5433
5434 state.apply(&Event::MoveCursor {
5436 cursor_id: state.cursors.primary_id(),
5437 old_position: 0, new_position: 0,
5439 old_anchor: None, new_anchor: None,
5441 old_sticky_column: 0,
5442 new_sticky_column: 0,
5443 });
5444
5445 let events = editor.action_to_events(Action::DeleteForward);
5446 assert!(events.is_some());
5447
5448 let events = events.unwrap();
5449 assert_eq!(events.len(), 1);
5450
5451 match &events[0] {
5452 Event::Delete {
5453 range,
5454 deleted_text,
5455 ..
5456 } => {
5457 assert_eq!(range.clone(), 0..1); assert_eq!(deleted_text, "h");
5459 }
5460 _ => panic!("Expected Delete event"),
5461 }
5462 }
5463
5464 #[test]
5465 fn test_action_to_events_select_right() {
5466 let config = Config::default();
5467 let (dir_context, _temp) = test_dir_context();
5468 let mut editor = Editor::new(
5469 config,
5470 80,
5471 24,
5472 dir_context,
5473 crate::view::color_support::ColorCapability::TrueColor,
5474 )
5475 .unwrap();
5476
5477 let state = editor.active_state_mut();
5479 state.apply(&Event::Insert {
5480 position: 0,
5481 text: "hello".to_string(),
5482 cursor_id: state.cursors.primary_id(),
5483 });
5484
5485 state.apply(&Event::MoveCursor {
5487 cursor_id: state.cursors.primary_id(),
5488 old_position: 0, new_position: 0,
5490 old_anchor: None, new_anchor: None,
5492 old_sticky_column: 0,
5493 new_sticky_column: 0,
5494 });
5495
5496 let events = editor.action_to_events(Action::SelectRight);
5497 assert!(events.is_some());
5498
5499 let events = events.unwrap();
5500 assert_eq!(events.len(), 1);
5501
5502 match &events[0] {
5503 Event::MoveCursor {
5504 new_position,
5505 new_anchor,
5506 ..
5507 } => {
5508 assert_eq!(*new_position, 1); assert_eq!(*new_anchor, Some(0)); }
5511 _ => panic!("Expected MoveCursor event"),
5512 }
5513 }
5514
5515 #[test]
5516 fn test_action_to_events_select_all() {
5517 let config = Config::default();
5518 let (dir_context, _temp) = test_dir_context();
5519 let mut editor = Editor::new(
5520 config,
5521 80,
5522 24,
5523 dir_context,
5524 crate::view::color_support::ColorCapability::TrueColor,
5525 )
5526 .unwrap();
5527
5528 let state = editor.active_state_mut();
5530 state.apply(&Event::Insert {
5531 position: 0,
5532 text: "hello world".to_string(),
5533 cursor_id: state.cursors.primary_id(),
5534 });
5535
5536 let events = editor.action_to_events(Action::SelectAll);
5537 assert!(events.is_some());
5538
5539 let events = events.unwrap();
5540 assert_eq!(events.len(), 1);
5541
5542 match &events[0] {
5543 Event::MoveCursor {
5544 new_position,
5545 new_anchor,
5546 ..
5547 } => {
5548 assert_eq!(*new_position, 11); assert_eq!(*new_anchor, Some(0)); }
5551 _ => panic!("Expected MoveCursor event"),
5552 }
5553 }
5554
5555 #[test]
5556 fn test_action_to_events_document_nav() {
5557 let config = Config::default();
5558 let (dir_context, _temp) = test_dir_context();
5559 let mut editor = Editor::new(
5560 config,
5561 80,
5562 24,
5563 dir_context,
5564 crate::view::color_support::ColorCapability::TrueColor,
5565 )
5566 .unwrap();
5567
5568 let state = editor.active_state_mut();
5570 state.apply(&Event::Insert {
5571 position: 0,
5572 text: "line1\nline2\nline3".to_string(),
5573 cursor_id: state.cursors.primary_id(),
5574 });
5575
5576 let events = editor.action_to_events(Action::MoveDocumentStart);
5578 assert!(events.is_some());
5579 let events = events.unwrap();
5580 match &events[0] {
5581 Event::MoveCursor { new_position, .. } => {
5582 assert_eq!(*new_position, 0);
5583 }
5584 _ => panic!("Expected MoveCursor event"),
5585 }
5586
5587 let events = editor.action_to_events(Action::MoveDocumentEnd);
5589 assert!(events.is_some());
5590 let events = events.unwrap();
5591 match &events[0] {
5592 Event::MoveCursor { new_position, .. } => {
5593 assert_eq!(*new_position, 17); }
5595 _ => panic!("Expected MoveCursor event"),
5596 }
5597 }
5598
5599 #[test]
5600 fn test_action_to_events_remove_secondary_cursors() {
5601 use crate::model::event::CursorId;
5602
5603 let config = Config::default();
5604 let (dir_context, _temp) = test_dir_context();
5605 let mut editor = Editor::new(
5606 config,
5607 80,
5608 24,
5609 dir_context,
5610 crate::view::color_support::ColorCapability::TrueColor,
5611 )
5612 .unwrap();
5613
5614 {
5616 let state = editor.active_state_mut();
5617 state.apply(&Event::Insert {
5618 position: 0,
5619 text: "hello world test".to_string(),
5620 cursor_id: state.cursors.primary_id(),
5621 });
5622 }
5623
5624 {
5626 let state = editor.active_state_mut();
5627 state.apply(&Event::AddCursor {
5628 cursor_id: CursorId(1),
5629 position: 5,
5630 anchor: None,
5631 });
5632 state.apply(&Event::AddCursor {
5633 cursor_id: CursorId(2),
5634 position: 10,
5635 anchor: None,
5636 });
5637
5638 assert_eq!(state.cursors.count(), 3);
5639 }
5640
5641 let first_id = editor
5643 .active_state()
5644 .cursors
5645 .iter()
5646 .map(|(id, _)| id)
5647 .min_by_key(|id| id.0)
5648 .expect("Should have at least one cursor");
5649
5650 let events = editor.action_to_events(Action::RemoveSecondaryCursors);
5652 assert!(events.is_some());
5653
5654 let events = events.unwrap();
5655 let remove_cursor_events: Vec<_> = events
5658 .iter()
5659 .filter_map(|e| match e {
5660 Event::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
5661 _ => None,
5662 })
5663 .collect();
5664
5665 assert_eq!(remove_cursor_events.len(), 2);
5667
5668 for cursor_id in &remove_cursor_events {
5669 assert_ne!(*cursor_id, first_id);
5671 }
5672 }
5673
5674 #[test]
5675 fn test_action_to_events_scroll() {
5676 let config = Config::default();
5677 let (dir_context, _temp) = test_dir_context();
5678 let mut editor = Editor::new(
5679 config,
5680 80,
5681 24,
5682 dir_context,
5683 crate::view::color_support::ColorCapability::TrueColor,
5684 )
5685 .unwrap();
5686
5687 let events = editor.action_to_events(Action::ScrollUp);
5689 assert!(events.is_some());
5690 let events = events.unwrap();
5691 assert_eq!(events.len(), 1);
5692 match &events[0] {
5693 Event::Scroll { line_offset } => {
5694 assert_eq!(*line_offset, -1);
5695 }
5696 _ => panic!("Expected Scroll event"),
5697 }
5698
5699 let events = editor.action_to_events(Action::ScrollDown);
5701 assert!(events.is_some());
5702 let events = events.unwrap();
5703 assert_eq!(events.len(), 1);
5704 match &events[0] {
5705 Event::Scroll { line_offset } => {
5706 assert_eq!(*line_offset, 1);
5707 }
5708 _ => panic!("Expected Scroll event"),
5709 }
5710 }
5711
5712 #[test]
5713 fn test_action_to_events_none() {
5714 let config = Config::default();
5715 let (dir_context, _temp) = test_dir_context();
5716 let mut editor = Editor::new(
5717 config,
5718 80,
5719 24,
5720 dir_context,
5721 crate::view::color_support::ColorCapability::TrueColor,
5722 )
5723 .unwrap();
5724
5725 let events = editor.action_to_events(Action::None);
5727 assert!(events.is_none());
5728 }
5729
5730 #[test]
5731 fn test_lsp_incremental_insert_generates_correct_range() {
5732 use crate::model::buffer::Buffer;
5735
5736 let buffer = Buffer::from_str_test("hello\nworld");
5737
5738 let position = 0;
5741 let (line, character) = buffer.position_to_lsp_position(position);
5742
5743 assert_eq!(line, 0, "Insertion at start should be line 0");
5744 assert_eq!(character, 0, "Insertion at start should be char 0");
5745
5746 let lsp_pos = Position::new(line as u32, character as u32);
5748 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
5749
5750 assert_eq!(lsp_range.start.line, 0);
5751 assert_eq!(lsp_range.start.character, 0);
5752 assert_eq!(lsp_range.end.line, 0);
5753 assert_eq!(lsp_range.end.character, 0);
5754 assert_eq!(
5755 lsp_range.start, lsp_range.end,
5756 "Insert should have zero-width range"
5757 );
5758
5759 let position = 3;
5761 let (line, character) = buffer.position_to_lsp_position(position);
5762
5763 assert_eq!(line, 0);
5764 assert_eq!(character, 3);
5765
5766 let position = 6;
5768 let (line, character) = buffer.position_to_lsp_position(position);
5769
5770 assert_eq!(line, 1, "Position after newline should be line 1");
5771 assert_eq!(character, 0, "Position at start of line 2 should be char 0");
5772 }
5773
5774 #[test]
5775 fn test_lsp_incremental_delete_generates_correct_range() {
5776 use crate::model::buffer::Buffer;
5779
5780 let buffer = Buffer::from_str_test("hello\nworld");
5781
5782 let range_start = 1;
5784 let range_end = 5;
5785
5786 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
5787 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
5788
5789 assert_eq!(start_line, 0);
5790 assert_eq!(start_char, 1);
5791 assert_eq!(end_line, 0);
5792 assert_eq!(end_char, 5);
5793
5794 let lsp_range = LspRange::new(
5795 Position::new(start_line as u32, start_char as u32),
5796 Position::new(end_line as u32, end_char as u32),
5797 );
5798
5799 assert_eq!(lsp_range.start.line, 0);
5800 assert_eq!(lsp_range.start.character, 1);
5801 assert_eq!(lsp_range.end.line, 0);
5802 assert_eq!(lsp_range.end.character, 5);
5803 assert_ne!(
5804 lsp_range.start, lsp_range.end,
5805 "Delete should have non-zero range"
5806 );
5807
5808 let range_start = 4;
5810 let range_end = 8;
5811
5812 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
5813 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
5814
5815 assert_eq!(start_line, 0, "Delete start on line 0");
5816 assert_eq!(start_char, 4, "Delete start at char 4");
5817 assert_eq!(end_line, 1, "Delete end on line 1");
5818 assert_eq!(end_char, 2, "Delete end at char 2 of line 1");
5819 }
5820
5821 #[test]
5822 fn test_lsp_incremental_utf16_encoding() {
5823 use crate::model::buffer::Buffer;
5826
5827 let buffer = Buffer::from_str_test("😀hello");
5829
5830 let (line, character) = buffer.position_to_lsp_position(4);
5832
5833 assert_eq!(line, 0);
5834 assert_eq!(character, 2, "Emoji should count as 2 UTF-16 code units");
5835
5836 let (line, character) = buffer.position_to_lsp_position(9);
5838
5839 assert_eq!(line, 0);
5840 assert_eq!(
5841 character, 7,
5842 "Should be 2 (emoji) + 5 (text) = 7 UTF-16 code units"
5843 );
5844
5845 let buffer = Buffer::from_str_test("café");
5847
5848 let (line, character) = buffer.position_to_lsp_position(3);
5850
5851 assert_eq!(line, 0);
5852 assert_eq!(character, 3);
5853
5854 let (line, character) = buffer.position_to_lsp_position(5);
5856
5857 assert_eq!(line, 0);
5858 assert_eq!(character, 4, "é should count as 1 UTF-16 code unit");
5859 }
5860
5861 #[test]
5862 fn test_lsp_content_change_event_structure() {
5863 let insert_change = TextDocumentContentChangeEvent {
5867 range: Some(LspRange::new(Position::new(0, 5), Position::new(0, 5))),
5868 range_length: None,
5869 text: "NEW".to_string(),
5870 };
5871
5872 assert!(insert_change.range.is_some());
5873 assert_eq!(insert_change.text, "NEW");
5874 let range = insert_change.range.unwrap();
5875 assert_eq!(
5876 range.start, range.end,
5877 "Insert should have zero-width range"
5878 );
5879
5880 let delete_change = TextDocumentContentChangeEvent {
5882 range: Some(LspRange::new(Position::new(0, 2), Position::new(0, 7))),
5883 range_length: None,
5884 text: String::new(),
5885 };
5886
5887 assert!(delete_change.range.is_some());
5888 assert_eq!(delete_change.text, "");
5889 let range = delete_change.range.unwrap();
5890 assert_ne!(range.start, range.end, "Delete should have non-zero range");
5891 assert_eq!(range.start.line, 0);
5892 assert_eq!(range.start.character, 2);
5893 assert_eq!(range.end.line, 0);
5894 assert_eq!(range.end.character, 7);
5895 }
5896
5897 #[test]
5898 fn test_goto_matching_bracket_forward() {
5899 let config = Config::default();
5900 let (dir_context, _temp) = test_dir_context();
5901 let mut editor = Editor::new(
5902 config,
5903 80,
5904 24,
5905 dir_context,
5906 crate::view::color_support::ColorCapability::TrueColor,
5907 )
5908 .unwrap();
5909
5910 let state = editor.active_state_mut();
5912 state.apply(&Event::Insert {
5913 position: 0,
5914 text: "fn main() { let x = (1 + 2); }".to_string(),
5915 cursor_id: state.cursors.primary_id(),
5916 });
5917
5918 state.apply(&Event::MoveCursor {
5920 cursor_id: state.cursors.primary_id(),
5921 old_position: 31,
5922 new_position: 10,
5923 old_anchor: None,
5924 new_anchor: None,
5925 old_sticky_column: 0,
5926 new_sticky_column: 0,
5927 });
5928
5929 assert_eq!(state.cursors.primary().position, 10);
5930
5931 editor.goto_matching_bracket();
5933
5934 assert_eq!(editor.active_state().cursors.primary().position, 29);
5939 }
5940
5941 #[test]
5942 fn test_goto_matching_bracket_backward() {
5943 let config = Config::default();
5944 let (dir_context, _temp) = test_dir_context();
5945 let mut editor = Editor::new(
5946 config,
5947 80,
5948 24,
5949 dir_context,
5950 crate::view::color_support::ColorCapability::TrueColor,
5951 )
5952 .unwrap();
5953
5954 let state = editor.active_state_mut();
5956 state.apply(&Event::Insert {
5957 position: 0,
5958 text: "fn main() { let x = (1 + 2); }".to_string(),
5959 cursor_id: state.cursors.primary_id(),
5960 });
5961
5962 state.apply(&Event::MoveCursor {
5964 cursor_id: state.cursors.primary_id(),
5965 old_position: 31,
5966 new_position: 26,
5967 old_anchor: None,
5968 new_anchor: None,
5969 old_sticky_column: 0,
5970 new_sticky_column: 0,
5971 });
5972
5973 editor.goto_matching_bracket();
5975
5976 assert_eq!(editor.active_state().cursors.primary().position, 20);
5978 }
5979
5980 #[test]
5981 fn test_goto_matching_bracket_nested() {
5982 let config = Config::default();
5983 let (dir_context, _temp) = test_dir_context();
5984 let mut editor = Editor::new(
5985 config,
5986 80,
5987 24,
5988 dir_context,
5989 crate::view::color_support::ColorCapability::TrueColor,
5990 )
5991 .unwrap();
5992
5993 let state = editor.active_state_mut();
5995 state.apply(&Event::Insert {
5996 position: 0,
5997 text: "{a{b{c}d}e}".to_string(),
5998 cursor_id: state.cursors.primary_id(),
5999 });
6000
6001 state.apply(&Event::MoveCursor {
6003 cursor_id: state.cursors.primary_id(),
6004 old_position: 11,
6005 new_position: 0,
6006 old_anchor: None,
6007 new_anchor: None,
6008 old_sticky_column: 0,
6009 new_sticky_column: 0,
6010 });
6011
6012 editor.goto_matching_bracket();
6014
6015 assert_eq!(editor.active_state().cursors.primary().position, 10);
6017 }
6018
6019 #[test]
6020 fn test_search_case_sensitive() {
6021 let config = Config::default();
6022 let (dir_context, _temp) = test_dir_context();
6023 let mut editor = Editor::new(
6024 config,
6025 80,
6026 24,
6027 dir_context,
6028 crate::view::color_support::ColorCapability::TrueColor,
6029 )
6030 .unwrap();
6031
6032 let state = editor.active_state_mut();
6034 state.apply(&Event::Insert {
6035 position: 0,
6036 text: "Hello hello HELLO".to_string(),
6037 cursor_id: state.cursors.primary_id(),
6038 });
6039
6040 editor.search_case_sensitive = false;
6042 editor.perform_search("hello");
6043
6044 let search_state = editor.search_state.as_ref().unwrap();
6045 assert_eq!(
6046 search_state.matches.len(),
6047 3,
6048 "Should find all 3 matches case-insensitively"
6049 );
6050
6051 editor.search_case_sensitive = true;
6053 editor.perform_search("hello");
6054
6055 let search_state = editor.search_state.as_ref().unwrap();
6056 assert_eq!(
6057 search_state.matches.len(),
6058 1,
6059 "Should find only 1 exact match"
6060 );
6061 assert_eq!(
6062 search_state.matches[0], 6,
6063 "Should find 'hello' at position 6"
6064 );
6065 }
6066
6067 #[test]
6068 fn test_search_whole_word() {
6069 let config = Config::default();
6070 let (dir_context, _temp) = test_dir_context();
6071 let mut editor = Editor::new(
6072 config,
6073 80,
6074 24,
6075 dir_context,
6076 crate::view::color_support::ColorCapability::TrueColor,
6077 )
6078 .unwrap();
6079
6080 let state = editor.active_state_mut();
6082 state.apply(&Event::Insert {
6083 position: 0,
6084 text: "test testing tested attest test".to_string(),
6085 cursor_id: state.cursors.primary_id(),
6086 });
6087
6088 editor.search_whole_word = false;
6090 editor.search_case_sensitive = true;
6091 editor.perform_search("test");
6092
6093 let search_state = editor.search_state.as_ref().unwrap();
6094 assert_eq!(
6095 search_state.matches.len(),
6096 5,
6097 "Should find 'test' in all occurrences"
6098 );
6099
6100 editor.search_whole_word = true;
6102 editor.perform_search("test");
6103
6104 let search_state = editor.search_state.as_ref().unwrap();
6105 assert_eq!(
6106 search_state.matches.len(),
6107 2,
6108 "Should find only whole word 'test'"
6109 );
6110 assert_eq!(search_state.matches[0], 0, "First match at position 0");
6111 assert_eq!(search_state.matches[1], 27, "Second match at position 27");
6112 }
6113
6114 #[test]
6115 fn test_bookmarks() {
6116 let config = Config::default();
6117 let (dir_context, _temp) = test_dir_context();
6118 let mut editor = Editor::new(
6119 config,
6120 80,
6121 24,
6122 dir_context,
6123 crate::view::color_support::ColorCapability::TrueColor,
6124 )
6125 .unwrap();
6126
6127 let state = editor.active_state_mut();
6129 state.apply(&Event::Insert {
6130 position: 0,
6131 text: "Line 1\nLine 2\nLine 3".to_string(),
6132 cursor_id: state.cursors.primary_id(),
6133 });
6134
6135 state.apply(&Event::MoveCursor {
6137 cursor_id: state.cursors.primary_id(),
6138 old_position: 21,
6139 new_position: 7,
6140 old_anchor: None,
6141 new_anchor: None,
6142 old_sticky_column: 0,
6143 new_sticky_column: 0,
6144 });
6145
6146 editor.set_bookmark('1');
6148 assert!(editor.bookmarks.contains_key(&'1'));
6149 assert_eq!(editor.bookmarks.get(&'1').unwrap().position, 7);
6150
6151 let state = editor.active_state_mut();
6153 state.apply(&Event::MoveCursor {
6154 cursor_id: state.cursors.primary_id(),
6155 old_position: 7,
6156 new_position: 14,
6157 old_anchor: None,
6158 new_anchor: None,
6159 old_sticky_column: 0,
6160 new_sticky_column: 0,
6161 });
6162
6163 editor.jump_to_bookmark('1');
6165 assert_eq!(editor.active_state().cursors.primary().position, 7);
6166
6167 editor.clear_bookmark('1');
6169 assert!(!editor.bookmarks.contains_key(&'1'));
6170 }
6171
6172 #[test]
6173 fn test_action_enum_new_variants() {
6174 use serde_json::json;
6176
6177 let args = HashMap::new();
6178 assert_eq!(
6179 Action::from_str("smart_home", &args),
6180 Some(Action::SmartHome)
6181 );
6182 assert_eq!(
6183 Action::from_str("dedent_selection", &args),
6184 Some(Action::DedentSelection)
6185 );
6186 assert_eq!(
6187 Action::from_str("toggle_comment", &args),
6188 Some(Action::ToggleComment)
6189 );
6190 assert_eq!(
6191 Action::from_str("goto_matching_bracket", &args),
6192 Some(Action::GoToMatchingBracket)
6193 );
6194 assert_eq!(
6195 Action::from_str("list_bookmarks", &args),
6196 Some(Action::ListBookmarks)
6197 );
6198 assert_eq!(
6199 Action::from_str("toggle_search_case_sensitive", &args),
6200 Some(Action::ToggleSearchCaseSensitive)
6201 );
6202 assert_eq!(
6203 Action::from_str("toggle_search_whole_word", &args),
6204 Some(Action::ToggleSearchWholeWord)
6205 );
6206
6207 let mut args_with_char = HashMap::new();
6209 args_with_char.insert("char".to_string(), json!("5"));
6210 assert_eq!(
6211 Action::from_str("set_bookmark", &args_with_char),
6212 Some(Action::SetBookmark('5'))
6213 );
6214 assert_eq!(
6215 Action::from_str("jump_to_bookmark", &args_with_char),
6216 Some(Action::JumpToBookmark('5'))
6217 );
6218 assert_eq!(
6219 Action::from_str("clear_bookmark", &args_with_char),
6220 Some(Action::ClearBookmark('5'))
6221 );
6222 }
6223
6224 #[test]
6225 fn test_keybinding_new_defaults() {
6226 use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
6227
6228 let mut config = Config::default();
6232 config.active_keybinding_map = crate::config::KeybindingMapName("default".to_string());
6233 let resolver = KeybindingResolver::new(&config);
6234
6235 let event = KeyEvent {
6237 code: KeyCode::Char('/'),
6238 modifiers: KeyModifiers::CONTROL,
6239 kind: KeyEventKind::Press,
6240 state: KeyEventState::NONE,
6241 };
6242 let action = resolver.resolve(&event, KeyContext::Normal);
6243 assert_eq!(action, Action::ToggleComment);
6244
6245 let event = KeyEvent {
6247 code: KeyCode::Char(']'),
6248 modifiers: KeyModifiers::CONTROL,
6249 kind: KeyEventKind::Press,
6250 state: KeyEventState::NONE,
6251 };
6252 let action = resolver.resolve(&event, KeyContext::Normal);
6253 assert_eq!(action, Action::GoToMatchingBracket);
6254
6255 let event = KeyEvent {
6257 code: KeyCode::Tab,
6258 modifiers: KeyModifiers::SHIFT,
6259 kind: KeyEventKind::Press,
6260 state: KeyEventState::NONE,
6261 };
6262 let action = resolver.resolve(&event, KeyContext::Normal);
6263 assert_eq!(action, Action::DedentSelection);
6264
6265 let event = KeyEvent {
6267 code: KeyCode::Char('g'),
6268 modifiers: KeyModifiers::CONTROL,
6269 kind: KeyEventKind::Press,
6270 state: KeyEventState::NONE,
6271 };
6272 let action = resolver.resolve(&event, KeyContext::Normal);
6273 assert_eq!(action, Action::GotoLine);
6274
6275 let event = KeyEvent {
6277 code: KeyCode::Char('5'),
6278 modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
6279 kind: KeyEventKind::Press,
6280 state: KeyEventState::NONE,
6281 };
6282 let action = resolver.resolve(&event, KeyContext::Normal);
6283 assert_eq!(action, Action::SetBookmark('5'));
6284
6285 let event = KeyEvent {
6286 code: KeyCode::Char('5'),
6287 modifiers: KeyModifiers::ALT,
6288 kind: KeyEventKind::Press,
6289 state: KeyEventState::NONE,
6290 };
6291 let action = resolver.resolve(&event, KeyContext::Normal);
6292 assert_eq!(action, Action::JumpToBookmark('5'));
6293 }
6294
6295 #[test]
6307 fn test_lsp_rename_didchange_positions_bug() {
6308 use crate::model::buffer::Buffer;
6309
6310 let config = Config::default();
6311 let (dir_context, _temp) = test_dir_context();
6312 let mut editor = Editor::new(
6313 config,
6314 80,
6315 24,
6316 dir_context,
6317 crate::view::color_support::ColorCapability::TrueColor,
6318 )
6319 .unwrap();
6320
6321 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
6325 editor.active_state_mut().buffer = Buffer::from_str(initial, 1024 * 1024);
6326
6327 let cursor_id = editor.active_state().cursors.primary_id();
6332
6333 let batch = Event::Batch {
6334 events: vec![
6335 Event::Delete {
6337 range: 23..26, deleted_text: "val".to_string(),
6339 cursor_id,
6340 },
6341 Event::Insert {
6342 position: 23,
6343 text: "value".to_string(),
6344 cursor_id,
6345 },
6346 Event::Delete {
6348 range: 7..10, deleted_text: "val".to_string(),
6350 cursor_id,
6351 },
6352 Event::Insert {
6353 position: 7,
6354 text: "value".to_string(),
6355 cursor_id,
6356 },
6357 ],
6358 description: "LSP Rename".to_string(),
6359 };
6360
6361 let lsp_changes_before = editor.collect_lsp_changes(&batch);
6363
6364 editor.active_state_mut().apply(&batch);
6366
6367 let lsp_changes_after = editor.collect_lsp_changes(&batch);
6370
6371 let final_content = editor.active_state().buffer.to_string().unwrap();
6373 assert_eq!(
6374 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
6375 "Buffer should have 'value' in both places"
6376 );
6377
6378 assert_eq!(lsp_changes_before.len(), 4, "Should have 4 changes");
6384
6385 let first_delete = &lsp_changes_before[0];
6386 let first_del_range = first_delete.range.unwrap();
6387 assert_eq!(
6388 first_del_range.start.line, 1,
6389 "First delete should be on line 1 (BEFORE)"
6390 );
6391 assert_eq!(
6392 first_del_range.start.character, 4,
6393 "First delete start should be at char 4 (BEFORE)"
6394 );
6395
6396 assert_eq!(lsp_changes_after.len(), 4, "Should have 4 changes");
6402
6403 let first_delete_after = &lsp_changes_after[0];
6404 let first_del_range_after = first_delete_after.range.unwrap();
6405
6406 eprintln!("BEFORE modification:");
6409 eprintln!(
6410 " Delete at line {}, char {}-{}",
6411 first_del_range.start.line,
6412 first_del_range.start.character,
6413 first_del_range.end.character
6414 );
6415 eprintln!("AFTER modification:");
6416 eprintln!(
6417 " Delete at line {}, char {}-{}",
6418 first_del_range_after.start.line,
6419 first_del_range_after.start.character,
6420 first_del_range_after.end.character
6421 );
6422
6423 assert_ne!(
6441 first_del_range_after.end.character, first_del_range.end.character,
6442 "BUG CONFIRMED: LSP positions are different when calculated after buffer modification!"
6443 );
6444
6445 eprintln!("\n=== BUG DEMONSTRATED ===");
6446 eprintln!("When collect_lsp_changes() is called AFTER buffer modification,");
6447 eprintln!("the positions are WRONG because they're calculated from the");
6448 eprintln!("modified buffer, not the original buffer.");
6449 eprintln!("This causes the second rename to fail with 'content modified' error.");
6450 eprintln!("========================\n");
6451 }
6452
6453 #[test]
6454 fn test_lsp_rename_preserves_cursor_position() {
6455 use crate::model::buffer::Buffer;
6456
6457 let config = Config::default();
6458 let (dir_context, _temp) = test_dir_context();
6459 let mut editor = Editor::new(
6460 config,
6461 80,
6462 24,
6463 dir_context,
6464 crate::view::color_support::ColorCapability::TrueColor,
6465 )
6466 .unwrap();
6467
6468 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
6472 editor.active_state_mut().buffer = Buffer::from_str(initial, 1024 * 1024);
6473
6474 let original_cursor_pos = 23;
6476 editor.active_state_mut().cursors.primary_mut().position = original_cursor_pos;
6477
6478 let buffer_text = editor.active_state().buffer.to_string().unwrap();
6480 let text_at_cursor = buffer_text[original_cursor_pos..original_cursor_pos + 3].to_string();
6481 assert_eq!(text_at_cursor, "val", "Cursor should be at 'val'");
6482
6483 let cursor_id = editor.active_state().cursors.primary_id();
6486 let buffer_id = editor.active_buffer();
6487
6488 let events = vec![
6489 Event::Delete {
6491 range: 23..26, deleted_text: "val".to_string(),
6493 cursor_id,
6494 },
6495 Event::Insert {
6496 position: 23,
6497 text: "value".to_string(),
6498 cursor_id,
6499 },
6500 Event::Delete {
6502 range: 7..10, deleted_text: "val".to_string(),
6504 cursor_id,
6505 },
6506 Event::Insert {
6507 position: 7,
6508 text: "value".to_string(),
6509 cursor_id,
6510 },
6511 ];
6512
6513 editor
6515 .apply_events_to_buffer_as_bulk_edit(buffer_id, events, "LSP Rename".to_string())
6516 .unwrap();
6517
6518 let final_content = editor.active_state().buffer.to_string().unwrap();
6520 assert_eq!(
6521 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
6522 "Buffer should have 'value' in both places"
6523 );
6524
6525 let final_cursor_pos = editor.active_state().cursors.primary().position;
6533 let expected_cursor_pos = 25; assert_eq!(
6536 final_cursor_pos, expected_cursor_pos,
6537 "Cursor should be at position {} (start of 'value' on line 1), but was at {}. \
6538 Original pos: {}, expected adjustment: +2 for first rename",
6539 expected_cursor_pos, final_cursor_pos, original_cursor_pos
6540 );
6541
6542 let text_at_new_cursor = &final_content[final_cursor_pos..final_cursor_pos + 5];
6544 assert_eq!(
6545 text_at_new_cursor, "value",
6546 "Cursor should be at the start of 'value' after rename"
6547 );
6548 }
6549
6550 #[test]
6551 fn test_lsp_rename_twice_consecutive() {
6552 use crate::model::buffer::Buffer;
6555
6556 let config = Config::default();
6557 let (dir_context, _temp) = test_dir_context();
6558 let mut editor = Editor::new(
6559 config,
6560 80,
6561 24,
6562 dir_context,
6563 crate::view::color_support::ColorCapability::TrueColor,
6564 )
6565 .unwrap();
6566
6567 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
6569 editor.active_state_mut().buffer = Buffer::from_str(initial, 1024 * 1024);
6570
6571 let cursor_id = editor.active_state().cursors.primary_id();
6572 let buffer_id = editor.active_buffer();
6573
6574 let events1 = vec![
6577 Event::Delete {
6579 range: 23..26,
6580 deleted_text: "val".to_string(),
6581 cursor_id,
6582 },
6583 Event::Insert {
6584 position: 23,
6585 text: "value".to_string(),
6586 cursor_id,
6587 },
6588 Event::Delete {
6590 range: 7..10,
6591 deleted_text: "val".to_string(),
6592 cursor_id,
6593 },
6594 Event::Insert {
6595 position: 7,
6596 text: "value".to_string(),
6597 cursor_id,
6598 },
6599 ];
6600
6601 let batch1 = Event::Batch {
6603 events: events1.clone(),
6604 description: "LSP Rename 1".to_string(),
6605 };
6606
6607 let lsp_changes1 = editor.collect_lsp_changes(&batch1);
6609
6610 assert_eq!(
6612 lsp_changes1.len(),
6613 4,
6614 "First rename should have 4 LSP changes"
6615 );
6616
6617 let first_del = &lsp_changes1[0];
6619 let first_del_range = first_del.range.unwrap();
6620 assert_eq!(first_del_range.start.line, 1, "First delete line");
6621 assert_eq!(
6622 first_del_range.start.character, 4,
6623 "First delete start char"
6624 );
6625 assert_eq!(first_del_range.end.character, 7, "First delete end char");
6626
6627 editor
6629 .apply_events_to_buffer_as_bulk_edit(buffer_id, events1, "LSP Rename 1".to_string())
6630 .unwrap();
6631
6632 let after_first = editor.active_state().buffer.to_string().unwrap();
6634 assert_eq!(
6635 after_first, "fn foo(value: i32) {\n value + 1\n}\n",
6636 "After first rename"
6637 );
6638
6639 let events2 = vec![
6649 Event::Delete {
6651 range: 25..30,
6652 deleted_text: "value".to_string(),
6653 cursor_id,
6654 },
6655 Event::Insert {
6656 position: 25,
6657 text: "x".to_string(),
6658 cursor_id,
6659 },
6660 Event::Delete {
6662 range: 7..12,
6663 deleted_text: "value".to_string(),
6664 cursor_id,
6665 },
6666 Event::Insert {
6667 position: 7,
6668 text: "x".to_string(),
6669 cursor_id,
6670 },
6671 ];
6672
6673 let batch2 = Event::Batch {
6675 events: events2.clone(),
6676 description: "LSP Rename 2".to_string(),
6677 };
6678
6679 let lsp_changes2 = editor.collect_lsp_changes(&batch2);
6681
6682 assert_eq!(
6686 lsp_changes2.len(),
6687 4,
6688 "Second rename should have 4 LSP changes"
6689 );
6690
6691 let second_first_del = &lsp_changes2[0];
6693 let second_first_del_range = second_first_del.range.unwrap();
6694 assert_eq!(
6695 second_first_del_range.start.line, 1,
6696 "Second rename first delete should be on line 1"
6697 );
6698 assert_eq!(
6699 second_first_del_range.start.character, 4,
6700 "Second rename first delete start should be at char 4"
6701 );
6702 assert_eq!(
6703 second_first_del_range.end.character, 9,
6704 "Second rename first delete end should be at char 9 (4 + 5 for 'value')"
6705 );
6706
6707 let second_third_del = &lsp_changes2[2];
6709 let second_third_del_range = second_third_del.range.unwrap();
6710 assert_eq!(
6711 second_third_del_range.start.line, 0,
6712 "Second rename third delete should be on line 0"
6713 );
6714 assert_eq!(
6715 second_third_del_range.start.character, 7,
6716 "Second rename third delete start should be at char 7"
6717 );
6718 assert_eq!(
6719 second_third_del_range.end.character, 12,
6720 "Second rename third delete end should be at char 12 (7 + 5 for 'value')"
6721 );
6722
6723 editor
6725 .apply_events_to_buffer_as_bulk_edit(buffer_id, events2, "LSP Rename 2".to_string())
6726 .unwrap();
6727
6728 let after_second = editor.active_state().buffer.to_string().unwrap();
6730 assert_eq!(
6731 after_second, "fn foo(x: i32) {\n x + 1\n}\n",
6732 "After second rename"
6733 );
6734 }
6735
6736 #[test]
6737 fn test_ensure_active_tab_visible_static_offset() {
6738 let config = Config::default();
6739 let (dir_context, _temp) = test_dir_context();
6740 let mut editor = Editor::new(
6741 config,
6742 80,
6743 24,
6744 dir_context,
6745 crate::view::color_support::ColorCapability::TrueColor,
6746 )
6747 .unwrap();
6748 let split_id = editor.split_manager.active_split();
6749
6750 let buf1 = editor.new_buffer();
6752 editor
6753 .buffers
6754 .get_mut(&buf1)
6755 .unwrap()
6756 .buffer
6757 .set_file_path(std::path::PathBuf::from("aaa_long_name_01.txt"));
6758 let buf2 = editor.new_buffer();
6759 editor
6760 .buffers
6761 .get_mut(&buf2)
6762 .unwrap()
6763 .buffer
6764 .set_file_path(std::path::PathBuf::from("bbb_long_name_02.txt"));
6765 let buf3 = editor.new_buffer();
6766 editor
6767 .buffers
6768 .get_mut(&buf3)
6769 .unwrap()
6770 .buffer
6771 .set_file_path(std::path::PathBuf::from("ccc_long_name_03.txt"));
6772
6773 {
6774 let view_state = editor.split_view_states.get_mut(&split_id).unwrap();
6775 view_state.open_buffers = vec![buf1, buf2, buf3];
6776 view_state.tab_scroll_offset = 50;
6777 }
6778
6779 editor.ensure_active_tab_visible(split_id, buf1, 25);
6783 assert_eq!(
6784 editor
6785 .split_view_states
6786 .get(&split_id)
6787 .unwrap()
6788 .tab_scroll_offset,
6789 0
6790 );
6791
6792 editor.ensure_active_tab_visible(split_id, buf3, 25);
6794 let view_state = editor.split_view_states.get(&split_id).unwrap();
6795 assert!(view_state.tab_scroll_offset > 0);
6796 let total_width: usize = view_state
6797 .open_buffers
6798 .iter()
6799 .enumerate()
6800 .map(|(idx, id)| {
6801 let state = editor.buffers.get(id).unwrap();
6802 let name_len = state
6803 .buffer
6804 .file_path()
6805 .and_then(|p| p.file_name())
6806 .and_then(|n| n.to_str())
6807 .map(|s| s.chars().count())
6808 .unwrap_or(0);
6809 let tab_width = 2 + name_len;
6810 if idx < view_state.open_buffers.len() - 1 {
6811 tab_width + 1 } else {
6813 tab_width
6814 }
6815 })
6816 .sum();
6817 assert!(view_state.tab_scroll_offset <= total_width);
6818 }
6819}