1mod async_messages;
2mod buffer_management;
3mod calibration_actions;
4pub mod calibration_wizard;
5mod clipboard;
6mod composite_buffer_actions;
7mod dabbrev_actions;
8pub mod event_debug;
9mod event_debug_actions;
10mod file_explorer;
11pub mod file_open;
12mod file_open_input;
13mod file_operations;
14mod help;
15mod input;
16mod input_dispatch;
17pub mod keybinding_editor;
18mod keybinding_editor_actions;
19mod lsp_actions;
20mod lsp_requests;
21mod menu_actions;
22mod menu_context;
23mod mouse_input;
24mod on_save_actions;
25mod plugin_commands;
26mod popup_actions;
27mod prompt_actions;
28mod recovery_actions;
29mod regex_replace;
30mod render;
31mod settings_actions;
32mod shell_command;
33mod split_actions;
34mod tab_drag;
35mod terminal;
36mod terminal_input;
37mod terminal_mouse;
38mod theme_inspect;
39mod toggle_actions;
40pub mod types;
41mod undo_actions;
42mod view_actions;
43pub mod warning_domains;
44pub mod workspace;
45
46use anyhow::Result as AnyhowResult;
47use rust_i18n::t;
48use std::path::Component;
49
50pub fn editor_tick(
55 editor: &mut Editor,
56 mut clear_terminal: impl FnMut() -> AnyhowResult<()>,
57) -> AnyhowResult<bool> {
58 let mut needs_render = false;
59
60 let async_messages = {
61 let _s = tracing::info_span!("process_async_messages").entered();
62 editor.process_async_messages()
63 };
64 if async_messages {
65 needs_render = true;
66 }
67 let pending_file_opens = {
68 let _s = tracing::info_span!("process_pending_file_opens").entered();
69 editor.process_pending_file_opens()
70 };
71 if pending_file_opens {
72 needs_render = true;
73 }
74 if editor.process_line_scan() {
75 needs_render = true;
76 }
77 let search_scan = {
78 let _s = tracing::info_span!("process_search_scan").entered();
79 editor.process_search_scan()
80 };
81 if search_scan {
82 needs_render = true;
83 }
84 let search_overlay_refresh = {
85 let _s = tracing::info_span!("check_search_overlay_refresh").entered();
86 editor.check_search_overlay_refresh()
87 };
88 if search_overlay_refresh {
89 needs_render = true;
90 }
91 if editor.check_mouse_hover_timer() {
92 needs_render = true;
93 }
94 if editor.check_semantic_highlight_timer() {
95 needs_render = true;
96 }
97 if editor.check_completion_trigger_timer() {
98 needs_render = true;
99 }
100 editor.check_diagnostic_pull_timer();
101 if editor.check_warning_log() {
102 needs_render = true;
103 }
104 if editor.poll_stdin_streaming() {
105 needs_render = true;
106 }
107
108 if let Err(e) = editor.auto_recovery_save_dirty_buffers() {
109 tracing::debug!("Auto-recovery-save error: {}", e);
110 }
111 if let Err(e) = editor.auto_save_persistent_buffers() {
112 tracing::debug!("Auto-save (disk) error: {}", e);
113 }
114
115 if editor.take_full_redraw_request() {
116 clear_terminal()?;
117 needs_render = true;
118 }
119
120 Ok(needs_render)
121}
122
123pub(crate) fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
126 let mut components = Vec::new();
127
128 for component in path.components() {
129 match component {
130 Component::CurDir => {
131 }
133 Component::ParentDir => {
134 if let Some(Component::Normal(_)) = components.last() {
136 components.pop();
137 } else {
138 components.push(component);
140 }
141 }
142 _ => {
143 components.push(component);
144 }
145 }
146 }
147
148 if components.is_empty() {
149 std::path::PathBuf::from(".")
150 } else {
151 components.iter().collect()
152 }
153}
154
155use self::types::{
156 Bookmark, CachedLayout, EventLineInfo, InteractiveReplaceState, LspMessageEntry,
157 LspProgressInfo, MacroRecordingState, MouseState, SearchState, TabContextMenu,
158 DEFAULT_BACKGROUND_FILE,
159};
160use crate::config::Config;
161use crate::config_io::{ConfigLayer, ConfigResolver, DirectoryContext};
162use crate::input::actions::action_to_events as convert_action_to_events;
163use crate::input::buffer_mode::ModeRegistry;
164use crate::input::command_registry::CommandRegistry;
165use crate::input::commands::Suggestion;
166use crate::input::keybindings::{Action, KeyContext, KeybindingResolver};
167use crate::input::position_history::PositionHistory;
168use crate::input::quick_open::{
169 FileProvider, GotoLineProvider, QuickOpenContext, QuickOpenProvider, QuickOpenRegistry,
170};
171use crate::model::cursor::Cursors;
172use crate::model::event::{Event, EventLog, LeafId, SplitDirection, SplitId};
173use crate::model::filesystem::FileSystem;
174use crate::services::async_bridge::{AsyncBridge, AsyncMessage};
175use crate::services::fs::FsManager;
176use crate::services::lsp::manager::LspManager;
177use crate::services::plugins::PluginManager;
178use crate::services::recovery::{RecoveryConfig, RecoveryService};
179use crate::services::time_source::{RealTimeSource, SharedTimeSource};
180use crate::state::EditorState;
181use crate::types::{LspLanguageConfig, LspServerConfig, ProcessLimits};
182use crate::view::file_tree::{FileTree, FileTreeView};
183use crate::view::prompt::{Prompt, PromptType};
184use crate::view::scroll_sync::ScrollSyncManager;
185use crate::view::split::{SplitManager, SplitViewState};
186use crate::view::ui::{
187 FileExplorerRenderer, SplitRenderer, StatusBarRenderer, SuggestionsRenderer,
188};
189use crossterm::event::{KeyCode, KeyModifiers};
190#[cfg(feature = "plugins")]
191use fresh_core::api::BufferSavedDiff;
192#[cfg(feature = "plugins")]
193use fresh_core::api::JsCallbackId;
194use fresh_core::api::PluginCommand;
195use lsp_types::{Position, Range as LspRange, TextDocumentContentChangeEvent};
196use ratatui::{
197 layout::{Constraint, Direction, Layout},
198 Frame,
199};
200use std::collections::{HashMap, HashSet};
201use std::ops::Range;
202use std::path::{Path, PathBuf};
203use std::sync::{Arc, RwLock};
204use std::time::Instant;
205
206pub use self::types::{BufferKind, BufferMetadata, HoverTarget};
208pub use self::warning_domains::{
209 GeneralWarningDomain, LspWarningDomain, WarningAction, WarningActionId, WarningDomain,
210 WarningDomainRegistry, WarningLevel, WarningPopupContent,
211};
212pub use crate::model::event::BufferId;
213
214fn uri_to_path(uri: &lsp_types::Uri) -> Result<PathBuf, String> {
216 fresh_core::file_uri::lsp_uri_to_path(uri).ok_or_else(|| "URI is not a file path".to_string())
217}
218
219#[derive(Clone, Debug)]
221pub struct PendingGrammar {
222 pub language: String,
224 pub grammar_path: String,
226 pub extensions: Vec<String>,
228}
229
230#[derive(Clone, Debug)]
232struct SemanticTokenRangeRequest {
233 buffer_id: BufferId,
234 version: u64,
235 range: Range<usize>,
236 start_line: usize,
237 end_line: usize,
238}
239
240#[derive(Clone, Copy, Debug)]
241enum SemanticTokensFullRequestKind {
242 Full,
243 FullDelta,
244}
245
246#[derive(Clone, Debug)]
247struct SemanticTokenFullRequest {
248 buffer_id: BufferId,
249 version: u64,
250 kind: SemanticTokensFullRequestKind,
251}
252
253#[derive(Clone, Debug)]
254struct FoldingRangeRequest {
255 buffer_id: BufferId,
256 version: u64,
257}
258
259#[derive(Debug, Clone)]
265pub struct DabbrevCycleState {
266 pub original_prefix: String,
268 pub word_start: usize,
270 pub candidates: Vec<String>,
272 pub index: usize,
274}
275
276pub struct Editor {
278 buffers: HashMap<BufferId, EditorState>,
280
281 event_logs: HashMap<BufferId, EventLog>,
286
287 next_buffer_id: usize,
289
290 config: Config,
292
293 user_config_raw: serde_json::Value,
295
296 dir_context: DirectoryContext,
298
299 grammar_registry: std::sync::Arc<crate::primitives::grammar::GrammarRegistry>,
301
302 pending_grammars: Vec<PendingGrammar>,
304
305 grammar_reload_pending: bool,
309
310 grammar_build_in_progress: bool,
313
314 needs_full_grammar_build: bool,
318
319 streaming_grep_cancellation: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
321
322 pending_grammar_callbacks: Vec<fresh_core::api::JsCallbackId>,
326
327 theme: crate::view::theme::Theme,
329
330 theme_registry: crate::view::theme::ThemeRegistry,
332
333 theme_cache: Arc<RwLock<HashMap<String, serde_json::Value>>>,
335
336 ansi_background: Option<crate::primitives::ansi_background::AnsiBackground>,
338
339 ansi_background_path: Option<PathBuf>,
341
342 background_fade: f32,
344
345 keybindings: KeybindingResolver,
347
348 clipboard: crate::services::clipboard::Clipboard,
350
351 should_quit: bool,
353
354 should_detach: bool,
356
357 session_mode: bool,
359
360 software_cursor_only: bool,
362
363 session_name: Option<String>,
365
366 pending_escape_sequences: Vec<u8>,
369
370 restart_with_dir: Option<PathBuf>,
373
374 status_message: Option<String>,
376
377 plugin_status_message: Option<String>,
379
380 plugin_errors: Vec<String>,
383
384 prompt: Option<Prompt>,
386
387 terminal_width: u16,
389 terminal_height: u16,
390
391 lsp: Option<LspManager>,
393
394 buffer_metadata: HashMap<BufferId, BufferMetadata>,
396
397 mode_registry: ModeRegistry,
399
400 tokio_runtime: Option<tokio::runtime::Runtime>,
402
403 async_bridge: Option<AsyncBridge>,
405
406 split_manager: SplitManager,
408
409 split_view_states: HashMap<LeafId, SplitViewState>,
413
414 previous_viewports: HashMap<LeafId, (usize, u16, u16)>,
418
419 scroll_sync_manager: ScrollSyncManager,
422
423 file_explorer: Option<FileTreeView>,
425
426 fs_manager: Arc<FsManager>,
428
429 filesystem: Arc<dyn FileSystem + Send + Sync>,
431
432 local_filesystem: Arc<dyn FileSystem + Send + Sync>,
435
436 process_spawner: Arc<dyn crate::services::remote::ProcessSpawner>,
438
439 file_explorer_visible: bool,
441
442 file_explorer_sync_in_progress: bool,
445
446 file_explorer_width_percent: f32,
449
450 pending_file_explorer_show_hidden: Option<bool>,
452
453 pending_file_explorer_show_gitignored: Option<bool>,
455
456 file_explorer_decorations: HashMap<String, Vec<crate::view::file_tree::FileExplorerDecoration>>,
458
459 file_explorer_decoration_cache: crate::view::file_tree::FileExplorerDecorationCache,
461
462 menu_bar_visible: bool,
464
465 menu_bar_auto_shown: bool,
468
469 tab_bar_visible: bool,
471
472 status_bar_visible: bool,
474
475 prompt_line_visible: bool,
477
478 mouse_enabled: bool,
480
481 same_buffer_scroll_sync: bool,
483
484 mouse_cursor_position: Option<(u16, u16)>,
488
489 gpm_active: bool,
491
492 key_context: KeyContext,
494
495 menu_state: crate::view::ui::MenuState,
497
498 menus: crate::config::MenuConfig,
500
501 working_dir: PathBuf,
503
504 pub position_history: PositionHistory,
506
507 in_navigation: bool,
509
510 next_lsp_request_id: u64,
512
513 pending_completion_requests: HashSet<u64>,
515
516 completion_items: Option<Vec<lsp_types::CompletionItem>>,
519
520 scheduled_completion_trigger: Option<Instant>,
523
524 completion_service: crate::services::completion::CompletionService,
527
528 dabbrev_state: Option<DabbrevCycleState>,
532
533 pending_goto_definition_request: Option<u64>,
535
536 pending_hover_request: Option<u64>,
538
539 pending_references_request: Option<u64>,
541
542 pending_references_symbol: String,
544
545 pending_signature_help_request: Option<u64>,
547
548 pending_code_actions_requests: HashSet<u64>,
550
551 pending_code_actions_server_names: HashMap<u64, String>,
553
554 pending_code_actions: Option<Vec<(String, lsp_types::CodeActionOrCommand)>>,
558
559 pending_inlay_hints_request: Option<u64>,
561
562 pending_folding_range_requests: HashMap<u64, FoldingRangeRequest>,
564
565 folding_ranges_in_flight: HashMap<BufferId, (u64, u64)>,
567
568 folding_ranges_debounce: HashMap<BufferId, Instant>,
570
571 pending_semantic_token_requests: HashMap<u64, SemanticTokenFullRequest>,
573
574 semantic_tokens_in_flight: HashMap<BufferId, (u64, u64, SemanticTokensFullRequestKind)>,
576
577 pending_semantic_token_range_requests: HashMap<u64, SemanticTokenRangeRequest>,
579
580 semantic_tokens_range_in_flight: HashMap<BufferId, (u64, usize, usize, u64)>,
582
583 semantic_tokens_range_last_request: HashMap<BufferId, (usize, usize, u64, Instant)>,
585
586 semantic_tokens_range_applied: HashMap<BufferId, (usize, usize, u64)>,
588
589 semantic_tokens_full_debounce: HashMap<BufferId, Instant>,
591
592 hover_symbol_range: Option<(usize, usize)>,
595
596 hover_symbol_overlay: Option<crate::view::overlay::OverlayHandle>,
598
599 mouse_hover_screen_position: Option<(u16, u16)>,
602
603 search_state: Option<SearchState>,
605
606 search_namespace: crate::view::overlay::OverlayNamespace,
608
609 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace,
611
612 pending_search_range: Option<Range<usize>>,
614
615 interactive_replace_state: Option<InteractiveReplaceState>,
617
618 lsp_status: String,
620
621 mouse_state: MouseState,
623
624 tab_context_menu: Option<TabContextMenu>,
626
627 theme_info_popup: Option<types::ThemeInfoPopup>,
629
630 pub(crate) cached_layout: CachedLayout,
632
633 command_registry: Arc<RwLock<CommandRegistry>>,
635
636 #[allow(dead_code)]
639 quick_open_registry: QuickOpenRegistry,
640
641 file_provider: Arc<FileProvider>,
643
644 plugin_manager: PluginManager,
646
647 plugin_dev_workspaces:
651 HashMap<BufferId, crate::services::plugins::plugin_dev_workspace::PluginDevWorkspace>,
652
653 seen_byte_ranges: HashMap<BufferId, std::collections::HashSet<(usize, usize)>>,
657
658 panel_ids: HashMap<String, BufferId>,
661
662 background_process_handles: HashMap<u64, tokio::task::AbortHandle>,
665
666 prompt_histories: HashMap<String, crate::input::input_history::InputHistory>,
669
670 pending_async_prompt_callback: Option<fresh_core::api::JsCallbackId>,
674
675 lsp_progress: std::collections::HashMap<String, LspProgressInfo>,
677
678 lsp_server_statuses:
680 std::collections::HashMap<(String, String), crate::services::async_bridge::LspServerStatus>,
681
682 lsp_window_messages: Vec<LspMessageEntry>,
684
685 lsp_log_messages: Vec<LspMessageEntry>,
687
688 diagnostic_result_ids: HashMap<String, String>,
691
692 scheduled_diagnostic_pull: Option<(BufferId, Instant)>,
695
696 stored_push_diagnostics: HashMap<String, HashMap<String, Vec<lsp_types::Diagnostic>>>,
699
700 stored_pull_diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
702
703 stored_diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
705
706 stored_folding_ranges: HashMap<String, Vec<lsp_types::FoldingRange>>,
709
710 event_broadcaster: crate::model::control_event::EventBroadcaster,
712
713 bookmarks: HashMap<char, Bookmark>,
715
716 search_case_sensitive: bool,
718 search_whole_word: bool,
719 search_use_regex: bool,
720 search_confirm_each: bool,
722
723 macros: HashMap<char, Vec<Action>>,
725
726 macro_recording: Option<MacroRecordingState>,
728
729 last_macro_register: Option<char>,
731
732 macro_playing: bool,
734
735 #[cfg(feature = "plugins")]
737 pending_plugin_actions: Vec<(
738 String,
739 crate::services::plugins::thread::oneshot::Receiver<anyhow::Result<()>>,
740 )>,
741
742 #[cfg(feature = "plugins")]
744 plugin_render_requested: bool,
745
746 chord_state: Vec<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)>,
749
750 pending_lsp_confirmation: Option<String>,
753
754 pending_close_buffer: Option<BufferId>,
757
758 auto_revert_enabled: bool,
760
761 last_auto_revert_poll: std::time::Instant,
763
764 last_file_tree_poll: std::time::Instant,
766
767 git_index_resolved: bool,
769
770 file_mod_times: HashMap<PathBuf, std::time::SystemTime>,
773
774 dir_mod_times: HashMap<PathBuf, std::time::SystemTime>,
777
778 file_rapid_change_counts: HashMap<PathBuf, (std::time::Instant, u32)>,
781
782 file_open_state: Option<file_open::FileOpenState>,
784
785 file_browser_layout: Option<crate::view::ui::FileBrowserLayout>,
787
788 recovery_service: RecoveryService,
790
791 full_redraw_requested: bool,
793
794 time_source: SharedTimeSource,
796
797 last_auto_recovery_save: std::time::Instant,
799
800 last_persistent_auto_save: std::time::Instant,
802
803 active_custom_contexts: HashSet<String>,
806
807 plugin_global_state: HashMap<String, HashMap<String, serde_json::Value>>,
810
811 editor_mode: Option<String>,
814
815 warning_log: Option<(std::sync::mpsc::Receiver<()>, PathBuf)>,
817
818 status_log_path: Option<PathBuf>,
820
821 warning_domains: WarningDomainRegistry,
824
825 update_checker: Option<crate::services::release_checker::PeriodicUpdateChecker>,
827
828 terminal_manager: crate::services::terminal::TerminalManager,
830
831 terminal_buffers: HashMap<BufferId, crate::services::terminal::TerminalId>,
833
834 terminal_backing_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
836
837 terminal_log_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
839
840 terminal_mode: bool,
842
843 keyboard_capture: bool,
847
848 terminal_mode_resume: std::collections::HashSet<BufferId>,
852
853 previous_click_time: Option<std::time::Instant>,
855
856 previous_click_position: Option<(u16, u16)>,
859
860 click_count: u8,
862
863 pub(crate) settings_state: Option<crate::view::settings::SettingsState>,
865
866 pub(crate) calibration_wizard: Option<calibration_wizard::CalibrationWizard>,
868
869 pub(crate) event_debug: Option<event_debug::EventDebug>,
871
872 pub(crate) keybinding_editor: Option<keybinding_editor::KeybindingEditor>,
874
875 pub(crate) key_translator: crate::input::key_translator::KeyTranslator,
877
878 color_capability: crate::view::color_support::ColorCapability,
880
881 review_hunks: Vec<fresh_core::api::ReviewHunk>,
883
884 active_action_popup: Option<(String, Vec<(String, String)>)>,
887
888 composite_buffers: HashMap<BufferId, crate::model::composite_buffer::CompositeBuffer>,
891
892 composite_view_states:
895 HashMap<(LeafId, BufferId), crate::view::composite_view::CompositeViewState>,
896
897 pending_file_opens: Vec<PendingFileOpen>,
901
902 pending_hot_exit_recovery: bool,
904
905 wait_tracking: HashMap<BufferId, (u64, bool)>,
907 completed_waits: Vec<u64>,
909
910 stdin_streaming: Option<StdinStreamingState>,
912
913 line_scan_state: Option<LineScanState>,
915
916 search_scan_state: Option<SearchScanState>,
918
919 search_overlay_top_byte: Option<usize>,
922}
923
924#[derive(Debug, Clone)]
926pub struct PendingFileOpen {
927 pub path: PathBuf,
929 pub line: Option<usize>,
931 pub column: Option<usize>,
933 pub end_line: Option<usize>,
935 pub end_column: Option<usize>,
937 pub message: Option<String>,
939 pub wait_id: Option<u64>,
941}
942
943#[allow(dead_code)] struct SearchScanState {
949 buffer_id: BufferId,
950 leaves: Vec<crate::model::piece_tree::LeafData>,
952 scan: crate::model::buffer::ChunkedSearchState,
954 query: String,
956 search_range: Option<std::ops::Range<usize>>,
958 case_sensitive: bool,
960 whole_word: bool,
961 use_regex: bool,
962}
963
964struct LineScanState {
966 buffer_id: BufferId,
967 leaves: Vec<crate::model::piece_tree::LeafData>,
969 chunks: Vec<crate::model::buffer::LineScanChunk>,
971 next_chunk: usize,
972 total_bytes: usize,
973 scanned_bytes: usize,
974 updates: Vec<(usize, usize)>,
976 open_goto_line_on_complete: bool,
979}
980
981pub struct StdinStreamingState {
983 pub temp_path: PathBuf,
985 pub buffer_id: BufferId,
987 pub last_known_size: usize,
989 pub complete: bool,
991 pub thread_handle: Option<std::thread::JoinHandle<anyhow::Result<()>>>,
993}
994
995impl Editor {
996 pub fn new(
999 config: Config,
1000 width: u16,
1001 height: u16,
1002 dir_context: DirectoryContext,
1003 color_capability: crate::view::color_support::ColorCapability,
1004 filesystem: Arc<dyn FileSystem + Send + Sync>,
1005 ) -> AnyhowResult<Self> {
1006 Self::with_working_dir(
1007 config,
1008 width,
1009 height,
1010 None,
1011 dir_context,
1012 true,
1013 color_capability,
1014 filesystem,
1015 )
1016 }
1017
1018 #[allow(clippy::too_many_arguments)]
1021 pub fn with_working_dir(
1022 config: Config,
1023 width: u16,
1024 height: u16,
1025 working_dir: Option<PathBuf>,
1026 dir_context: DirectoryContext,
1027 plugins_enabled: bool,
1028 color_capability: crate::view::color_support::ColorCapability,
1029 filesystem: Arc<dyn FileSystem + Send + Sync>,
1030 ) -> AnyhowResult<Self> {
1031 tracing::info!("Building default grammar registry...");
1032 let start = std::time::Instant::now();
1033 let grammar_registry = crate::primitives::grammar::GrammarRegistry::defaults_only();
1034 tracing::info!("Default grammar registry built in {:?}", start.elapsed());
1035 Self::with_options(
1039 config,
1040 width,
1041 height,
1042 working_dir,
1043 filesystem,
1044 plugins_enabled,
1045 dir_context,
1046 None,
1047 color_capability,
1048 grammar_registry,
1049 )
1050 }
1051
1052 #[allow(clippy::too_many_arguments)]
1057 pub fn for_test(
1058 config: Config,
1059 width: u16,
1060 height: u16,
1061 working_dir: Option<PathBuf>,
1062 dir_context: DirectoryContext,
1063 color_capability: crate::view::color_support::ColorCapability,
1064 filesystem: Arc<dyn FileSystem + Send + Sync>,
1065 time_source: Option<SharedTimeSource>,
1066 grammar_registry: Option<Arc<crate::primitives::grammar::GrammarRegistry>>,
1067 ) -> AnyhowResult<Self> {
1068 let grammar_registry =
1069 grammar_registry.unwrap_or_else(crate::primitives::grammar::GrammarRegistry::empty);
1070 let mut editor = Self::with_options(
1071 config,
1072 width,
1073 height,
1074 working_dir,
1075 filesystem,
1076 true,
1077 dir_context,
1078 time_source,
1079 color_capability,
1080 grammar_registry,
1081 )?;
1082 editor.needs_full_grammar_build = false;
1085 Ok(editor)
1086 }
1087
1088 #[allow(clippy::too_many_arguments)]
1092 fn with_options(
1093 mut config: Config,
1094 width: u16,
1095 height: u16,
1096 working_dir: Option<PathBuf>,
1097 filesystem: Arc<dyn FileSystem + Send + Sync>,
1098 enable_plugins: bool,
1099 dir_context: DirectoryContext,
1100 time_source: Option<SharedTimeSource>,
1101 color_capability: crate::view::color_support::ColorCapability,
1102 grammar_registry: Arc<crate::primitives::grammar::GrammarRegistry>,
1103 ) -> AnyhowResult<Self> {
1104 let time_source = time_source.unwrap_or_else(RealTimeSource::shared);
1106 tracing::info!("Editor::new called with width={}, height={}", width, height);
1107
1108 let working_dir = working_dir
1110 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
1111
1112 let working_dir = working_dir.canonicalize().unwrap_or(working_dir);
1115
1116 tracing::info!("Loading themes...");
1118 let theme_loader = crate::view::theme::ThemeLoader::new(dir_context.themes_dir());
1119 let scan_result =
1123 crate::services::packages::scan_installed_packages(&dir_context.config_dir);
1124
1125 for (lang_id, lang_config) in &scan_result.language_configs {
1127 config
1128 .languages
1129 .entry(lang_id.clone())
1130 .or_insert_with(|| lang_config.clone());
1131 }
1132
1133 for (lang_id, lsp_config) in &scan_result.lsp_configs {
1135 config
1136 .lsp
1137 .entry(lang_id.clone())
1138 .or_insert_with(|| LspLanguageConfig::Multi(vec![lsp_config.clone()]));
1139 }
1140
1141 let theme_registry = theme_loader.load_all(&scan_result.bundle_theme_dirs);
1142 tracing::info!("Themes loaded");
1143
1144 let theme = theme_registry.get_cloned(&config.theme).unwrap_or_else(|| {
1146 tracing::warn!(
1147 "Theme '{}' not found, falling back to default theme",
1148 config.theme.0
1149 );
1150 theme_registry
1151 .get_cloned(&crate::config::ThemeName(
1152 crate::view::theme::THEME_HIGH_CONTRAST.to_string(),
1153 ))
1154 .expect("Default theme must exist")
1155 });
1156
1157 theme.set_terminal_cursor_color();
1159
1160 let keybindings = KeybindingResolver::new(&config);
1161
1162 let mut buffers = HashMap::new();
1164 let mut event_logs = HashMap::new();
1165
1166 let buffer_id = BufferId(1);
1171 let mut state = EditorState::new(
1172 width,
1173 height,
1174 config.editor.large_file_threshold_bytes as usize,
1175 Arc::clone(&filesystem),
1176 );
1177 state
1179 .margins
1180 .configure_for_line_numbers(config.editor.line_numbers);
1181 state.buffer_settings.tab_size = config.editor.tab_size;
1182 state.buffer_settings.auto_close = config.editor.auto_close;
1183 tracing::info!("EditorState created for buffer {:?}", buffer_id);
1185 buffers.insert(buffer_id, state);
1186 event_logs.insert(buffer_id, EventLog::new());
1187
1188 let mut buffer_metadata = HashMap::new();
1190 buffer_metadata.insert(buffer_id, BufferMetadata::new());
1191
1192 let root_uri = types::file_path_to_lsp_uri(&working_dir);
1194
1195 let tokio_runtime = tokio::runtime::Builder::new_multi_thread()
1197 .worker_threads(2) .thread_name("editor-async")
1199 .enable_all()
1200 .build()
1201 .ok();
1202
1203 let async_bridge = AsyncBridge::new();
1205
1206 if tokio_runtime.is_none() {
1207 tracing::warn!("Failed to create Tokio runtime - async features disabled");
1208 }
1209
1210 let mut lsp = LspManager::new(root_uri);
1212
1213 if let Some(ref runtime) = tokio_runtime {
1215 lsp.set_runtime(runtime.handle().clone(), async_bridge.clone());
1216 }
1217
1218 for (language, lsp_configs) in &config.lsp {
1220 lsp.set_language_configs(language.clone(), lsp_configs.as_slice().to_vec());
1221 }
1222
1223 if working_dir.join("deno.json").exists() || working_dir.join("deno.jsonc").exists() {
1226 tracing::info!("Detected Deno project (deno.json found), using deno lsp for JS/TS");
1227 let deno_config = LspServerConfig {
1228 command: "deno".to_string(),
1229 args: vec!["lsp".to_string()],
1230 enabled: true,
1231 auto_start: false,
1232 process_limits: ProcessLimits::default(),
1233 initialization_options: Some(serde_json::json!({"enable": true})),
1234 ..Default::default()
1235 };
1236 lsp.set_language_config("javascript".to_string(), deno_config.clone());
1237 lsp.set_language_config("typescript".to_string(), deno_config);
1238 }
1239
1240 let split_manager = SplitManager::new(buffer_id);
1242
1243 let mut split_view_states = HashMap::new();
1245 let initial_split_id = split_manager.active_split();
1246 let mut initial_view_state = SplitViewState::with_buffer(width, height, buffer_id);
1247 initial_view_state.apply_config_defaults(
1248 config.editor.line_numbers,
1249 config.editor.highlight_current_line,
1250 config.editor.line_wrap,
1251 config.editor.wrap_indent,
1252 config.editor.wrap_column,
1253 config.editor.rulers.clone(),
1254 );
1255 split_view_states.insert(initial_split_id, initial_view_state);
1256
1257 let fs_manager = Arc::new(FsManager::new(Arc::clone(&filesystem)));
1259
1260 let command_registry = Arc::new(RwLock::new(CommandRegistry::new()));
1262
1263 let file_provider = Arc::new(FileProvider::new());
1265
1266 let mut quick_open_registry = QuickOpenRegistry::new();
1268 quick_open_registry.register(Box::new(GotoLineProvider::new()));
1269 let theme_cache = Arc::new(RwLock::new(theme_registry.to_json_map()));
1274
1275 let plugin_manager = PluginManager::new(
1277 enable_plugins,
1278 Arc::clone(&command_registry),
1279 dir_context.clone(),
1280 Arc::clone(&theme_cache),
1281 );
1282
1283 #[cfg(feature = "plugins")]
1286 if let Some(snapshot_handle) = plugin_manager.state_snapshot_handle() {
1287 let mut snapshot = snapshot_handle.write().unwrap();
1288 snapshot.working_dir = working_dir.clone();
1289 }
1290
1291 if plugin_manager.is_active() {
1298 let mut plugin_dirs: Vec<std::path::PathBuf> = vec![];
1299
1300 if let Ok(exe_path) = std::env::current_exe() {
1302 if let Some(exe_dir) = exe_path.parent() {
1303 let exe_plugin_dir = exe_dir.join("plugins");
1304 if exe_plugin_dir.exists() {
1305 plugin_dirs.push(exe_plugin_dir);
1306 }
1307 }
1308 }
1309
1310 let working_plugin_dir = working_dir.join("plugins");
1312 if working_plugin_dir.exists() && !plugin_dirs.contains(&working_plugin_dir) {
1313 plugin_dirs.push(working_plugin_dir);
1314 }
1315
1316 #[cfg(feature = "embed-plugins")]
1318 if plugin_dirs.is_empty() {
1319 if let Some(embedded_dir) =
1320 crate::services::plugins::embedded::get_embedded_plugins_dir()
1321 {
1322 tracing::info!("Using embedded plugins from: {:?}", embedded_dir);
1323 plugin_dirs.push(embedded_dir.clone());
1324 }
1325 }
1326
1327 let user_plugins_dir = dir_context.config_dir.join("plugins");
1329 if user_plugins_dir.exists() && !plugin_dirs.contains(&user_plugins_dir) {
1330 tracing::info!("Found user plugins directory: {:?}", user_plugins_dir);
1331 plugin_dirs.push(user_plugins_dir.clone());
1332 }
1333
1334 let packages_dir = dir_context.config_dir.join("plugins").join("packages");
1336 if packages_dir.exists() {
1337 if let Ok(entries) = std::fs::read_dir(&packages_dir) {
1338 for entry in entries.flatten() {
1339 let path = entry.path();
1340 if path.is_dir() {
1342 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
1343 if !name.starts_with('.') {
1344 tracing::info!("Found package manager plugin: {:?}", path);
1345 plugin_dirs.push(path);
1346 }
1347 }
1348 }
1349 }
1350 }
1351 }
1352
1353 for dir in &scan_result.bundle_plugin_dirs {
1355 tracing::info!("Found bundle plugin directory: {:?}", dir);
1356 plugin_dirs.push(dir.clone());
1357 }
1358
1359 if plugin_dirs.is_empty() {
1360 tracing::debug!(
1361 "No plugins directory found next to executable or in working dir: {:?}",
1362 working_dir
1363 );
1364 }
1365
1366 for plugin_dir in plugin_dirs {
1368 tracing::info!("Loading TypeScript plugins from: {:?}", plugin_dir);
1369 let (errors, discovered_plugins) =
1370 plugin_manager.load_plugins_from_dir_with_config(&plugin_dir, &config.plugins);
1371
1372 for (name, plugin_config) in discovered_plugins {
1375 config.plugins.insert(name, plugin_config);
1376 }
1377
1378 if !errors.is_empty() {
1379 for err in &errors {
1380 tracing::error!("TypeScript plugin load error: {}", err);
1381 }
1382 #[cfg(debug_assertions)]
1384 panic!(
1385 "TypeScript plugin loading failed with {} error(s): {}",
1386 errors.len(),
1387 errors.join("; ")
1388 );
1389 }
1390 }
1391 }
1392
1393 let file_explorer_width = config.file_explorer.width;
1395 let recovery_enabled = config.editor.recovery_enabled;
1396 let check_for_updates = config.check_for_updates;
1397 let show_menu_bar = config.editor.show_menu_bar;
1398 let show_tab_bar = config.editor.show_tab_bar;
1399 let show_status_bar = config.editor.show_status_bar;
1400 let show_prompt_line = config.editor.show_prompt_line;
1401
1402 let update_checker = if check_for_updates {
1404 tracing::debug!("Update checking enabled, starting periodic checker");
1405 Some(
1406 crate::services::release_checker::start_periodic_update_check(
1407 crate::services::release_checker::DEFAULT_RELEASES_URL,
1408 time_source.clone(),
1409 dir_context.data_dir.clone(),
1410 ),
1411 )
1412 } else {
1413 tracing::debug!("Update checking disabled by config");
1414 None
1415 };
1416
1417 let user_config_raw = Config::read_user_config_raw(&working_dir);
1419
1420 let mut editor = Editor {
1421 buffers,
1422 event_logs,
1423 next_buffer_id: 2,
1424 config,
1425 user_config_raw,
1426 dir_context: dir_context.clone(),
1427 grammar_registry,
1428 pending_grammars: scan_result
1429 .additional_grammars
1430 .iter()
1431 .map(|g| PendingGrammar {
1432 language: g.language.clone(),
1433 grammar_path: g.path.to_string_lossy().to_string(),
1434 extensions: g.extensions.clone(),
1435 })
1436 .collect(),
1437 grammar_reload_pending: false,
1438 grammar_build_in_progress: false,
1439 needs_full_grammar_build: true,
1440 streaming_grep_cancellation: None,
1441 pending_grammar_callbacks: Vec::new(),
1442 theme,
1443 theme_registry,
1444 theme_cache,
1445 ansi_background: None,
1446 ansi_background_path: None,
1447 background_fade: crate::primitives::ansi_background::DEFAULT_BACKGROUND_FADE,
1448 keybindings,
1449 clipboard: crate::services::clipboard::Clipboard::new(),
1450 should_quit: false,
1451 should_detach: false,
1452 session_mode: false,
1453 software_cursor_only: false,
1454 session_name: None,
1455 pending_escape_sequences: Vec::new(),
1456 restart_with_dir: None,
1457 status_message: None,
1458 plugin_status_message: None,
1459 plugin_errors: Vec::new(),
1460 prompt: None,
1461 terminal_width: width,
1462 terminal_height: height,
1463 lsp: Some(lsp),
1464 buffer_metadata,
1465 mode_registry: ModeRegistry::new(),
1466 tokio_runtime,
1467 async_bridge: Some(async_bridge),
1468 split_manager,
1469 split_view_states,
1470 previous_viewports: HashMap::new(),
1471 scroll_sync_manager: ScrollSyncManager::new(),
1472 file_explorer: None,
1473 fs_manager,
1474 filesystem,
1475 local_filesystem: Arc::new(crate::model::filesystem::StdFileSystem),
1476 process_spawner: Arc::new(crate::services::remote::LocalProcessSpawner),
1477 file_explorer_visible: false,
1478 file_explorer_sync_in_progress: false,
1479 file_explorer_width_percent: file_explorer_width,
1480 pending_file_explorer_show_hidden: None,
1481 pending_file_explorer_show_gitignored: None,
1482 menu_bar_visible: show_menu_bar,
1483 file_explorer_decorations: HashMap::new(),
1484 file_explorer_decoration_cache:
1485 crate::view::file_tree::FileExplorerDecorationCache::default(),
1486 menu_bar_auto_shown: false,
1487 tab_bar_visible: show_tab_bar,
1488 status_bar_visible: show_status_bar,
1489 prompt_line_visible: show_prompt_line,
1490 mouse_enabled: true,
1491 same_buffer_scroll_sync: false,
1492 mouse_cursor_position: None,
1493 gpm_active: false,
1494 key_context: KeyContext::Normal,
1495 menu_state: crate::view::ui::MenuState::new(dir_context.themes_dir()),
1496 menus: crate::config::MenuConfig::translated(),
1497 working_dir,
1498 position_history: PositionHistory::new(),
1499 in_navigation: false,
1500 next_lsp_request_id: 0,
1501 pending_completion_requests: HashSet::new(),
1502 completion_items: None,
1503 scheduled_completion_trigger: None,
1504 completion_service: crate::services::completion::CompletionService::new(),
1505 dabbrev_state: None,
1506 pending_goto_definition_request: None,
1507 pending_hover_request: None,
1508 pending_references_request: None,
1509 pending_references_symbol: String::new(),
1510 pending_signature_help_request: None,
1511 pending_code_actions_requests: HashSet::new(),
1512 pending_code_actions_server_names: HashMap::new(),
1513 pending_code_actions: None,
1514 pending_inlay_hints_request: None,
1515 pending_folding_range_requests: HashMap::new(),
1516 folding_ranges_in_flight: HashMap::new(),
1517 folding_ranges_debounce: HashMap::new(),
1518 pending_semantic_token_requests: HashMap::new(),
1519 semantic_tokens_in_flight: HashMap::new(),
1520 pending_semantic_token_range_requests: HashMap::new(),
1521 semantic_tokens_range_in_flight: HashMap::new(),
1522 semantic_tokens_range_last_request: HashMap::new(),
1523 semantic_tokens_range_applied: HashMap::new(),
1524 semantic_tokens_full_debounce: HashMap::new(),
1525 hover_symbol_range: None,
1526 hover_symbol_overlay: None,
1527 mouse_hover_screen_position: None,
1528 search_state: None,
1529 search_namespace: crate::view::overlay::OverlayNamespace::from_string(
1530 "search".to_string(),
1531 ),
1532 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace::from_string(
1533 "lsp-diagnostic".to_string(),
1534 ),
1535 pending_search_range: None,
1536 interactive_replace_state: None,
1537 lsp_status: String::new(),
1538 mouse_state: MouseState::default(),
1539 tab_context_menu: None,
1540 theme_info_popup: None,
1541 cached_layout: CachedLayout::default(),
1542 command_registry,
1543 quick_open_registry,
1544 file_provider,
1545 plugin_manager,
1546 plugin_dev_workspaces: HashMap::new(),
1547 seen_byte_ranges: HashMap::new(),
1548 panel_ids: HashMap::new(),
1549 background_process_handles: HashMap::new(),
1550 prompt_histories: {
1551 let mut histories = HashMap::new();
1553 for history_name in ["search", "replace", "goto_line"] {
1554 let path = dir_context.prompt_history_path(history_name);
1555 let history = crate::input::input_history::InputHistory::load_from_file(&path)
1556 .unwrap_or_else(|e| {
1557 tracing::warn!("Failed to load {} history: {}", history_name, e);
1558 crate::input::input_history::InputHistory::new()
1559 });
1560 histories.insert(history_name.to_string(), history);
1561 }
1562 histories
1563 },
1564 pending_async_prompt_callback: None,
1565 lsp_progress: std::collections::HashMap::new(),
1566 lsp_server_statuses: std::collections::HashMap::new(),
1567 lsp_window_messages: Vec::new(),
1568 lsp_log_messages: Vec::new(),
1569 diagnostic_result_ids: HashMap::new(),
1570 scheduled_diagnostic_pull: None,
1571 stored_push_diagnostics: HashMap::new(),
1572 stored_pull_diagnostics: HashMap::new(),
1573 stored_diagnostics: HashMap::new(),
1574 stored_folding_ranges: HashMap::new(),
1575 event_broadcaster: crate::model::control_event::EventBroadcaster::default(),
1576 bookmarks: HashMap::new(),
1577 search_case_sensitive: true,
1578 search_whole_word: false,
1579 search_use_regex: false,
1580 search_confirm_each: false,
1581 macros: HashMap::new(),
1582 macro_recording: None,
1583 last_macro_register: None,
1584 macro_playing: false,
1585 #[cfg(feature = "plugins")]
1586 pending_plugin_actions: Vec::new(),
1587 #[cfg(feature = "plugins")]
1588 plugin_render_requested: false,
1589 chord_state: Vec::new(),
1590 pending_lsp_confirmation: None,
1591 pending_close_buffer: None,
1592 auto_revert_enabled: true,
1593 last_auto_revert_poll: time_source.now(),
1594 last_file_tree_poll: time_source.now(),
1595 git_index_resolved: false,
1596 file_mod_times: HashMap::new(),
1597 dir_mod_times: HashMap::new(),
1598 file_rapid_change_counts: HashMap::new(),
1599 file_open_state: None,
1600 file_browser_layout: None,
1601 recovery_service: {
1602 let recovery_config = RecoveryConfig {
1603 enabled: recovery_enabled,
1604 ..RecoveryConfig::default()
1605 };
1606 RecoveryService::with_config_and_dir(recovery_config, dir_context.recovery_dir())
1607 },
1608 full_redraw_requested: false,
1609 time_source: time_source.clone(),
1610 last_auto_recovery_save: time_source.now(),
1611 last_persistent_auto_save: time_source.now(),
1612 active_custom_contexts: HashSet::new(),
1613 plugin_global_state: HashMap::new(),
1614 editor_mode: None,
1615 warning_log: None,
1616 status_log_path: None,
1617 warning_domains: WarningDomainRegistry::new(),
1618 update_checker,
1619 terminal_manager: crate::services::terminal::TerminalManager::new(),
1620 terminal_buffers: HashMap::new(),
1621 terminal_backing_files: HashMap::new(),
1622 terminal_log_files: HashMap::new(),
1623 terminal_mode: false,
1624 keyboard_capture: false,
1625 terminal_mode_resume: std::collections::HashSet::new(),
1626 previous_click_time: None,
1627 previous_click_position: None,
1628 click_count: 0,
1629 settings_state: None,
1630 calibration_wizard: None,
1631 event_debug: None,
1632 keybinding_editor: None,
1633 key_translator: crate::input::key_translator::KeyTranslator::load_from_config_dir(
1634 &dir_context.config_dir,
1635 )
1636 .unwrap_or_default(),
1637 color_capability,
1638 pending_file_opens: Vec::new(),
1639 pending_hot_exit_recovery: false,
1640 wait_tracking: HashMap::new(),
1641 completed_waits: Vec::new(),
1642 stdin_streaming: None,
1643 line_scan_state: None,
1644 search_scan_state: None,
1645 search_overlay_top_byte: None,
1646 review_hunks: Vec::new(),
1647 active_action_popup: None,
1648 composite_buffers: HashMap::new(),
1649 composite_view_states: HashMap::new(),
1650 };
1651
1652 editor.clipboard.apply_config(&editor.config.clipboard);
1654
1655 #[cfg(feature = "plugins")]
1656 {
1657 editor.update_plugin_state_snapshot();
1658 if editor.plugin_manager.is_active() {
1659 editor.plugin_manager.run_hook(
1660 "editor_initialized",
1661 crate::services::plugins::hooks::HookArgs::EditorInitialized,
1662 );
1663 }
1664 }
1665
1666 Ok(editor)
1667 }
1668
1669 pub fn event_broadcaster(&self) -> &crate::model::control_event::EventBroadcaster {
1671 &self.event_broadcaster
1672 }
1673
1674 fn start_background_grammar_build(
1679 &mut self,
1680 additional: Vec<crate::primitives::grammar::GrammarSpec>,
1681 callback_ids: Vec<fresh_core::api::JsCallbackId>,
1682 ) {
1683 let Some(bridge) = &self.async_bridge else {
1684 return;
1685 };
1686 self.grammar_build_in_progress = true;
1687 let sender = bridge.sender();
1688 let config_dir = self.dir_context.config_dir.clone();
1689 tracing::info!(
1690 "Spawning background grammar build thread ({} plugin grammars)...",
1691 additional.len()
1692 );
1693 std::thread::Builder::new()
1694 .name("grammar-build".to_string())
1695 .spawn(move || {
1696 tracing::info!("[grammar-build] Thread started");
1697 let start = std::time::Instant::now();
1698 let registry = if additional.is_empty() {
1699 crate::primitives::grammar::GrammarRegistry::for_editor(config_dir)
1700 } else {
1701 crate::primitives::grammar::GrammarRegistry::for_editor_with_additional(
1702 config_dir,
1703 &additional,
1704 )
1705 };
1706 tracing::info!("[grammar-build] Complete in {:?}", start.elapsed());
1707 drop(sender.send(
1708 crate::services::async_bridge::AsyncMessage::GrammarRegistryBuilt {
1709 registry,
1710 callback_ids,
1711 },
1712 ));
1713 })
1714 .ok();
1715 }
1716
1717 pub fn async_bridge(&self) -> Option<&AsyncBridge> {
1719 self.async_bridge.as_ref()
1720 }
1721
1722 pub fn config(&self) -> &Config {
1724 &self.config
1725 }
1726
1727 pub fn key_translator(&self) -> &crate::input::key_translator::KeyTranslator {
1729 &self.key_translator
1730 }
1731
1732 pub fn time_source(&self) -> &SharedTimeSource {
1734 &self.time_source
1735 }
1736
1737 pub fn emit_event(&self, name: impl Into<String>, data: serde_json::Value) {
1739 self.event_broadcaster.emit_named(name, data);
1740 }
1741
1742 fn send_plugin_response(&self, response: fresh_core::api::PluginResponse) {
1744 self.plugin_manager.deliver_response(response);
1745 }
1746
1747 fn take_pending_semantic_token_request(
1749 &mut self,
1750 request_id: u64,
1751 ) -> Option<SemanticTokenFullRequest> {
1752 if let Some(request) = self.pending_semantic_token_requests.remove(&request_id) {
1753 self.semantic_tokens_in_flight.remove(&request.buffer_id);
1754 Some(request)
1755 } else {
1756 None
1757 }
1758 }
1759
1760 fn take_pending_semantic_token_range_request(
1762 &mut self,
1763 request_id: u64,
1764 ) -> Option<SemanticTokenRangeRequest> {
1765 if let Some(request) = self
1766 .pending_semantic_token_range_requests
1767 .remove(&request_id)
1768 {
1769 self.semantic_tokens_range_in_flight
1770 .remove(&request.buffer_id);
1771 Some(request)
1772 } else {
1773 None
1774 }
1775 }
1776
1777 pub fn get_all_keybindings(&self) -> Vec<(String, String)> {
1779 self.keybindings.get_all_bindings()
1780 }
1781
1782 pub fn get_keybinding_for_action(&self, action_name: &str) -> Option<String> {
1785 self.keybindings
1786 .find_keybinding_for_action(action_name, self.key_context.clone())
1787 }
1788
1789 pub fn mode_registry_mut(&mut self) -> &mut ModeRegistry {
1791 &mut self.mode_registry
1792 }
1793
1794 pub fn mode_registry(&self) -> &ModeRegistry {
1796 &self.mode_registry
1797 }
1798
1799 #[inline]
1804 pub fn active_buffer(&self) -> BufferId {
1805 self.split_manager
1806 .active_buffer_id()
1807 .expect("Editor always has at least one buffer")
1808 }
1809
1810 pub fn active_buffer_mode(&self) -> Option<&str> {
1812 self.buffer_metadata
1813 .get(&self.active_buffer())
1814 .and_then(|meta| meta.virtual_mode())
1815 }
1816
1817 pub fn is_active_buffer_read_only(&self) -> bool {
1819 if let Some(metadata) = self.buffer_metadata.get(&self.active_buffer()) {
1820 if metadata.read_only {
1821 return true;
1822 }
1823 if let Some(mode_name) = metadata.virtual_mode() {
1825 return self.mode_registry.is_read_only(mode_name);
1826 }
1827 }
1828 false
1829 }
1830
1831 pub fn is_editing_disabled(&self) -> bool {
1834 self.active_state().editing_disabled
1835 }
1836
1837 pub fn mark_buffer_read_only(&mut self, buffer_id: BufferId, read_only: bool) {
1840 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
1841 metadata.read_only = read_only;
1842 }
1843 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1844 state.editing_disabled = read_only;
1845 }
1846 }
1847
1848 pub fn effective_mode(&self) -> Option<&str> {
1854 self.active_buffer_mode().or(self.editor_mode.as_deref())
1855 }
1856
1857 pub fn has_active_lsp_progress(&self) -> bool {
1859 !self.lsp_progress.is_empty()
1860 }
1861
1862 pub fn get_lsp_progress(&self) -> Vec<(String, String, Option<String>)> {
1864 self.lsp_progress
1865 .iter()
1866 .map(|(token, info)| (token.clone(), info.title.clone(), info.message.clone()))
1867 .collect()
1868 }
1869
1870 pub fn is_lsp_server_ready(&self, language: &str) -> bool {
1872 use crate::services::async_bridge::LspServerStatus;
1873 self.lsp_server_statuses.iter().any(|((lang, _), status)| {
1874 lang == language && matches!(status, LspServerStatus::Running)
1875 })
1876 }
1877
1878 pub fn get_lsp_status(&self) -> &str {
1880 &self.lsp_status
1881 }
1882
1883 pub fn get_stored_diagnostics(&self) -> &HashMap<String, Vec<lsp_types::Diagnostic>> {
1886 &self.stored_diagnostics
1887 }
1888
1889 pub fn is_update_available(&self) -> bool {
1891 self.update_checker
1892 .as_ref()
1893 .map(|c| c.is_update_available())
1894 .unwrap_or(false)
1895 }
1896
1897 pub fn latest_version(&self) -> Option<&str> {
1899 self.update_checker
1900 .as_ref()
1901 .and_then(|c| c.latest_version())
1902 }
1903
1904 pub fn get_update_result(
1906 &self,
1907 ) -> Option<&crate::services::release_checker::ReleaseCheckResult> {
1908 self.update_checker
1909 .as_ref()
1910 .and_then(|c| c.get_cached_result())
1911 }
1912
1913 #[doc(hidden)]
1918 pub fn set_update_checker(
1919 &mut self,
1920 checker: crate::services::release_checker::PeriodicUpdateChecker,
1921 ) {
1922 self.update_checker = Some(checker);
1923 }
1924
1925 pub fn set_lsp_config(&mut self, language: String, config: Vec<LspServerConfig>) {
1927 if let Some(ref mut lsp) = self.lsp {
1928 lsp.set_language_configs(language, config);
1929 }
1930 }
1931
1932 pub fn running_lsp_servers(&self) -> Vec<String> {
1934 self.lsp
1935 .as_ref()
1936 .map(|lsp| lsp.running_servers())
1937 .unwrap_or_default()
1938 }
1939
1940 pub fn pending_completion_requests_count(&self) -> usize {
1942 self.pending_completion_requests.len()
1943 }
1944
1945 pub fn completion_items_count(&self) -> usize {
1947 self.completion_items.as_ref().map_or(0, |v| v.len())
1948 }
1949
1950 pub fn initialized_lsp_server_count(&self, language: &str) -> usize {
1952 self.lsp
1953 .as_ref()
1954 .map(|lsp| {
1955 lsp.get_handles(language)
1956 .iter()
1957 .filter(|sh| sh.capabilities.initialized)
1958 .count()
1959 })
1960 .unwrap_or(0)
1961 }
1962
1963 pub fn shutdown_lsp_server(&mut self, language: &str) -> bool {
1967 if let Some(ref mut lsp) = self.lsp {
1968 lsp.shutdown_server(language)
1969 } else {
1970 false
1971 }
1972 }
1973
1974 pub fn enable_event_streaming<P: AsRef<Path>>(&mut self, path: P) -> AnyhowResult<()> {
1976 for event_log in self.event_logs.values_mut() {
1978 event_log.enable_streaming(&path)?;
1979 }
1980 Ok(())
1981 }
1982
1983 pub fn log_keystroke(&mut self, key_code: &str, modifiers: &str) {
1985 if let Some(event_log) = self.event_logs.get_mut(&self.active_buffer()) {
1986 event_log.log_keystroke(key_code, modifiers);
1987 }
1988 }
1989
1990 pub fn set_warning_log(&mut self, receiver: std::sync::mpsc::Receiver<()>, path: PathBuf) {
1995 self.warning_log = Some((receiver, path));
1996 }
1997
1998 pub fn set_status_log_path(&mut self, path: PathBuf) {
2000 self.status_log_path = Some(path);
2001 }
2002
2003 pub fn set_process_spawner(
2006 &mut self,
2007 spawner: Arc<dyn crate::services::remote::ProcessSpawner>,
2008 ) {
2009 self.process_spawner = spawner;
2010 }
2011
2012 pub fn remote_connection_info(&self) -> Option<&str> {
2016 self.filesystem.remote_connection_info()
2017 }
2018
2019 pub fn get_status_log_path(&self) -> Option<&PathBuf> {
2021 self.status_log_path.as_ref()
2022 }
2023
2024 pub fn open_status_log(&mut self) {
2026 if let Some(path) = self.status_log_path.clone() {
2027 match self.open_local_file(&path) {
2029 Ok(buffer_id) => {
2030 self.mark_buffer_read_only(buffer_id, true);
2031 }
2032 Err(e) => {
2033 tracing::error!("Failed to open status log: {}", e);
2034 }
2035 }
2036 } else {
2037 self.set_status_message("Status log not available".to_string());
2038 }
2039 }
2040
2041 pub fn check_warning_log(&mut self) -> bool {
2046 let Some((receiver, path)) = &self.warning_log else {
2047 return false;
2048 };
2049
2050 let mut new_warning_count = 0usize;
2052 while receiver.try_recv().is_ok() {
2053 new_warning_count += 1;
2054 }
2055
2056 if new_warning_count > 0 {
2057 self.warning_domains.general.add_warnings(new_warning_count);
2059 self.warning_domains.general.set_log_path(path.clone());
2060 }
2061
2062 new_warning_count > 0
2063 }
2064
2065 pub fn get_warning_domains(&self) -> &WarningDomainRegistry {
2067 &self.warning_domains
2068 }
2069
2070 pub fn get_warning_log_path(&self) -> Option<&PathBuf> {
2072 self.warning_domains.general.log_path.as_ref()
2073 }
2074
2075 pub fn open_warning_log(&mut self) {
2077 if let Some(path) = self.warning_domains.general.log_path.clone() {
2078 match self.open_local_file(&path) {
2080 Ok(buffer_id) => {
2081 self.mark_buffer_read_only(buffer_id, true);
2082 }
2083 Err(e) => {
2084 tracing::error!("Failed to open warning log: {}", e);
2085 }
2086 }
2087 }
2088 }
2089
2090 pub fn clear_warning_indicator(&mut self) {
2092 self.warning_domains.general.clear();
2093 }
2094
2095 pub fn clear_warnings(&mut self) {
2097 self.warning_domains.general.clear();
2098 self.warning_domains.lsp.clear();
2099 self.status_message = Some("Warnings cleared".to_string());
2100 }
2101
2102 pub fn has_lsp_error(&self) -> bool {
2104 self.warning_domains.lsp.level() == WarningLevel::Error
2105 }
2106
2107 pub fn get_effective_warning_level(&self) -> WarningLevel {
2110 self.warning_domains.lsp.level()
2111 }
2112
2113 pub fn get_general_warning_level(&self) -> WarningLevel {
2115 self.warning_domains.general.level()
2116 }
2117
2118 pub fn get_general_warning_count(&self) -> usize {
2120 self.warning_domains.general.count
2121 }
2122
2123 pub fn update_lsp_warning_domain(&mut self) {
2125 self.warning_domains
2126 .lsp
2127 .update_from_statuses(&self.lsp_server_statuses);
2128 }
2129
2130 pub fn check_mouse_hover_timer(&mut self) -> bool {
2136 if !self.config.editor.mouse_hover_enabled {
2138 return false;
2139 }
2140
2141 let hover_delay = std::time::Duration::from_millis(self.config.editor.mouse_hover_delay_ms);
2142
2143 let hover_info = match self.mouse_state.lsp_hover_state {
2145 Some((byte_pos, start_time, screen_x, screen_y)) => {
2146 if self.mouse_state.lsp_hover_request_sent {
2147 return false; }
2149 if start_time.elapsed() < hover_delay {
2150 return false; }
2152 Some((byte_pos, screen_x, screen_y))
2153 }
2154 None => return false,
2155 };
2156
2157 let Some((byte_pos, screen_x, screen_y)) = hover_info else {
2158 return false;
2159 };
2160
2161 self.mouse_hover_screen_position = Some((screen_x, screen_y));
2163
2164 match self.request_hover_at_position(byte_pos) {
2166 Ok(true) => {
2167 self.mouse_state.lsp_hover_request_sent = true;
2168 true
2169 }
2170 Ok(false) => false, Err(e) => {
2172 tracing::debug!("Failed to request hover: {}", e);
2173 false
2174 }
2175 }
2176 }
2177
2178 pub fn check_semantic_highlight_timer(&self) -> bool {
2183 for state in self.buffers.values() {
2185 if let Some(remaining) = state.reference_highlight_overlay.needs_redraw() {
2186 if remaining.is_zero() {
2187 return true;
2188 }
2189 }
2190 }
2191 false
2192 }
2193
2194 pub fn check_diagnostic_pull_timer(&mut self) -> bool {
2199 let Some((buffer_id, trigger_time)) = self.scheduled_diagnostic_pull else {
2200 return false;
2201 };
2202
2203 if Instant::now() < trigger_time {
2204 return false;
2205 }
2206
2207 self.scheduled_diagnostic_pull = None;
2208
2209 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
2211 return false;
2212 };
2213 let Some(uri) = metadata.file_uri().cloned() else {
2214 return false;
2215 };
2216 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
2217 return false;
2218 };
2219
2220 let Some(lsp) = self.lsp.as_mut() else {
2221 return false;
2222 };
2223 let Some(sh) = lsp.handle_for_feature_mut(&language, crate::types::LspFeature::Diagnostics)
2224 else {
2225 return false;
2226 };
2227 let client = &mut sh.handle;
2228
2229 let request_id = self.next_lsp_request_id;
2230 self.next_lsp_request_id += 1;
2231 let previous_result_id = self.diagnostic_result_ids.get(uri.as_str()).cloned();
2232 if let Err(e) = client.document_diagnostic(request_id, uri.clone(), previous_result_id) {
2233 tracing::debug!(
2234 "Failed to pull diagnostics after edit for {}: {}",
2235 uri.as_str(),
2236 e
2237 );
2238 } else {
2239 tracing::debug!(
2240 "Pulling diagnostics after edit for {} (request_id={})",
2241 uri.as_str(),
2242 request_id
2243 );
2244 }
2245
2246 false }
2248
2249 pub fn check_completion_trigger_timer(&mut self) -> bool {
2255 let Some(trigger_time) = self.scheduled_completion_trigger else {
2257 return false;
2258 };
2259
2260 if Instant::now() < trigger_time {
2262 return false;
2263 }
2264
2265 self.scheduled_completion_trigger = None;
2267
2268 if self.active_state().popups.is_visible() {
2270 return false;
2271 }
2272
2273 self.request_completion();
2275
2276 true
2277 }
2278
2279 fn load_ansi_background(&mut self, input: &str) -> AnyhowResult<()> {
2281 let trimmed = input.trim();
2282
2283 if trimmed.is_empty() {
2284 self.ansi_background = None;
2285 self.ansi_background_path = None;
2286 self.set_status_message(t!("status.background_cleared").to_string());
2287 return Ok(());
2288 }
2289
2290 let input_path = Path::new(trimmed);
2291 let resolved = if input_path.is_absolute() {
2292 input_path.to_path_buf()
2293 } else {
2294 self.working_dir.join(input_path)
2295 };
2296
2297 let canonical = resolved.canonicalize().unwrap_or_else(|_| resolved.clone());
2298
2299 let parsed = crate::primitives::ansi_background::AnsiBackground::from_file(&canonical)?;
2300
2301 self.ansi_background = Some(parsed);
2302 self.ansi_background_path = Some(canonical.clone());
2303 self.set_status_message(
2304 t!(
2305 "view.background_set",
2306 path = canonical.display().to_string()
2307 )
2308 .to_string(),
2309 );
2310
2311 Ok(())
2312 }
2313
2314 fn effective_tabs_width(&self) -> u16 {
2319 if self.file_explorer_visible && self.file_explorer.is_some() {
2320 let editor_percent = 1.0 - self.file_explorer_width_percent;
2322 (self.terminal_width as f32 * editor_percent) as u16
2323 } else {
2324 self.terminal_width
2325 }
2326 }
2327
2328 fn set_active_buffer(&mut self, buffer_id: BufferId) {
2338 if self.active_buffer() == buffer_id {
2339 return; }
2341
2342 self.on_editor_focus_lost();
2344
2345 self.cancel_search_prompt_if_active();
2348
2349 let previous = self.active_buffer();
2351
2352 if self.terminal_mode && self.is_terminal_buffer(previous) {
2354 self.terminal_mode_resume.insert(previous);
2355 self.terminal_mode = false;
2356 self.key_context = crate::input::keybindings::KeyContext::Normal;
2357 }
2358
2359 self.split_manager.set_active_buffer_id(buffer_id);
2361
2362 let active_split = self.split_manager.active_split();
2364 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
2365 view_state.switch_buffer(buffer_id);
2366 view_state.add_buffer(buffer_id);
2367 view_state.push_focus(previous);
2369 }
2370
2371 if self.terminal_mode_resume.contains(&buffer_id) && self.is_terminal_buffer(buffer_id) {
2373 self.terminal_mode = true;
2374 self.key_context = crate::input::keybindings::KeyContext::Terminal;
2375 } else if self.is_terminal_buffer(buffer_id) {
2376 self.sync_terminal_to_buffer(buffer_id);
2379 }
2380
2381 self.ensure_active_tab_visible(active_split, buffer_id, self.effective_tabs_width());
2383
2384 #[cfg(feature = "plugins")]
2390 self.update_plugin_state_snapshot();
2391
2392 self.plugin_manager.run_hook(
2394 "buffer_activated",
2395 crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
2396 );
2397 }
2398
2399 pub(super) fn focus_split(&mut self, split_id: LeafId, buffer_id: BufferId) {
2410 let previous_split = self.split_manager.active_split();
2411 let previous_buffer = self.active_buffer(); let split_changed = previous_split != split_id;
2413
2414 if split_changed {
2415 if self.terminal_mode && self.is_terminal_buffer(previous_buffer) {
2417 self.terminal_mode = false;
2418 self.key_context = crate::input::keybindings::KeyContext::Normal;
2419 }
2420
2421 self.split_manager.set_active_split(split_id);
2423
2424 self.split_manager.set_active_buffer_id(buffer_id);
2426
2427 if self.is_terminal_buffer(buffer_id) {
2429 self.terminal_mode = true;
2430 self.key_context = crate::input::keybindings::KeyContext::Terminal;
2431 } else {
2432 self.key_context = crate::input::keybindings::KeyContext::Normal;
2435 }
2436
2437 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2440 view_state.switch_buffer(buffer_id);
2441 }
2442
2443 if previous_buffer != buffer_id {
2445 self.position_history.commit_pending_movement();
2446 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2447 view_state.add_buffer(buffer_id);
2448 view_state.push_focus(previous_buffer);
2449 }
2450 }
2453 } else {
2454 self.set_active_buffer(buffer_id);
2456 }
2457 }
2458
2459 pub fn active_state(&self) -> &EditorState {
2461 self.buffers.get(&self.active_buffer()).unwrap()
2462 }
2463
2464 pub fn active_state_mut(&mut self) -> &mut EditorState {
2466 self.buffers.get_mut(&self.active_buffer()).unwrap()
2467 }
2468
2469 pub fn active_cursors(&self) -> &Cursors {
2471 let split_id = self.split_manager.active_split();
2472 &self.split_view_states.get(&split_id).unwrap().cursors
2473 }
2474
2475 pub fn active_cursors_mut(&mut self) -> &mut Cursors {
2477 let split_id = self.split_manager.active_split();
2478 &mut self.split_view_states.get_mut(&split_id).unwrap().cursors
2479 }
2480
2481 pub fn set_completion_items(&mut self, items: Vec<lsp_types::CompletionItem>) {
2483 self.completion_items = Some(items);
2484 }
2485
2486 pub fn active_viewport(&self) -> &crate::view::viewport::Viewport {
2488 let active_split = self.split_manager.active_split();
2489 &self.split_view_states.get(&active_split).unwrap().viewport
2490 }
2491
2492 pub fn active_viewport_mut(&mut self) -> &mut crate::view::viewport::Viewport {
2494 let active_split = self.split_manager.active_split();
2495 &mut self
2496 .split_view_states
2497 .get_mut(&active_split)
2498 .unwrap()
2499 .viewport
2500 }
2501
2502 pub fn get_buffer_display_name(&self, buffer_id: BufferId) -> String {
2504 if let Some(composite) = self.composite_buffers.get(&buffer_id) {
2506 return composite.name.clone();
2507 }
2508
2509 self.buffer_metadata
2510 .get(&buffer_id)
2511 .map(|m| m.display_name.clone())
2512 .or_else(|| {
2513 self.buffers.get(&buffer_id).and_then(|state| {
2514 state
2515 .buffer
2516 .file_path()
2517 .and_then(|p| p.file_name())
2518 .and_then(|n| n.to_str())
2519 .map(|s| s.to_string())
2520 })
2521 })
2522 .unwrap_or_else(|| "[No Name]".to_string())
2523 }
2524
2525 pub fn log_and_apply_event(&mut self, event: &Event) {
2537 if let Event::Delete { range, .. } = event {
2539 let displaced = self.active_state().capture_displaced_markers(range);
2540 self.active_event_log_mut().append(event.clone());
2541 if !displaced.is_empty() {
2542 self.active_event_log_mut()
2543 .set_displaced_markers_on_last(displaced);
2544 }
2545 } else {
2546 self.active_event_log_mut().append(event.clone());
2547 }
2548 self.apply_event_to_active_buffer(event);
2549 }
2550
2551 pub fn apply_event_to_active_buffer(&mut self, event: &Event) {
2552 match event {
2555 Event::Scroll { line_offset } => {
2556 self.handle_scroll_event(*line_offset);
2557 return;
2558 }
2559 Event::SetViewport { top_line } => {
2560 self.handle_set_viewport_event(*top_line);
2561 return;
2562 }
2563 Event::Recenter => {
2564 self.handle_recenter_event();
2565 return;
2566 }
2567 _ => {}
2568 }
2569
2570 let lsp_changes = self.collect_lsp_changes(event);
2574
2575 let line_info = self.calculate_event_line_info(event);
2577
2578 {
2581 let split_id = self.split_manager.active_split();
2582 let active_buf = self.active_buffer();
2583 let cursors = &mut self
2584 .split_view_states
2585 .get_mut(&split_id)
2586 .unwrap()
2587 .keyed_states
2588 .get_mut(&active_buf)
2589 .unwrap()
2590 .cursors;
2591 let state = self.buffers.get_mut(&active_buf).unwrap();
2592 state.apply(cursors, event);
2593 }
2594
2595 match event {
2598 Event::Insert { .. } | Event::Delete { .. } | Event::BulkEdit { .. } => {
2599 self.invalidate_layouts_for_buffer(self.active_buffer());
2600 self.schedule_semantic_tokens_full_refresh(self.active_buffer());
2601 self.schedule_folding_ranges_refresh(self.active_buffer());
2602 }
2603 Event::Batch { events, .. } => {
2604 let has_edits = events
2605 .iter()
2606 .any(|e| matches!(e, Event::Insert { .. } | Event::Delete { .. }));
2607 if has_edits {
2608 self.invalidate_layouts_for_buffer(self.active_buffer());
2609 self.schedule_semantic_tokens_full_refresh(self.active_buffer());
2610 self.schedule_folding_ranges_refresh(self.active_buffer());
2611 }
2612 }
2613 _ => {}
2614 }
2615
2616 self.adjust_other_split_cursors_for_event(event);
2618
2619 let in_interactive_replace = self.interactive_replace_state.is_some();
2623
2624 let _ = in_interactive_replace; self.trigger_plugin_hooks_for_event(event, line_info);
2633
2634 if lsp_changes.is_empty() && event.modifies_buffer() {
2640 if let Some(full_text) = self.active_state().buffer.to_string() {
2641 let full_change = vec![TextDocumentContentChangeEvent {
2642 range: None,
2643 range_length: None,
2644 text: full_text,
2645 }];
2646 self.send_lsp_changes_for_buffer(self.active_buffer(), full_change);
2647 }
2648 } else {
2649 self.send_lsp_changes_for_buffer(self.active_buffer(), lsp_changes);
2650 }
2651 }
2652
2653 pub fn apply_events_as_bulk_edit(
2667 &mut self,
2668 events: Vec<Event>,
2669 description: String,
2670 ) -> Option<Event> {
2671 use crate::model::event::CursorId;
2672
2673 let has_buffer_mods = events
2675 .iter()
2676 .any(|e| matches!(e, Event::Insert { .. } | Event::Delete { .. }));
2677
2678 if !has_buffer_mods {
2679 return None;
2681 }
2682
2683 let active_buf = self.active_buffer();
2684 let split_id = self.split_manager.active_split();
2685
2686 let old_cursors: Vec<(CursorId, usize, Option<usize>)> = self
2688 .split_view_states
2689 .get(&split_id)
2690 .unwrap()
2691 .keyed_states
2692 .get(&active_buf)
2693 .unwrap()
2694 .cursors
2695 .iter()
2696 .map(|(id, c)| (id, c.position, c.anchor))
2697 .collect();
2698
2699 let state = self.buffers.get_mut(&active_buf).unwrap();
2700
2701 let old_snapshot = state.buffer.snapshot_buffer_state();
2703
2704 let mut edits: Vec<(usize, usize, String)> = Vec::new();
2708
2709 for event in &events {
2710 match event {
2711 Event::Insert { position, text, .. } => {
2712 edits.push((*position, 0, text.clone()));
2713 }
2714 Event::Delete { range, .. } => {
2715 edits.push((range.start, range.len(), String::new()));
2716 }
2717 _ => {}
2718 }
2719 }
2720
2721 edits.sort_by(|a, b| b.0.cmp(&a.0));
2723
2724 let edit_refs: Vec<(usize, usize, &str)> = edits
2726 .iter()
2727 .map(|(pos, del, text)| (*pos, *del, text.as_str()))
2728 .collect();
2729
2730 let displaced_markers = state.capture_displaced_markers_bulk(&edits);
2732
2733 let _delta = state.buffer.apply_bulk_edits(&edit_refs);
2735
2736 let edit_lengths: Vec<(usize, usize, usize)> = {
2742 let mut lengths: Vec<(usize, usize, usize)> = Vec::new();
2743 for (pos, del_len, text) in &edits {
2744 if let Some(last) = lengths.last_mut() {
2745 if last.0 == *pos {
2746 last.1 += del_len;
2748 last.2 += text.len();
2749 continue;
2750 }
2751 }
2752 lengths.push((*pos, *del_len, text.len()));
2753 }
2754 lengths
2755 };
2756
2757 for &(pos, del_len, ins_len) in &edit_lengths {
2762 if del_len > 0 && ins_len > 0 {
2763 if ins_len > del_len {
2765 state.marker_list.adjust_for_insert(pos, ins_len - del_len);
2766 state.margins.adjust_for_insert(pos, ins_len - del_len);
2767 } else if del_len > ins_len {
2768 state.marker_list.adjust_for_delete(pos, del_len - ins_len);
2769 state.margins.adjust_for_delete(pos, del_len - ins_len);
2770 }
2771 } else if del_len > 0 {
2773 state.marker_list.adjust_for_delete(pos, del_len);
2774 state.margins.adjust_for_delete(pos, del_len);
2775 } else if ins_len > 0 {
2776 state.marker_list.adjust_for_insert(pos, ins_len);
2777 state.margins.adjust_for_insert(pos, ins_len);
2778 }
2779 }
2780
2781 let new_snapshot = state.buffer.snapshot_buffer_state();
2783
2784 let mut new_cursors: Vec<(CursorId, usize, Option<usize>)> = old_cursors.clone();
2787
2788 let mut position_deltas: Vec<(usize, isize)> = Vec::new();
2791 for (pos, del_len, text) in &edits {
2792 let delta = text.len() as isize - *del_len as isize;
2793 position_deltas.push((*pos, delta));
2794 }
2795 position_deltas.sort_by_key(|(pos, _)| *pos);
2796
2797 let calc_shift = |original_pos: usize| -> isize {
2799 let mut shift: isize = 0;
2800 for (edit_pos, delta) in &position_deltas {
2801 if *edit_pos < original_pos {
2802 shift += delta;
2803 }
2804 }
2805 shift
2806 };
2807
2808 for (cursor_id, ref mut pos, ref mut anchor) in &mut new_cursors {
2812 let mut found_move_cursor = false;
2813 let original_pos = *pos;
2815
2816 let insert_at_cursor_pos = events.iter().any(|e| {
2820 matches!(e, Event::Insert { position, cursor_id: c, .. }
2821 if *c == *cursor_id && *position == original_pos)
2822 });
2823
2824 for event in &events {
2826 if let Event::MoveCursor {
2827 cursor_id: event_cursor,
2828 new_position,
2829 new_anchor,
2830 ..
2831 } = event
2832 {
2833 if event_cursor == cursor_id {
2834 let shift = if insert_at_cursor_pos {
2838 calc_shift(original_pos)
2839 } else {
2840 0
2841 };
2842 *pos = (*new_position as isize + shift).max(0) as usize;
2843 *anchor = *new_anchor;
2844 found_move_cursor = true;
2845 }
2846 }
2847 }
2848
2849 if !found_move_cursor {
2851 let mut found_edit = false;
2852 for event in &events {
2853 match event {
2854 Event::Insert {
2855 position,
2856 text,
2857 cursor_id: event_cursor,
2858 } if event_cursor == cursor_id => {
2859 let shift = calc_shift(*position);
2862 let adjusted_pos = (*position as isize + shift).max(0) as usize;
2863 *pos = adjusted_pos.saturating_add(text.len());
2864 *anchor = None;
2865 found_edit = true;
2866 }
2867 Event::Delete {
2868 range,
2869 cursor_id: event_cursor,
2870 ..
2871 } if event_cursor == cursor_id => {
2872 let shift = calc_shift(range.start);
2875 *pos = (range.start as isize + shift).max(0) as usize;
2876 *anchor = None;
2877 found_edit = true;
2878 }
2879 _ => {}
2880 }
2881 }
2882
2883 if !found_edit {
2887 let shift = calc_shift(original_pos);
2888 *pos = (original_pos as isize + shift).max(0) as usize;
2889 }
2890 }
2891 }
2892
2893 {
2895 let cursors = &mut self
2896 .split_view_states
2897 .get_mut(&split_id)
2898 .unwrap()
2899 .keyed_states
2900 .get_mut(&active_buf)
2901 .unwrap()
2902 .cursors;
2903 for (cursor_id, position, anchor) in &new_cursors {
2904 if let Some(cursor) = cursors.get_mut(*cursor_id) {
2905 cursor.position = *position;
2906 cursor.anchor = *anchor;
2907 }
2908 }
2909 }
2910
2911 self.buffers
2913 .get_mut(&active_buf)
2914 .unwrap()
2915 .highlighter
2916 .invalidate_all();
2917
2918 let bulk_edit = Event::BulkEdit {
2920 old_snapshot: Some(old_snapshot),
2921 new_snapshot: Some(new_snapshot),
2922 old_cursors,
2923 new_cursors,
2924 description,
2925 edits: edit_lengths,
2926 displaced_markers,
2927 };
2928
2929 self.invalidate_layouts_for_buffer(self.active_buffer());
2931 self.adjust_other_split_cursors_for_event(&bulk_edit);
2932 let buffer_id = self.active_buffer();
2939 let full_content_change = self
2940 .buffers
2941 .get(&buffer_id)
2942 .and_then(|s| s.buffer.to_string())
2943 .map(|text| {
2944 vec![TextDocumentContentChangeEvent {
2945 range: None,
2946 range_length: None,
2947 text,
2948 }]
2949 })
2950 .unwrap_or_default();
2951 if !full_content_change.is_empty() {
2952 self.send_lsp_changes_for_buffer(buffer_id, full_content_change);
2953 }
2954
2955 Some(bulk_edit)
2956 }
2957
2958 fn trigger_plugin_hooks_for_event(&mut self, event: &Event, line_info: EventLineInfo) {
2961 let buffer_id = self.active_buffer();
2962
2963 let mut cursor_changed_lines = false;
2965 let hook_args = match event {
2966 Event::Insert { position, text, .. } => {
2967 let insert_position = *position;
2968 let insert_len = text.len();
2969
2970 if let Some(seen) = self.seen_byte_ranges.get_mut(&buffer_id) {
2972 let adjusted: std::collections::HashSet<(usize, usize)> = seen
2977 .iter()
2978 .filter_map(|&(start, end)| {
2979 if end <= insert_position {
2980 Some((start, end))
2982 } else if start >= insert_position {
2983 Some((start + insert_len, end + insert_len))
2985 } else {
2986 None
2988 }
2989 })
2990 .collect();
2991 *seen = adjusted;
2992 }
2993
2994 Some((
2995 "after_insert",
2996 crate::services::plugins::hooks::HookArgs::AfterInsert {
2997 buffer_id,
2998 position: *position,
2999 text: text.clone(),
3000 affected_start: insert_position,
3002 affected_end: insert_position + insert_len,
3003 start_line: line_info.start_line,
3005 end_line: line_info.end_line,
3006 lines_added: line_info.line_delta.max(0) as usize,
3007 },
3008 ))
3009 }
3010 Event::Delete {
3011 range,
3012 deleted_text,
3013 ..
3014 } => {
3015 let delete_start = range.start;
3016
3017 let delete_end = range.end;
3019 let delete_len = delete_end - delete_start;
3020 if let Some(seen) = self.seen_byte_ranges.get_mut(&buffer_id) {
3021 let adjusted: std::collections::HashSet<(usize, usize)> = seen
3026 .iter()
3027 .filter_map(|&(start, end)| {
3028 if end <= delete_start {
3029 Some((start, end))
3031 } else if start >= delete_end {
3032 Some((start - delete_len, end - delete_len))
3034 } else {
3035 None
3037 }
3038 })
3039 .collect();
3040 *seen = adjusted;
3041 }
3042
3043 Some((
3044 "after_delete",
3045 crate::services::plugins::hooks::HookArgs::AfterDelete {
3046 buffer_id,
3047 range: range.clone(),
3048 deleted_text: deleted_text.clone(),
3049 affected_start: delete_start,
3051 deleted_len: deleted_text.len(),
3052 start_line: line_info.start_line,
3054 end_line: line_info.end_line,
3055 lines_removed: (-line_info.line_delta).max(0) as usize,
3056 },
3057 ))
3058 }
3059 Event::Batch { events, .. } => {
3060 for e in events {
3064 let sub_line_info = self.calculate_event_line_info(e);
3067 self.trigger_plugin_hooks_for_event(e, sub_line_info);
3068 }
3069 None
3070 }
3071 Event::MoveCursor {
3072 cursor_id,
3073 old_position,
3074 new_position,
3075 ..
3076 } => {
3077 let old_line = self.active_state().buffer.get_line_number(*old_position) + 1;
3079 let line = self.active_state().buffer.get_line_number(*new_position) + 1;
3080 cursor_changed_lines = old_line != line;
3081 let text_props = self
3082 .active_state()
3083 .text_properties
3084 .get_at(*new_position)
3085 .into_iter()
3086 .map(|tp| tp.properties.clone())
3087 .collect();
3088 Some((
3089 "cursor_moved",
3090 crate::services::plugins::hooks::HookArgs::CursorMoved {
3091 buffer_id,
3092 cursor_id: *cursor_id,
3093 old_position: *old_position,
3094 new_position: *new_position,
3095 line,
3096 text_properties: text_props,
3097 },
3098 ))
3099 }
3100 _ => None,
3101 };
3102
3103 if let Some((hook_name, ref args)) = hook_args {
3105 #[cfg(feature = "plugins")]
3109 self.update_plugin_state_snapshot();
3110
3111 self.plugin_manager.run_hook(hook_name, args.clone());
3112 }
3113
3114 if cursor_changed_lines {
3125 self.handle_refresh_lines(buffer_id);
3126 }
3127 }
3128
3129 fn handle_scroll_event(&mut self, line_offset: isize) {
3135 use crate::view::ui::view_pipeline::ViewLineIterator;
3136
3137 let active_split = self.split_manager.active_split();
3138
3139 if let Some(group) = self
3143 .scroll_sync_manager
3144 .find_group_for_split(active_split.into())
3145 {
3146 let left = group.left_split;
3147 let right = group.right_split;
3148 if let Some(vs) = self.split_view_states.get_mut(&LeafId(left)) {
3149 vs.viewport.set_skip_ensure_visible();
3150 }
3151 if let Some(vs) = self.split_view_states.get_mut(&LeafId(right)) {
3152 vs.viewport.set_skip_ensure_visible();
3153 }
3154 }
3156
3157 let sync_group = self
3159 .split_view_states
3160 .get(&active_split)
3161 .and_then(|vs| vs.sync_group);
3162 let splits_to_scroll = if let Some(group_id) = sync_group {
3163 self.split_manager
3164 .get_splits_in_group(group_id, &self.split_view_states)
3165 } else {
3166 vec![active_split]
3167 };
3168
3169 for split_id in splits_to_scroll {
3170 let buffer_id = if let Some(id) = self.split_manager.buffer_for_split(split_id) {
3171 id
3172 } else {
3173 continue;
3174 };
3175 let tab_size = self.config.editor.tab_size;
3176
3177 let view_transform_tokens = self
3179 .split_view_states
3180 .get(&split_id)
3181 .and_then(|vs| vs.view_transform.as_ref())
3182 .map(|vt| vt.tokens.clone());
3183
3184 if let Some(state) = self.buffers.get_mut(&buffer_id) {
3186 let buffer = &mut state.buffer;
3187 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
3188 if let Some(tokens) = view_transform_tokens {
3189 let view_lines: Vec<_> =
3191 ViewLineIterator::new(&tokens, false, false, tab_size, false).collect();
3192 view_state
3193 .viewport
3194 .scroll_view_lines(&view_lines, line_offset);
3195 } else {
3196 if line_offset > 0 {
3198 view_state
3199 .viewport
3200 .scroll_down(buffer, line_offset as usize);
3201 } else {
3202 view_state
3203 .viewport
3204 .scroll_up(buffer, line_offset.unsigned_abs());
3205 }
3206 }
3207 view_state.viewport.set_skip_ensure_visible();
3209 }
3210 }
3211 }
3212 }
3213
3214 fn handle_set_viewport_event(&mut self, top_line: usize) {
3216 let active_split = self.split_manager.active_split();
3217
3218 if self
3221 .scroll_sync_manager
3222 .is_split_synced(active_split.into())
3223 {
3224 if let Some(group) = self
3225 .scroll_sync_manager
3226 .find_group_for_split_mut(active_split.into())
3227 {
3228 let scroll_line = if group.is_left_split(active_split.into()) {
3230 top_line
3231 } else {
3232 group.right_to_left_line(top_line)
3233 };
3234 group.set_scroll_line(scroll_line);
3235 }
3236
3237 if let Some(group) = self
3239 .scroll_sync_manager
3240 .find_group_for_split(active_split.into())
3241 {
3242 let left = group.left_split;
3243 let right = group.right_split;
3244 if let Some(vs) = self.split_view_states.get_mut(&LeafId(left)) {
3245 vs.viewport.set_skip_ensure_visible();
3246 }
3247 if let Some(vs) = self.split_view_states.get_mut(&LeafId(right)) {
3248 vs.viewport.set_skip_ensure_visible();
3249 }
3250 }
3251 return;
3252 }
3253
3254 let sync_group = self
3256 .split_view_states
3257 .get(&active_split)
3258 .and_then(|vs| vs.sync_group);
3259 let splits_to_scroll = if let Some(group_id) = sync_group {
3260 self.split_manager
3261 .get_splits_in_group(group_id, &self.split_view_states)
3262 } else {
3263 vec![active_split]
3264 };
3265
3266 for split_id in splits_to_scroll {
3267 let buffer_id = if let Some(id) = self.split_manager.buffer_for_split(split_id) {
3268 id
3269 } else {
3270 continue;
3271 };
3272
3273 if let Some(state) = self.buffers.get_mut(&buffer_id) {
3274 let buffer = &mut state.buffer;
3275 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
3276 view_state.viewport.scroll_to(buffer, top_line);
3277 view_state.viewport.set_skip_ensure_visible();
3279 }
3280 }
3281 }
3282 }
3283
3284 fn handle_recenter_event(&mut self) {
3286 let active_split = self.split_manager.active_split();
3287
3288 let sync_group = self
3290 .split_view_states
3291 .get(&active_split)
3292 .and_then(|vs| vs.sync_group);
3293 let splits_to_recenter = if let Some(group_id) = sync_group {
3294 self.split_manager
3295 .get_splits_in_group(group_id, &self.split_view_states)
3296 } else {
3297 vec![active_split]
3298 };
3299
3300 for split_id in splits_to_recenter {
3301 let buffer_id = if let Some(id) = self.split_manager.buffer_for_split(split_id) {
3302 id
3303 } else {
3304 continue;
3305 };
3306
3307 if let Some(state) = self.buffers.get_mut(&buffer_id) {
3308 let buffer = &mut state.buffer;
3309 let view_state = self.split_view_states.get_mut(&split_id);
3310
3311 if let Some(view_state) = view_state {
3312 let cursor = *view_state.cursors.primary();
3314 let viewport_height = view_state.viewport.visible_line_count();
3315 let target_rows_from_top = viewport_height / 2;
3316
3317 let mut iter = buffer.line_iterator(cursor.position, 80);
3319 for _ in 0..target_rows_from_top {
3320 if iter.prev().is_none() {
3321 break;
3322 }
3323 }
3324 let new_top_byte = iter.current_position();
3325 view_state.viewport.top_byte = new_top_byte;
3326 view_state.viewport.set_skip_ensure_visible();
3328 }
3329 }
3330 }
3331 }
3332
3333 fn invalidate_layouts_for_buffer(&mut self, buffer_id: BufferId) {
3340 let splits_for_buffer = self.split_manager.splits_for_buffer(buffer_id);
3342
3343 for split_id in splits_for_buffer {
3345 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
3346 view_state.invalidate_layout();
3347 view_state.view_transform = None;
3351 view_state.view_transform_stale = true;
3354 }
3355 }
3356 }
3357
3358 pub fn active_event_log(&self) -> &EventLog {
3360 self.event_logs.get(&self.active_buffer()).unwrap()
3361 }
3362
3363 pub fn active_event_log_mut(&mut self) -> &mut EventLog {
3365 self.event_logs.get_mut(&self.active_buffer()).unwrap()
3366 }
3367
3368 pub(super) fn update_modified_from_event_log(&mut self) {
3372 let is_at_saved = self
3373 .event_logs
3374 .get(&self.active_buffer())
3375 .map(|log| log.is_at_saved_position())
3376 .unwrap_or(false);
3377
3378 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
3379 state.buffer.set_modified(!is_at_saved);
3380 }
3381 }
3382
3383 pub fn should_quit(&self) -> bool {
3385 self.should_quit
3386 }
3387
3388 pub fn should_detach(&self) -> bool {
3390 self.should_detach
3391 }
3392
3393 pub fn clear_detach(&mut self) {
3395 self.should_detach = false;
3396 }
3397
3398 pub fn set_session_mode(&mut self, session_mode: bool) {
3400 self.session_mode = session_mode;
3401 self.clipboard.set_session_mode(session_mode);
3402 if session_mode {
3404 self.active_custom_contexts
3405 .insert(crate::types::context_keys::SESSION_MODE.to_string());
3406 } else {
3407 self.active_custom_contexts
3408 .remove(crate::types::context_keys::SESSION_MODE);
3409 }
3410 }
3411
3412 pub fn is_session_mode(&self) -> bool {
3414 self.session_mode
3415 }
3416
3417 pub fn set_software_cursor_only(&mut self, enabled: bool) {
3420 self.software_cursor_only = enabled;
3421 }
3422
3423 pub fn set_session_name(&mut self, name: Option<String>) {
3429 if let Some(ref session_name) = name {
3430 let base_recovery_dir = self.dir_context.recovery_dir();
3431 let scope = crate::services::recovery::RecoveryScope::Session {
3432 name: session_name.clone(),
3433 };
3434 let recovery_config = RecoveryConfig {
3435 enabled: self.recovery_service.is_enabled(),
3436 ..RecoveryConfig::default()
3437 };
3438 self.recovery_service =
3439 RecoveryService::with_scope(recovery_config, &base_recovery_dir, &scope);
3440 }
3441 self.session_name = name;
3442 }
3443
3444 pub fn session_name(&self) -> Option<&str> {
3446 self.session_name.as_deref()
3447 }
3448
3449 pub fn queue_escape_sequences(&mut self, sequences: &[u8]) {
3451 self.pending_escape_sequences.extend_from_slice(sequences);
3452 }
3453
3454 pub fn take_pending_escape_sequences(&mut self) -> Vec<u8> {
3456 std::mem::take(&mut self.pending_escape_sequences)
3457 }
3458
3459 pub fn take_pending_clipboard(
3461 &mut self,
3462 ) -> Option<crate::services::clipboard::PendingClipboard> {
3463 self.clipboard.take_pending_clipboard()
3464 }
3465
3466 pub fn should_restart(&self) -> bool {
3468 self.restart_with_dir.is_some()
3469 }
3470
3471 pub fn take_restart_dir(&mut self) -> Option<PathBuf> {
3474 self.restart_with_dir.take()
3475 }
3476
3477 pub fn request_full_redraw(&mut self) {
3482 self.full_redraw_requested = true;
3483 }
3484
3485 pub fn take_full_redraw_request(&mut self) -> bool {
3487 let requested = self.full_redraw_requested;
3488 self.full_redraw_requested = false;
3489 requested
3490 }
3491
3492 pub fn request_restart(&mut self, new_working_dir: PathBuf) {
3493 tracing::info!(
3494 "Restart requested with new working directory: {}",
3495 new_working_dir.display()
3496 );
3497 self.restart_with_dir = Some(new_working_dir);
3498 self.should_quit = true;
3500 }
3501
3502 pub fn theme(&self) -> &crate::view::theme::Theme {
3504 &self.theme
3505 }
3506
3507 pub fn is_settings_open(&self) -> bool {
3509 self.settings_state.as_ref().is_some_and(|s| s.visible)
3510 }
3511
3512 pub fn quit(&mut self) {
3514 let modified_count = self.count_modified_buffers_needing_prompt();
3516 if modified_count > 0 {
3517 let save_key = t!("prompt.key.save").to_string();
3518 let cancel_key = t!("prompt.key.cancel").to_string();
3519 let hot_exit = self.config.editor.hot_exit;
3520
3521 let msg = if hot_exit {
3522 let quit_key = t!("prompt.key.quit").to_string();
3524 if modified_count == 1 {
3525 t!(
3526 "prompt.quit_modified_hot_one",
3527 save_key = save_key,
3528 quit_key = quit_key,
3529 cancel_key = cancel_key
3530 )
3531 .to_string()
3532 } else {
3533 t!(
3534 "prompt.quit_modified_hot_many",
3535 count = modified_count,
3536 save_key = save_key,
3537 quit_key = quit_key,
3538 cancel_key = cancel_key
3539 )
3540 .to_string()
3541 }
3542 } else {
3543 let discard_key = t!("prompt.key.discard").to_string();
3545 if modified_count == 1 {
3546 t!(
3547 "prompt.quit_modified_one",
3548 save_key = save_key,
3549 discard_key = discard_key,
3550 cancel_key = cancel_key
3551 )
3552 .to_string()
3553 } else {
3554 t!(
3555 "prompt.quit_modified_many",
3556 count = modified_count,
3557 save_key = save_key,
3558 discard_key = discard_key,
3559 cancel_key = cancel_key
3560 )
3561 .to_string()
3562 }
3563 };
3564 self.start_prompt(msg, PromptType::ConfirmQuitWithModified);
3565 } else {
3566 self.should_quit = true;
3567 }
3568 }
3569
3570 fn count_modified_buffers_needing_prompt(&self) -> usize {
3578 let hot_exit = self.config.editor.hot_exit;
3579 let auto_save = self.config.editor.auto_save_enabled;
3580
3581 self.buffers
3582 .iter()
3583 .filter(|(buffer_id, state)| {
3584 if !state.buffer.is_modified() {
3585 return false;
3586 }
3587 if let Some(meta) = self.buffer_metadata.get(buffer_id) {
3588 if let Some(path) = meta.file_path() {
3589 let is_unnamed = path.as_os_str().is_empty();
3590 if is_unnamed && hot_exit {
3591 return false; }
3593 if !is_unnamed && auto_save {
3594 return false; }
3596 }
3597 }
3598 true
3599 })
3600 .count()
3601 }
3602
3603 pub fn focus_gained(&mut self) {
3605 self.plugin_manager.run_hook(
3606 "focus_gained",
3607 crate::services::plugins::hooks::HookArgs::FocusGained,
3608 );
3609 }
3610
3611 pub fn resize(&mut self, width: u16, height: u16) {
3613 self.terminal_width = width;
3615 self.terminal_height = height;
3616
3617 for view_state in self.split_view_states.values_mut() {
3619 view_state.viewport.resize(width, height);
3620 }
3621
3622 self.resize_visible_terminals();
3624
3625 self.plugin_manager.run_hook(
3627 "resize",
3628 fresh_core::hooks::HookArgs::Resize { width, height },
3629 );
3630 }
3631
3632 pub fn start_prompt(&mut self, message: String, prompt_type: PromptType) {
3636 self.start_prompt_with_suggestions(message, prompt_type, Vec::new());
3637 }
3638
3639 fn start_search_prompt(
3644 &mut self,
3645 message: String,
3646 prompt_type: PromptType,
3647 use_selection_range: bool,
3648 ) {
3649 self.pending_search_range = None;
3651
3652 let selection_range = self.active_cursors().primary().selection_range();
3653
3654 let selected_text = if let Some(range) = selection_range.clone() {
3655 let state = self.active_state_mut();
3656 let text = state.get_text_range(range.start, range.end);
3657 if !text.contains('\n') && !text.is_empty() {
3658 Some(text)
3659 } else {
3660 None
3661 }
3662 } else {
3663 None
3664 };
3665
3666 if use_selection_range {
3667 self.pending_search_range = selection_range;
3668 }
3669
3670 let from_history = selected_text.is_none();
3672 let default_text = selected_text.or_else(|| {
3673 self.get_prompt_history("search")
3674 .and_then(|h| h.last().map(|s| s.to_string()))
3675 });
3676
3677 self.start_prompt(message, prompt_type);
3679
3680 if let Some(text) = default_text {
3682 if let Some(ref mut prompt) = self.prompt {
3683 prompt.set_input(text.clone());
3684 prompt.selection_anchor = Some(0);
3685 prompt.cursor_pos = text.len();
3686 }
3687 if from_history {
3688 self.get_or_create_prompt_history("search").init_at_last();
3689 }
3690 self.update_search_highlights(&text);
3691 }
3692 }
3693
3694 pub fn start_prompt_with_suggestions(
3696 &mut self,
3697 message: String,
3698 prompt_type: PromptType,
3699 suggestions: Vec<Suggestion>,
3700 ) {
3701 self.on_editor_focus_lost();
3703
3704 match prompt_type {
3707 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
3708 self.clear_search_highlights();
3709 }
3710 _ => {}
3711 }
3712
3713 let needs_suggestions = matches!(
3715 prompt_type,
3716 PromptType::OpenFile
3717 | PromptType::SwitchProject
3718 | PromptType::SaveFileAs
3719 | PromptType::Command
3720 );
3721
3722 self.prompt = Some(Prompt::with_suggestions(message, prompt_type, suggestions));
3723
3724 if needs_suggestions {
3726 self.update_prompt_suggestions();
3727 }
3728 }
3729
3730 pub fn start_prompt_with_initial_text(
3732 &mut self,
3733 message: String,
3734 prompt_type: PromptType,
3735 initial_text: String,
3736 ) {
3737 self.on_editor_focus_lost();
3739
3740 self.prompt = Some(Prompt::with_initial_text(
3741 message,
3742 prompt_type,
3743 initial_text,
3744 ));
3745 }
3746
3747 pub fn start_quick_open(&mut self) {
3749 self.on_editor_focus_lost();
3751
3752 self.status_message = None;
3754
3755 let mut prompt = Prompt::with_suggestions(String::new(), PromptType::QuickOpen, vec![]);
3757 prompt.input = ">".to_string();
3758 prompt.cursor_pos = 1;
3759 self.prompt = Some(prompt);
3760
3761 self.update_quick_open_suggestions(">");
3763 }
3764
3765 fn update_quick_open_suggestions(&mut self, input: &str) {
3767 let suggestions = if let Some(query) = input.strip_prefix('>') {
3768 let active_buffer_mode = self
3770 .buffer_metadata
3771 .get(&self.active_buffer())
3772 .and_then(|m| m.virtual_mode());
3773 let has_lsp_config = {
3774 let language = self
3775 .buffers
3776 .get(&self.active_buffer())
3777 .map(|s| s.language.as_str());
3778 language
3779 .and_then(|lang| self.lsp.as_ref().and_then(|lsp| lsp.get_config(lang)))
3780 .is_some()
3781 };
3782 self.command_registry.read().unwrap().filter(
3783 query,
3784 self.key_context.clone(),
3785 &self.keybindings,
3786 self.has_active_selection(),
3787 &self.active_custom_contexts,
3788 active_buffer_mode,
3789 has_lsp_config,
3790 )
3791 } else if let Some(query) = input.strip_prefix('#') {
3792 self.get_buffer_suggestions(query)
3794 } else if let Some(line_str) = input.strip_prefix(':') {
3795 self.get_goto_line_suggestions(line_str)
3797 } else {
3798 let (path_part, _, _) = prompt_actions::parse_path_line_col(input);
3801 let query = if path_part.is_empty() {
3802 input
3803 } else {
3804 &path_part
3805 };
3806 self.get_file_suggestions(query)
3807 };
3808
3809 if let Some(prompt) = &mut self.prompt {
3810 prompt.suggestions = suggestions;
3811 prompt.selected_suggestion = if prompt.suggestions.is_empty() {
3812 None
3813 } else {
3814 Some(0)
3815 };
3816 }
3817 }
3818
3819 fn get_buffer_suggestions(&self, query: &str) -> Vec<Suggestion> {
3821 use crate::input::fuzzy::fuzzy_match;
3822
3823 let mut suggestions: Vec<(Suggestion, i32)> = self
3824 .buffers
3825 .iter()
3826 .filter_map(|(buffer_id, state)| {
3827 let path = state.buffer.file_path()?;
3828 let name = path
3829 .file_name()
3830 .map(|n| n.to_string_lossy().to_string())
3831 .unwrap_or_else(|| format!("Buffer {}", buffer_id.0));
3832
3833 let match_result = if query.is_empty() {
3834 crate::input::fuzzy::FuzzyMatch {
3835 matched: true,
3836 score: 0,
3837 match_positions: vec![],
3838 }
3839 } else {
3840 fuzzy_match(query, &name)
3841 };
3842
3843 if match_result.matched {
3844 let modified = state.buffer.is_modified();
3845 let display_name = if modified {
3846 format!("{} [+]", name)
3847 } else {
3848 name
3849 };
3850
3851 Some((
3852 Suggestion {
3853 text: display_name,
3854 description: Some(path.display().to_string()),
3855 value: Some(buffer_id.0.to_string()),
3856 disabled: false,
3857 keybinding: None,
3858 source: None,
3859 },
3860 match_result.score,
3861 ))
3862 } else {
3863 None
3864 }
3865 })
3866 .collect();
3867
3868 suggestions.sort_by(|a, b| b.1.cmp(&a.1));
3869 suggestions.into_iter().map(|(s, _)| s).collect()
3870 }
3871
3872 fn get_goto_line_suggestions(&self, line_str: &str) -> Vec<Suggestion> {
3874 if line_str.is_empty() {
3875 return vec![Suggestion {
3876 text: t!("quick_open.goto_line_hint").to_string(),
3877 description: Some(t!("quick_open.goto_line_desc").to_string()),
3878 value: None,
3879 disabled: true,
3880 keybinding: None,
3881 source: None,
3882 }];
3883 }
3884
3885 if let Ok(line_num) = line_str.parse::<usize>() {
3886 if line_num > 0 {
3887 return vec![Suggestion {
3888 text: t!("quick_open.goto_line", line = line_num.to_string()).to_string(),
3889 description: Some(t!("quick_open.press_enter").to_string()),
3890 value: Some(line_num.to_string()),
3891 disabled: false,
3892 keybinding: None,
3893 source: None,
3894 }];
3895 }
3896 }
3897
3898 vec![Suggestion {
3899 text: t!("quick_open.invalid_line").to_string(),
3900 description: Some(line_str.to_string()),
3901 value: None,
3902 disabled: true,
3903 keybinding: None,
3904 source: None,
3905 }]
3906 }
3907
3908 fn get_file_suggestions(&self, query: &str) -> Vec<Suggestion> {
3910 let cwd = self.working_dir.display().to_string();
3912 let context = QuickOpenContext {
3913 cwd: cwd.clone(),
3914 open_buffers: vec![], active_buffer_id: self.active_buffer().0,
3916 active_buffer_path: self
3917 .active_state()
3918 .buffer
3919 .file_path()
3920 .map(|p| p.display().to_string()),
3921 has_selection: self.has_active_selection(),
3922 key_context: self.key_context.clone(),
3923 custom_contexts: self.active_custom_contexts.clone(),
3924 buffer_mode: self
3925 .buffer_metadata
3926 .get(&self.active_buffer())
3927 .and_then(|m| m.virtual_mode())
3928 .map(|s| s.to_string()),
3929 has_lsp_config: false, };
3931
3932 self.file_provider.suggestions(query, &context)
3933 }
3934
3935 fn cancel_search_prompt_if_active(&mut self) {
3938 if let Some(ref prompt) = self.prompt {
3939 if matches!(
3940 prompt.prompt_type,
3941 PromptType::Search
3942 | PromptType::ReplaceSearch
3943 | PromptType::Replace { .. }
3944 | PromptType::QueryReplaceSearch
3945 | PromptType::QueryReplace { .. }
3946 | PromptType::QueryReplaceConfirm
3947 ) {
3948 self.prompt = None;
3949 self.interactive_replace_state = None;
3951 let ns = self.search_namespace.clone();
3953 let state = self.active_state_mut();
3954 state.overlays.clear_namespace(&ns, &mut state.marker_list);
3955 }
3956 }
3957 }
3958
3959 fn prefill_open_file_prompt(&mut self) {
3961 if let Some(prompt) = self.prompt.as_mut() {
3965 if prompt.prompt_type == PromptType::OpenFile {
3966 prompt.input.clear();
3967 prompt.cursor_pos = 0;
3968 prompt.selection_anchor = None;
3969 }
3970 }
3971 }
3972
3973 fn init_file_open_state(&mut self) {
3979 let buffer_id = self.active_buffer();
3981
3982 let initial_dir = if self.is_terminal_buffer(buffer_id) {
3985 self.get_terminal_id(buffer_id)
3986 .and_then(|tid| self.terminal_manager.get(tid))
3987 .and_then(|handle| handle.cwd())
3988 .unwrap_or_else(|| self.working_dir.clone())
3989 } else {
3990 self.active_state()
3991 .buffer
3992 .file_path()
3993 .and_then(|path| path.parent())
3994 .map(|p| p.to_path_buf())
3995 .unwrap_or_else(|| self.working_dir.clone())
3996 };
3997
3998 let show_hidden = self.config.file_browser.show_hidden;
4000 self.file_open_state = Some(file_open::FileOpenState::new(
4001 initial_dir.clone(),
4002 show_hidden,
4003 self.filesystem.clone(),
4004 ));
4005
4006 self.load_file_open_directory(initial_dir);
4008 self.load_file_open_shortcuts_async();
4009 }
4010
4011 fn init_folder_open_state(&mut self) {
4016 let initial_dir = self.working_dir.clone();
4018
4019 let show_hidden = self.config.file_browser.show_hidden;
4021 self.file_open_state = Some(file_open::FileOpenState::new(
4022 initial_dir.clone(),
4023 show_hidden,
4024 self.filesystem.clone(),
4025 ));
4026
4027 self.load_file_open_directory(initial_dir);
4029 self.load_file_open_shortcuts_async();
4030 }
4031
4032 pub fn change_working_dir(&mut self, new_path: PathBuf) {
4042 let new_path = new_path.canonicalize().unwrap_or(new_path);
4044
4045 self.request_restart(new_path);
4048 }
4049
4050 fn load_file_open_directory(&mut self, path: PathBuf) {
4052 if let Some(state) = &mut self.file_open_state {
4054 state.current_dir = path.clone();
4055 state.loading = true;
4056 state.error = None;
4057 state.update_shortcuts();
4058 }
4059
4060 if let Some(ref runtime) = self.tokio_runtime {
4062 let fs_manager = self.fs_manager.clone();
4063 let sender = self.async_bridge.as_ref().map(|b| b.sender());
4064
4065 runtime.spawn(async move {
4066 let result = fs_manager.list_dir_with_metadata(path).await;
4067 if let Some(sender) = sender {
4068 #[allow(clippy::let_underscore_must_use)]
4070 let _ = sender.send(AsyncMessage::FileOpenDirectoryLoaded(result));
4071 }
4072 });
4073 } else {
4074 if let Some(state) = &mut self.file_open_state {
4076 state.set_error("Async runtime not available".to_string());
4077 }
4078 }
4079 }
4080
4081 pub(super) fn handle_file_open_directory_loaded(
4083 &mut self,
4084 result: std::io::Result<Vec<crate::services::fs::DirEntry>>,
4085 ) {
4086 match result {
4087 Ok(entries) => {
4088 if let Some(state) = &mut self.file_open_state {
4089 state.set_entries(entries);
4090 }
4091 let filter = self
4093 .prompt
4094 .as_ref()
4095 .map(|p| p.input.clone())
4096 .unwrap_or_default();
4097 if !filter.is_empty() {
4098 if let Some(state) = &mut self.file_open_state {
4099 state.apply_filter(&filter);
4100 }
4101 }
4102 }
4103 Err(e) => {
4104 if let Some(state) = &mut self.file_open_state {
4105 state.set_error(e.to_string());
4106 }
4107 }
4108 }
4109 }
4110
4111 fn load_file_open_shortcuts_async(&mut self) {
4115 if let Some(ref runtime) = self.tokio_runtime {
4116 let filesystem = self.filesystem.clone();
4117 let sender = self.async_bridge.as_ref().map(|b| b.sender());
4118
4119 runtime.spawn(async move {
4120 let shortcuts = tokio::task::spawn_blocking(move || {
4122 file_open::FileOpenState::build_shortcuts_async(&*filesystem)
4123 })
4124 .await
4125 .unwrap_or_default();
4126
4127 if let Some(sender) = sender {
4128 #[allow(clippy::let_underscore_must_use)]
4130 let _ = sender.send(AsyncMessage::FileOpenShortcutsLoaded(shortcuts));
4131 }
4132 });
4133 }
4134 }
4135
4136 pub(super) fn handle_file_open_shortcuts_loaded(
4138 &mut self,
4139 shortcuts: Vec<file_open::NavigationShortcut>,
4140 ) {
4141 if let Some(state) = &mut self.file_open_state {
4142 state.merge_async_shortcuts(shortcuts);
4143 }
4144 }
4145
4146 pub fn cancel_prompt(&mut self) {
4148 let theme_to_restore = if let Some(ref prompt) = self.prompt {
4150 if let PromptType::SelectTheme { original_theme } = &prompt.prompt_type {
4151 Some(original_theme.clone())
4152 } else {
4153 None
4154 }
4155 } else {
4156 None
4157 };
4158
4159 if let Some(ref prompt) = self.prompt {
4161 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
4163 if let Some(history) = self.prompt_histories.get_mut(&key) {
4164 history.reset_navigation();
4165 }
4166 }
4167 match &prompt.prompt_type {
4168 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
4169 self.clear_search_highlights();
4170 }
4171 PromptType::Plugin { custom_type } => {
4172 use crate::services::plugins::hooks::HookArgs;
4174 self.plugin_manager.run_hook(
4175 "prompt_cancelled",
4176 HookArgs::PromptCancelled {
4177 prompt_type: custom_type.clone(),
4178 input: prompt.input.clone(),
4179 },
4180 );
4181 }
4182 PromptType::LspRename { overlay_handle, .. } => {
4183 let remove_overlay_event = crate::model::event::Event::RemoveOverlay {
4185 handle: overlay_handle.clone(),
4186 };
4187 self.apply_event_to_active_buffer(&remove_overlay_event);
4188 }
4189 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs => {
4190 self.file_open_state = None;
4192 self.file_browser_layout = None;
4193 }
4194 PromptType::AsyncPrompt => {
4195 if let Some(callback_id) = self.pending_async_prompt_callback.take() {
4197 self.plugin_manager
4198 .resolve_callback(callback_id, "null".to_string());
4199 }
4200 }
4201 _ => {}
4202 }
4203 }
4204
4205 self.prompt = None;
4206 self.pending_search_range = None;
4207 self.status_message = Some(t!("search.cancelled").to_string());
4208
4209 if let Some(original_theme) = theme_to_restore {
4211 self.preview_theme(&original_theme);
4212 }
4213 }
4214
4215 pub fn handle_prompt_scroll(&mut self, delta: i32) -> bool {
4218 if let Some(ref mut prompt) = self.prompt {
4219 if prompt.suggestions.is_empty() {
4220 return false;
4221 }
4222
4223 let current = prompt.selected_suggestion.unwrap_or(0);
4224 let len = prompt.suggestions.len();
4225
4226 let new_selected = if delta < 0 {
4229 current.saturating_sub((-delta) as usize)
4231 } else {
4232 (current + delta as usize).min(len.saturating_sub(1))
4234 };
4235
4236 prompt.selected_suggestion = Some(new_selected);
4237
4238 if !matches!(prompt.prompt_type, PromptType::Plugin { .. }) {
4240 if let Some(suggestion) = prompt.suggestions.get(new_selected) {
4241 prompt.input = suggestion.get_value().to_string();
4242 prompt.cursor_pos = prompt.input.len();
4243 }
4244 }
4245
4246 return true;
4247 }
4248 false
4249 }
4250
4251 pub fn confirm_prompt(&mut self) -> Option<(String, PromptType, Option<usize>)> {
4256 if let Some(prompt) = self.prompt.take() {
4257 let selected_index = prompt.selected_suggestion;
4258 let mut final_input = if prompt.sync_input_on_navigate {
4260 prompt.input.clone()
4263 } else if matches!(
4264 prompt.prompt_type,
4265 PromptType::Command
4266 | PromptType::OpenFile
4267 | PromptType::SwitchProject
4268 | PromptType::SaveFileAs
4269 | PromptType::StopLspServer
4270 | PromptType::RestartLspServer
4271 | PromptType::SelectTheme { .. }
4272 | PromptType::SelectLocale
4273 | PromptType::SwitchToTab
4274 | PromptType::SetLanguage
4275 | PromptType::SetEncoding
4276 | PromptType::SetLineEnding
4277 | PromptType::Plugin { .. }
4278 ) {
4279 if let Some(selected_idx) = prompt.selected_suggestion {
4281 if let Some(suggestion) = prompt.suggestions.get(selected_idx) {
4282 if suggestion.disabled {
4284 if matches!(prompt.prompt_type, PromptType::Command) {
4286 self.command_registry
4287 .write()
4288 .unwrap()
4289 .record_usage(&suggestion.text);
4290 }
4291 self.set_status_message(
4292 t!(
4293 "error.command_not_available",
4294 command = suggestion.text.clone()
4295 )
4296 .to_string(),
4297 );
4298 return None;
4299 }
4300 suggestion.get_value().to_string()
4302 } else {
4303 prompt.input.clone()
4304 }
4305 } else {
4306 prompt.input.clone()
4307 }
4308 } else {
4309 prompt.input.clone()
4310 };
4311
4312 if matches!(
4314 prompt.prompt_type,
4315 PromptType::StopLspServer | PromptType::RestartLspServer
4316 ) {
4317 let is_valid = prompt
4318 .suggestions
4319 .iter()
4320 .any(|s| s.text == final_input || s.get_value() == final_input);
4321 if !is_valid {
4322 self.prompt = Some(prompt);
4324 self.set_status_message(
4325 t!("error.no_lsp_match", input = final_input.clone()).to_string(),
4326 );
4327 return None;
4328 }
4329 }
4330
4331 if matches!(prompt.prompt_type, PromptType::RemoveRuler) {
4335 if prompt.input.is_empty() {
4336 if let Some(selected_idx) = prompt.selected_suggestion {
4338 if let Some(suggestion) = prompt.suggestions.get(selected_idx) {
4339 final_input = suggestion.get_value().to_string();
4340 }
4341 } else {
4342 self.prompt = Some(prompt);
4343 return None;
4344 }
4345 } else {
4346 let typed = prompt.input.trim().to_string();
4348 let matched = prompt.suggestions.iter().find(|s| s.get_value() == typed);
4349 if let Some(suggestion) = matched {
4350 final_input = suggestion.get_value().to_string();
4351 } else {
4352 self.prompt = Some(prompt);
4354 return None;
4355 }
4356 }
4357 }
4358
4359 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
4361 let history = self.get_or_create_prompt_history(&key);
4362 history.push(final_input.clone());
4363 history.reset_navigation();
4364 }
4365
4366 Some((final_input, prompt.prompt_type, selected_index))
4367 } else {
4368 None
4369 }
4370 }
4371
4372 pub fn is_prompting(&self) -> bool {
4374 self.prompt.is_some()
4375 }
4376
4377 fn get_or_create_prompt_history(
4379 &mut self,
4380 key: &str,
4381 ) -> &mut crate::input::input_history::InputHistory {
4382 self.prompt_histories.entry(key.to_string()).or_default()
4383 }
4384
4385 fn get_prompt_history(&self, key: &str) -> Option<&crate::input::input_history::InputHistory> {
4387 self.prompt_histories.get(key)
4388 }
4389
4390 fn prompt_type_to_history_key(prompt_type: &crate::view::prompt::PromptType) -> Option<String> {
4392 use crate::view::prompt::PromptType;
4393 match prompt_type {
4394 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
4395 Some("search".to_string())
4396 }
4397 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
4398 Some("replace".to_string())
4399 }
4400 PromptType::GotoLine => Some("goto_line".to_string()),
4401 PromptType::Plugin { custom_type } => Some(format!("plugin:{}", custom_type)),
4402 _ => None,
4403 }
4404 }
4405
4406 pub fn editor_mode(&self) -> Option<String> {
4409 self.editor_mode.clone()
4410 }
4411
4412 pub fn command_registry(&self) -> &Arc<RwLock<CommandRegistry>> {
4414 &self.command_registry
4415 }
4416
4417 pub fn plugin_manager(&self) -> &PluginManager {
4419 &self.plugin_manager
4420 }
4421
4422 pub fn plugin_manager_mut(&mut self) -> &mut PluginManager {
4424 &mut self.plugin_manager
4425 }
4426
4427 pub fn file_explorer_is_focused(&self) -> bool {
4429 self.key_context == KeyContext::FileExplorer
4430 }
4431
4432 pub fn prompt_input(&self) -> Option<&str> {
4434 self.prompt.as_ref().map(|p| p.input.as_str())
4435 }
4436
4437 pub fn has_active_selection(&self) -> bool {
4439 self.active_cursors().primary().selection_range().is_some()
4440 }
4441
4442 pub fn prompt_mut(&mut self) -> Option<&mut Prompt> {
4444 self.prompt.as_mut()
4445 }
4446
4447 pub fn set_status_message(&mut self, message: String) {
4449 tracing::info!(target: "status", "{}", message);
4450 self.plugin_status_message = None;
4451 self.status_message = Some(message);
4452 }
4453
4454 pub fn get_status_message(&self) -> Option<&String> {
4456 self.plugin_status_message
4457 .as_ref()
4458 .or(self.status_message.as_ref())
4459 }
4460
4461 pub fn get_plugin_errors(&self) -> &[String] {
4464 &self.plugin_errors
4465 }
4466
4467 pub fn clear_plugin_errors(&mut self) {
4469 self.plugin_errors.clear();
4470 }
4471
4472 pub fn update_prompt_suggestions(&mut self) {
4474 let (prompt_type, input) = if let Some(prompt) = &self.prompt {
4476 (prompt.prompt_type.clone(), prompt.input.clone())
4477 } else {
4478 return;
4479 };
4480
4481 match prompt_type {
4482 PromptType::Command => {
4483 let selection_active = self.has_active_selection();
4484 let active_buffer_mode = self
4485 .buffer_metadata
4486 .get(&self.active_buffer())
4487 .and_then(|m| m.virtual_mode());
4488 let has_lsp_config = {
4489 let language = self
4490 .buffers
4491 .get(&self.active_buffer())
4492 .map(|s| s.language.as_str());
4493 language
4494 .and_then(|lang| self.lsp.as_ref().and_then(|lsp| lsp.get_config(lang)))
4495 .is_some()
4496 };
4497 if let Some(prompt) = &mut self.prompt {
4498 prompt.suggestions = self.command_registry.read().unwrap().filter(
4500 &input,
4501 self.key_context.clone(),
4502 &self.keybindings,
4503 selection_active,
4504 &self.active_custom_contexts,
4505 active_buffer_mode,
4506 has_lsp_config,
4507 );
4508 prompt.selected_suggestion = if prompt.suggestions.is_empty() {
4509 None
4510 } else {
4511 Some(0)
4512 };
4513 }
4514 }
4515 PromptType::QuickOpen => {
4516 self.update_quick_open_suggestions(&input);
4518 }
4519 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
4520 self.update_search_highlights(&input);
4522 if let Some(history) = self.prompt_histories.get_mut("search") {
4524 history.reset_navigation();
4525 }
4526 }
4527 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
4528 if let Some(history) = self.prompt_histories.get_mut("replace") {
4530 history.reset_navigation();
4531 }
4532 }
4533 PromptType::GotoLine => {
4534 if let Some(history) = self.prompt_histories.get_mut("goto_line") {
4536 history.reset_navigation();
4537 }
4538 }
4539 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs => {
4540 self.update_file_open_filter();
4542 }
4543 PromptType::Plugin { custom_type } => {
4544 let key = format!("plugin:{}", custom_type);
4546 if let Some(history) = self.prompt_histories.get_mut(&key) {
4547 history.reset_navigation();
4548 }
4549 use crate::services::plugins::hooks::HookArgs;
4551 self.plugin_manager.run_hook(
4552 "prompt_changed",
4553 HookArgs::PromptChanged {
4554 prompt_type: custom_type,
4555 input,
4556 },
4557 );
4558 if let Some(prompt) = &mut self.prompt {
4563 prompt.filter_suggestions(false);
4564 }
4565 }
4566 PromptType::SwitchToTab
4567 | PromptType::SelectTheme { .. }
4568 | PromptType::StopLspServer
4569 | PromptType::RestartLspServer
4570 | PromptType::SetLanguage
4571 | PromptType::SetEncoding
4572 | PromptType::SetLineEnding => {
4573 if let Some(prompt) = &mut self.prompt {
4574 prompt.filter_suggestions(false);
4575 }
4576 }
4577 PromptType::SelectLocale => {
4578 if let Some(prompt) = &mut self.prompt {
4580 prompt.filter_suggestions(true);
4581 }
4582 }
4583 _ => {}
4584 }
4585 }
4586
4587 pub fn process_async_messages(&mut self) -> bool {
4595 self.plugin_manager.check_thread_health();
4598
4599 let Some(bridge) = &self.async_bridge else {
4600 return false;
4601 };
4602
4603 let messages = {
4604 let _s = tracing::info_span!("try_recv_all").entered();
4605 bridge.try_recv_all()
4606 };
4607 let needs_render = !messages.is_empty();
4608 tracing::trace!(
4609 async_message_count = messages.len(),
4610 "received async messages"
4611 );
4612
4613 for message in messages {
4614 match message {
4615 AsyncMessage::LspDiagnostics {
4616 uri,
4617 diagnostics,
4618 server_name,
4619 } => {
4620 self.handle_lsp_diagnostics(uri, diagnostics, server_name);
4621 }
4622 AsyncMessage::LspInitialized {
4623 language,
4624 server_name,
4625 capabilities,
4626 } => {
4627 tracing::info!(
4628 "LSP server '{}' initialized for language: {}",
4629 server_name,
4630 language
4631 );
4632 self.status_message = Some(format!("LSP ({}) ready", language));
4633
4634 if let Some(lsp) = &mut self.lsp {
4636 lsp.set_server_capabilities(&language, &server_name, capabilities);
4637 }
4638
4639 self.resend_did_open_for_language(&language);
4641 self.request_semantic_tokens_for_language(&language);
4642 self.request_folding_ranges_for_language(&language);
4643 }
4644 AsyncMessage::LspError {
4645 language,
4646 error,
4647 stderr_log_path,
4648 } => {
4649 tracing::error!("LSP error for {}: {}", language, error);
4650 self.status_message = Some(format!("LSP error ({}): {}", language, error));
4651
4652 let server_command = self
4654 .config
4655 .lsp
4656 .get(&language)
4657 .and_then(|configs| configs.as_slice().first())
4658 .map(|c| c.command.clone())
4659 .unwrap_or_else(|| "unknown".to_string());
4660
4661 let error_type = if error.contains("not found") || error.contains("NotFound") {
4663 "not_found"
4664 } else if error.contains("permission") || error.contains("PermissionDenied") {
4665 "spawn_failed"
4666 } else if error.contains("timeout") {
4667 "timeout"
4668 } else {
4669 "spawn_failed"
4670 }
4671 .to_string();
4672
4673 self.plugin_manager.run_hook(
4675 "lsp_server_error",
4676 crate::services::plugins::hooks::HookArgs::LspServerError {
4677 language: language.clone(),
4678 server_command,
4679 error_type,
4680 message: error.clone(),
4681 },
4682 );
4683
4684 if let Some(log_path) = stderr_log_path {
4687 let has_content = log_path.metadata().map(|m| m.len() > 0).unwrap_or(false);
4688 if has_content {
4689 tracing::info!("Opening LSP stderr log in background: {:?}", log_path);
4690 match self.open_file_no_focus(&log_path) {
4691 Ok(buffer_id) => {
4692 self.mark_buffer_read_only(buffer_id, true);
4693 self.status_message = Some(format!(
4694 "LSP error ({}): {} - See stderr log",
4695 language, error
4696 ));
4697 }
4698 Err(e) => {
4699 tracing::error!("Failed to open LSP stderr log: {}", e);
4700 }
4701 }
4702 }
4703 }
4704 }
4705 AsyncMessage::LspCompletion { request_id, items } => {
4706 if let Err(e) = self.handle_completion_response(request_id, items) {
4707 tracing::error!("Error handling completion response: {}", e);
4708 }
4709 }
4710 AsyncMessage::LspGotoDefinition {
4711 request_id,
4712 locations,
4713 } => {
4714 if let Err(e) = self.handle_goto_definition_response(request_id, locations) {
4715 tracing::error!("Error handling goto definition response: {}", e);
4716 }
4717 }
4718 AsyncMessage::LspRename { request_id, result } => {
4719 if let Err(e) = self.handle_rename_response(request_id, result) {
4720 tracing::error!("Error handling rename response: {}", e);
4721 }
4722 }
4723 AsyncMessage::LspHover {
4724 request_id,
4725 contents,
4726 is_markdown,
4727 range,
4728 } => {
4729 self.handle_hover_response(request_id, contents, is_markdown, range);
4730 }
4731 AsyncMessage::LspReferences {
4732 request_id,
4733 locations,
4734 } => {
4735 if let Err(e) = self.handle_references_response(request_id, locations) {
4736 tracing::error!("Error handling references response: {}", e);
4737 }
4738 }
4739 AsyncMessage::LspSignatureHelp {
4740 request_id,
4741 signature_help,
4742 } => {
4743 self.handle_signature_help_response(request_id, signature_help);
4744 }
4745 AsyncMessage::LspCodeActions {
4746 request_id,
4747 actions,
4748 } => {
4749 self.handle_code_actions_response(request_id, actions);
4750 }
4751 AsyncMessage::LspApplyEdit { edit, label } => {
4752 tracing::info!("Applying workspace edit from server (label: {:?})", label);
4753 match self.apply_workspace_edit(edit) {
4754 Ok(n) => {
4755 if let Some(label) = label {
4756 self.set_status_message(
4757 t!("lsp.code_action_applied", title = &label, count = n)
4758 .to_string(),
4759 );
4760 }
4761 }
4762 Err(e) => {
4763 tracing::error!("Failed to apply workspace edit: {}", e);
4764 }
4765 }
4766 }
4767 AsyncMessage::LspCodeActionResolved {
4768 request_id: _,
4769 action,
4770 } => match action {
4771 Ok(resolved) => {
4772 self.execute_resolved_code_action(resolved);
4773 }
4774 Err(e) => {
4775 tracing::warn!("codeAction/resolve failed: {}", e);
4776 self.set_status_message(format!("Code action resolve failed: {e}"));
4777 }
4778 },
4779 AsyncMessage::LspCompletionResolved {
4780 request_id: _,
4781 item,
4782 } => {
4783 if let Ok(resolved) = item {
4784 self.handle_completion_resolved(resolved);
4785 }
4786 }
4787 AsyncMessage::LspFormatting {
4788 request_id: _,
4789 uri,
4790 edits,
4791 } => {
4792 if !edits.is_empty() {
4793 if let Err(e) = self.apply_formatting_edits(&uri, edits) {
4794 tracing::error!("Failed to apply formatting: {}", e);
4795 }
4796 }
4797 }
4798 AsyncMessage::LspPrepareRename {
4799 request_id: _,
4800 result,
4801 } => {
4802 self.handle_prepare_rename_response(result);
4803 }
4804 AsyncMessage::LspPulledDiagnostics {
4805 request_id: _,
4806 uri,
4807 result_id,
4808 diagnostics,
4809 unchanged,
4810 } => {
4811 self.handle_lsp_pulled_diagnostics(uri, result_id, diagnostics, unchanged);
4812 }
4813 AsyncMessage::LspInlayHints {
4814 request_id,
4815 uri,
4816 hints,
4817 } => {
4818 self.handle_lsp_inlay_hints(request_id, uri, hints);
4819 }
4820 AsyncMessage::LspFoldingRanges {
4821 request_id,
4822 uri,
4823 ranges,
4824 } => {
4825 self.handle_lsp_folding_ranges(request_id, uri, ranges);
4826 }
4827 AsyncMessage::LspSemanticTokens {
4828 request_id,
4829 uri,
4830 response,
4831 } => {
4832 self.handle_lsp_semantic_tokens(request_id, uri, response);
4833 }
4834 AsyncMessage::LspServerQuiescent { language } => {
4835 self.handle_lsp_server_quiescent(language);
4836 }
4837 AsyncMessage::LspDiagnosticRefresh { language } => {
4838 self.handle_lsp_diagnostic_refresh(language);
4839 }
4840 AsyncMessage::FileChanged { path } => {
4841 self.handle_async_file_changed(path);
4842 }
4843 AsyncMessage::GitStatusChanged { status } => {
4844 tracing::info!("Git status changed: {}", status);
4845 }
4847 AsyncMessage::FileExplorerInitialized(view) => {
4848 self.handle_file_explorer_initialized(view);
4849 }
4850 AsyncMessage::FileExplorerToggleNode(node_id) => {
4851 self.handle_file_explorer_toggle_node(node_id);
4852 }
4853 AsyncMessage::FileExplorerRefreshNode(node_id) => {
4854 self.handle_file_explorer_refresh_node(node_id);
4855 }
4856 AsyncMessage::FileExplorerExpandedToPath(view) => {
4857 self.handle_file_explorer_expanded_to_path(view);
4858 }
4859 AsyncMessage::Plugin(plugin_msg) => {
4860 use fresh_core::api::{JsCallbackId, PluginAsyncMessage};
4861 match plugin_msg {
4862 PluginAsyncMessage::ProcessOutput {
4863 process_id,
4864 stdout,
4865 stderr,
4866 exit_code,
4867 } => {
4868 self.handle_plugin_process_output(
4869 JsCallbackId::from(process_id),
4870 stdout,
4871 stderr,
4872 exit_code,
4873 );
4874 }
4875 PluginAsyncMessage::DelayComplete { callback_id } => {
4876 self.plugin_manager.resolve_callback(
4877 JsCallbackId::from(callback_id),
4878 "null".to_string(),
4879 );
4880 }
4881 PluginAsyncMessage::ProcessStdout { process_id, data } => {
4882 self.plugin_manager.run_hook(
4883 "onProcessStdout",
4884 crate::services::plugins::hooks::HookArgs::ProcessOutput {
4885 process_id,
4886 data,
4887 },
4888 );
4889 }
4890 PluginAsyncMessage::ProcessStderr { process_id, data } => {
4891 self.plugin_manager.run_hook(
4892 "onProcessStderr",
4893 crate::services::plugins::hooks::HookArgs::ProcessOutput {
4894 process_id,
4895 data,
4896 },
4897 );
4898 }
4899 PluginAsyncMessage::ProcessExit {
4900 process_id,
4901 callback_id,
4902 exit_code,
4903 } => {
4904 self.background_process_handles.remove(&process_id);
4905 let result = fresh_core::api::BackgroundProcessResult {
4906 process_id,
4907 exit_code,
4908 };
4909 self.plugin_manager.resolve_callback(
4910 JsCallbackId::from(callback_id),
4911 serde_json::to_string(&result).unwrap(),
4912 );
4913 }
4914 PluginAsyncMessage::LspResponse {
4915 language: _,
4916 request_id,
4917 result,
4918 } => {
4919 self.handle_plugin_lsp_response(request_id, result);
4920 }
4921 PluginAsyncMessage::PluginResponse(response) => {
4922 self.handle_plugin_response(response);
4923 }
4924 PluginAsyncMessage::GrepStreamingProgress {
4925 search_id,
4926 matches_json,
4927 } => {
4928 tracing::info!(
4929 "GrepStreamingProgress: search_id={} json_len={}",
4930 search_id,
4931 matches_json.len()
4932 );
4933 self.plugin_manager.call_streaming_callback(
4934 JsCallbackId::from(search_id),
4935 matches_json,
4936 false,
4937 );
4938 }
4939 PluginAsyncMessage::GrepStreamingComplete {
4940 search_id: _,
4941 callback_id,
4942 total_matches,
4943 truncated,
4944 } => {
4945 self.streaming_grep_cancellation = None;
4946 self.plugin_manager.resolve_callback(
4947 JsCallbackId::from(callback_id),
4948 format!(
4949 r#"{{"totalMatches":{},"truncated":{}}}"#,
4950 total_matches, truncated
4951 ),
4952 );
4953 }
4954 }
4955 }
4956 AsyncMessage::LspProgress {
4957 language,
4958 token,
4959 value,
4960 } => {
4961 self.handle_lsp_progress(language, token, value);
4962 }
4963 AsyncMessage::LspWindowMessage {
4964 language,
4965 message_type,
4966 message,
4967 } => {
4968 self.handle_lsp_window_message(language, message_type, message);
4969 }
4970 AsyncMessage::LspLogMessage {
4971 language,
4972 message_type,
4973 message,
4974 } => {
4975 self.handle_lsp_log_message(language, message_type, message);
4976 }
4977 AsyncMessage::LspStatusUpdate {
4978 language,
4979 server_name,
4980 status,
4981 message: _,
4982 } => {
4983 self.handle_lsp_status_update(language, server_name, status);
4984 }
4985 AsyncMessage::FileOpenDirectoryLoaded(result) => {
4986 self.handle_file_open_directory_loaded(result);
4987 }
4988 AsyncMessage::FileOpenShortcutsLoaded(shortcuts) => {
4989 self.handle_file_open_shortcuts_loaded(shortcuts);
4990 }
4991 AsyncMessage::TerminalOutput { terminal_id } => {
4992 tracing::trace!("Terminal output received for {:?}", terminal_id);
4994
4995 if self.config.terminal.jump_to_end_on_output && !self.terminal_mode {
4998 if let Some(&active_terminal_id) =
5000 self.terminal_buffers.get(&self.active_buffer())
5001 {
5002 if active_terminal_id == terminal_id {
5003 self.enter_terminal_mode();
5004 }
5005 }
5006 }
5007
5008 if self.terminal_mode {
5010 if let Some(handle) = self.terminal_manager.get(terminal_id) {
5011 if let Ok(mut state) = handle.state.lock() {
5012 state.scroll_to_bottom();
5013 }
5014 }
5015 }
5016 }
5017 AsyncMessage::TerminalExited { terminal_id } => {
5018 tracing::info!("Terminal {:?} exited", terminal_id);
5019 if let Some((&buffer_id, _)) = self
5021 .terminal_buffers
5022 .iter()
5023 .find(|(_, &tid)| tid == terminal_id)
5024 {
5025 if self.active_buffer() == buffer_id && self.terminal_mode {
5027 self.terminal_mode = false;
5028 self.key_context = crate::input::keybindings::KeyContext::Normal;
5029 }
5030
5031 self.sync_terminal_to_buffer(buffer_id);
5033
5034 let exit_msg = "\n[Terminal process exited]\n";
5036
5037 if let Some(backing_path) =
5038 self.terminal_backing_files.get(&terminal_id).cloned()
5039 {
5040 if let Ok(mut file) =
5041 self.filesystem.open_file_for_append(&backing_path)
5042 {
5043 use std::io::Write;
5044 if let Err(e) = file.write_all(exit_msg.as_bytes()) {
5045 tracing::warn!("Failed to write terminal exit message: {}", e);
5046 }
5047 }
5048
5049 if let Err(e) = self.revert_buffer_by_id(buffer_id, &backing_path) {
5051 tracing::warn!("Failed to revert terminal buffer: {}", e);
5052 }
5053 }
5054
5055 if let Some(state) = self.buffers.get_mut(&buffer_id) {
5057 state.editing_disabled = true;
5058 state.margins.configure_for_line_numbers(false);
5059 state.buffer.set_modified(false);
5060 }
5061
5062 self.terminal_buffers.remove(&buffer_id);
5064
5065 self.set_status_message(
5066 t!("terminal.exited", id = terminal_id.0).to_string(),
5067 );
5068 }
5069 self.terminal_manager.close(terminal_id);
5070 }
5071
5072 AsyncMessage::LspServerRequest {
5073 language,
5074 server_command,
5075 method,
5076 params,
5077 } => {
5078 self.handle_lsp_server_request(language, server_command, method, params);
5079 }
5080 AsyncMessage::PluginLspResponse {
5081 language: _,
5082 request_id,
5083 result,
5084 } => {
5085 self.handle_plugin_lsp_response(request_id, result);
5086 }
5087 AsyncMessage::PluginProcessOutput {
5088 process_id,
5089 stdout,
5090 stderr,
5091 exit_code,
5092 } => {
5093 self.handle_plugin_process_output(
5094 fresh_core::api::JsCallbackId::from(process_id),
5095 stdout,
5096 stderr,
5097 exit_code,
5098 );
5099 }
5100 AsyncMessage::GrammarRegistryBuilt {
5101 registry,
5102 callback_ids,
5103 } => {
5104 tracing::info!(
5105 "Background grammar build completed ({} syntaxes)",
5106 registry.available_syntaxes().len()
5107 );
5108 self.grammar_registry = registry;
5109 self.grammar_build_in_progress = false;
5110
5111 let buffers_to_update: Vec<_> = self
5113 .buffer_metadata
5114 .iter()
5115 .filter_map(|(id, meta)| meta.file_path().map(|p| (*id, p.to_path_buf())))
5116 .collect();
5117
5118 for (buf_id, path) in buffers_to_update {
5119 if let Some(state) = self.buffers.get_mut(&buf_id) {
5120 let detected =
5121 crate::primitives::detected_language::DetectedLanguage::from_path(
5122 &path,
5123 &self.grammar_registry,
5124 &self.config.languages,
5125 );
5126
5127 if detected.highlighter.has_highlighting()
5128 || !state.highlighter.has_highlighting()
5129 {
5130 state.apply_language(detected);
5131 }
5132 }
5133 }
5134
5135 #[cfg(feature = "plugins")]
5137 for cb_id in callback_ids {
5138 self.plugin_manager
5139 .resolve_callback(cb_id, "null".to_string());
5140 }
5141
5142 self.flush_pending_grammars();
5144 }
5145 }
5146 }
5147
5148 #[cfg(feature = "plugins")]
5151 {
5152 let _s = tracing::info_span!("update_plugin_state_snapshot").entered();
5153 self.update_plugin_state_snapshot();
5154 }
5155
5156 let processed_any_commands = {
5158 let _s = tracing::info_span!("process_plugin_commands").entered();
5159 self.process_plugin_commands()
5160 };
5161
5162 #[cfg(feature = "plugins")]
5166 if processed_any_commands {
5167 let _s = tracing::info_span!("update_plugin_state_snapshot_post").entered();
5168 self.update_plugin_state_snapshot();
5169 }
5170
5171 #[cfg(feature = "plugins")]
5173 {
5174 let _s = tracing::info_span!("process_pending_plugin_actions").entered();
5175 self.process_pending_plugin_actions();
5176 }
5177
5178 {
5180 let _s = tracing::info_span!("process_pending_lsp_restarts").entered();
5181 self.process_pending_lsp_restarts();
5182 }
5183
5184 #[cfg(feature = "plugins")]
5186 let plugin_render = {
5187 let render = self.plugin_render_requested;
5188 self.plugin_render_requested = false;
5189 render
5190 };
5191 #[cfg(not(feature = "plugins"))]
5192 let plugin_render = false;
5193
5194 if let Some(ref mut checker) = self.update_checker {
5196 let _ = checker.poll_result();
5198 }
5199
5200 let file_changes = {
5202 let _s = tracing::info_span!("poll_file_changes").entered();
5203 self.poll_file_changes()
5204 };
5205 let tree_changes = {
5206 let _s = tracing::info_span!("poll_file_tree_changes").entered();
5207 self.poll_file_tree_changes()
5208 };
5209
5210 needs_render || processed_any_commands || plugin_render || file_changes || tree_changes
5212 }
5213
5214 fn update_lsp_status_from_progress(&mut self) {
5216 if self.lsp_progress.is_empty() {
5217 self.update_lsp_status_from_server_statuses();
5219 return;
5220 }
5221
5222 if let Some((_, info)) = self.lsp_progress.iter().next() {
5224 let mut status = format!("LSP ({}): {}", info.language, info.title);
5225 if let Some(ref msg) = info.message {
5226 status.push_str(&format!(" - {}", msg));
5227 }
5228 if let Some(pct) = info.percentage {
5229 status.push_str(&format!(" ({}%)", pct));
5230 }
5231 self.lsp_status = status;
5232 }
5233 }
5234
5235 fn update_lsp_status_from_server_statuses(&mut self) {
5237 use crate::services::async_bridge::LspServerStatus;
5238
5239 let mut statuses: Vec<((String, String), LspServerStatus)> = self
5241 .lsp_server_statuses
5242 .iter()
5243 .map(|((lang, name), status)| ((lang.clone(), name.clone()), *status))
5244 .collect();
5245
5246 if statuses.is_empty() {
5247 self.lsp_status = String::new();
5248 return;
5249 }
5250
5251 statuses.sort_by(|a, b| a.0.cmp(&b.0));
5253
5254 let mut lang_counts: std::collections::HashMap<&str, usize> =
5256 std::collections::HashMap::new();
5257 for ((lang, _), _) in &statuses {
5258 *lang_counts.entry(lang.as_str()).or_default() += 1;
5259 }
5260
5261 let status_parts: Vec<String> = statuses
5263 .iter()
5264 .map(|((lang, name), status)| {
5265 let status_str = match status {
5266 LspServerStatus::Starting => "starting",
5267 LspServerStatus::Initializing => "initializing",
5268 LspServerStatus::Running => "ready",
5269 LspServerStatus::Error => "error",
5270 LspServerStatus::Shutdown => "shutdown",
5271 };
5272 if lang_counts.get(lang.as_str()).copied().unwrap_or(0) > 1 {
5274 format!("{}/{}: {}", lang, name, status_str)
5275 } else {
5276 format!("{}: {}", lang, status_str)
5277 }
5278 })
5279 .collect();
5280
5281 self.lsp_status = format!("LSP [{}]", status_parts.join(", "));
5282 }
5283
5284 #[cfg(feature = "plugins")]
5286 fn update_plugin_state_snapshot(&mut self) {
5287 if let Some(snapshot_handle) = self.plugin_manager.state_snapshot_handle() {
5289 use fresh_core::api::{BufferInfo, CursorInfo, ViewportInfo};
5290 let mut snapshot = snapshot_handle.write().unwrap();
5291
5292 let grammar_count = self.grammar_registry.available_syntaxes().len();
5294 if snapshot.available_grammars.len() != grammar_count {
5295 snapshot.available_grammars = self
5296 .grammar_registry
5297 .available_grammar_info()
5298 .into_iter()
5299 .map(|g| fresh_core::api::GrammarInfoSnapshot {
5300 name: g.name,
5301 source: g.source.to_string(),
5302 file_extensions: g.file_extensions,
5303 })
5304 .collect();
5305 }
5306
5307 snapshot.active_buffer_id = self.active_buffer();
5309
5310 snapshot.active_split_id = self.split_manager.active_split().0 .0;
5312
5313 snapshot.buffers.clear();
5315 snapshot.buffer_saved_diffs.clear();
5316 snapshot.buffer_cursor_positions.clear();
5317 snapshot.buffer_text_properties.clear();
5318
5319 for (buffer_id, state) in &self.buffers {
5320 let is_virtual = self
5321 .buffer_metadata
5322 .get(buffer_id)
5323 .map(|m| m.is_virtual())
5324 .unwrap_or(false);
5325 let active_split = self.split_manager.active_split();
5330 let active_vs = self.split_view_states.get(&active_split);
5331 let view_mode = active_vs
5332 .and_then(|vs| vs.buffer_state(*buffer_id))
5333 .map(|bs| match bs.view_mode {
5334 crate::state::ViewMode::Source => "source",
5335 crate::state::ViewMode::PageView => "compose",
5336 })
5337 .unwrap_or("source");
5338 let compose_width = active_vs
5339 .and_then(|vs| vs.buffer_state(*buffer_id))
5340 .and_then(|bs| bs.compose_width);
5341 let is_composing_in_any_split = self.split_view_states.values().any(|vs| {
5342 vs.buffer_state(*buffer_id)
5343 .map(|bs| matches!(bs.view_mode, crate::state::ViewMode::PageView))
5344 .unwrap_or(false)
5345 });
5346 let buffer_info = BufferInfo {
5347 id: *buffer_id,
5348 path: state.buffer.file_path().map(|p| p.to_path_buf()),
5349 modified: state.buffer.is_modified(),
5350 length: state.buffer.len(),
5351 is_virtual,
5352 view_mode: view_mode.to_string(),
5353 is_composing_in_any_split,
5354 compose_width,
5355 language: state.language.clone(),
5356 };
5357 snapshot.buffers.insert(*buffer_id, buffer_info);
5358
5359 let diff = {
5360 let diff = state.buffer.diff_since_saved();
5361 BufferSavedDiff {
5362 equal: diff.equal,
5363 byte_ranges: diff.byte_ranges.clone(),
5364 }
5365 };
5366 snapshot.buffer_saved_diffs.insert(*buffer_id, diff);
5367
5368 let cursor_pos = self
5370 .split_view_states
5371 .values()
5372 .find_map(|vs| vs.buffer_state(*buffer_id))
5373 .map(|bs| bs.cursors.primary().position)
5374 .unwrap_or(0);
5375 snapshot
5376 .buffer_cursor_positions
5377 .insert(*buffer_id, cursor_pos);
5378
5379 if !state.text_properties.is_empty() {
5381 snapshot
5382 .buffer_text_properties
5383 .insert(*buffer_id, state.text_properties.all().to_vec());
5384 }
5385 }
5386
5387 if let Some(active_vs) = self
5389 .split_view_states
5390 .get(&self.split_manager.active_split())
5391 {
5392 let active_cursors = &active_vs.cursors;
5394 let primary = active_cursors.primary();
5395 let primary_position = primary.position;
5396 let primary_selection = primary.selection_range();
5397
5398 snapshot.primary_cursor = Some(CursorInfo {
5399 position: primary_position,
5400 selection: primary_selection.clone(),
5401 });
5402
5403 snapshot.all_cursors = active_cursors
5405 .iter()
5406 .map(|(_, cursor)| CursorInfo {
5407 position: cursor.position,
5408 selection: cursor.selection_range(),
5409 })
5410 .collect();
5411
5412 if let Some(range) = primary_selection {
5414 if let Some(active_state) = self.buffers.get_mut(&self.active_buffer()) {
5415 snapshot.selected_text =
5416 Some(active_state.get_text_range(range.start, range.end));
5417 }
5418 }
5419
5420 let top_line = self.buffers.get(&self.active_buffer()).and_then(|state| {
5422 if state.buffer.line_count().is_some() {
5423 Some(state.buffer.get_line_number(active_vs.viewport.top_byte))
5424 } else {
5425 None
5426 }
5427 });
5428 snapshot.viewport = Some(ViewportInfo {
5429 top_byte: active_vs.viewport.top_byte,
5430 top_line,
5431 left_column: active_vs.viewport.left_column,
5432 width: active_vs.viewport.width,
5433 height: active_vs.viewport.height,
5434 });
5435 } else {
5436 snapshot.primary_cursor = None;
5437 snapshot.all_cursors.clear();
5438 snapshot.viewport = None;
5439 snapshot.selected_text = None;
5440 }
5441
5442 snapshot.clipboard = self.clipboard.get_internal().to_string();
5444
5445 snapshot.working_dir = self.working_dir.clone();
5447
5448 snapshot.diagnostics = self.stored_diagnostics.clone();
5450
5451 snapshot.folding_ranges = self.stored_folding_ranges.clone();
5453
5454 snapshot.config = serde_json::to_value(&self.config).unwrap_or(serde_json::Value::Null);
5456
5457 snapshot.user_config = self.user_config_raw.clone();
5460
5461 snapshot.editor_mode = self.editor_mode.clone();
5463
5464 for (plugin_name, state_map) in &self.plugin_global_state {
5467 let entry = snapshot
5468 .plugin_global_states
5469 .entry(plugin_name.clone())
5470 .or_default();
5471 for (key, value) in state_map {
5472 entry.entry(key.clone()).or_insert_with(|| value.clone());
5473 }
5474 }
5475
5476 let active_split_id = self.split_manager.active_split().0 .0;
5481 let split_changed = snapshot.plugin_view_states_split != active_split_id;
5482 if split_changed {
5483 snapshot.plugin_view_states.clear();
5484 snapshot.plugin_view_states_split = active_split_id;
5485 }
5486
5487 {
5489 let open_bids: Vec<_> = snapshot.buffers.keys().copied().collect();
5490 snapshot
5491 .plugin_view_states
5492 .retain(|bid, _| open_bids.contains(bid));
5493 }
5494
5495 if let Some(active_vs) = self
5497 .split_view_states
5498 .get(&self.split_manager.active_split())
5499 {
5500 for (buffer_id, buf_state) in &active_vs.keyed_states {
5501 if !buf_state.plugin_state.is_empty() {
5502 let entry = snapshot.plugin_view_states.entry(*buffer_id).or_default();
5503 for (key, value) in &buf_state.plugin_state {
5504 entry.entry(key.clone()).or_insert_with(|| value.clone());
5506 }
5507 }
5508 }
5509 }
5510 }
5511 }
5512
5513 pub fn handle_plugin_command(&mut self, command: PluginCommand) -> AnyhowResult<()> {
5515 match command {
5516 PluginCommand::InsertText {
5518 buffer_id,
5519 position,
5520 text,
5521 } => {
5522 self.handle_insert_text(buffer_id, position, text);
5523 }
5524 PluginCommand::DeleteRange { buffer_id, range } => {
5525 self.handle_delete_range(buffer_id, range);
5526 }
5527 PluginCommand::InsertAtCursor { text } => {
5528 self.handle_insert_at_cursor(text);
5529 }
5530 PluginCommand::DeleteSelection => {
5531 self.handle_delete_selection();
5532 }
5533
5534 PluginCommand::AddOverlay {
5536 buffer_id,
5537 namespace,
5538 range,
5539 options,
5540 } => {
5541 self.handle_add_overlay(buffer_id, namespace, range, options);
5542 }
5543 PluginCommand::RemoveOverlay { buffer_id, handle } => {
5544 self.handle_remove_overlay(buffer_id, handle);
5545 }
5546 PluginCommand::ClearAllOverlays { buffer_id } => {
5547 self.handle_clear_all_overlays(buffer_id);
5548 }
5549 PluginCommand::ClearNamespace {
5550 buffer_id,
5551 namespace,
5552 } => {
5553 self.handle_clear_namespace(buffer_id, namespace);
5554 }
5555 PluginCommand::ClearOverlaysInRange {
5556 buffer_id,
5557 start,
5558 end,
5559 } => {
5560 self.handle_clear_overlays_in_range(buffer_id, start, end);
5561 }
5562
5563 PluginCommand::AddVirtualText {
5565 buffer_id,
5566 virtual_text_id,
5567 position,
5568 text,
5569 color,
5570 use_bg,
5571 before,
5572 } => {
5573 self.handle_add_virtual_text(
5574 buffer_id,
5575 virtual_text_id,
5576 position,
5577 text,
5578 color,
5579 use_bg,
5580 before,
5581 );
5582 }
5583 PluginCommand::RemoveVirtualText {
5584 buffer_id,
5585 virtual_text_id,
5586 } => {
5587 self.handle_remove_virtual_text(buffer_id, virtual_text_id);
5588 }
5589 PluginCommand::RemoveVirtualTextsByPrefix { buffer_id, prefix } => {
5590 self.handle_remove_virtual_texts_by_prefix(buffer_id, prefix);
5591 }
5592 PluginCommand::ClearVirtualTexts { buffer_id } => {
5593 self.handle_clear_virtual_texts(buffer_id);
5594 }
5595 PluginCommand::AddVirtualLine {
5596 buffer_id,
5597 position,
5598 text,
5599 fg_color,
5600 bg_color,
5601 above,
5602 namespace,
5603 priority,
5604 } => {
5605 self.handle_add_virtual_line(
5606 buffer_id, position, text, fg_color, bg_color, above, namespace, priority,
5607 );
5608 }
5609 PluginCommand::ClearVirtualTextNamespace {
5610 buffer_id,
5611 namespace,
5612 } => {
5613 self.handle_clear_virtual_text_namespace(buffer_id, namespace);
5614 }
5615
5616 PluginCommand::AddConceal {
5618 buffer_id,
5619 namespace,
5620 start,
5621 end,
5622 replacement,
5623 } => {
5624 self.handle_add_conceal(buffer_id, namespace, start, end, replacement);
5625 }
5626 PluginCommand::ClearConcealNamespace {
5627 buffer_id,
5628 namespace,
5629 } => {
5630 self.handle_clear_conceal_namespace(buffer_id, namespace);
5631 }
5632 PluginCommand::ClearConcealsInRange {
5633 buffer_id,
5634 start,
5635 end,
5636 } => {
5637 self.handle_clear_conceals_in_range(buffer_id, start, end);
5638 }
5639
5640 PluginCommand::AddSoftBreak {
5642 buffer_id,
5643 namespace,
5644 position,
5645 indent,
5646 } => {
5647 self.handle_add_soft_break(buffer_id, namespace, position, indent);
5648 }
5649 PluginCommand::ClearSoftBreakNamespace {
5650 buffer_id,
5651 namespace,
5652 } => {
5653 self.handle_clear_soft_break_namespace(buffer_id, namespace);
5654 }
5655 PluginCommand::ClearSoftBreaksInRange {
5656 buffer_id,
5657 start,
5658 end,
5659 } => {
5660 self.handle_clear_soft_breaks_in_range(buffer_id, start, end);
5661 }
5662
5663 PluginCommand::AddMenuItem {
5665 menu_label,
5666 item,
5667 position,
5668 } => {
5669 self.handle_add_menu_item(menu_label, item, position);
5670 }
5671 PluginCommand::AddMenu { menu, position } => {
5672 self.handle_add_menu(menu, position);
5673 }
5674 PluginCommand::RemoveMenuItem {
5675 menu_label,
5676 item_label,
5677 } => {
5678 self.handle_remove_menu_item(menu_label, item_label);
5679 }
5680 PluginCommand::RemoveMenu { menu_label } => {
5681 self.handle_remove_menu(menu_label);
5682 }
5683
5684 PluginCommand::FocusSplit { split_id } => {
5686 self.handle_focus_split(split_id);
5687 }
5688 PluginCommand::SetSplitBuffer {
5689 split_id,
5690 buffer_id,
5691 } => {
5692 self.handle_set_split_buffer(split_id, buffer_id);
5693 }
5694 PluginCommand::SetSplitScroll { split_id, top_byte } => {
5695 self.handle_set_split_scroll(split_id, top_byte);
5696 }
5697 PluginCommand::RequestHighlights {
5698 buffer_id,
5699 range,
5700 request_id,
5701 } => {
5702 self.handle_request_highlights(buffer_id, range, request_id);
5703 }
5704 PluginCommand::CloseSplit { split_id } => {
5705 self.handle_close_split(split_id);
5706 }
5707 PluginCommand::SetSplitRatio { split_id, ratio } => {
5708 self.handle_set_split_ratio(split_id, ratio);
5709 }
5710 PluginCommand::SetSplitLabel { split_id, label } => {
5711 self.split_manager.set_label(LeafId(split_id), label);
5712 }
5713 PluginCommand::ClearSplitLabel { split_id } => {
5714 self.split_manager.clear_label(split_id);
5715 }
5716 PluginCommand::GetSplitByLabel { label, request_id } => {
5717 let split_id = self.split_manager.find_split_by_label(&label);
5718 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
5719 let json = serde_json::to_string(&split_id.map(|s| s.0 .0))
5720 .unwrap_or_else(|_| "null".to_string());
5721 self.plugin_manager.resolve_callback(callback_id, json);
5722 }
5723 PluginCommand::DistributeSplitsEvenly { split_ids: _ } => {
5724 self.handle_distribute_splits_evenly();
5725 }
5726 PluginCommand::SetBufferCursor {
5727 buffer_id,
5728 position,
5729 } => {
5730 self.handle_set_buffer_cursor(buffer_id, position);
5731 }
5732
5733 PluginCommand::SetLayoutHints {
5735 buffer_id,
5736 split_id,
5737 range: _,
5738 hints,
5739 } => {
5740 self.handle_set_layout_hints(buffer_id, split_id, hints);
5741 }
5742 PluginCommand::SetLineNumbers { buffer_id, enabled } => {
5743 self.handle_set_line_numbers(buffer_id, enabled);
5744 }
5745 PluginCommand::SetViewMode { buffer_id, mode } => {
5746 self.handle_set_view_mode(buffer_id, &mode);
5747 }
5748 PluginCommand::SetLineWrap {
5749 buffer_id,
5750 split_id,
5751 enabled,
5752 } => {
5753 self.handle_set_line_wrap(buffer_id, split_id, enabled);
5754 }
5755 PluginCommand::SubmitViewTransform {
5756 buffer_id,
5757 split_id,
5758 payload,
5759 } => {
5760 self.handle_submit_view_transform(buffer_id, split_id, payload);
5761 }
5762 PluginCommand::ClearViewTransform {
5763 buffer_id: _,
5764 split_id,
5765 } => {
5766 self.handle_clear_view_transform(split_id);
5767 }
5768 PluginCommand::SetViewState {
5769 buffer_id,
5770 key,
5771 value,
5772 } => {
5773 self.handle_set_view_state(buffer_id, key, value);
5774 }
5775 PluginCommand::SetGlobalState {
5776 plugin_name,
5777 key,
5778 value,
5779 } => {
5780 self.handle_set_global_state(plugin_name, key, value);
5781 }
5782 PluginCommand::RefreshLines { buffer_id } => {
5783 self.handle_refresh_lines(buffer_id);
5784 }
5785 PluginCommand::RefreshAllLines => {
5786 self.handle_refresh_all_lines();
5787 }
5788 PluginCommand::HookCompleted { .. } => {
5789 }
5791 PluginCommand::SetLineIndicator {
5792 buffer_id,
5793 line,
5794 namespace,
5795 symbol,
5796 color,
5797 priority,
5798 } => {
5799 self.handle_set_line_indicator(buffer_id, line, namespace, symbol, color, priority);
5800 }
5801 PluginCommand::SetLineIndicators {
5802 buffer_id,
5803 lines,
5804 namespace,
5805 symbol,
5806 color,
5807 priority,
5808 } => {
5809 self.handle_set_line_indicators(
5810 buffer_id, lines, namespace, symbol, color, priority,
5811 );
5812 }
5813 PluginCommand::ClearLineIndicators {
5814 buffer_id,
5815 namespace,
5816 } => {
5817 self.handle_clear_line_indicators(buffer_id, namespace);
5818 }
5819 PluginCommand::SetFileExplorerDecorations {
5820 namespace,
5821 decorations,
5822 } => {
5823 self.handle_set_file_explorer_decorations(namespace, decorations);
5824 }
5825 PluginCommand::ClearFileExplorerDecorations { namespace } => {
5826 self.handle_clear_file_explorer_decorations(&namespace);
5827 }
5828
5829 PluginCommand::SetStatus { message } => {
5831 self.handle_set_status(message);
5832 }
5833 PluginCommand::ApplyTheme { theme_name } => {
5834 self.apply_theme(&theme_name);
5835 }
5836 PluginCommand::ReloadConfig => {
5837 self.reload_config();
5838 }
5839 PluginCommand::ReloadThemes { apply_theme } => {
5840 self.reload_themes();
5841 if let Some(theme_name) = apply_theme {
5842 self.apply_theme(&theme_name);
5843 }
5844 }
5845 PluginCommand::RegisterGrammar {
5846 language,
5847 grammar_path,
5848 extensions,
5849 } => {
5850 self.handle_register_grammar(language, grammar_path, extensions);
5851 }
5852 PluginCommand::RegisterLanguageConfig { language, config } => {
5853 self.handle_register_language_config(language, config);
5854 }
5855 PluginCommand::RegisterLspServer { language, config } => {
5856 self.handle_register_lsp_server(language, config);
5857 }
5858 PluginCommand::ReloadGrammars { callback_id } => {
5859 self.handle_reload_grammars(callback_id);
5860 }
5861 PluginCommand::StartPrompt { label, prompt_type } => {
5862 self.handle_start_prompt(label, prompt_type);
5863 }
5864 PluginCommand::StartPromptWithInitial {
5865 label,
5866 prompt_type,
5867 initial_value,
5868 } => {
5869 self.handle_start_prompt_with_initial(label, prompt_type, initial_value);
5870 }
5871 PluginCommand::StartPromptAsync {
5872 label,
5873 initial_value,
5874 callback_id,
5875 } => {
5876 self.handle_start_prompt_async(label, initial_value, callback_id);
5877 }
5878 PluginCommand::SetPromptSuggestions { suggestions } => {
5879 self.handle_set_prompt_suggestions(suggestions);
5880 }
5881 PluginCommand::SetPromptInputSync { sync } => {
5882 if let Some(prompt) = &mut self.prompt {
5883 prompt.sync_input_on_navigate = sync;
5884 }
5885 }
5886
5887 PluginCommand::RegisterCommand { command } => {
5889 self.handle_register_command(command);
5890 }
5891 PluginCommand::UnregisterCommand { name } => {
5892 self.handle_unregister_command(name);
5893 }
5894 PluginCommand::DefineMode {
5895 name,
5896 bindings,
5897 read_only,
5898 allow_text_input,
5899 plugin_name,
5900 } => {
5901 self.handle_define_mode(name, bindings, read_only, allow_text_input, plugin_name);
5902 }
5903
5904 PluginCommand::OpenFileInBackground { path } => {
5906 self.handle_open_file_in_background(path);
5907 }
5908 PluginCommand::OpenFileAtLocation { path, line, column } => {
5909 return self.handle_open_file_at_location(path, line, column);
5910 }
5911 PluginCommand::OpenFileInSplit {
5912 split_id,
5913 path,
5914 line,
5915 column,
5916 } => {
5917 return self.handle_open_file_in_split(split_id, path, line, column);
5918 }
5919 PluginCommand::ShowBuffer { buffer_id } => {
5920 self.handle_show_buffer(buffer_id);
5921 }
5922 PluginCommand::CloseBuffer { buffer_id } => {
5923 self.handle_close_buffer(buffer_id);
5924 }
5925
5926 PluginCommand::SendLspRequest {
5928 language,
5929 method,
5930 params,
5931 request_id,
5932 } => {
5933 self.handle_send_lsp_request(language, method, params, request_id);
5934 }
5935
5936 PluginCommand::SetClipboard { text } => {
5938 self.handle_set_clipboard(text);
5939 }
5940
5941 PluginCommand::SpawnProcess {
5943 command,
5944 args,
5945 cwd,
5946 callback_id,
5947 } => {
5948 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
5951 let effective_cwd = cwd.or_else(|| {
5952 std::env::current_dir()
5953 .map(|p| p.to_string_lossy().to_string())
5954 .ok()
5955 });
5956 let sender = bridge.sender();
5957 let spawner = self.process_spawner.clone();
5958
5959 runtime.spawn(async move {
5960 #[allow(clippy::let_underscore_must_use)]
5962 match spawner.spawn(command, args, effective_cwd).await {
5963 Ok(result) => {
5964 let _ = sender.send(AsyncMessage::PluginProcessOutput {
5965 process_id: callback_id.as_u64(),
5966 stdout: result.stdout,
5967 stderr: result.stderr,
5968 exit_code: result.exit_code,
5969 });
5970 }
5971 Err(e) => {
5972 let _ = sender.send(AsyncMessage::PluginProcessOutput {
5973 process_id: callback_id.as_u64(),
5974 stdout: String::new(),
5975 stderr: e.to_string(),
5976 exit_code: -1,
5977 });
5978 }
5979 }
5980 });
5981 } else {
5982 self.plugin_manager
5984 .reject_callback(callback_id, "Async runtime not available".to_string());
5985 }
5986 }
5987
5988 PluginCommand::SpawnProcessWait {
5989 process_id,
5990 callback_id,
5991 } => {
5992 tracing::warn!(
5995 "SpawnProcessWait not fully implemented - process_id={}",
5996 process_id
5997 );
5998 self.plugin_manager.reject_callback(
5999 callback_id,
6000 format!(
6001 "SpawnProcessWait not yet fully implemented for process_id={}",
6002 process_id
6003 ),
6004 );
6005 }
6006
6007 PluginCommand::Delay {
6008 callback_id,
6009 duration_ms,
6010 } => {
6011 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
6013 let sender = bridge.sender();
6014 let callback_id_u64 = callback_id.as_u64();
6015 runtime.spawn(async move {
6016 tokio::time::sleep(tokio::time::Duration::from_millis(duration_ms)).await;
6017 #[allow(clippy::let_underscore_must_use)]
6019 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
6020 fresh_core::api::PluginAsyncMessage::DelayComplete {
6021 callback_id: callback_id_u64,
6022 },
6023 ));
6024 });
6025 } else {
6026 std::thread::sleep(std::time::Duration::from_millis(duration_ms));
6028 self.plugin_manager
6029 .resolve_callback(callback_id, "null".to_string());
6030 }
6031 }
6032
6033 PluginCommand::SpawnBackgroundProcess {
6034 process_id,
6035 command,
6036 args,
6037 cwd,
6038 callback_id,
6039 } => {
6040 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
6042 use tokio::io::{AsyncBufReadExt, BufReader};
6043 use tokio::process::Command as TokioCommand;
6044
6045 let effective_cwd = cwd.unwrap_or_else(|| {
6046 std::env::current_dir()
6047 .map(|p| p.to_string_lossy().to_string())
6048 .unwrap_or_else(|_| ".".to_string())
6049 });
6050
6051 let sender = bridge.sender();
6052 let sender_stdout = sender.clone();
6053 let sender_stderr = sender.clone();
6054 let callback_id_u64 = callback_id.as_u64();
6055
6056 #[allow(clippy::let_underscore_must_use)]
6058 let handle = runtime.spawn(async move {
6059 let mut child = match TokioCommand::new(&command)
6060 .args(&args)
6061 .current_dir(&effective_cwd)
6062 .stdout(std::process::Stdio::piped())
6063 .stderr(std::process::Stdio::piped())
6064 .spawn()
6065 {
6066 Ok(child) => child,
6067 Err(e) => {
6068 let _ = sender.send(
6069 crate::services::async_bridge::AsyncMessage::Plugin(
6070 fresh_core::api::PluginAsyncMessage::ProcessExit {
6071 process_id,
6072 callback_id: callback_id_u64,
6073 exit_code: -1,
6074 },
6075 ),
6076 );
6077 tracing::error!("Failed to spawn background process: {}", e);
6078 return;
6079 }
6080 };
6081
6082 let stdout = child.stdout.take();
6084 let stderr = child.stderr.take();
6085 let pid = process_id;
6086
6087 if let Some(stdout) = stdout {
6089 let sender = sender_stdout;
6090 tokio::spawn(async move {
6091 let reader = BufReader::new(stdout);
6092 let mut lines = reader.lines();
6093 while let Ok(Some(line)) = lines.next_line().await {
6094 let _ = sender.send(
6095 crate::services::async_bridge::AsyncMessage::Plugin(
6096 fresh_core::api::PluginAsyncMessage::ProcessStdout {
6097 process_id: pid,
6098 data: line + "\n",
6099 },
6100 ),
6101 );
6102 }
6103 });
6104 }
6105
6106 if let Some(stderr) = stderr {
6108 let sender = sender_stderr;
6109 tokio::spawn(async move {
6110 let reader = BufReader::new(stderr);
6111 let mut lines = reader.lines();
6112 while let Ok(Some(line)) = lines.next_line().await {
6113 let _ = sender.send(
6114 crate::services::async_bridge::AsyncMessage::Plugin(
6115 fresh_core::api::PluginAsyncMessage::ProcessStderr {
6116 process_id: pid,
6117 data: line + "\n",
6118 },
6119 ),
6120 );
6121 }
6122 });
6123 }
6124
6125 let exit_code = match child.wait().await {
6127 Ok(status) => status.code().unwrap_or(-1),
6128 Err(_) => -1,
6129 };
6130
6131 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
6132 fresh_core::api::PluginAsyncMessage::ProcessExit {
6133 process_id,
6134 callback_id: callback_id_u64,
6135 exit_code,
6136 },
6137 ));
6138 });
6139
6140 self.background_process_handles
6142 .insert(process_id, handle.abort_handle());
6143 } else {
6144 self.plugin_manager
6146 .reject_callback(callback_id, "Async runtime not available".to_string());
6147 }
6148 }
6149
6150 PluginCommand::KillBackgroundProcess { process_id } => {
6151 if let Some(handle) = self.background_process_handles.remove(&process_id) {
6152 handle.abort();
6153 tracing::debug!("Killed background process {}", process_id);
6154 }
6155 }
6156
6157 PluginCommand::CreateVirtualBuffer {
6159 name,
6160 mode,
6161 read_only,
6162 } => {
6163 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
6164 tracing::info!(
6165 "Created virtual buffer '{}' with mode '{}' (id={:?})",
6166 name,
6167 mode,
6168 buffer_id
6169 );
6170 }
6172 PluginCommand::CreateVirtualBufferWithContent {
6173 name,
6174 mode,
6175 read_only,
6176 entries,
6177 show_line_numbers,
6178 show_cursors,
6179 editing_disabled,
6180 hidden_from_tabs,
6181 request_id,
6182 } => {
6183 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
6184 tracing::info!(
6185 "Created virtual buffer '{}' with mode '{}' (id={:?})",
6186 name,
6187 mode,
6188 buffer_id
6189 );
6190
6191 if let Some(state) = self.buffers.get_mut(&buffer_id) {
6198 state.margins.configure_for_line_numbers(show_line_numbers);
6199 state.show_cursors = show_cursors;
6200 state.editing_disabled = editing_disabled;
6201 tracing::debug!(
6202 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
6203 buffer_id,
6204 show_line_numbers,
6205 show_cursors,
6206 editing_disabled
6207 );
6208 }
6209 let active_split = self.split_manager.active_split();
6210 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
6211 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
6212 }
6213
6214 if hidden_from_tabs {
6216 if let Some(meta) = self.buffer_metadata.get_mut(&buffer_id) {
6217 meta.hidden_from_tabs = true;
6218 }
6219 }
6220
6221 match self.set_virtual_buffer_content(buffer_id, entries) {
6223 Ok(()) => {
6224 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
6225 self.set_active_buffer(buffer_id);
6227 tracing::debug!("Switched to virtual buffer {:?}", buffer_id);
6228
6229 if let Some(req_id) = request_id {
6231 tracing::info!(
6232 "CreateVirtualBufferWithContent: resolving callback for request_id={}, buffer_id={:?}",
6233 req_id,
6234 buffer_id
6235 );
6236 let result = fresh_core::api::VirtualBufferResult {
6238 buffer_id: buffer_id.0 as u64,
6239 split_id: None,
6240 };
6241 self.plugin_manager.resolve_callback(
6242 fresh_core::api::JsCallbackId::from(req_id),
6243 serde_json::to_string(&result).unwrap_or_default(),
6244 );
6245 tracing::info!("CreateVirtualBufferWithContent: resolve_callback sent for request_id={}", req_id);
6246 }
6247 }
6248 Err(e) => {
6249 tracing::error!("Failed to set virtual buffer content: {}", e);
6250 }
6251 }
6252 }
6253 PluginCommand::CreateVirtualBufferInSplit {
6254 name,
6255 mode,
6256 read_only,
6257 entries,
6258 ratio,
6259 direction,
6260 panel_id,
6261 show_line_numbers,
6262 show_cursors,
6263 editing_disabled,
6264 line_wrap,
6265 before,
6266 request_id,
6267 } => {
6268 if let Some(pid) = &panel_id {
6270 if let Some(&existing_buffer_id) = self.panel_ids.get(pid) {
6271 if self.buffers.contains_key(&existing_buffer_id) {
6273 if let Err(e) =
6275 self.set_virtual_buffer_content(existing_buffer_id, entries)
6276 {
6277 tracing::error!("Failed to update panel content: {}", e);
6278 } else {
6279 tracing::info!("Updated existing panel '{}' content", pid);
6280 }
6281
6282 let splits = self.split_manager.splits_for_buffer(existing_buffer_id);
6284 if let Some(&split_id) = splits.first() {
6285 self.split_manager.set_active_split(split_id);
6286 self.split_manager.set_active_buffer_id(existing_buffer_id);
6289 tracing::debug!(
6290 "Focused split {:?} containing panel buffer",
6291 split_id
6292 );
6293 }
6294
6295 if let Some(req_id) = request_id {
6297 let result = fresh_core::api::VirtualBufferResult {
6298 buffer_id: existing_buffer_id.0 as u64,
6299 split_id: splits.first().map(|s| s.0 .0 as u64),
6300 };
6301 self.plugin_manager.resolve_callback(
6302 fresh_core::api::JsCallbackId::from(req_id),
6303 serde_json::to_string(&result).unwrap_or_default(),
6304 );
6305 }
6306 return Ok(());
6307 } else {
6308 tracing::warn!(
6310 "Removing stale panel_id '{}' pointing to non-existent buffer {:?}",
6311 pid,
6312 existing_buffer_id
6313 );
6314 self.panel_ids.remove(pid);
6315 }
6317 }
6318 }
6319
6320 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
6322 tracing::info!(
6323 "Created virtual buffer '{}' with mode '{}' in split (id={:?})",
6324 name,
6325 mode,
6326 buffer_id
6327 );
6328
6329 if let Some(state) = self.buffers.get_mut(&buffer_id) {
6331 state.margins.configure_for_line_numbers(show_line_numbers);
6332 state.show_cursors = show_cursors;
6333 state.editing_disabled = editing_disabled;
6334 tracing::debug!(
6335 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
6336 buffer_id,
6337 show_line_numbers,
6338 show_cursors,
6339 editing_disabled
6340 );
6341 }
6342
6343 if let Some(pid) = panel_id {
6345 self.panel_ids.insert(pid, buffer_id);
6346 }
6347
6348 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
6350 tracing::error!("Failed to set virtual buffer content: {}", e);
6351 return Ok(());
6352 }
6353
6354 let split_dir = match direction.as_deref() {
6356 Some("vertical") => crate::model::event::SplitDirection::Vertical,
6357 _ => crate::model::event::SplitDirection::Horizontal,
6358 };
6359
6360 let created_split_id = match self
6362 .split_manager
6363 .split_active_positioned(split_dir, buffer_id, ratio, before)
6364 {
6365 Ok(new_split_id) => {
6366 let mut view_state = SplitViewState::with_buffer(
6368 self.terminal_width,
6369 self.terminal_height,
6370 buffer_id,
6371 );
6372 view_state.apply_config_defaults(
6373 self.config.editor.line_numbers,
6374 self.config.editor.highlight_current_line,
6375 line_wrap
6376 .unwrap_or_else(|| self.resolve_line_wrap_for_buffer(buffer_id)),
6377 self.config.editor.wrap_indent,
6378 self.resolve_wrap_column_for_buffer(buffer_id),
6379 self.config.editor.rulers.clone(),
6380 );
6381 view_state.ensure_buffer_state(buffer_id).show_line_numbers =
6383 show_line_numbers;
6384 self.split_view_states.insert(new_split_id, view_state);
6385
6386 self.split_manager.set_active_split(new_split_id);
6388 tracing::info!(
6391 "Created {:?} split with virtual buffer {:?}",
6392 split_dir,
6393 buffer_id
6394 );
6395 Some(new_split_id)
6396 }
6397 Err(e) => {
6398 tracing::error!("Failed to create split: {}", e);
6399 self.set_active_buffer(buffer_id);
6401 None
6402 }
6403 };
6404
6405 if let Some(req_id) = request_id {
6408 tracing::trace!("CreateVirtualBufferInSplit: resolving callback for request_id={}, buffer_id={:?}, split_id={:?}", req_id, buffer_id, created_split_id);
6409 let result = fresh_core::api::VirtualBufferResult {
6410 buffer_id: buffer_id.0 as u64,
6411 split_id: created_split_id.map(|s| s.0 .0 as u64),
6412 };
6413 self.plugin_manager.resolve_callback(
6414 fresh_core::api::JsCallbackId::from(req_id),
6415 serde_json::to_string(&result).unwrap_or_default(),
6416 );
6417 }
6418 }
6419 PluginCommand::SetVirtualBufferContent { buffer_id, entries } => {
6420 match self.set_virtual_buffer_content(buffer_id, entries) {
6421 Ok(()) => {
6422 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
6423 }
6424 Err(e) => {
6425 tracing::error!("Failed to set virtual buffer content: {}", e);
6426 }
6427 }
6428 }
6429 PluginCommand::GetTextPropertiesAtCursor { buffer_id } => {
6430 if let Some(state) = self.buffers.get(&buffer_id) {
6432 let cursor_pos = self
6433 .split_view_states
6434 .values()
6435 .find_map(|vs| vs.buffer_state(buffer_id))
6436 .map(|bs| bs.cursors.primary().position)
6437 .unwrap_or(0);
6438 let properties = state.text_properties.get_at(cursor_pos);
6439 tracing::debug!(
6440 "Text properties at cursor in {:?}: {} properties found",
6441 buffer_id,
6442 properties.len()
6443 );
6444 }
6446 }
6447 PluginCommand::CreateVirtualBufferInExistingSplit {
6448 name,
6449 mode,
6450 read_only,
6451 entries,
6452 split_id,
6453 show_line_numbers,
6454 show_cursors,
6455 editing_disabled,
6456 line_wrap,
6457 request_id,
6458 } => {
6459 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
6461 tracing::info!(
6462 "Created virtual buffer '{}' with mode '{}' for existing split {:?} (id={:?})",
6463 name,
6464 mode,
6465 split_id,
6466 buffer_id
6467 );
6468
6469 if let Some(state) = self.buffers.get_mut(&buffer_id) {
6471 state.margins.configure_for_line_numbers(show_line_numbers);
6472 state.show_cursors = show_cursors;
6473 state.editing_disabled = editing_disabled;
6474 }
6475
6476 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
6478 tracing::error!("Failed to set virtual buffer content: {}", e);
6479 return Ok(());
6480 }
6481
6482 let leaf_id = LeafId(split_id);
6484 self.split_manager.set_split_buffer(leaf_id, buffer_id);
6485
6486 self.split_manager.set_active_split(leaf_id);
6488 self.split_manager.set_active_buffer_id(buffer_id);
6489
6490 if let Some(view_state) = self.split_view_states.get_mut(&leaf_id) {
6492 view_state.switch_buffer(buffer_id);
6493 view_state.add_buffer(buffer_id);
6494 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
6495
6496 if let Some(wrap) = line_wrap {
6498 view_state.active_state_mut().viewport.line_wrap_enabled = wrap;
6499 }
6500 }
6501
6502 tracing::info!(
6503 "Displayed virtual buffer {:?} in split {:?}",
6504 buffer_id,
6505 split_id
6506 );
6507
6508 if let Some(req_id) = request_id {
6510 let result = fresh_core::api::VirtualBufferResult {
6511 buffer_id: buffer_id.0 as u64,
6512 split_id: Some(split_id.0 as u64),
6513 };
6514 self.plugin_manager.resolve_callback(
6515 fresh_core::api::JsCallbackId::from(req_id),
6516 serde_json::to_string(&result).unwrap_or_default(),
6517 );
6518 }
6519 }
6520
6521 PluginCommand::SetContext { name, active } => {
6523 if active {
6524 self.active_custom_contexts.insert(name.clone());
6525 tracing::debug!("Set custom context: {}", name);
6526 } else {
6527 self.active_custom_contexts.remove(&name);
6528 tracing::debug!("Unset custom context: {}", name);
6529 }
6530 }
6531
6532 PluginCommand::SetReviewDiffHunks { hunks } => {
6534 self.review_hunks = hunks;
6535 tracing::debug!("Set {} review hunks", self.review_hunks.len());
6536 }
6537
6538 PluginCommand::ExecuteAction { action_name } => {
6540 self.handle_execute_action(action_name);
6541 }
6542 PluginCommand::ExecuteActions { actions } => {
6543 self.handle_execute_actions(actions);
6544 }
6545 PluginCommand::GetBufferText {
6546 buffer_id,
6547 start,
6548 end,
6549 request_id,
6550 } => {
6551 self.handle_get_buffer_text(buffer_id, start, end, request_id);
6552 }
6553 PluginCommand::GetLineStartPosition {
6554 buffer_id,
6555 line,
6556 request_id,
6557 } => {
6558 self.handle_get_line_start_position(buffer_id, line, request_id);
6559 }
6560 PluginCommand::GetLineEndPosition {
6561 buffer_id,
6562 line,
6563 request_id,
6564 } => {
6565 self.handle_get_line_end_position(buffer_id, line, request_id);
6566 }
6567 PluginCommand::GetBufferLineCount {
6568 buffer_id,
6569 request_id,
6570 } => {
6571 self.handle_get_buffer_line_count(buffer_id, request_id);
6572 }
6573 PluginCommand::ScrollToLineCenter {
6574 split_id,
6575 buffer_id,
6576 line,
6577 } => {
6578 self.handle_scroll_to_line_center(split_id, buffer_id, line);
6579 }
6580 PluginCommand::SetEditorMode { mode } => {
6581 self.handle_set_editor_mode(mode);
6582 }
6583
6584 PluginCommand::ShowActionPopup {
6586 popup_id,
6587 title,
6588 message,
6589 actions,
6590 } => {
6591 tracing::info!(
6592 "Action popup requested: id={}, title={}, actions={}",
6593 popup_id,
6594 title,
6595 actions.len()
6596 );
6597
6598 let items: Vec<crate::model::event::PopupListItemData> = actions
6600 .iter()
6601 .map(|action| crate::model::event::PopupListItemData {
6602 text: action.label.clone(),
6603 detail: None,
6604 icon: None,
6605 data: Some(action.id.clone()),
6606 })
6607 .collect();
6608
6609 let action_ids: Vec<(String, String)> =
6611 actions.into_iter().map(|a| (a.id, a.label)).collect();
6612 self.active_action_popup = Some((popup_id.clone(), action_ids));
6613
6614 let popup = crate::model::event::PopupData {
6616 kind: crate::model::event::PopupKindHint::List,
6617 title: Some(title),
6618 description: Some(message),
6619 transient: false,
6620 content: crate::model::event::PopupContentData::List { items, selected: 0 },
6621 position: crate::model::event::PopupPositionData::BottomRight,
6622 width: 60,
6623 max_height: 15,
6624 bordered: true,
6625 };
6626
6627 self.show_popup(popup);
6628 tracing::info!(
6629 "Action popup shown: id={}, active_action_popup={:?}",
6630 popup_id,
6631 self.active_action_popup.as_ref().map(|(id, _)| id)
6632 );
6633 }
6634
6635 PluginCommand::DisableLspForLanguage { language } => {
6636 tracing::info!("Disabling LSP for language: {}", language);
6637
6638 if let Some(ref mut lsp) = self.lsp {
6640 lsp.shutdown_server(&language);
6641 tracing::info!("Stopped LSP server for {}", language);
6642 }
6643
6644 if let Some(lsp_configs) = self.config.lsp.get_mut(&language) {
6646 for c in lsp_configs.as_mut_slice() {
6647 c.enabled = false;
6648 c.auto_start = false;
6649 }
6650 tracing::info!("Disabled LSP config for {}", language);
6651 }
6652
6653 if let Err(e) = self.save_config() {
6655 tracing::error!("Failed to save config: {}", e);
6656 self.status_message = Some(format!(
6657 "LSP disabled for {} (config save failed)",
6658 language
6659 ));
6660 } else {
6661 self.status_message = Some(format!("LSP disabled for {}", language));
6662 }
6663
6664 self.warning_domains.lsp.clear();
6666 }
6667
6668 PluginCommand::RestartLspForLanguage { language } => {
6669 tracing::info!("Plugin restarting LSP for language: {}", language);
6670
6671 let file_path = self
6672 .buffer_metadata
6673 .get(&self.active_buffer())
6674 .and_then(|meta| meta.file_path().cloned());
6675 let success = if let Some(ref mut lsp) = self.lsp {
6676 let (ok, msg) = lsp.manual_restart(&language, file_path.as_deref());
6677 self.status_message = Some(msg);
6678 ok
6679 } else {
6680 self.status_message = Some("No LSP manager available".to_string());
6681 false
6682 };
6683
6684 if success {
6685 self.reopen_buffers_for_language(&language);
6686 }
6687 }
6688
6689 PluginCommand::SetLspRootUri { language, uri } => {
6690 tracing::info!("Plugin setting LSP root URI for {}: {}", language, uri);
6691
6692 match uri.parse::<lsp_types::Uri>() {
6694 Ok(parsed_uri) => {
6695 if let Some(ref mut lsp) = self.lsp {
6696 let restarted = lsp.set_language_root_uri(&language, parsed_uri);
6697 if restarted {
6698 self.status_message = Some(format!(
6699 "LSP root updated for {} (restarting server)",
6700 language
6701 ));
6702 } else {
6703 self.status_message =
6704 Some(format!("LSP root set for {}", language));
6705 }
6706 }
6707 }
6708 Err(e) => {
6709 tracing::error!("Invalid LSP root URI '{}': {}", uri, e);
6710 self.status_message = Some(format!("Invalid LSP root URI: {}", e));
6711 }
6712 }
6713 }
6714
6715 PluginCommand::CreateScrollSyncGroup {
6717 group_id,
6718 left_split,
6719 right_split,
6720 } => {
6721 let success = self.scroll_sync_manager.create_group_with_id(
6722 group_id,
6723 left_split,
6724 right_split,
6725 );
6726 if success {
6727 tracing::debug!(
6728 "Created scroll sync group {} for splits {:?} and {:?}",
6729 group_id,
6730 left_split,
6731 right_split
6732 );
6733 } else {
6734 tracing::warn!(
6735 "Failed to create scroll sync group {} (ID already exists)",
6736 group_id
6737 );
6738 }
6739 }
6740 PluginCommand::SetScrollSyncAnchors { group_id, anchors } => {
6741 use crate::view::scroll_sync::SyncAnchor;
6742 let anchor_count = anchors.len();
6743 let sync_anchors: Vec<SyncAnchor> = anchors
6744 .into_iter()
6745 .map(|(left_line, right_line)| SyncAnchor {
6746 left_line,
6747 right_line,
6748 })
6749 .collect();
6750 self.scroll_sync_manager.set_anchors(group_id, sync_anchors);
6751 tracing::debug!(
6752 "Set {} anchors for scroll sync group {}",
6753 anchor_count,
6754 group_id
6755 );
6756 }
6757 PluginCommand::RemoveScrollSyncGroup { group_id } => {
6758 if self.scroll_sync_manager.remove_group(group_id) {
6759 tracing::debug!("Removed scroll sync group {}", group_id);
6760 } else {
6761 tracing::warn!("Scroll sync group {} not found", group_id);
6762 }
6763 }
6764
6765 PluginCommand::CreateCompositeBuffer {
6767 name,
6768 mode,
6769 layout,
6770 sources,
6771 hunks,
6772 request_id,
6773 } => {
6774 self.handle_create_composite_buffer(name, mode, layout, sources, hunks, request_id);
6775 }
6776 PluginCommand::UpdateCompositeAlignment { buffer_id, hunks } => {
6777 self.handle_update_composite_alignment(buffer_id, hunks);
6778 }
6779 PluginCommand::CloseCompositeBuffer { buffer_id } => {
6780 self.close_composite_buffer(buffer_id);
6781 }
6782
6783 PluginCommand::SaveBufferToPath { buffer_id, path } => {
6785 self.handle_save_buffer_to_path(buffer_id, path);
6786 }
6787
6788 #[cfg(feature = "plugins")]
6790 PluginCommand::LoadPlugin { path, callback_id } => {
6791 self.handle_load_plugin(path, callback_id);
6792 }
6793 #[cfg(feature = "plugins")]
6794 PluginCommand::UnloadPlugin { name, callback_id } => {
6795 self.handle_unload_plugin(name, callback_id);
6796 }
6797 #[cfg(feature = "plugins")]
6798 PluginCommand::ReloadPlugin { name, callback_id } => {
6799 self.handle_reload_plugin(name, callback_id);
6800 }
6801 #[cfg(feature = "plugins")]
6802 PluginCommand::ListPlugins { callback_id } => {
6803 self.handle_list_plugins(callback_id);
6804 }
6805 #[cfg(not(feature = "plugins"))]
6807 PluginCommand::LoadPlugin { .. }
6808 | PluginCommand::UnloadPlugin { .. }
6809 | PluginCommand::ReloadPlugin { .. }
6810 | PluginCommand::ListPlugins { .. } => {
6811 tracing::warn!("Plugin management commands require the 'plugins' feature");
6812 }
6813
6814 PluginCommand::CreateTerminal {
6816 cwd,
6817 direction,
6818 ratio,
6819 focus,
6820 request_id,
6821 } => {
6822 let (cols, rows) = self.get_terminal_dimensions();
6823
6824 if let Some(ref bridge) = self.async_bridge {
6826 self.terminal_manager.set_async_bridge(bridge.clone());
6827 }
6828
6829 let working_dir = cwd
6831 .map(std::path::PathBuf::from)
6832 .unwrap_or_else(|| self.working_dir.clone());
6833
6834 let terminal_root = self.dir_context.terminal_dir_for(&working_dir);
6836 if let Err(e) = self.filesystem.create_dir_all(&terminal_root) {
6837 tracing::warn!("Failed to create terminal directory: {}", e);
6838 }
6839 let predicted_terminal_id = self.terminal_manager.next_terminal_id();
6840 let log_path =
6841 terminal_root.join(format!("fresh-terminal-{}.log", predicted_terminal_id.0));
6842 let backing_path =
6843 terminal_root.join(format!("fresh-terminal-{}.txt", predicted_terminal_id.0));
6844 self.terminal_backing_files
6845 .insert(predicted_terminal_id, backing_path);
6846 let backing_path_for_spawn = self
6847 .terminal_backing_files
6848 .get(&predicted_terminal_id)
6849 .cloned();
6850
6851 match self.terminal_manager.spawn(
6852 cols,
6853 rows,
6854 Some(working_dir),
6855 Some(log_path.clone()),
6856 backing_path_for_spawn,
6857 ) {
6858 Ok(terminal_id) => {
6859 self.terminal_log_files
6861 .insert(terminal_id, log_path.clone());
6862 if terminal_id != predicted_terminal_id {
6864 self.terminal_backing_files.remove(&predicted_terminal_id);
6865 let backing_path =
6866 terminal_root.join(format!("fresh-terminal-{}.txt", terminal_id.0));
6867 self.terminal_backing_files
6868 .insert(terminal_id, backing_path);
6869 }
6870
6871 let active_split = self.split_manager.active_split();
6873 let buffer_id =
6874 self.create_terminal_buffer_attached(terminal_id, active_split);
6875
6876 let created_split_id = if let Some(dir_str) = direction.as_deref() {
6880 let split_dir = match dir_str {
6881 "horizontal" => crate::model::event::SplitDirection::Horizontal,
6882 _ => crate::model::event::SplitDirection::Vertical,
6883 };
6884
6885 let split_ratio = ratio.unwrap_or(0.5);
6886 match self
6887 .split_manager
6888 .split_active(split_dir, buffer_id, split_ratio)
6889 {
6890 Ok(new_split_id) => {
6891 let mut view_state = SplitViewState::with_buffer(
6892 self.terminal_width,
6893 self.terminal_height,
6894 buffer_id,
6895 );
6896 view_state.apply_config_defaults(
6897 self.config.editor.line_numbers,
6898 self.config.editor.highlight_current_line,
6899 false,
6900 false,
6901 None,
6902 self.config.editor.rulers.clone(),
6903 );
6904 self.split_view_states.insert(new_split_id, view_state);
6905
6906 if focus.unwrap_or(true) {
6907 self.split_manager.set_active_split(new_split_id);
6908 }
6909
6910 tracing::info!(
6911 "Created {:?} split for terminal {:?} with buffer {:?}",
6912 split_dir,
6913 terminal_id,
6914 buffer_id
6915 );
6916 Some(new_split_id)
6917 }
6918 Err(e) => {
6919 tracing::error!("Failed to create split for terminal: {}", e);
6920 self.set_active_buffer(buffer_id);
6921 None
6922 }
6923 }
6924 } else {
6925 self.set_active_buffer(buffer_id);
6927 None
6928 };
6929
6930 self.resize_visible_terminals();
6932
6933 let result = fresh_core::api::TerminalResult {
6935 buffer_id: buffer_id.0 as u64,
6936 terminal_id: terminal_id.0 as u64,
6937 split_id: created_split_id.map(|s| s.0 .0 as u64),
6938 };
6939 self.plugin_manager.resolve_callback(
6940 fresh_core::api::JsCallbackId::from(request_id),
6941 serde_json::to_string(&result).unwrap_or_default(),
6942 );
6943
6944 tracing::info!(
6945 "Plugin created terminal {:?} with buffer {:?}",
6946 terminal_id,
6947 buffer_id
6948 );
6949 }
6950 Err(e) => {
6951 tracing::error!("Failed to create terminal for plugin: {}", e);
6952 self.plugin_manager.reject_callback(
6953 fresh_core::api::JsCallbackId::from(request_id),
6954 format!("Failed to create terminal: {}", e),
6955 );
6956 }
6957 }
6958 }
6959
6960 PluginCommand::SendTerminalInput { terminal_id, data } => {
6961 if let Some(handle) = self.terminal_manager.get(terminal_id) {
6962 handle.write(data.as_bytes());
6963 tracing::trace!(
6964 "Plugin sent {} bytes to terminal {:?}",
6965 data.len(),
6966 terminal_id
6967 );
6968 } else {
6969 tracing::warn!(
6970 "Plugin tried to send input to non-existent terminal {:?}",
6971 terminal_id
6972 );
6973 }
6974 }
6975
6976 PluginCommand::CloseTerminal { terminal_id } => {
6977 let buffer_to_close = self
6979 .terminal_buffers
6980 .iter()
6981 .find(|(_, &tid)| tid == terminal_id)
6982 .map(|(&bid, _)| bid);
6983
6984 if let Some(buffer_id) = buffer_to_close {
6985 if let Err(e) = self.close_buffer(buffer_id) {
6986 tracing::warn!("Failed to close terminal buffer: {}", e);
6987 }
6988 tracing::info!("Plugin closed terminal {:?}", terminal_id);
6989 } else {
6990 self.terminal_manager.close(terminal_id);
6992 tracing::info!("Plugin closed terminal {:?} (no buffer found)", terminal_id);
6993 }
6994 }
6995
6996 PluginCommand::GrepProject {
6997 pattern,
6998 fixed_string,
6999 case_sensitive,
7000 max_results,
7001 whole_words,
7002 callback_id,
7003 } => {
7004 self.handle_grep_project(
7005 pattern,
7006 fixed_string,
7007 case_sensitive,
7008 max_results,
7009 whole_words,
7010 callback_id,
7011 );
7012 }
7013
7014 PluginCommand::GrepProjectStreaming {
7015 pattern,
7016 fixed_string,
7017 case_sensitive,
7018 max_results,
7019 whole_words,
7020 search_id,
7021 callback_id,
7022 } => {
7023 self.handle_grep_project_streaming(
7024 pattern,
7025 fixed_string,
7026 case_sensitive,
7027 max_results,
7028 whole_words,
7029 search_id,
7030 callback_id,
7031 );
7032 }
7033
7034 PluginCommand::ReplaceInBuffer {
7035 file_path,
7036 matches,
7037 replacement,
7038 callback_id,
7039 } => {
7040 self.handle_replace_in_buffer(file_path, matches, replacement, callback_id);
7041 }
7042 }
7043 Ok(())
7044 }
7045
7046 fn handle_save_buffer_to_path(&mut self, buffer_id: BufferId, path: std::path::PathBuf) {
7048 if let Some(state) = self.buffers.get_mut(&buffer_id) {
7049 match state.buffer.save_to_file(&path) {
7051 Ok(()) => {
7052 if let Err(e) = self.finalize_save(Some(path)) {
7055 tracing::warn!("Failed to finalize save: {}", e);
7056 }
7057 tracing::debug!("Saved buffer {:?} to path", buffer_id);
7058 }
7059 Err(e) => {
7060 self.handle_set_status(format!("Error saving: {}", e));
7061 tracing::error!("Failed to save buffer to path: {}", e);
7062 }
7063 }
7064 } else {
7065 self.handle_set_status(format!("Buffer {:?} not found", buffer_id));
7066 tracing::warn!("SaveBufferToPath: buffer {:?} not found", buffer_id);
7067 }
7068 }
7069
7070 #[cfg(feature = "plugins")]
7072 fn handle_load_plugin(&mut self, path: std::path::PathBuf, callback_id: JsCallbackId) {
7073 match self.plugin_manager.load_plugin(&path) {
7074 Ok(()) => {
7075 tracing::info!("Loaded plugin from {:?}", path);
7076 self.plugin_manager
7077 .resolve_callback(callback_id, "true".to_string());
7078 }
7079 Err(e) => {
7080 tracing::error!("Failed to load plugin from {:?}: {}", path, e);
7081 self.plugin_manager
7082 .reject_callback(callback_id, format!("{}", e));
7083 }
7084 }
7085 }
7086
7087 #[cfg(feature = "plugins")]
7089 fn handle_unload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
7090 match self.plugin_manager.unload_plugin(&name) {
7091 Ok(()) => {
7092 tracing::info!("Unloaded plugin: {}", name);
7093 self.plugin_manager
7094 .resolve_callback(callback_id, "true".to_string());
7095 }
7096 Err(e) => {
7097 tracing::error!("Failed to unload plugin '{}': {}", name, e);
7098 self.plugin_manager
7099 .reject_callback(callback_id, format!("{}", e));
7100 }
7101 }
7102 }
7103
7104 #[cfg(feature = "plugins")]
7106 fn handle_reload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
7107 match self.plugin_manager.reload_plugin(&name) {
7108 Ok(()) => {
7109 tracing::info!("Reloaded plugin: {}", name);
7110 self.plugin_manager
7111 .resolve_callback(callback_id, "true".to_string());
7112 }
7113 Err(e) => {
7114 tracing::error!("Failed to reload plugin '{}': {}", name, e);
7115 self.plugin_manager
7116 .reject_callback(callback_id, format!("{}", e));
7117 }
7118 }
7119 }
7120
7121 #[cfg(feature = "plugins")]
7123 fn handle_list_plugins(&mut self, callback_id: JsCallbackId) {
7124 let plugins = self.plugin_manager.list_plugins();
7125 let json_array: Vec<serde_json::Value> = plugins
7127 .iter()
7128 .map(|p| {
7129 serde_json::json!({
7130 "name": p.name,
7131 "path": p.path.to_string_lossy(),
7132 "enabled": p.enabled
7133 })
7134 })
7135 .collect();
7136 let json_str = serde_json::to_string(&json_array).unwrap_or_else(|_| "[]".to_string());
7137 self.plugin_manager.resolve_callback(callback_id, json_str);
7138 }
7139
7140 fn handle_execute_action(&mut self, action_name: String) {
7142 use crate::input::keybindings::Action;
7143 use std::collections::HashMap;
7144
7145 if let Some(action) = Action::from_str(&action_name, &HashMap::new()) {
7147 if let Err(e) = self.handle_action(action) {
7149 tracing::warn!("Failed to execute action '{}': {}", action_name, e);
7150 } else {
7151 tracing::debug!("Executed action: {}", action_name);
7152 }
7153 } else {
7154 tracing::warn!("Unknown action: {}", action_name);
7155 }
7156 }
7157
7158 fn handle_execute_actions(&mut self, actions: Vec<fresh_core::api::ActionSpec>) {
7161 use crate::input::keybindings::Action;
7162 use std::collections::HashMap;
7163
7164 for action_spec in actions {
7165 if let Some(action) = Action::from_str(&action_spec.action, &HashMap::new()) {
7166 for _ in 0..action_spec.count {
7168 if let Err(e) = self.handle_action(action.clone()) {
7169 tracing::warn!("Failed to execute action '{}': {}", action_spec.action, e);
7170 return; }
7172 }
7173 tracing::debug!(
7174 "Executed action '{}' {} time(s)",
7175 action_spec.action,
7176 action_spec.count
7177 );
7178 } else {
7179 tracing::warn!("Unknown action: {}", action_spec.action);
7180 return; }
7182 }
7183 }
7184
7185 fn handle_get_buffer_text(
7187 &mut self,
7188 buffer_id: BufferId,
7189 start: usize,
7190 end: usize,
7191 request_id: u64,
7192 ) {
7193 let result = if let Some(state) = self.buffers.get_mut(&buffer_id) {
7194 let len = state.buffer.len();
7196 if start <= end && end <= len {
7197 Ok(state.get_text_range(start, end))
7198 } else {
7199 Err(format!(
7200 "Invalid range {}..{} for buffer of length {}",
7201 start, end, len
7202 ))
7203 }
7204 } else {
7205 Err(format!("Buffer {:?} not found", buffer_id))
7206 };
7207
7208 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
7210 match result {
7211 Ok(text) => {
7212 let json = serde_json::to_string(&text).unwrap_or_else(|_| "null".to_string());
7214 self.plugin_manager.resolve_callback(callback_id, json);
7215 }
7216 Err(error) => {
7217 self.plugin_manager.reject_callback(callback_id, error);
7218 }
7219 }
7220 }
7221
7222 fn handle_set_editor_mode(&mut self, mode: Option<String>) {
7224 self.editor_mode = mode.clone();
7225 tracing::debug!("Set editor mode: {:?}", mode);
7226 }
7227
7228 fn handle_get_line_start_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
7230 let actual_buffer_id = if buffer_id.0 == 0 {
7232 self.active_buffer_id()
7233 } else {
7234 buffer_id
7235 };
7236
7237 let result = if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
7238 let line_number = line as usize;
7240 let buffer_len = state.buffer.len();
7241
7242 if line_number == 0 {
7243 Some(0)
7245 } else {
7246 let mut current_line = 0;
7248 let mut line_start = None;
7249
7250 let content = state.get_text_range(0, buffer_len);
7252 for (byte_idx, c) in content.char_indices() {
7253 if c == '\n' {
7254 current_line += 1;
7255 if current_line == line_number {
7256 line_start = Some(byte_idx + 1);
7258 break;
7259 }
7260 }
7261 }
7262 line_start
7263 }
7264 } else {
7265 None
7266 };
7267
7268 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
7270 let json = serde_json::to_string(&result).unwrap_or_else(|_| "null".to_string());
7272 self.plugin_manager.resolve_callback(callback_id, json);
7273 }
7274
7275 fn handle_get_line_end_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
7278 let actual_buffer_id = if buffer_id.0 == 0 {
7280 self.active_buffer_id()
7281 } else {
7282 buffer_id
7283 };
7284
7285 let result = if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
7286 let line_number = line as usize;
7287 let buffer_len = state.buffer.len();
7288
7289 let content = state.get_text_range(0, buffer_len);
7291 let mut current_line = 0;
7292 let mut line_end = None;
7293
7294 for (byte_idx, c) in content.char_indices() {
7295 if c == '\n' {
7296 if current_line == line_number {
7297 line_end = Some(byte_idx);
7299 break;
7300 }
7301 current_line += 1;
7302 }
7303 }
7304
7305 if line_end.is_none() && current_line == line_number {
7307 line_end = Some(buffer_len);
7308 }
7309
7310 line_end
7311 } else {
7312 None
7313 };
7314
7315 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
7316 let json = serde_json::to_string(&result).unwrap_or_else(|_| "null".to_string());
7317 self.plugin_manager.resolve_callback(callback_id, json);
7318 }
7319
7320 fn handle_get_buffer_line_count(&mut self, buffer_id: BufferId, request_id: u64) {
7322 let actual_buffer_id = if buffer_id.0 == 0 {
7324 self.active_buffer_id()
7325 } else {
7326 buffer_id
7327 };
7328
7329 let result = if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
7330 let buffer_len = state.buffer.len();
7331 let content = state.get_text_range(0, buffer_len);
7332
7333 if content.is_empty() {
7335 Some(1) } else {
7337 let newline_count = content.chars().filter(|&c| c == '\n').count();
7338 let ends_with_newline = content.ends_with('\n');
7340 if ends_with_newline {
7341 Some(newline_count)
7342 } else {
7343 Some(newline_count + 1)
7344 }
7345 }
7346 } else {
7347 None
7348 };
7349
7350 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
7351 let json = serde_json::to_string(&result).unwrap_or_else(|_| "null".to_string());
7352 self.plugin_manager.resolve_callback(callback_id, json);
7353 }
7354
7355 fn handle_scroll_to_line_center(
7357 &mut self,
7358 split_id: SplitId,
7359 buffer_id: BufferId,
7360 line: usize,
7361 ) {
7362 let actual_split_id = if split_id.0 == 0 {
7364 self.split_manager.active_split()
7365 } else {
7366 LeafId(split_id)
7367 };
7368
7369 let actual_buffer_id = if buffer_id.0 == 0 {
7371 self.active_buffer()
7372 } else {
7373 buffer_id
7374 };
7375
7376 let viewport_height = if let Some(view_state) = self.split_view_states.get(&actual_split_id)
7378 {
7379 view_state.viewport.height as usize
7380 } else {
7381 return;
7382 };
7383
7384 let lines_above = viewport_height / 2;
7386 let target_line = line.saturating_sub(lines_above);
7387
7388 if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
7390 let buffer = &mut state.buffer;
7391 if let Some(view_state) = self.split_view_states.get_mut(&actual_split_id) {
7392 view_state.viewport.scroll_to(buffer, target_line);
7393 view_state.viewport.set_skip_ensure_visible();
7395 }
7396 }
7397 }
7398}
7399
7400fn parse_key_string(key_str: &str) -> Option<(KeyCode, KeyModifiers)> {
7409 use crossterm::event::{KeyCode, KeyModifiers};
7410
7411 let mut modifiers = KeyModifiers::NONE;
7412 let mut remaining = key_str;
7413
7414 loop {
7416 if remaining.starts_with("C-") {
7417 modifiers |= KeyModifiers::CONTROL;
7418 remaining = &remaining[2..];
7419 } else if remaining.starts_with("M-") {
7420 modifiers |= KeyModifiers::ALT;
7421 remaining = &remaining[2..];
7422 } else if remaining.starts_with("S-") {
7423 modifiers |= KeyModifiers::SHIFT;
7424 remaining = &remaining[2..];
7425 } else {
7426 break;
7427 }
7428 }
7429
7430 let upper = remaining.to_uppercase();
7433 let code = match upper.as_str() {
7434 "RET" | "RETURN" | "ENTER" => KeyCode::Enter,
7435 "TAB" => KeyCode::Tab,
7436 "BACKTAB" => KeyCode::BackTab,
7437 "ESC" | "ESCAPE" => KeyCode::Esc,
7438 "SPC" | "SPACE" => KeyCode::Char(' '),
7439 "DEL" | "DELETE" => KeyCode::Delete,
7440 "BS" | "BACKSPACE" => KeyCode::Backspace,
7441 "UP" => KeyCode::Up,
7442 "DOWN" => KeyCode::Down,
7443 "LEFT" => KeyCode::Left,
7444 "RIGHT" => KeyCode::Right,
7445 "HOME" => KeyCode::Home,
7446 "END" => KeyCode::End,
7447 "PAGEUP" | "PGUP" => KeyCode::PageUp,
7448 "PAGEDOWN" | "PGDN" => KeyCode::PageDown,
7449 s if s.starts_with('F') && s.len() > 1 => {
7450 if let Ok(n) = s[1..].parse::<u8>() {
7452 KeyCode::F(n)
7453 } else {
7454 return None;
7455 }
7456 }
7457 _ if remaining.len() == 1 => {
7458 let c = remaining.chars().next()?;
7461 if c.is_ascii_uppercase() {
7462 modifiers |= KeyModifiers::SHIFT;
7463 }
7464 KeyCode::Char(c.to_ascii_lowercase())
7465 }
7466 _ => return None,
7467 };
7468
7469 Some((code, modifiers))
7470}
7471
7472#[cfg(test)]
7473mod tests {
7474 use super::*;
7475 use tempfile::TempDir;
7476
7477 fn test_dir_context() -> (DirectoryContext, TempDir) {
7479 let temp_dir = TempDir::new().unwrap();
7480 let dir_context = DirectoryContext::for_testing(temp_dir.path());
7481 (dir_context, temp_dir)
7482 }
7483
7484 fn test_filesystem() -> Arc<dyn FileSystem + Send + Sync> {
7486 Arc::new(crate::model::filesystem::StdFileSystem)
7487 }
7488
7489 #[test]
7490 fn test_editor_new() {
7491 let config = Config::default();
7492 let (dir_context, _temp) = test_dir_context();
7493 let editor = Editor::new(
7494 config,
7495 80,
7496 24,
7497 dir_context,
7498 crate::view::color_support::ColorCapability::TrueColor,
7499 test_filesystem(),
7500 )
7501 .unwrap();
7502
7503 assert_eq!(editor.buffers.len(), 1);
7504 assert!(!editor.should_quit());
7505 }
7506
7507 #[test]
7508 fn test_new_buffer() {
7509 let config = Config::default();
7510 let (dir_context, _temp) = test_dir_context();
7511 let mut editor = Editor::new(
7512 config,
7513 80,
7514 24,
7515 dir_context,
7516 crate::view::color_support::ColorCapability::TrueColor,
7517 test_filesystem(),
7518 )
7519 .unwrap();
7520
7521 let id = editor.new_buffer();
7522 assert_eq!(editor.buffers.len(), 2);
7523 assert_eq!(editor.active_buffer(), id);
7524 }
7525
7526 #[test]
7527 #[ignore]
7528 fn test_clipboard() {
7529 let config = Config::default();
7530 let (dir_context, _temp) = test_dir_context();
7531 let mut editor = Editor::new(
7532 config,
7533 80,
7534 24,
7535 dir_context,
7536 crate::view::color_support::ColorCapability::TrueColor,
7537 test_filesystem(),
7538 )
7539 .unwrap();
7540
7541 editor.clipboard.set_internal("test".to_string());
7543
7544 editor.paste();
7546
7547 let content = editor.active_state().buffer.to_string().unwrap();
7548 assert_eq!(content, "test");
7549 }
7550
7551 #[test]
7552 fn test_action_to_events_insert_char() {
7553 let config = Config::default();
7554 let (dir_context, _temp) = test_dir_context();
7555 let mut editor = Editor::new(
7556 config,
7557 80,
7558 24,
7559 dir_context,
7560 crate::view::color_support::ColorCapability::TrueColor,
7561 test_filesystem(),
7562 )
7563 .unwrap();
7564
7565 let events = editor.action_to_events(Action::InsertChar('a'));
7566 assert!(events.is_some());
7567
7568 let events = events.unwrap();
7569 assert_eq!(events.len(), 1);
7570
7571 match &events[0] {
7572 Event::Insert { position, text, .. } => {
7573 assert_eq!(*position, 0);
7574 assert_eq!(text, "a");
7575 }
7576 _ => panic!("Expected Insert event"),
7577 }
7578 }
7579
7580 #[test]
7581 fn test_action_to_events_move_right() {
7582 let config = Config::default();
7583 let (dir_context, _temp) = test_dir_context();
7584 let mut editor = Editor::new(
7585 config,
7586 80,
7587 24,
7588 dir_context,
7589 crate::view::color_support::ColorCapability::TrueColor,
7590 test_filesystem(),
7591 )
7592 .unwrap();
7593
7594 let cursor_id = editor.active_cursors().primary_id();
7596 editor.apply_event_to_active_buffer(&Event::Insert {
7597 position: 0,
7598 text: "hello".to_string(),
7599 cursor_id,
7600 });
7601
7602 let events = editor.action_to_events(Action::MoveRight);
7603 assert!(events.is_some());
7604
7605 let events = events.unwrap();
7606 assert_eq!(events.len(), 1);
7607
7608 match &events[0] {
7609 Event::MoveCursor {
7610 new_position,
7611 new_anchor,
7612 ..
7613 } => {
7614 assert_eq!(*new_position, 5);
7616 assert_eq!(*new_anchor, None); }
7618 _ => panic!("Expected MoveCursor event"),
7619 }
7620 }
7621
7622 #[test]
7623 fn test_action_to_events_move_up_down() {
7624 let config = Config::default();
7625 let (dir_context, _temp) = test_dir_context();
7626 let mut editor = Editor::new(
7627 config,
7628 80,
7629 24,
7630 dir_context,
7631 crate::view::color_support::ColorCapability::TrueColor,
7632 test_filesystem(),
7633 )
7634 .unwrap();
7635
7636 let cursor_id = editor.active_cursors().primary_id();
7638 editor.apply_event_to_active_buffer(&Event::Insert {
7639 position: 0,
7640 text: "line1\nline2\nline3".to_string(),
7641 cursor_id,
7642 });
7643
7644 editor.apply_event_to_active_buffer(&Event::MoveCursor {
7646 cursor_id,
7647 old_position: 0, new_position: 6,
7649 old_anchor: None, new_anchor: None,
7651 old_sticky_column: 0,
7652 new_sticky_column: 0,
7653 });
7654
7655 let events = editor.action_to_events(Action::MoveUp);
7657 assert!(events.is_some());
7658 let events = events.unwrap();
7659 assert_eq!(events.len(), 1);
7660
7661 match &events[0] {
7662 Event::MoveCursor { new_position, .. } => {
7663 assert_eq!(*new_position, 0); }
7665 _ => panic!("Expected MoveCursor event"),
7666 }
7667 }
7668
7669 #[test]
7670 fn test_action_to_events_insert_newline() {
7671 let config = Config::default();
7672 let (dir_context, _temp) = test_dir_context();
7673 let mut editor = Editor::new(
7674 config,
7675 80,
7676 24,
7677 dir_context,
7678 crate::view::color_support::ColorCapability::TrueColor,
7679 test_filesystem(),
7680 )
7681 .unwrap();
7682
7683 let events = editor.action_to_events(Action::InsertNewline);
7684 assert!(events.is_some());
7685
7686 let events = events.unwrap();
7687 assert_eq!(events.len(), 1);
7688
7689 match &events[0] {
7690 Event::Insert { text, .. } => {
7691 assert_eq!(text, "\n");
7692 }
7693 _ => panic!("Expected Insert event"),
7694 }
7695 }
7696
7697 #[test]
7698 fn test_action_to_events_unimplemented() {
7699 let config = Config::default();
7700 let (dir_context, _temp) = test_dir_context();
7701 let mut editor = Editor::new(
7702 config,
7703 80,
7704 24,
7705 dir_context,
7706 crate::view::color_support::ColorCapability::TrueColor,
7707 test_filesystem(),
7708 )
7709 .unwrap();
7710
7711 assert!(editor.action_to_events(Action::Save).is_none());
7713 assert!(editor.action_to_events(Action::Quit).is_none());
7714 assert!(editor.action_to_events(Action::Undo).is_none());
7715 }
7716
7717 #[test]
7718 fn test_action_to_events_delete_backward() {
7719 let config = Config::default();
7720 let (dir_context, _temp) = test_dir_context();
7721 let mut editor = Editor::new(
7722 config,
7723 80,
7724 24,
7725 dir_context,
7726 crate::view::color_support::ColorCapability::TrueColor,
7727 test_filesystem(),
7728 )
7729 .unwrap();
7730
7731 let cursor_id = editor.active_cursors().primary_id();
7733 editor.apply_event_to_active_buffer(&Event::Insert {
7734 position: 0,
7735 text: "hello".to_string(),
7736 cursor_id,
7737 });
7738
7739 let events = editor.action_to_events(Action::DeleteBackward);
7740 assert!(events.is_some());
7741
7742 let events = events.unwrap();
7743 assert_eq!(events.len(), 1);
7744
7745 match &events[0] {
7746 Event::Delete {
7747 range,
7748 deleted_text,
7749 ..
7750 } => {
7751 assert_eq!(range.clone(), 4..5); assert_eq!(deleted_text, "o");
7753 }
7754 _ => panic!("Expected Delete event"),
7755 }
7756 }
7757
7758 #[test]
7759 fn test_action_to_events_delete_forward() {
7760 let config = Config::default();
7761 let (dir_context, _temp) = test_dir_context();
7762 let mut editor = Editor::new(
7763 config,
7764 80,
7765 24,
7766 dir_context,
7767 crate::view::color_support::ColorCapability::TrueColor,
7768 test_filesystem(),
7769 )
7770 .unwrap();
7771
7772 let cursor_id = editor.active_cursors().primary_id();
7774 editor.apply_event_to_active_buffer(&Event::Insert {
7775 position: 0,
7776 text: "hello".to_string(),
7777 cursor_id,
7778 });
7779
7780 editor.apply_event_to_active_buffer(&Event::MoveCursor {
7782 cursor_id,
7783 old_position: 0, new_position: 0,
7785 old_anchor: None, new_anchor: None,
7787 old_sticky_column: 0,
7788 new_sticky_column: 0,
7789 });
7790
7791 let events = editor.action_to_events(Action::DeleteForward);
7792 assert!(events.is_some());
7793
7794 let events = events.unwrap();
7795 assert_eq!(events.len(), 1);
7796
7797 match &events[0] {
7798 Event::Delete {
7799 range,
7800 deleted_text,
7801 ..
7802 } => {
7803 assert_eq!(range.clone(), 0..1); assert_eq!(deleted_text, "h");
7805 }
7806 _ => panic!("Expected Delete event"),
7807 }
7808 }
7809
7810 #[test]
7811 fn test_action_to_events_select_right() {
7812 let config = Config::default();
7813 let (dir_context, _temp) = test_dir_context();
7814 let mut editor = Editor::new(
7815 config,
7816 80,
7817 24,
7818 dir_context,
7819 crate::view::color_support::ColorCapability::TrueColor,
7820 test_filesystem(),
7821 )
7822 .unwrap();
7823
7824 let cursor_id = editor.active_cursors().primary_id();
7826 editor.apply_event_to_active_buffer(&Event::Insert {
7827 position: 0,
7828 text: "hello".to_string(),
7829 cursor_id,
7830 });
7831
7832 editor.apply_event_to_active_buffer(&Event::MoveCursor {
7834 cursor_id,
7835 old_position: 0, new_position: 0,
7837 old_anchor: None, new_anchor: None,
7839 old_sticky_column: 0,
7840 new_sticky_column: 0,
7841 });
7842
7843 let events = editor.action_to_events(Action::SelectRight);
7844 assert!(events.is_some());
7845
7846 let events = events.unwrap();
7847 assert_eq!(events.len(), 1);
7848
7849 match &events[0] {
7850 Event::MoveCursor {
7851 new_position,
7852 new_anchor,
7853 ..
7854 } => {
7855 assert_eq!(*new_position, 1); assert_eq!(*new_anchor, Some(0)); }
7858 _ => panic!("Expected MoveCursor event"),
7859 }
7860 }
7861
7862 #[test]
7863 fn test_action_to_events_select_all() {
7864 let config = Config::default();
7865 let (dir_context, _temp) = test_dir_context();
7866 let mut editor = Editor::new(
7867 config,
7868 80,
7869 24,
7870 dir_context,
7871 crate::view::color_support::ColorCapability::TrueColor,
7872 test_filesystem(),
7873 )
7874 .unwrap();
7875
7876 let cursor_id = editor.active_cursors().primary_id();
7878 editor.apply_event_to_active_buffer(&Event::Insert {
7879 position: 0,
7880 text: "hello world".to_string(),
7881 cursor_id,
7882 });
7883
7884 let events = editor.action_to_events(Action::SelectAll);
7885 assert!(events.is_some());
7886
7887 let events = events.unwrap();
7888 assert_eq!(events.len(), 1);
7889
7890 match &events[0] {
7891 Event::MoveCursor {
7892 new_position,
7893 new_anchor,
7894 ..
7895 } => {
7896 assert_eq!(*new_position, 11); assert_eq!(*new_anchor, Some(0)); }
7899 _ => panic!("Expected MoveCursor event"),
7900 }
7901 }
7902
7903 #[test]
7904 fn test_action_to_events_document_nav() {
7905 let config = Config::default();
7906 let (dir_context, _temp) = test_dir_context();
7907 let mut editor = Editor::new(
7908 config,
7909 80,
7910 24,
7911 dir_context,
7912 crate::view::color_support::ColorCapability::TrueColor,
7913 test_filesystem(),
7914 )
7915 .unwrap();
7916
7917 let cursor_id = editor.active_cursors().primary_id();
7919 editor.apply_event_to_active_buffer(&Event::Insert {
7920 position: 0,
7921 text: "line1\nline2\nline3".to_string(),
7922 cursor_id,
7923 });
7924
7925 let events = editor.action_to_events(Action::MoveDocumentStart);
7927 assert!(events.is_some());
7928 let events = events.unwrap();
7929 match &events[0] {
7930 Event::MoveCursor { new_position, .. } => {
7931 assert_eq!(*new_position, 0);
7932 }
7933 _ => panic!("Expected MoveCursor event"),
7934 }
7935
7936 let events = editor.action_to_events(Action::MoveDocumentEnd);
7938 assert!(events.is_some());
7939 let events = events.unwrap();
7940 match &events[0] {
7941 Event::MoveCursor { new_position, .. } => {
7942 assert_eq!(*new_position, 17); }
7944 _ => panic!("Expected MoveCursor event"),
7945 }
7946 }
7947
7948 #[test]
7949 fn test_action_to_events_remove_secondary_cursors() {
7950 use crate::model::event::CursorId;
7951
7952 let config = Config::default();
7953 let (dir_context, _temp) = test_dir_context();
7954 let mut editor = Editor::new(
7955 config,
7956 80,
7957 24,
7958 dir_context,
7959 crate::view::color_support::ColorCapability::TrueColor,
7960 test_filesystem(),
7961 )
7962 .unwrap();
7963
7964 let cursor_id = editor.active_cursors().primary_id();
7966 editor.apply_event_to_active_buffer(&Event::Insert {
7967 position: 0,
7968 text: "hello world test".to_string(),
7969 cursor_id,
7970 });
7971
7972 editor.apply_event_to_active_buffer(&Event::AddCursor {
7974 cursor_id: CursorId(1),
7975 position: 5,
7976 anchor: None,
7977 });
7978 editor.apply_event_to_active_buffer(&Event::AddCursor {
7979 cursor_id: CursorId(2),
7980 position: 10,
7981 anchor: None,
7982 });
7983
7984 assert_eq!(editor.active_cursors().count(), 3);
7985
7986 let first_id = editor
7988 .active_cursors()
7989 .iter()
7990 .map(|(id, _)| id)
7991 .min_by_key(|id| id.0)
7992 .expect("Should have at least one cursor");
7993
7994 let events = editor.action_to_events(Action::RemoveSecondaryCursors);
7996 assert!(events.is_some());
7997
7998 let events = events.unwrap();
7999 let remove_cursor_events: Vec<_> = events
8002 .iter()
8003 .filter_map(|e| match e {
8004 Event::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
8005 _ => None,
8006 })
8007 .collect();
8008
8009 assert_eq!(remove_cursor_events.len(), 2);
8011
8012 for cursor_id in &remove_cursor_events {
8013 assert_ne!(*cursor_id, first_id);
8015 }
8016 }
8017
8018 #[test]
8019 fn test_action_to_events_scroll() {
8020 let config = Config::default();
8021 let (dir_context, _temp) = test_dir_context();
8022 let mut editor = Editor::new(
8023 config,
8024 80,
8025 24,
8026 dir_context,
8027 crate::view::color_support::ColorCapability::TrueColor,
8028 test_filesystem(),
8029 )
8030 .unwrap();
8031
8032 let events = editor.action_to_events(Action::ScrollUp);
8034 assert!(events.is_some());
8035 let events = events.unwrap();
8036 assert_eq!(events.len(), 1);
8037 match &events[0] {
8038 Event::Scroll { line_offset } => {
8039 assert_eq!(*line_offset, -1);
8040 }
8041 _ => panic!("Expected Scroll event"),
8042 }
8043
8044 let events = editor.action_to_events(Action::ScrollDown);
8046 assert!(events.is_some());
8047 let events = events.unwrap();
8048 assert_eq!(events.len(), 1);
8049 match &events[0] {
8050 Event::Scroll { line_offset } => {
8051 assert_eq!(*line_offset, 1);
8052 }
8053 _ => panic!("Expected Scroll event"),
8054 }
8055 }
8056
8057 #[test]
8058 fn test_action_to_events_none() {
8059 let config = Config::default();
8060 let (dir_context, _temp) = test_dir_context();
8061 let mut editor = Editor::new(
8062 config,
8063 80,
8064 24,
8065 dir_context,
8066 crate::view::color_support::ColorCapability::TrueColor,
8067 test_filesystem(),
8068 )
8069 .unwrap();
8070
8071 let events = editor.action_to_events(Action::None);
8073 assert!(events.is_none());
8074 }
8075
8076 #[test]
8077 fn test_lsp_incremental_insert_generates_correct_range() {
8078 use crate::model::buffer::Buffer;
8081
8082 let buffer = Buffer::from_str_test("hello\nworld");
8083
8084 let position = 0;
8087 let (line, character) = buffer.position_to_lsp_position(position);
8088
8089 assert_eq!(line, 0, "Insertion at start should be line 0");
8090 assert_eq!(character, 0, "Insertion at start should be char 0");
8091
8092 let lsp_pos = Position::new(line as u32, character as u32);
8094 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
8095
8096 assert_eq!(lsp_range.start.line, 0);
8097 assert_eq!(lsp_range.start.character, 0);
8098 assert_eq!(lsp_range.end.line, 0);
8099 assert_eq!(lsp_range.end.character, 0);
8100 assert_eq!(
8101 lsp_range.start, lsp_range.end,
8102 "Insert should have zero-width range"
8103 );
8104
8105 let position = 3;
8107 let (line, character) = buffer.position_to_lsp_position(position);
8108
8109 assert_eq!(line, 0);
8110 assert_eq!(character, 3);
8111
8112 let position = 6;
8114 let (line, character) = buffer.position_to_lsp_position(position);
8115
8116 assert_eq!(line, 1, "Position after newline should be line 1");
8117 assert_eq!(character, 0, "Position at start of line 2 should be char 0");
8118 }
8119
8120 #[test]
8121 fn test_lsp_incremental_delete_generates_correct_range() {
8122 use crate::model::buffer::Buffer;
8125
8126 let buffer = Buffer::from_str_test("hello\nworld");
8127
8128 let range_start = 1;
8130 let range_end = 5;
8131
8132 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
8133 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
8134
8135 assert_eq!(start_line, 0);
8136 assert_eq!(start_char, 1);
8137 assert_eq!(end_line, 0);
8138 assert_eq!(end_char, 5);
8139
8140 let lsp_range = LspRange::new(
8141 Position::new(start_line as u32, start_char as u32),
8142 Position::new(end_line as u32, end_char as u32),
8143 );
8144
8145 assert_eq!(lsp_range.start.line, 0);
8146 assert_eq!(lsp_range.start.character, 1);
8147 assert_eq!(lsp_range.end.line, 0);
8148 assert_eq!(lsp_range.end.character, 5);
8149 assert_ne!(
8150 lsp_range.start, lsp_range.end,
8151 "Delete should have non-zero range"
8152 );
8153
8154 let range_start = 4;
8156 let range_end = 8;
8157
8158 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
8159 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
8160
8161 assert_eq!(start_line, 0, "Delete start on line 0");
8162 assert_eq!(start_char, 4, "Delete start at char 4");
8163 assert_eq!(end_line, 1, "Delete end on line 1");
8164 assert_eq!(end_char, 2, "Delete end at char 2 of line 1");
8165 }
8166
8167 #[test]
8168 fn test_lsp_incremental_utf16_encoding() {
8169 use crate::model::buffer::Buffer;
8172
8173 let buffer = Buffer::from_str_test("😀hello");
8175
8176 let (line, character) = buffer.position_to_lsp_position(4);
8178
8179 assert_eq!(line, 0);
8180 assert_eq!(character, 2, "Emoji should count as 2 UTF-16 code units");
8181
8182 let (line, character) = buffer.position_to_lsp_position(9);
8184
8185 assert_eq!(line, 0);
8186 assert_eq!(
8187 character, 7,
8188 "Should be 2 (emoji) + 5 (text) = 7 UTF-16 code units"
8189 );
8190
8191 let buffer = Buffer::from_str_test("café");
8193
8194 let (line, character) = buffer.position_to_lsp_position(3);
8196
8197 assert_eq!(line, 0);
8198 assert_eq!(character, 3);
8199
8200 let (line, character) = buffer.position_to_lsp_position(5);
8202
8203 assert_eq!(line, 0);
8204 assert_eq!(character, 4, "é should count as 1 UTF-16 code unit");
8205 }
8206
8207 #[test]
8208 fn test_lsp_content_change_event_structure() {
8209 let insert_change = TextDocumentContentChangeEvent {
8213 range: Some(LspRange::new(Position::new(0, 5), Position::new(0, 5))),
8214 range_length: None,
8215 text: "NEW".to_string(),
8216 };
8217
8218 assert!(insert_change.range.is_some());
8219 assert_eq!(insert_change.text, "NEW");
8220 let range = insert_change.range.unwrap();
8221 assert_eq!(
8222 range.start, range.end,
8223 "Insert should have zero-width range"
8224 );
8225
8226 let delete_change = TextDocumentContentChangeEvent {
8228 range: Some(LspRange::new(Position::new(0, 2), Position::new(0, 7))),
8229 range_length: None,
8230 text: String::new(),
8231 };
8232
8233 assert!(delete_change.range.is_some());
8234 assert_eq!(delete_change.text, "");
8235 let range = delete_change.range.unwrap();
8236 assert_ne!(range.start, range.end, "Delete should have non-zero range");
8237 assert_eq!(range.start.line, 0);
8238 assert_eq!(range.start.character, 2);
8239 assert_eq!(range.end.line, 0);
8240 assert_eq!(range.end.character, 7);
8241 }
8242
8243 #[test]
8244 fn test_goto_matching_bracket_forward() {
8245 let config = Config::default();
8246 let (dir_context, _temp) = test_dir_context();
8247 let mut editor = Editor::new(
8248 config,
8249 80,
8250 24,
8251 dir_context,
8252 crate::view::color_support::ColorCapability::TrueColor,
8253 test_filesystem(),
8254 )
8255 .unwrap();
8256
8257 let cursor_id = editor.active_cursors().primary_id();
8259 editor.apply_event_to_active_buffer(&Event::Insert {
8260 position: 0,
8261 text: "fn main() { let x = (1 + 2); }".to_string(),
8262 cursor_id,
8263 });
8264
8265 editor.apply_event_to_active_buffer(&Event::MoveCursor {
8267 cursor_id,
8268 old_position: 31,
8269 new_position: 10,
8270 old_anchor: None,
8271 new_anchor: None,
8272 old_sticky_column: 0,
8273 new_sticky_column: 0,
8274 });
8275
8276 assert_eq!(editor.active_cursors().primary().position, 10);
8277
8278 editor.goto_matching_bracket();
8280
8281 assert_eq!(editor.active_cursors().primary().position, 29);
8286 }
8287
8288 #[test]
8289 fn test_goto_matching_bracket_backward() {
8290 let config = Config::default();
8291 let (dir_context, _temp) = test_dir_context();
8292 let mut editor = Editor::new(
8293 config,
8294 80,
8295 24,
8296 dir_context,
8297 crate::view::color_support::ColorCapability::TrueColor,
8298 test_filesystem(),
8299 )
8300 .unwrap();
8301
8302 let cursor_id = editor.active_cursors().primary_id();
8304 editor.apply_event_to_active_buffer(&Event::Insert {
8305 position: 0,
8306 text: "fn main() { let x = (1 + 2); }".to_string(),
8307 cursor_id,
8308 });
8309
8310 editor.apply_event_to_active_buffer(&Event::MoveCursor {
8312 cursor_id,
8313 old_position: 31,
8314 new_position: 26,
8315 old_anchor: None,
8316 new_anchor: None,
8317 old_sticky_column: 0,
8318 new_sticky_column: 0,
8319 });
8320
8321 editor.goto_matching_bracket();
8323
8324 assert_eq!(editor.active_cursors().primary().position, 20);
8326 }
8327
8328 #[test]
8329 fn test_goto_matching_bracket_nested() {
8330 let config = Config::default();
8331 let (dir_context, _temp) = test_dir_context();
8332 let mut editor = Editor::new(
8333 config,
8334 80,
8335 24,
8336 dir_context,
8337 crate::view::color_support::ColorCapability::TrueColor,
8338 test_filesystem(),
8339 )
8340 .unwrap();
8341
8342 let cursor_id = editor.active_cursors().primary_id();
8344 editor.apply_event_to_active_buffer(&Event::Insert {
8345 position: 0,
8346 text: "{a{b{c}d}e}".to_string(),
8347 cursor_id,
8348 });
8349
8350 editor.apply_event_to_active_buffer(&Event::MoveCursor {
8352 cursor_id,
8353 old_position: 11,
8354 new_position: 0,
8355 old_anchor: None,
8356 new_anchor: None,
8357 old_sticky_column: 0,
8358 new_sticky_column: 0,
8359 });
8360
8361 editor.goto_matching_bracket();
8363
8364 assert_eq!(editor.active_cursors().primary().position, 10);
8366 }
8367
8368 #[test]
8369 fn test_search_case_sensitive() {
8370 let config = Config::default();
8371 let (dir_context, _temp) = test_dir_context();
8372 let mut editor = Editor::new(
8373 config,
8374 80,
8375 24,
8376 dir_context,
8377 crate::view::color_support::ColorCapability::TrueColor,
8378 test_filesystem(),
8379 )
8380 .unwrap();
8381
8382 let cursor_id = editor.active_cursors().primary_id();
8384 editor.apply_event_to_active_buffer(&Event::Insert {
8385 position: 0,
8386 text: "Hello hello HELLO".to_string(),
8387 cursor_id,
8388 });
8389
8390 editor.search_case_sensitive = false;
8392 editor.perform_search("hello");
8393
8394 let search_state = editor.search_state.as_ref().unwrap();
8395 assert_eq!(
8396 search_state.matches.len(),
8397 3,
8398 "Should find all 3 matches case-insensitively"
8399 );
8400
8401 editor.search_case_sensitive = true;
8403 editor.perform_search("hello");
8404
8405 let search_state = editor.search_state.as_ref().unwrap();
8406 assert_eq!(
8407 search_state.matches.len(),
8408 1,
8409 "Should find only 1 exact match"
8410 );
8411 assert_eq!(
8412 search_state.matches[0], 6,
8413 "Should find 'hello' at position 6"
8414 );
8415 }
8416
8417 #[test]
8418 fn test_search_whole_word() {
8419 let config = Config::default();
8420 let (dir_context, _temp) = test_dir_context();
8421 let mut editor = Editor::new(
8422 config,
8423 80,
8424 24,
8425 dir_context,
8426 crate::view::color_support::ColorCapability::TrueColor,
8427 test_filesystem(),
8428 )
8429 .unwrap();
8430
8431 let cursor_id = editor.active_cursors().primary_id();
8433 editor.apply_event_to_active_buffer(&Event::Insert {
8434 position: 0,
8435 text: "test testing tested attest test".to_string(),
8436 cursor_id,
8437 });
8438
8439 editor.search_whole_word = false;
8441 editor.search_case_sensitive = true;
8442 editor.perform_search("test");
8443
8444 let search_state = editor.search_state.as_ref().unwrap();
8445 assert_eq!(
8446 search_state.matches.len(),
8447 5,
8448 "Should find 'test' in all occurrences"
8449 );
8450
8451 editor.search_whole_word = true;
8453 editor.perform_search("test");
8454
8455 let search_state = editor.search_state.as_ref().unwrap();
8456 assert_eq!(
8457 search_state.matches.len(),
8458 2,
8459 "Should find only whole word 'test'"
8460 );
8461 assert_eq!(search_state.matches[0], 0, "First match at position 0");
8462 assert_eq!(search_state.matches[1], 27, "Second match at position 27");
8463 }
8464
8465 #[test]
8466 fn test_search_scan_completes_when_capped() {
8467 let config = Config::default();
8473 let (dir_context, _temp) = test_dir_context();
8474 let mut editor = Editor::new(
8475 config,
8476 80,
8477 24,
8478 dir_context,
8479 crate::view::color_support::ColorCapability::TrueColor,
8480 test_filesystem(),
8481 )
8482 .unwrap();
8483
8484 let buffer_id = editor.active_buffer();
8487 let regex = regex::bytes::Regex::new("test").unwrap();
8488 let fake_chunks = vec![
8489 crate::model::buffer::LineScanChunk {
8490 leaf_index: 0,
8491 byte_len: 100,
8492 already_known: true,
8493 },
8494 crate::model::buffer::LineScanChunk {
8495 leaf_index: 1,
8496 byte_len: 100,
8497 already_known: true,
8498 },
8499 ];
8500
8501 editor.search_scan_state = Some(SearchScanState {
8502 buffer_id,
8503 leaves: Vec::new(),
8504 scan: crate::model::buffer::ChunkedSearchState {
8505 chunks: fake_chunks,
8506 next_chunk: 1, next_doc_offset: 100,
8508 total_bytes: 200,
8509 scanned_bytes: 100,
8510 regex,
8511 matches: vec![
8512 crate::model::buffer::SearchMatch {
8513 byte_offset: 10,
8514 length: 4,
8515 line: 1,
8516 column: 11,
8517 context: String::new(),
8518 },
8519 crate::model::buffer::SearchMatch {
8520 byte_offset: 50,
8521 length: 4,
8522 line: 1,
8523 column: 51,
8524 context: String::new(),
8525 },
8526 ],
8527 overlap_tail: Vec::new(),
8528 overlap_doc_offset: 0,
8529 max_matches: 10_000,
8530 capped: true, query_len: 4,
8532 running_line: 1,
8533 },
8534 query: "test".to_string(),
8535 search_range: None,
8536 case_sensitive: false,
8537 whole_word: false,
8538 use_regex: false,
8539 });
8540
8541 let result = editor.process_search_scan();
8543 assert!(
8544 result,
8545 "process_search_scan should return true (needs render)"
8546 );
8547
8548 assert!(
8550 editor.search_scan_state.is_none(),
8551 "search_scan_state should be None after capped scan completes"
8552 );
8553
8554 let search_state = editor
8556 .search_state
8557 .as_ref()
8558 .expect("search_state should be set after scan finishes");
8559 assert_eq!(search_state.matches.len(), 2, "Should have 2 matches");
8560 assert_eq!(search_state.query, "test");
8561 assert!(
8562 search_state.capped,
8563 "search_state should be marked as capped"
8564 );
8565 }
8566
8567 #[test]
8568 fn test_bookmarks() {
8569 let config = Config::default();
8570 let (dir_context, _temp) = test_dir_context();
8571 let mut editor = Editor::new(
8572 config,
8573 80,
8574 24,
8575 dir_context,
8576 crate::view::color_support::ColorCapability::TrueColor,
8577 test_filesystem(),
8578 )
8579 .unwrap();
8580
8581 let cursor_id = editor.active_cursors().primary_id();
8583 editor.apply_event_to_active_buffer(&Event::Insert {
8584 position: 0,
8585 text: "Line 1\nLine 2\nLine 3".to_string(),
8586 cursor_id,
8587 });
8588
8589 editor.apply_event_to_active_buffer(&Event::MoveCursor {
8591 cursor_id,
8592 old_position: 21,
8593 new_position: 7,
8594 old_anchor: None,
8595 new_anchor: None,
8596 old_sticky_column: 0,
8597 new_sticky_column: 0,
8598 });
8599
8600 editor.set_bookmark('1');
8602 assert!(editor.bookmarks.contains_key(&'1'));
8603 assert_eq!(editor.bookmarks.get(&'1').unwrap().position, 7);
8604
8605 editor.apply_event_to_active_buffer(&Event::MoveCursor {
8607 cursor_id,
8608 old_position: 7,
8609 new_position: 14,
8610 old_anchor: None,
8611 new_anchor: None,
8612 old_sticky_column: 0,
8613 new_sticky_column: 0,
8614 });
8615
8616 editor.jump_to_bookmark('1');
8618 assert_eq!(editor.active_cursors().primary().position, 7);
8619
8620 editor.clear_bookmark('1');
8622 assert!(!editor.bookmarks.contains_key(&'1'));
8623 }
8624
8625 #[test]
8626 fn test_action_enum_new_variants() {
8627 use serde_json::json;
8629
8630 let args = HashMap::new();
8631 assert_eq!(
8632 Action::from_str("smart_home", &args),
8633 Some(Action::SmartHome)
8634 );
8635 assert_eq!(
8636 Action::from_str("dedent_selection", &args),
8637 Some(Action::DedentSelection)
8638 );
8639 assert_eq!(
8640 Action::from_str("toggle_comment", &args),
8641 Some(Action::ToggleComment)
8642 );
8643 assert_eq!(
8644 Action::from_str("goto_matching_bracket", &args),
8645 Some(Action::GoToMatchingBracket)
8646 );
8647 assert_eq!(
8648 Action::from_str("list_bookmarks", &args),
8649 Some(Action::ListBookmarks)
8650 );
8651 assert_eq!(
8652 Action::from_str("toggle_search_case_sensitive", &args),
8653 Some(Action::ToggleSearchCaseSensitive)
8654 );
8655 assert_eq!(
8656 Action::from_str("toggle_search_whole_word", &args),
8657 Some(Action::ToggleSearchWholeWord)
8658 );
8659
8660 let mut args_with_char = HashMap::new();
8662 args_with_char.insert("char".to_string(), json!("5"));
8663 assert_eq!(
8664 Action::from_str("set_bookmark", &args_with_char),
8665 Some(Action::SetBookmark('5'))
8666 );
8667 assert_eq!(
8668 Action::from_str("jump_to_bookmark", &args_with_char),
8669 Some(Action::JumpToBookmark('5'))
8670 );
8671 assert_eq!(
8672 Action::from_str("clear_bookmark", &args_with_char),
8673 Some(Action::ClearBookmark('5'))
8674 );
8675 }
8676
8677 #[test]
8678 fn test_keybinding_new_defaults() {
8679 use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
8680
8681 let mut config = Config::default();
8685 config.active_keybinding_map = crate::config::KeybindingMapName("default".to_string());
8686 let resolver = KeybindingResolver::new(&config);
8687
8688 let event = KeyEvent {
8690 code: KeyCode::Char('/'),
8691 modifiers: KeyModifiers::CONTROL,
8692 kind: KeyEventKind::Press,
8693 state: KeyEventState::NONE,
8694 };
8695 let action = resolver.resolve(&event, KeyContext::Normal);
8696 assert_eq!(action, Action::ToggleComment);
8697
8698 let event = KeyEvent {
8700 code: KeyCode::Char(']'),
8701 modifiers: KeyModifiers::CONTROL,
8702 kind: KeyEventKind::Press,
8703 state: KeyEventState::NONE,
8704 };
8705 let action = resolver.resolve(&event, KeyContext::Normal);
8706 assert_eq!(action, Action::GoToMatchingBracket);
8707
8708 let event = KeyEvent {
8710 code: KeyCode::Tab,
8711 modifiers: KeyModifiers::SHIFT,
8712 kind: KeyEventKind::Press,
8713 state: KeyEventState::NONE,
8714 };
8715 let action = resolver.resolve(&event, KeyContext::Normal);
8716 assert_eq!(action, Action::DedentSelection);
8717
8718 let event = KeyEvent {
8720 code: KeyCode::Char('g'),
8721 modifiers: KeyModifiers::CONTROL,
8722 kind: KeyEventKind::Press,
8723 state: KeyEventState::NONE,
8724 };
8725 let action = resolver.resolve(&event, KeyContext::Normal);
8726 assert_eq!(action, Action::GotoLine);
8727
8728 let event = KeyEvent {
8730 code: KeyCode::Char('5'),
8731 modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
8732 kind: KeyEventKind::Press,
8733 state: KeyEventState::NONE,
8734 };
8735 let action = resolver.resolve(&event, KeyContext::Normal);
8736 assert_eq!(action, Action::SetBookmark('5'));
8737
8738 let event = KeyEvent {
8739 code: KeyCode::Char('5'),
8740 modifiers: KeyModifiers::ALT,
8741 kind: KeyEventKind::Press,
8742 state: KeyEventState::NONE,
8743 };
8744 let action = resolver.resolve(&event, KeyContext::Normal);
8745 assert_eq!(action, Action::JumpToBookmark('5'));
8746 }
8747
8748 #[test]
8760 fn test_lsp_rename_didchange_positions_bug() {
8761 use crate::model::buffer::Buffer;
8762
8763 let config = Config::default();
8764 let (dir_context, _temp) = test_dir_context();
8765 let mut editor = Editor::new(
8766 config,
8767 80,
8768 24,
8769 dir_context,
8770 crate::view::color_support::ColorCapability::TrueColor,
8771 test_filesystem(),
8772 )
8773 .unwrap();
8774
8775 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
8779 editor.active_state_mut().buffer =
8780 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
8781
8782 let cursor_id = editor.active_cursors().primary_id();
8787
8788 let batch = Event::Batch {
8789 events: vec![
8790 Event::Delete {
8792 range: 23..26, deleted_text: "val".to_string(),
8794 cursor_id,
8795 },
8796 Event::Insert {
8797 position: 23,
8798 text: "value".to_string(),
8799 cursor_id,
8800 },
8801 Event::Delete {
8803 range: 7..10, deleted_text: "val".to_string(),
8805 cursor_id,
8806 },
8807 Event::Insert {
8808 position: 7,
8809 text: "value".to_string(),
8810 cursor_id,
8811 },
8812 ],
8813 description: "LSP Rename".to_string(),
8814 };
8815
8816 let lsp_changes_before = editor.collect_lsp_changes(&batch);
8818
8819 editor.apply_event_to_active_buffer(&batch);
8821
8822 let lsp_changes_after = editor.collect_lsp_changes(&batch);
8825
8826 let final_content = editor.active_state().buffer.to_string().unwrap();
8828 assert_eq!(
8829 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
8830 "Buffer should have 'value' in both places"
8831 );
8832
8833 assert_eq!(lsp_changes_before.len(), 4, "Should have 4 changes");
8839
8840 let first_delete = &lsp_changes_before[0];
8841 let first_del_range = first_delete.range.unwrap();
8842 assert_eq!(
8843 first_del_range.start.line, 1,
8844 "First delete should be on line 1 (BEFORE)"
8845 );
8846 assert_eq!(
8847 first_del_range.start.character, 4,
8848 "First delete start should be at char 4 (BEFORE)"
8849 );
8850
8851 assert_eq!(lsp_changes_after.len(), 4, "Should have 4 changes");
8857
8858 let first_delete_after = &lsp_changes_after[0];
8859 let first_del_range_after = first_delete_after.range.unwrap();
8860
8861 eprintln!("BEFORE modification:");
8864 eprintln!(
8865 " Delete at line {}, char {}-{}",
8866 first_del_range.start.line,
8867 first_del_range.start.character,
8868 first_del_range.end.character
8869 );
8870 eprintln!("AFTER modification:");
8871 eprintln!(
8872 " Delete at line {}, char {}-{}",
8873 first_del_range_after.start.line,
8874 first_del_range_after.start.character,
8875 first_del_range_after.end.character
8876 );
8877
8878 assert_ne!(
8896 first_del_range_after.end.character, first_del_range.end.character,
8897 "BUG CONFIRMED: LSP positions are different when calculated after buffer modification!"
8898 );
8899
8900 eprintln!("\n=== BUG DEMONSTRATED ===");
8901 eprintln!("When collect_lsp_changes() is called AFTER buffer modification,");
8902 eprintln!("the positions are WRONG because they're calculated from the");
8903 eprintln!("modified buffer, not the original buffer.");
8904 eprintln!("This causes the second rename to fail with 'content modified' error.");
8905 eprintln!("========================\n");
8906 }
8907
8908 #[test]
8909 fn test_lsp_rename_preserves_cursor_position() {
8910 use crate::model::buffer::Buffer;
8911
8912 let config = Config::default();
8913 let (dir_context, _temp) = test_dir_context();
8914 let mut editor = Editor::new(
8915 config,
8916 80,
8917 24,
8918 dir_context,
8919 crate::view::color_support::ColorCapability::TrueColor,
8920 test_filesystem(),
8921 )
8922 .unwrap();
8923
8924 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
8928 editor.active_state_mut().buffer =
8929 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
8930
8931 let original_cursor_pos = 23;
8933 editor.active_cursors_mut().primary_mut().position = original_cursor_pos;
8934
8935 let buffer_text = editor.active_state().buffer.to_string().unwrap();
8937 let text_at_cursor = buffer_text[original_cursor_pos..original_cursor_pos + 3].to_string();
8938 assert_eq!(text_at_cursor, "val", "Cursor should be at 'val'");
8939
8940 let cursor_id = editor.active_cursors().primary_id();
8943 let buffer_id = editor.active_buffer();
8944
8945 let events = vec![
8946 Event::Delete {
8948 range: 23..26, deleted_text: "val".to_string(),
8950 cursor_id,
8951 },
8952 Event::Insert {
8953 position: 23,
8954 text: "value".to_string(),
8955 cursor_id,
8956 },
8957 Event::Delete {
8959 range: 7..10, deleted_text: "val".to_string(),
8961 cursor_id,
8962 },
8963 Event::Insert {
8964 position: 7,
8965 text: "value".to_string(),
8966 cursor_id,
8967 },
8968 ];
8969
8970 editor
8972 .apply_events_to_buffer_as_bulk_edit(buffer_id, events, "LSP Rename".to_string())
8973 .unwrap();
8974
8975 let final_content = editor.active_state().buffer.to_string().unwrap();
8977 assert_eq!(
8978 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
8979 "Buffer should have 'value' in both places"
8980 );
8981
8982 let final_cursor_pos = editor.active_cursors().primary().position;
8990 let expected_cursor_pos = 25; assert_eq!(
8993 final_cursor_pos, expected_cursor_pos,
8994 "Cursor should be at position {} (start of 'value' on line 1), but was at {}. \
8995 Original pos: {}, expected adjustment: +2 for first rename",
8996 expected_cursor_pos, final_cursor_pos, original_cursor_pos
8997 );
8998
8999 let text_at_new_cursor = &final_content[final_cursor_pos..final_cursor_pos + 5];
9001 assert_eq!(
9002 text_at_new_cursor, "value",
9003 "Cursor should be at the start of 'value' after rename"
9004 );
9005 }
9006
9007 #[test]
9008 fn test_lsp_rename_twice_consecutive() {
9009 use crate::model::buffer::Buffer;
9012
9013 let config = Config::default();
9014 let (dir_context, _temp) = test_dir_context();
9015 let mut editor = Editor::new(
9016 config,
9017 80,
9018 24,
9019 dir_context,
9020 crate::view::color_support::ColorCapability::TrueColor,
9021 test_filesystem(),
9022 )
9023 .unwrap();
9024
9025 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
9027 editor.active_state_mut().buffer =
9028 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
9029
9030 let cursor_id = editor.active_cursors().primary_id();
9031 let buffer_id = editor.active_buffer();
9032
9033 let events1 = vec![
9036 Event::Delete {
9038 range: 23..26,
9039 deleted_text: "val".to_string(),
9040 cursor_id,
9041 },
9042 Event::Insert {
9043 position: 23,
9044 text: "value".to_string(),
9045 cursor_id,
9046 },
9047 Event::Delete {
9049 range: 7..10,
9050 deleted_text: "val".to_string(),
9051 cursor_id,
9052 },
9053 Event::Insert {
9054 position: 7,
9055 text: "value".to_string(),
9056 cursor_id,
9057 },
9058 ];
9059
9060 let batch1 = Event::Batch {
9062 events: events1.clone(),
9063 description: "LSP Rename 1".to_string(),
9064 };
9065
9066 let lsp_changes1 = editor.collect_lsp_changes(&batch1);
9068
9069 assert_eq!(
9071 lsp_changes1.len(),
9072 4,
9073 "First rename should have 4 LSP changes"
9074 );
9075
9076 let first_del = &lsp_changes1[0];
9078 let first_del_range = first_del.range.unwrap();
9079 assert_eq!(first_del_range.start.line, 1, "First delete line");
9080 assert_eq!(
9081 first_del_range.start.character, 4,
9082 "First delete start char"
9083 );
9084 assert_eq!(first_del_range.end.character, 7, "First delete end char");
9085
9086 editor
9088 .apply_events_to_buffer_as_bulk_edit(buffer_id, events1, "LSP Rename 1".to_string())
9089 .unwrap();
9090
9091 let after_first = editor.active_state().buffer.to_string().unwrap();
9093 assert_eq!(
9094 after_first, "fn foo(value: i32) {\n value + 1\n}\n",
9095 "After first rename"
9096 );
9097
9098 let events2 = vec![
9108 Event::Delete {
9110 range: 25..30,
9111 deleted_text: "value".to_string(),
9112 cursor_id,
9113 },
9114 Event::Insert {
9115 position: 25,
9116 text: "x".to_string(),
9117 cursor_id,
9118 },
9119 Event::Delete {
9121 range: 7..12,
9122 deleted_text: "value".to_string(),
9123 cursor_id,
9124 },
9125 Event::Insert {
9126 position: 7,
9127 text: "x".to_string(),
9128 cursor_id,
9129 },
9130 ];
9131
9132 let batch2 = Event::Batch {
9134 events: events2.clone(),
9135 description: "LSP Rename 2".to_string(),
9136 };
9137
9138 let lsp_changes2 = editor.collect_lsp_changes(&batch2);
9140
9141 assert_eq!(
9145 lsp_changes2.len(),
9146 4,
9147 "Second rename should have 4 LSP changes"
9148 );
9149
9150 let second_first_del = &lsp_changes2[0];
9152 let second_first_del_range = second_first_del.range.unwrap();
9153 assert_eq!(
9154 second_first_del_range.start.line, 1,
9155 "Second rename first delete should be on line 1"
9156 );
9157 assert_eq!(
9158 second_first_del_range.start.character, 4,
9159 "Second rename first delete start should be at char 4"
9160 );
9161 assert_eq!(
9162 second_first_del_range.end.character, 9,
9163 "Second rename first delete end should be at char 9 (4 + 5 for 'value')"
9164 );
9165
9166 let second_third_del = &lsp_changes2[2];
9168 let second_third_del_range = second_third_del.range.unwrap();
9169 assert_eq!(
9170 second_third_del_range.start.line, 0,
9171 "Second rename third delete should be on line 0"
9172 );
9173 assert_eq!(
9174 second_third_del_range.start.character, 7,
9175 "Second rename third delete start should be at char 7"
9176 );
9177 assert_eq!(
9178 second_third_del_range.end.character, 12,
9179 "Second rename third delete end should be at char 12 (7 + 5 for 'value')"
9180 );
9181
9182 editor
9184 .apply_events_to_buffer_as_bulk_edit(buffer_id, events2, "LSP Rename 2".to_string())
9185 .unwrap();
9186
9187 let after_second = editor.active_state().buffer.to_string().unwrap();
9189 assert_eq!(
9190 after_second, "fn foo(x: i32) {\n x + 1\n}\n",
9191 "After second rename"
9192 );
9193 }
9194
9195 #[test]
9196 fn test_ensure_active_tab_visible_static_offset() {
9197 let config = Config::default();
9198 let (dir_context, _temp) = test_dir_context();
9199 let mut editor = Editor::new(
9200 config,
9201 80,
9202 24,
9203 dir_context,
9204 crate::view::color_support::ColorCapability::TrueColor,
9205 test_filesystem(),
9206 )
9207 .unwrap();
9208 let split_id = editor.split_manager.active_split();
9209
9210 let buf1 = editor.new_buffer();
9212 editor
9213 .buffers
9214 .get_mut(&buf1)
9215 .unwrap()
9216 .buffer
9217 .rename_file_path(std::path::PathBuf::from("aaa_long_name_01.txt"));
9218 let buf2 = editor.new_buffer();
9219 editor
9220 .buffers
9221 .get_mut(&buf2)
9222 .unwrap()
9223 .buffer
9224 .rename_file_path(std::path::PathBuf::from("bbb_long_name_02.txt"));
9225 let buf3 = editor.new_buffer();
9226 editor
9227 .buffers
9228 .get_mut(&buf3)
9229 .unwrap()
9230 .buffer
9231 .rename_file_path(std::path::PathBuf::from("ccc_long_name_03.txt"));
9232
9233 {
9234 let view_state = editor.split_view_states.get_mut(&split_id).unwrap();
9235 view_state.open_buffers = vec![buf1, buf2, buf3];
9236 view_state.tab_scroll_offset = 50;
9237 }
9238
9239 editor.ensure_active_tab_visible(split_id, buf1, 25);
9243 assert_eq!(
9244 editor
9245 .split_view_states
9246 .get(&split_id)
9247 .unwrap()
9248 .tab_scroll_offset,
9249 0
9250 );
9251
9252 editor.ensure_active_tab_visible(split_id, buf3, 25);
9254 let view_state = editor.split_view_states.get(&split_id).unwrap();
9255 assert!(view_state.tab_scroll_offset > 0);
9256 let total_width: usize = view_state
9257 .open_buffers
9258 .iter()
9259 .enumerate()
9260 .map(|(idx, id)| {
9261 let state = editor.buffers.get(id).unwrap();
9262 let name_len = state
9263 .buffer
9264 .file_path()
9265 .and_then(|p| p.file_name())
9266 .and_then(|n| n.to_str())
9267 .map(|s| s.chars().count())
9268 .unwrap_or(0);
9269 let tab_width = 2 + name_len;
9270 if idx < view_state.open_buffers.len() - 1 {
9271 tab_width + 1 } else {
9273 tab_width
9274 }
9275 })
9276 .sum();
9277 assert!(view_state.tab_scroll_offset <= total_width);
9278 }
9279}