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 BufferInfo, BufferProvider, CommandProvider, FileProvider, GotoLineProvider, QuickOpenContext,
170 QuickOpenRegistry,
171};
172use crate::model::cursor::Cursors;
173use crate::model::event::{Event, EventLog, LeafId, SplitDirection, SplitId};
174use crate::model::filesystem::FileSystem;
175use crate::services::async_bridge::{AsyncBridge, AsyncMessage};
176use crate::services::fs::FsManager;
177use crate::services::lsp::manager::LspManager;
178use crate::services::plugins::PluginManager;
179use crate::services::recovery::{RecoveryConfig, RecoveryService};
180use crate::services::time_source::{RealTimeSource, SharedTimeSource};
181use crate::state::EditorState;
182use crate::types::{LspLanguageConfig, LspServerConfig, ProcessLimits};
183use crate::view::file_tree::{FileTree, FileTreeView};
184use crate::view::prompt::{Prompt, PromptType};
185use crate::view::scroll_sync::ScrollSyncManager;
186use crate::view::split::{SplitManager, SplitViewState};
187use crate::view::ui::{
188 FileExplorerRenderer, SplitRenderer, StatusBarRenderer, SuggestionsRenderer,
189};
190use crossterm::event::{KeyCode, KeyModifiers};
191#[cfg(feature = "plugins")]
192use fresh_core::api::BufferSavedDiff;
193#[cfg(feature = "plugins")]
194use fresh_core::api::JsCallbackId;
195use fresh_core::api::PluginCommand;
196use lsp_types::{Position, Range as LspRange, TextDocumentContentChangeEvent};
197use ratatui::{
198 layout::{Constraint, Direction, Layout},
199 Frame,
200};
201use std::collections::{HashMap, HashSet};
202use std::ops::Range;
203use std::path::{Path, PathBuf};
204use std::sync::{Arc, RwLock};
205use std::time::Instant;
206
207pub use self::types::{BufferKind, BufferMetadata, HoverTarget};
209pub use self::warning_domains::{
210 GeneralWarningDomain, LspWarningDomain, WarningAction, WarningActionId, WarningDomain,
211 WarningDomainRegistry, WarningLevel, WarningPopupContent,
212};
213pub use crate::model::event::BufferId;
214
215fn uri_to_path(uri: &lsp_types::Uri) -> Result<PathBuf, String> {
217 fresh_core::file_uri::lsp_uri_to_path(uri).ok_or_else(|| "URI is not a file path".to_string())
218}
219
220#[derive(Clone, Debug)]
222pub struct PendingGrammar {
223 pub language: String,
225 pub grammar_path: String,
227 pub extensions: Vec<String>,
229}
230
231#[derive(Clone, Debug)]
233struct SemanticTokenRangeRequest {
234 buffer_id: BufferId,
235 version: u64,
236 range: Range<usize>,
237 start_line: usize,
238 end_line: usize,
239}
240
241#[derive(Clone, Copy, Debug)]
242enum SemanticTokensFullRequestKind {
243 Full,
244 FullDelta,
245}
246
247#[derive(Clone, Debug)]
248struct SemanticTokenFullRequest {
249 buffer_id: BufferId,
250 version: u64,
251 kind: SemanticTokensFullRequestKind,
252}
253
254#[derive(Clone, Debug)]
255struct FoldingRangeRequest {
256 buffer_id: BufferId,
257 version: u64,
258}
259
260#[derive(Debug, Clone)]
266pub struct DabbrevCycleState {
267 pub original_prefix: String,
269 pub word_start: usize,
271 pub candidates: Vec<String>,
273 pub index: usize,
275}
276
277pub struct Editor {
279 buffers: HashMap<BufferId, EditorState>,
281
282 event_logs: HashMap<BufferId, EventLog>,
287
288 next_buffer_id: usize,
290
291 config: Config,
293
294 user_config_raw: serde_json::Value,
296
297 dir_context: DirectoryContext,
299
300 grammar_registry: std::sync::Arc<crate::primitives::grammar::GrammarRegistry>,
302
303 pending_grammars: Vec<PendingGrammar>,
305
306 grammar_reload_pending: bool,
310
311 grammar_build_in_progress: bool,
314
315 needs_full_grammar_build: bool,
319
320 streaming_grep_cancellation: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
322
323 pending_grammar_callbacks: Vec<fresh_core::api::JsCallbackId>,
327
328 theme: crate::view::theme::Theme,
330
331 theme_registry: crate::view::theme::ThemeRegistry,
333
334 theme_cache: Arc<RwLock<HashMap<String, serde_json::Value>>>,
336
337 ansi_background: Option<crate::primitives::ansi_background::AnsiBackground>,
339
340 ansi_background_path: Option<PathBuf>,
342
343 background_fade: f32,
345
346 keybindings: Arc<RwLock<KeybindingResolver>>,
348
349 clipboard: crate::services::clipboard::Clipboard,
351
352 should_quit: bool,
354
355 should_detach: bool,
357
358 session_mode: bool,
360
361 software_cursor_only: bool,
363
364 session_name: Option<String>,
366
367 pending_escape_sequences: Vec<u8>,
370
371 restart_with_dir: Option<PathBuf>,
374
375 status_message: Option<String>,
377
378 plugin_status_message: Option<String>,
380
381 plugin_errors: Vec<String>,
384
385 prompt: Option<Prompt>,
387
388 terminal_width: u16,
390 terminal_height: u16,
391
392 lsp: Option<LspManager>,
394
395 buffer_metadata: HashMap<BufferId, BufferMetadata>,
397
398 mode_registry: ModeRegistry,
400
401 tokio_runtime: Option<tokio::runtime::Runtime>,
403
404 async_bridge: Option<AsyncBridge>,
406
407 split_manager: SplitManager,
409
410 split_view_states: HashMap<LeafId, SplitViewState>,
414
415 previous_viewports: HashMap<LeafId, (usize, u16, u16)>,
419
420 scroll_sync_manager: ScrollSyncManager,
423
424 file_explorer: Option<FileTreeView>,
426
427 fs_manager: Arc<FsManager>,
429
430 filesystem: Arc<dyn FileSystem + Send + Sync>,
432
433 local_filesystem: Arc<dyn FileSystem + Send + Sync>,
436
437 process_spawner: Arc<dyn crate::services::remote::ProcessSpawner>,
439
440 file_explorer_visible: bool,
442
443 file_explorer_sync_in_progress: bool,
446
447 file_explorer_width_percent: f32,
450
451 pending_file_explorer_show_hidden: Option<bool>,
453
454 pending_file_explorer_show_gitignored: Option<bool>,
456
457 file_explorer_decorations: HashMap<String, Vec<crate::view::file_tree::FileExplorerDecoration>>,
459
460 file_explorer_decoration_cache: crate::view::file_tree::FileExplorerDecorationCache,
462
463 menu_bar_visible: bool,
465
466 menu_bar_auto_shown: bool,
469
470 tab_bar_visible: bool,
472
473 status_bar_visible: bool,
475
476 prompt_line_visible: bool,
478
479 mouse_enabled: bool,
481
482 same_buffer_scroll_sync: bool,
484
485 mouse_cursor_position: Option<(u16, u16)>,
489
490 gpm_active: bool,
492
493 key_context: KeyContext,
495
496 menu_state: crate::view::ui::MenuState,
498
499 menus: crate::config::MenuConfig,
501
502 working_dir: PathBuf,
504
505 pub position_history: PositionHistory,
507
508 in_navigation: bool,
510
511 next_lsp_request_id: u64,
513
514 pending_completion_requests: HashSet<u64>,
516
517 completion_items: Option<Vec<lsp_types::CompletionItem>>,
520
521 scheduled_completion_trigger: Option<Instant>,
524
525 completion_service: crate::services::completion::CompletionService,
528
529 dabbrev_state: Option<DabbrevCycleState>,
533
534 pending_goto_definition_request: Option<u64>,
536
537 pending_hover_request: Option<u64>,
539
540 pending_references_request: Option<u64>,
542
543 pending_references_symbol: String,
545
546 pending_signature_help_request: Option<u64>,
548
549 pending_code_actions_requests: HashSet<u64>,
551
552 pending_code_actions_server_names: HashMap<u64, String>,
554
555 pending_code_actions: Option<Vec<(String, lsp_types::CodeActionOrCommand)>>,
559
560 pending_inlay_hints_request: Option<u64>,
562
563 pending_folding_range_requests: HashMap<u64, FoldingRangeRequest>,
565
566 folding_ranges_in_flight: HashMap<BufferId, (u64, u64)>,
568
569 folding_ranges_debounce: HashMap<BufferId, Instant>,
571
572 pending_semantic_token_requests: HashMap<u64, SemanticTokenFullRequest>,
574
575 semantic_tokens_in_flight: HashMap<BufferId, (u64, u64, SemanticTokensFullRequestKind)>,
577
578 pending_semantic_token_range_requests: HashMap<u64, SemanticTokenRangeRequest>,
580
581 semantic_tokens_range_in_flight: HashMap<BufferId, (u64, usize, usize, u64)>,
583
584 semantic_tokens_range_last_request: HashMap<BufferId, (usize, usize, u64, Instant)>,
586
587 semantic_tokens_range_applied: HashMap<BufferId, (usize, usize, u64)>,
589
590 semantic_tokens_full_debounce: HashMap<BufferId, Instant>,
592
593 hover_symbol_range: Option<(usize, usize)>,
596
597 hover_symbol_overlay: Option<crate::view::overlay::OverlayHandle>,
599
600 mouse_hover_screen_position: Option<(u16, u16)>,
603
604 search_state: Option<SearchState>,
606
607 search_namespace: crate::view::overlay::OverlayNamespace,
609
610 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace,
612
613 pending_search_range: Option<Range<usize>>,
615
616 interactive_replace_state: Option<InteractiveReplaceState>,
618
619 lsp_status: String,
621
622 mouse_state: MouseState,
624
625 tab_context_menu: Option<TabContextMenu>,
627
628 theme_info_popup: Option<types::ThemeInfoPopup>,
630
631 pub(crate) cached_layout: CachedLayout,
633
634 command_registry: Arc<RwLock<CommandRegistry>>,
636
637 quick_open_registry: QuickOpenRegistry,
639
640 plugin_manager: PluginManager,
642
643 plugin_dev_workspaces:
647 HashMap<BufferId, crate::services::plugins::plugin_dev_workspace::PluginDevWorkspace>,
648
649 seen_byte_ranges: HashMap<BufferId, std::collections::HashSet<(usize, usize)>>,
653
654 panel_ids: HashMap<String, BufferId>,
657
658 background_process_handles: HashMap<u64, tokio::task::AbortHandle>,
661
662 prompt_histories: HashMap<String, crate::input::input_history::InputHistory>,
665
666 pending_async_prompt_callback: Option<fresh_core::api::JsCallbackId>,
670
671 lsp_progress: std::collections::HashMap<String, LspProgressInfo>,
673
674 lsp_server_statuses:
676 std::collections::HashMap<(String, String), crate::services::async_bridge::LspServerStatus>,
677
678 lsp_window_messages: Vec<LspMessageEntry>,
680
681 lsp_log_messages: Vec<LspMessageEntry>,
683
684 diagnostic_result_ids: HashMap<String, String>,
687
688 scheduled_diagnostic_pull: Option<(BufferId, Instant)>,
691
692 stored_push_diagnostics: HashMap<String, HashMap<String, Vec<lsp_types::Diagnostic>>>,
695
696 stored_pull_diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
698
699 stored_diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
701
702 stored_folding_ranges: HashMap<String, Vec<lsp_types::FoldingRange>>,
705
706 event_broadcaster: crate::model::control_event::EventBroadcaster,
708
709 bookmarks: HashMap<char, Bookmark>,
711
712 search_case_sensitive: bool,
714 search_whole_word: bool,
715 search_use_regex: bool,
716 search_confirm_each: bool,
718
719 macros: HashMap<char, Vec<Action>>,
721
722 macro_recording: Option<MacroRecordingState>,
724
725 last_macro_register: Option<char>,
727
728 macro_playing: bool,
730
731 #[cfg(feature = "plugins")]
733 pending_plugin_actions: Vec<(
734 String,
735 crate::services::plugins::thread::oneshot::Receiver<anyhow::Result<()>>,
736 )>,
737
738 #[cfg(feature = "plugins")]
740 plugin_render_requested: bool,
741
742 chord_state: Vec<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)>,
745
746 pending_lsp_confirmation: Option<String>,
749
750 pending_close_buffer: Option<BufferId>,
753
754 auto_revert_enabled: bool,
756
757 last_auto_revert_poll: std::time::Instant,
759
760 last_file_tree_poll: std::time::Instant,
762
763 git_index_resolved: bool,
765
766 file_mod_times: HashMap<PathBuf, std::time::SystemTime>,
769
770 dir_mod_times: HashMap<PathBuf, std::time::SystemTime>,
773
774 #[allow(clippy::type_complexity)]
778 pending_file_poll_rx:
779 Option<std::sync::mpsc::Receiver<Vec<(PathBuf, Option<std::time::SystemTime>)>>>,
780
781 #[allow(clippy::type_complexity)]
784 pending_dir_poll_rx: Option<
785 std::sync::mpsc::Receiver<(
786 Vec<(
787 crate::view::file_tree::NodeId,
788 PathBuf,
789 Option<std::time::SystemTime>,
790 )>,
791 Option<(PathBuf, std::time::SystemTime)>,
792 )>,
793 >,
794
795 file_rapid_change_counts: HashMap<PathBuf, (std::time::Instant, u32)>,
798
799 file_open_state: Option<file_open::FileOpenState>,
801
802 file_browser_layout: Option<crate::view::ui::FileBrowserLayout>,
804
805 recovery_service: RecoveryService,
807
808 full_redraw_requested: bool,
810
811 time_source: SharedTimeSource,
813
814 last_auto_recovery_save: std::time::Instant,
816
817 last_persistent_auto_save: std::time::Instant,
819
820 active_custom_contexts: HashSet<String>,
823
824 plugin_global_state: HashMap<String, HashMap<String, serde_json::Value>>,
827
828 editor_mode: Option<String>,
831
832 warning_log: Option<(std::sync::mpsc::Receiver<()>, PathBuf)>,
834
835 status_log_path: Option<PathBuf>,
837
838 warning_domains: WarningDomainRegistry,
841
842 update_checker: Option<crate::services::release_checker::PeriodicUpdateChecker>,
844
845 terminal_manager: crate::services::terminal::TerminalManager,
847
848 terminal_buffers: HashMap<BufferId, crate::services::terminal::TerminalId>,
850
851 terminal_backing_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
853
854 terminal_log_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
856
857 terminal_mode: bool,
859
860 keyboard_capture: bool,
864
865 terminal_mode_resume: std::collections::HashSet<BufferId>,
869
870 previous_click_time: Option<std::time::Instant>,
872
873 previous_click_position: Option<(u16, u16)>,
876
877 click_count: u8,
879
880 pub(crate) settings_state: Option<crate::view::settings::SettingsState>,
882
883 pub(crate) calibration_wizard: Option<calibration_wizard::CalibrationWizard>,
885
886 pub(crate) event_debug: Option<event_debug::EventDebug>,
888
889 pub(crate) keybinding_editor: Option<keybinding_editor::KeybindingEditor>,
891
892 pub(crate) key_translator: crate::input::key_translator::KeyTranslator,
894
895 color_capability: crate::view::color_support::ColorCapability,
897
898 review_hunks: Vec<fresh_core::api::ReviewHunk>,
900
901 active_action_popup: Option<(String, Vec<(String, String)>)>,
904
905 composite_buffers: HashMap<BufferId, crate::model::composite_buffer::CompositeBuffer>,
908
909 composite_view_states:
912 HashMap<(LeafId, BufferId), crate::view::composite_view::CompositeViewState>,
913
914 pending_file_opens: Vec<PendingFileOpen>,
918
919 pending_hot_exit_recovery: bool,
921
922 wait_tracking: HashMap<BufferId, (u64, bool)>,
924 completed_waits: Vec<u64>,
926
927 stdin_streaming: Option<StdinStreamingState>,
929
930 line_scan_state: Option<LineScanState>,
932
933 search_scan_state: Option<SearchScanState>,
935
936 search_overlay_top_byte: Option<usize>,
939}
940
941#[derive(Debug, Clone)]
943pub struct PendingFileOpen {
944 pub path: PathBuf,
946 pub line: Option<usize>,
948 pub column: Option<usize>,
950 pub end_line: Option<usize>,
952 pub end_column: Option<usize>,
954 pub message: Option<String>,
956 pub wait_id: Option<u64>,
958}
959
960#[allow(dead_code)] struct SearchScanState {
966 buffer_id: BufferId,
967 leaves: Vec<crate::model::piece_tree::LeafData>,
969 scan: crate::model::buffer::ChunkedSearchState,
971 query: String,
973 search_range: Option<std::ops::Range<usize>>,
975 case_sensitive: bool,
977 whole_word: bool,
978 use_regex: bool,
979}
980
981struct LineScanState {
983 buffer_id: BufferId,
984 leaves: Vec<crate::model::piece_tree::LeafData>,
986 chunks: Vec<crate::model::buffer::LineScanChunk>,
988 next_chunk: usize,
989 total_bytes: usize,
990 scanned_bytes: usize,
991 updates: Vec<(usize, usize)>,
993 open_goto_line_on_complete: bool,
996}
997
998pub struct StdinStreamingState {
1000 pub temp_path: PathBuf,
1002 pub buffer_id: BufferId,
1004 pub last_known_size: usize,
1006 pub complete: bool,
1008 pub thread_handle: Option<std::thread::JoinHandle<anyhow::Result<()>>>,
1010}
1011
1012impl Editor {
1013 pub fn new(
1016 config: Config,
1017 width: u16,
1018 height: u16,
1019 dir_context: DirectoryContext,
1020 color_capability: crate::view::color_support::ColorCapability,
1021 filesystem: Arc<dyn FileSystem + Send + Sync>,
1022 ) -> AnyhowResult<Self> {
1023 Self::with_working_dir(
1024 config,
1025 width,
1026 height,
1027 None,
1028 dir_context,
1029 true,
1030 color_capability,
1031 filesystem,
1032 )
1033 }
1034
1035 #[allow(clippy::too_many_arguments)]
1038 pub fn with_working_dir(
1039 config: Config,
1040 width: u16,
1041 height: u16,
1042 working_dir: Option<PathBuf>,
1043 dir_context: DirectoryContext,
1044 plugins_enabled: bool,
1045 color_capability: crate::view::color_support::ColorCapability,
1046 filesystem: Arc<dyn FileSystem + Send + Sync>,
1047 ) -> AnyhowResult<Self> {
1048 tracing::info!("Building default grammar registry...");
1049 let start = std::time::Instant::now();
1050 let grammar_registry = crate::primitives::grammar::GrammarRegistry::defaults_only();
1051 tracing::info!("Default grammar registry built in {:?}", start.elapsed());
1052 Self::with_options(
1056 config,
1057 width,
1058 height,
1059 working_dir,
1060 filesystem,
1061 plugins_enabled,
1062 dir_context,
1063 None,
1064 color_capability,
1065 grammar_registry,
1066 )
1067 }
1068
1069 #[allow(clippy::too_many_arguments)]
1074 pub fn for_test(
1075 config: Config,
1076 width: u16,
1077 height: u16,
1078 working_dir: Option<PathBuf>,
1079 dir_context: DirectoryContext,
1080 color_capability: crate::view::color_support::ColorCapability,
1081 filesystem: Arc<dyn FileSystem + Send + Sync>,
1082 time_source: Option<SharedTimeSource>,
1083 grammar_registry: Option<Arc<crate::primitives::grammar::GrammarRegistry>>,
1084 ) -> AnyhowResult<Self> {
1085 let grammar_registry =
1086 grammar_registry.unwrap_or_else(crate::primitives::grammar::GrammarRegistry::empty);
1087 let mut editor = Self::with_options(
1088 config,
1089 width,
1090 height,
1091 working_dir,
1092 filesystem,
1093 true,
1094 dir_context,
1095 time_source,
1096 color_capability,
1097 grammar_registry,
1098 )?;
1099 editor.needs_full_grammar_build = false;
1102 Ok(editor)
1103 }
1104
1105 #[allow(clippy::too_many_arguments)]
1109 fn with_options(
1110 mut config: Config,
1111 width: u16,
1112 height: u16,
1113 working_dir: Option<PathBuf>,
1114 filesystem: Arc<dyn FileSystem + Send + Sync>,
1115 enable_plugins: bool,
1116 dir_context: DirectoryContext,
1117 time_source: Option<SharedTimeSource>,
1118 color_capability: crate::view::color_support::ColorCapability,
1119 grammar_registry: Arc<crate::primitives::grammar::GrammarRegistry>,
1120 ) -> AnyhowResult<Self> {
1121 let time_source = time_source.unwrap_or_else(RealTimeSource::shared);
1123 tracing::info!("Editor::new called with width={}, height={}", width, height);
1124
1125 let working_dir = working_dir
1127 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
1128
1129 let working_dir = working_dir.canonicalize().unwrap_or(working_dir);
1132
1133 tracing::info!("Loading themes...");
1135 let theme_loader = crate::view::theme::ThemeLoader::new(dir_context.themes_dir());
1136 let scan_result =
1140 crate::services::packages::scan_installed_packages(&dir_context.config_dir);
1141
1142 for (lang_id, lang_config) in &scan_result.language_configs {
1144 config
1145 .languages
1146 .entry(lang_id.clone())
1147 .or_insert_with(|| lang_config.clone());
1148 }
1149
1150 for (lang_id, lsp_config) in &scan_result.lsp_configs {
1152 config
1153 .lsp
1154 .entry(lang_id.clone())
1155 .or_insert_with(|| LspLanguageConfig::Multi(vec![lsp_config.clone()]));
1156 }
1157
1158 let theme_registry = theme_loader.load_all(&scan_result.bundle_theme_dirs);
1159 tracing::info!("Themes loaded");
1160
1161 let theme = theme_registry.get_cloned(&config.theme).unwrap_or_else(|| {
1163 tracing::warn!(
1164 "Theme '{}' not found, falling back to default theme",
1165 config.theme.0
1166 );
1167 theme_registry
1168 .get_cloned(&crate::config::ThemeName(
1169 crate::view::theme::THEME_HIGH_CONTRAST.to_string(),
1170 ))
1171 .expect("Default theme must exist")
1172 });
1173
1174 theme.set_terminal_cursor_color();
1176
1177 let keybindings = Arc::new(RwLock::new(KeybindingResolver::new(&config)));
1178
1179 let mut buffers = HashMap::new();
1181 let mut event_logs = HashMap::new();
1182
1183 let buffer_id = BufferId(1);
1188 let mut state = EditorState::new(
1189 width,
1190 height,
1191 config.editor.large_file_threshold_bytes as usize,
1192 Arc::clone(&filesystem),
1193 );
1194 state
1196 .margins
1197 .configure_for_line_numbers(config.editor.line_numbers);
1198 state.buffer_settings.tab_size = config.editor.tab_size;
1199 state.buffer_settings.auto_close = config.editor.auto_close;
1200 tracing::info!("EditorState created for buffer {:?}", buffer_id);
1202 buffers.insert(buffer_id, state);
1203 event_logs.insert(buffer_id, EventLog::new());
1204
1205 let mut buffer_metadata = HashMap::new();
1207 buffer_metadata.insert(buffer_id, BufferMetadata::new());
1208
1209 let root_uri = types::file_path_to_lsp_uri(&working_dir);
1211
1212 let tokio_runtime = tokio::runtime::Builder::new_multi_thread()
1214 .worker_threads(2) .thread_name("editor-async")
1216 .enable_all()
1217 .build()
1218 .ok();
1219
1220 let async_bridge = AsyncBridge::new();
1222
1223 if tokio_runtime.is_none() {
1224 tracing::warn!("Failed to create Tokio runtime - async features disabled");
1225 }
1226
1227 let mut lsp = LspManager::new(root_uri);
1229
1230 if let Some(ref runtime) = tokio_runtime {
1232 lsp.set_runtime(runtime.handle().clone(), async_bridge.clone());
1233 }
1234
1235 for (language, lsp_configs) in &config.lsp {
1237 lsp.set_language_configs(language.clone(), lsp_configs.as_slice().to_vec());
1238 }
1239
1240 let universal_servers: Vec<LspServerConfig> = config
1242 .universal_lsp
1243 .values()
1244 .flat_map(|lc| lc.as_slice().to_vec())
1245 .filter(|c| c.enabled)
1246 .collect();
1247 if !universal_servers.is_empty() {
1248 for language in lsp.configured_languages() {
1249 lsp.append_language_configs(language, universal_servers.clone());
1250 }
1251 }
1252
1253 if working_dir.join("deno.json").exists() || working_dir.join("deno.jsonc").exists() {
1256 tracing::info!("Detected Deno project (deno.json found), using deno lsp for JS/TS");
1257 let deno_config = LspServerConfig {
1258 command: "deno".to_string(),
1259 args: vec!["lsp".to_string()],
1260 enabled: true,
1261 auto_start: false,
1262 process_limits: ProcessLimits::default(),
1263 initialization_options: Some(serde_json::json!({"enable": true})),
1264 ..Default::default()
1265 };
1266 lsp.set_language_config("javascript".to_string(), deno_config.clone());
1267 lsp.set_language_config("typescript".to_string(), deno_config);
1268 }
1269
1270 let split_manager = SplitManager::new(buffer_id);
1272
1273 let mut split_view_states = HashMap::new();
1275 let initial_split_id = split_manager.active_split();
1276 let mut initial_view_state = SplitViewState::with_buffer(width, height, buffer_id);
1277 initial_view_state.apply_config_defaults(
1278 config.editor.line_numbers,
1279 config.editor.highlight_current_line,
1280 config.editor.line_wrap,
1281 config.editor.wrap_indent,
1282 config.editor.wrap_column,
1283 config.editor.rulers.clone(),
1284 );
1285 split_view_states.insert(initial_split_id, initial_view_state);
1286
1287 let fs_manager = Arc::new(FsManager::new(Arc::clone(&filesystem)));
1289
1290 let command_registry = Arc::new(RwLock::new(CommandRegistry::new()));
1292
1293 let mut quick_open_registry = QuickOpenRegistry::new();
1295 let process_spawner: Arc<dyn crate::services::remote::ProcessSpawner> =
1296 Arc::new(crate::services::remote::LocalProcessSpawner);
1297 quick_open_registry.register(Box::new(FileProvider::new(
1298 Arc::clone(&filesystem),
1299 Arc::clone(&process_spawner),
1300 tokio_runtime.as_ref().map(|rt| rt.handle().clone()),
1301 )));
1302 quick_open_registry.register(Box::new(CommandProvider::new(
1303 Arc::clone(&command_registry),
1304 Arc::clone(&keybindings),
1305 )));
1306 quick_open_registry.register(Box::new(BufferProvider::new()));
1307 quick_open_registry.register(Box::new(GotoLineProvider::new()));
1308
1309 let theme_cache = Arc::new(RwLock::new(theme_registry.to_json_map()));
1311
1312 let plugin_manager = PluginManager::new(
1314 enable_plugins,
1315 Arc::clone(&command_registry),
1316 dir_context.clone(),
1317 Arc::clone(&theme_cache),
1318 );
1319
1320 #[cfg(feature = "plugins")]
1323 if let Some(snapshot_handle) = plugin_manager.state_snapshot_handle() {
1324 let mut snapshot = snapshot_handle.write().unwrap();
1325 snapshot.working_dir = working_dir.clone();
1326 }
1327
1328 if plugin_manager.is_active() {
1335 let mut plugin_dirs: Vec<std::path::PathBuf> = vec![];
1336
1337 if let Ok(exe_path) = std::env::current_exe() {
1339 if let Some(exe_dir) = exe_path.parent() {
1340 let exe_plugin_dir = exe_dir.join("plugins");
1341 if exe_plugin_dir.exists() {
1342 plugin_dirs.push(exe_plugin_dir);
1343 }
1344 }
1345 }
1346
1347 let working_plugin_dir = working_dir.join("plugins");
1349 if working_plugin_dir.exists() && !plugin_dirs.contains(&working_plugin_dir) {
1350 plugin_dirs.push(working_plugin_dir);
1351 }
1352
1353 #[cfg(feature = "embed-plugins")]
1355 if plugin_dirs.is_empty() {
1356 if let Some(embedded_dir) =
1357 crate::services::plugins::embedded::get_embedded_plugins_dir()
1358 {
1359 tracing::info!("Using embedded plugins from: {:?}", embedded_dir);
1360 plugin_dirs.push(embedded_dir.clone());
1361 }
1362 }
1363
1364 let user_plugins_dir = dir_context.config_dir.join("plugins");
1366 if user_plugins_dir.exists() && !plugin_dirs.contains(&user_plugins_dir) {
1367 tracing::info!("Found user plugins directory: {:?}", user_plugins_dir);
1368 plugin_dirs.push(user_plugins_dir.clone());
1369 }
1370
1371 let packages_dir = dir_context.config_dir.join("plugins").join("packages");
1373 if packages_dir.exists() {
1374 if let Ok(entries) = std::fs::read_dir(&packages_dir) {
1375 for entry in entries.flatten() {
1376 let path = entry.path();
1377 if path.is_dir() {
1379 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
1380 if !name.starts_with('.') {
1381 tracing::info!("Found package manager plugin: {:?}", path);
1382 plugin_dirs.push(path);
1383 }
1384 }
1385 }
1386 }
1387 }
1388 }
1389
1390 for dir in &scan_result.bundle_plugin_dirs {
1392 tracing::info!("Found bundle plugin directory: {:?}", dir);
1393 plugin_dirs.push(dir.clone());
1394 }
1395
1396 if plugin_dirs.is_empty() {
1397 tracing::debug!(
1398 "No plugins directory found next to executable or in working dir: {:?}",
1399 working_dir
1400 );
1401 }
1402
1403 for plugin_dir in plugin_dirs {
1405 tracing::info!("Loading TypeScript plugins from: {:?}", plugin_dir);
1406 let (errors, discovered_plugins) =
1407 plugin_manager.load_plugins_from_dir_with_config(&plugin_dir, &config.plugins);
1408
1409 for (name, plugin_config) in discovered_plugins {
1412 config.plugins.insert(name, plugin_config);
1413 }
1414
1415 if !errors.is_empty() {
1416 for err in &errors {
1417 tracing::error!("TypeScript plugin load error: {}", err);
1418 }
1419 #[cfg(debug_assertions)]
1421 panic!(
1422 "TypeScript plugin loading failed with {} error(s): {}",
1423 errors.len(),
1424 errors.join("; ")
1425 );
1426 }
1427 }
1428 }
1429
1430 let file_explorer_width = config.file_explorer.width;
1432 let recovery_enabled = config.editor.recovery_enabled;
1433 let check_for_updates = config.check_for_updates;
1434 let show_menu_bar = config.editor.show_menu_bar;
1435 let show_tab_bar = config.editor.show_tab_bar;
1436 let show_status_bar = config.editor.show_status_bar;
1437 let show_prompt_line = config.editor.show_prompt_line;
1438
1439 let update_checker = if check_for_updates {
1441 tracing::debug!("Update checking enabled, starting periodic checker");
1442 Some(
1443 crate::services::release_checker::start_periodic_update_check(
1444 crate::services::release_checker::DEFAULT_RELEASES_URL,
1445 time_source.clone(),
1446 dir_context.data_dir.clone(),
1447 ),
1448 )
1449 } else {
1450 tracing::debug!("Update checking disabled by config");
1451 None
1452 };
1453
1454 let user_config_raw = Config::read_user_config_raw(&working_dir);
1456
1457 let mut editor = Editor {
1458 buffers,
1459 event_logs,
1460 next_buffer_id: 2,
1461 config,
1462 user_config_raw,
1463 dir_context: dir_context.clone(),
1464 grammar_registry,
1465 pending_grammars: scan_result
1466 .additional_grammars
1467 .iter()
1468 .map(|g| PendingGrammar {
1469 language: g.language.clone(),
1470 grammar_path: g.path.to_string_lossy().to_string(),
1471 extensions: g.extensions.clone(),
1472 })
1473 .collect(),
1474 grammar_reload_pending: false,
1475 grammar_build_in_progress: false,
1476 needs_full_grammar_build: true,
1477 streaming_grep_cancellation: None,
1478 pending_grammar_callbacks: Vec::new(),
1479 theme,
1480 theme_registry,
1481 theme_cache,
1482 ansi_background: None,
1483 ansi_background_path: None,
1484 background_fade: crate::primitives::ansi_background::DEFAULT_BACKGROUND_FADE,
1485 keybindings,
1486 clipboard: crate::services::clipboard::Clipboard::new(),
1487 should_quit: false,
1488 should_detach: false,
1489 session_mode: false,
1490 software_cursor_only: false,
1491 session_name: None,
1492 pending_escape_sequences: Vec::new(),
1493 restart_with_dir: None,
1494 status_message: None,
1495 plugin_status_message: None,
1496 plugin_errors: Vec::new(),
1497 prompt: None,
1498 terminal_width: width,
1499 terminal_height: height,
1500 lsp: Some(lsp),
1501 buffer_metadata,
1502 mode_registry: ModeRegistry::new(),
1503 tokio_runtime,
1504 async_bridge: Some(async_bridge),
1505 split_manager,
1506 split_view_states,
1507 previous_viewports: HashMap::new(),
1508 scroll_sync_manager: ScrollSyncManager::new(),
1509 file_explorer: None,
1510 fs_manager,
1511 filesystem,
1512 local_filesystem: Arc::new(crate::model::filesystem::StdFileSystem),
1513 process_spawner,
1514 file_explorer_visible: false,
1515 file_explorer_sync_in_progress: false,
1516 file_explorer_width_percent: file_explorer_width,
1517 pending_file_explorer_show_hidden: None,
1518 pending_file_explorer_show_gitignored: None,
1519 menu_bar_visible: show_menu_bar,
1520 file_explorer_decorations: HashMap::new(),
1521 file_explorer_decoration_cache:
1522 crate::view::file_tree::FileExplorerDecorationCache::default(),
1523 menu_bar_auto_shown: false,
1524 tab_bar_visible: show_tab_bar,
1525 status_bar_visible: show_status_bar,
1526 prompt_line_visible: show_prompt_line,
1527 mouse_enabled: true,
1528 same_buffer_scroll_sync: false,
1529 mouse_cursor_position: None,
1530 gpm_active: false,
1531 key_context: KeyContext::Normal,
1532 menu_state: crate::view::ui::MenuState::new(dir_context.themes_dir()),
1533 menus: crate::config::MenuConfig::translated(),
1534 working_dir,
1535 position_history: PositionHistory::new(),
1536 in_navigation: false,
1537 next_lsp_request_id: 0,
1538 pending_completion_requests: HashSet::new(),
1539 completion_items: None,
1540 scheduled_completion_trigger: None,
1541 completion_service: crate::services::completion::CompletionService::new(),
1542 dabbrev_state: None,
1543 pending_goto_definition_request: None,
1544 pending_hover_request: None,
1545 pending_references_request: None,
1546 pending_references_symbol: String::new(),
1547 pending_signature_help_request: None,
1548 pending_code_actions_requests: HashSet::new(),
1549 pending_code_actions_server_names: HashMap::new(),
1550 pending_code_actions: None,
1551 pending_inlay_hints_request: None,
1552 pending_folding_range_requests: HashMap::new(),
1553 folding_ranges_in_flight: HashMap::new(),
1554 folding_ranges_debounce: HashMap::new(),
1555 pending_semantic_token_requests: HashMap::new(),
1556 semantic_tokens_in_flight: HashMap::new(),
1557 pending_semantic_token_range_requests: HashMap::new(),
1558 semantic_tokens_range_in_flight: HashMap::new(),
1559 semantic_tokens_range_last_request: HashMap::new(),
1560 semantic_tokens_range_applied: HashMap::new(),
1561 semantic_tokens_full_debounce: HashMap::new(),
1562 hover_symbol_range: None,
1563 hover_symbol_overlay: None,
1564 mouse_hover_screen_position: None,
1565 search_state: None,
1566 search_namespace: crate::view::overlay::OverlayNamespace::from_string(
1567 "search".to_string(),
1568 ),
1569 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace::from_string(
1570 "lsp-diagnostic".to_string(),
1571 ),
1572 pending_search_range: None,
1573 interactive_replace_state: None,
1574 lsp_status: String::new(),
1575 mouse_state: MouseState::default(),
1576 tab_context_menu: None,
1577 theme_info_popup: None,
1578 cached_layout: CachedLayout::default(),
1579 command_registry,
1580 quick_open_registry,
1581 plugin_manager,
1582 plugin_dev_workspaces: HashMap::new(),
1583 seen_byte_ranges: HashMap::new(),
1584 panel_ids: HashMap::new(),
1585 background_process_handles: HashMap::new(),
1586 prompt_histories: {
1587 let mut histories = HashMap::new();
1589 for history_name in ["search", "replace", "goto_line"] {
1590 let path = dir_context.prompt_history_path(history_name);
1591 let history = crate::input::input_history::InputHistory::load_from_file(&path)
1592 .unwrap_or_else(|e| {
1593 tracing::warn!("Failed to load {} history: {}", history_name, e);
1594 crate::input::input_history::InputHistory::new()
1595 });
1596 histories.insert(history_name.to_string(), history);
1597 }
1598 histories
1599 },
1600 pending_async_prompt_callback: None,
1601 lsp_progress: std::collections::HashMap::new(),
1602 lsp_server_statuses: std::collections::HashMap::new(),
1603 lsp_window_messages: Vec::new(),
1604 lsp_log_messages: Vec::new(),
1605 diagnostic_result_ids: HashMap::new(),
1606 scheduled_diagnostic_pull: None,
1607 stored_push_diagnostics: HashMap::new(),
1608 stored_pull_diagnostics: HashMap::new(),
1609 stored_diagnostics: HashMap::new(),
1610 stored_folding_ranges: HashMap::new(),
1611 event_broadcaster: crate::model::control_event::EventBroadcaster::default(),
1612 bookmarks: HashMap::new(),
1613 search_case_sensitive: true,
1614 search_whole_word: false,
1615 search_use_regex: false,
1616 search_confirm_each: false,
1617 macros: HashMap::new(),
1618 macro_recording: None,
1619 last_macro_register: None,
1620 macro_playing: false,
1621 #[cfg(feature = "plugins")]
1622 pending_plugin_actions: Vec::new(),
1623 #[cfg(feature = "plugins")]
1624 plugin_render_requested: false,
1625 chord_state: Vec::new(),
1626 pending_lsp_confirmation: None,
1627 pending_close_buffer: None,
1628 auto_revert_enabled: true,
1629 last_auto_revert_poll: time_source.now(),
1630 last_file_tree_poll: time_source.now(),
1631 git_index_resolved: false,
1632 file_mod_times: HashMap::new(),
1633 dir_mod_times: HashMap::new(),
1634 pending_file_poll_rx: None,
1635 pending_dir_poll_rx: None,
1636 file_rapid_change_counts: HashMap::new(),
1637 file_open_state: None,
1638 file_browser_layout: None,
1639 recovery_service: {
1640 let recovery_config = RecoveryConfig {
1641 enabled: recovery_enabled,
1642 ..RecoveryConfig::default()
1643 };
1644 RecoveryService::with_config_and_dir(recovery_config, dir_context.recovery_dir())
1645 },
1646 full_redraw_requested: false,
1647 time_source: time_source.clone(),
1648 last_auto_recovery_save: time_source.now(),
1649 last_persistent_auto_save: time_source.now(),
1650 active_custom_contexts: HashSet::new(),
1651 plugin_global_state: HashMap::new(),
1652 editor_mode: None,
1653 warning_log: None,
1654 status_log_path: None,
1655 warning_domains: WarningDomainRegistry::new(),
1656 update_checker,
1657 terminal_manager: crate::services::terminal::TerminalManager::new(),
1658 terminal_buffers: HashMap::new(),
1659 terminal_backing_files: HashMap::new(),
1660 terminal_log_files: HashMap::new(),
1661 terminal_mode: false,
1662 keyboard_capture: false,
1663 terminal_mode_resume: std::collections::HashSet::new(),
1664 previous_click_time: None,
1665 previous_click_position: None,
1666 click_count: 0,
1667 settings_state: None,
1668 calibration_wizard: None,
1669 event_debug: None,
1670 keybinding_editor: None,
1671 key_translator: crate::input::key_translator::KeyTranslator::load_from_config_dir(
1672 &dir_context.config_dir,
1673 )
1674 .unwrap_or_default(),
1675 color_capability,
1676 pending_file_opens: Vec::new(),
1677 pending_hot_exit_recovery: false,
1678 wait_tracking: HashMap::new(),
1679 completed_waits: Vec::new(),
1680 stdin_streaming: None,
1681 line_scan_state: None,
1682 search_scan_state: None,
1683 search_overlay_top_byte: None,
1684 review_hunks: Vec::new(),
1685 active_action_popup: None,
1686 composite_buffers: HashMap::new(),
1687 composite_view_states: HashMap::new(),
1688 };
1689
1690 editor.clipboard.apply_config(&editor.config.clipboard);
1692
1693 #[cfg(feature = "plugins")]
1694 {
1695 editor.update_plugin_state_snapshot();
1696 if editor.plugin_manager.is_active() {
1697 editor.plugin_manager.run_hook(
1698 "editor_initialized",
1699 crate::services::plugins::hooks::HookArgs::EditorInitialized,
1700 );
1701 }
1702 }
1703
1704 Ok(editor)
1705 }
1706
1707 pub fn event_broadcaster(&self) -> &crate::model::control_event::EventBroadcaster {
1709 &self.event_broadcaster
1710 }
1711
1712 fn start_background_grammar_build(
1717 &mut self,
1718 additional: Vec<crate::primitives::grammar::GrammarSpec>,
1719 callback_ids: Vec<fresh_core::api::JsCallbackId>,
1720 ) {
1721 let Some(bridge) = &self.async_bridge else {
1722 return;
1723 };
1724 self.grammar_build_in_progress = true;
1725 let sender = bridge.sender();
1726 let config_dir = self.dir_context.config_dir.clone();
1727 tracing::info!(
1728 "Spawning background grammar build thread ({} plugin grammars)...",
1729 additional.len()
1730 );
1731 std::thread::Builder::new()
1732 .name("grammar-build".to_string())
1733 .spawn(move || {
1734 tracing::info!("[grammar-build] Thread started");
1735 let start = std::time::Instant::now();
1736 let registry = if additional.is_empty() {
1737 crate::primitives::grammar::GrammarRegistry::for_editor(config_dir)
1738 } else {
1739 crate::primitives::grammar::GrammarRegistry::for_editor_with_additional(
1740 config_dir,
1741 &additional,
1742 )
1743 };
1744 tracing::info!("[grammar-build] Complete in {:?}", start.elapsed());
1745 drop(sender.send(
1746 crate::services::async_bridge::AsyncMessage::GrammarRegistryBuilt {
1747 registry,
1748 callback_ids,
1749 },
1750 ));
1751 })
1752 .ok();
1753 }
1754
1755 pub fn async_bridge(&self) -> Option<&AsyncBridge> {
1757 self.async_bridge.as_ref()
1758 }
1759
1760 pub fn config(&self) -> &Config {
1762 &self.config
1763 }
1764
1765 pub fn key_translator(&self) -> &crate::input::key_translator::KeyTranslator {
1767 &self.key_translator
1768 }
1769
1770 pub fn time_source(&self) -> &SharedTimeSource {
1772 &self.time_source
1773 }
1774
1775 pub fn emit_event(&self, name: impl Into<String>, data: serde_json::Value) {
1777 self.event_broadcaster.emit_named(name, data);
1778 }
1779
1780 fn send_plugin_response(&self, response: fresh_core::api::PluginResponse) {
1782 self.plugin_manager.deliver_response(response);
1783 }
1784
1785 fn take_pending_semantic_token_request(
1787 &mut self,
1788 request_id: u64,
1789 ) -> Option<SemanticTokenFullRequest> {
1790 if let Some(request) = self.pending_semantic_token_requests.remove(&request_id) {
1791 self.semantic_tokens_in_flight.remove(&request.buffer_id);
1792 Some(request)
1793 } else {
1794 None
1795 }
1796 }
1797
1798 fn take_pending_semantic_token_range_request(
1800 &mut self,
1801 request_id: u64,
1802 ) -> Option<SemanticTokenRangeRequest> {
1803 if let Some(request) = self
1804 .pending_semantic_token_range_requests
1805 .remove(&request_id)
1806 {
1807 self.semantic_tokens_range_in_flight
1808 .remove(&request.buffer_id);
1809 Some(request)
1810 } else {
1811 None
1812 }
1813 }
1814
1815 pub fn get_all_keybindings(&self) -> Vec<(String, String)> {
1817 self.keybindings.read().unwrap().get_all_bindings()
1818 }
1819
1820 pub fn get_keybinding_for_action(&self, action_name: &str) -> Option<String> {
1823 self.keybindings
1824 .read()
1825 .unwrap()
1826 .find_keybinding_for_action(action_name, self.key_context.clone())
1827 }
1828
1829 pub fn mode_registry_mut(&mut self) -> &mut ModeRegistry {
1831 &mut self.mode_registry
1832 }
1833
1834 pub fn mode_registry(&self) -> &ModeRegistry {
1836 &self.mode_registry
1837 }
1838
1839 #[inline]
1844 pub fn active_buffer(&self) -> BufferId {
1845 self.split_manager
1846 .active_buffer_id()
1847 .expect("Editor always has at least one buffer")
1848 }
1849
1850 pub fn active_buffer_mode(&self) -> Option<&str> {
1852 self.buffer_metadata
1853 .get(&self.active_buffer())
1854 .and_then(|meta| meta.virtual_mode())
1855 }
1856
1857 pub fn is_active_buffer_read_only(&self) -> bool {
1859 if let Some(metadata) = self.buffer_metadata.get(&self.active_buffer()) {
1860 if metadata.read_only {
1861 return true;
1862 }
1863 if let Some(mode_name) = metadata.virtual_mode() {
1865 return self.mode_registry.is_read_only(mode_name);
1866 }
1867 }
1868 false
1869 }
1870
1871 pub fn is_editing_disabled(&self) -> bool {
1874 self.active_state().editing_disabled
1875 }
1876
1877 pub fn mark_buffer_read_only(&mut self, buffer_id: BufferId, read_only: bool) {
1880 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
1881 metadata.read_only = read_only;
1882 }
1883 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1884 state.editing_disabled = read_only;
1885 }
1886 }
1887
1888 pub fn effective_mode(&self) -> Option<&str> {
1894 self.active_buffer_mode().or(self.editor_mode.as_deref())
1895 }
1896
1897 pub fn has_active_lsp_progress(&self) -> bool {
1899 !self.lsp_progress.is_empty()
1900 }
1901
1902 pub fn get_lsp_progress(&self) -> Vec<(String, String, Option<String>)> {
1904 self.lsp_progress
1905 .iter()
1906 .map(|(token, info)| (token.clone(), info.title.clone(), info.message.clone()))
1907 .collect()
1908 }
1909
1910 pub fn is_lsp_server_ready(&self, language: &str) -> bool {
1912 use crate::services::async_bridge::LspServerStatus;
1913 self.lsp_server_statuses.iter().any(|((lang, _), status)| {
1914 lang == language && matches!(status, LspServerStatus::Running)
1915 })
1916 }
1917
1918 pub fn get_lsp_status(&self) -> &str {
1920 &self.lsp_status
1921 }
1922
1923 pub fn get_stored_diagnostics(&self) -> &HashMap<String, Vec<lsp_types::Diagnostic>> {
1926 &self.stored_diagnostics
1927 }
1928
1929 pub fn is_update_available(&self) -> bool {
1931 self.update_checker
1932 .as_ref()
1933 .map(|c| c.is_update_available())
1934 .unwrap_or(false)
1935 }
1936
1937 pub fn latest_version(&self) -> Option<&str> {
1939 self.update_checker
1940 .as_ref()
1941 .and_then(|c| c.latest_version())
1942 }
1943
1944 pub fn get_update_result(
1946 &self,
1947 ) -> Option<&crate::services::release_checker::ReleaseCheckResult> {
1948 self.update_checker
1949 .as_ref()
1950 .and_then(|c| c.get_cached_result())
1951 }
1952
1953 #[doc(hidden)]
1958 pub fn set_update_checker(
1959 &mut self,
1960 checker: crate::services::release_checker::PeriodicUpdateChecker,
1961 ) {
1962 self.update_checker = Some(checker);
1963 }
1964
1965 pub fn set_lsp_config(&mut self, language: String, config: Vec<LspServerConfig>) {
1967 if let Some(ref mut lsp) = self.lsp {
1968 lsp.set_language_configs(language, config);
1969 }
1970 }
1971
1972 pub fn running_lsp_servers(&self) -> Vec<String> {
1974 self.lsp
1975 .as_ref()
1976 .map(|lsp| lsp.running_servers())
1977 .unwrap_or_default()
1978 }
1979
1980 pub fn pending_completion_requests_count(&self) -> usize {
1982 self.pending_completion_requests.len()
1983 }
1984
1985 pub fn completion_items_count(&self) -> usize {
1987 self.completion_items.as_ref().map_or(0, |v| v.len())
1988 }
1989
1990 pub fn initialized_lsp_server_count(&self, language: &str) -> usize {
1992 self.lsp
1993 .as_ref()
1994 .map(|lsp| {
1995 lsp.get_handles(language)
1996 .iter()
1997 .filter(|sh| sh.capabilities.initialized)
1998 .count()
1999 })
2000 .unwrap_or(0)
2001 }
2002
2003 pub fn shutdown_lsp_server(&mut self, language: &str) -> bool {
2007 if let Some(ref mut lsp) = self.lsp {
2008 lsp.shutdown_server(language)
2009 } else {
2010 false
2011 }
2012 }
2013
2014 pub fn enable_event_streaming<P: AsRef<Path>>(&mut self, path: P) -> AnyhowResult<()> {
2016 for event_log in self.event_logs.values_mut() {
2018 event_log.enable_streaming(&path)?;
2019 }
2020 Ok(())
2021 }
2022
2023 pub fn log_keystroke(&mut self, key_code: &str, modifiers: &str) {
2025 if let Some(event_log) = self.event_logs.get_mut(&self.active_buffer()) {
2026 event_log.log_keystroke(key_code, modifiers);
2027 }
2028 }
2029
2030 pub fn set_warning_log(&mut self, receiver: std::sync::mpsc::Receiver<()>, path: PathBuf) {
2035 self.warning_log = Some((receiver, path));
2036 }
2037
2038 pub fn set_status_log_path(&mut self, path: PathBuf) {
2040 self.status_log_path = Some(path);
2041 }
2042
2043 pub fn set_process_spawner(
2046 &mut self,
2047 spawner: Arc<dyn crate::services::remote::ProcessSpawner>,
2048 ) {
2049 self.process_spawner = spawner;
2050 }
2051
2052 pub fn remote_connection_info(&self) -> Option<&str> {
2056 self.filesystem.remote_connection_info()
2057 }
2058
2059 pub fn get_status_log_path(&self) -> Option<&PathBuf> {
2061 self.status_log_path.as_ref()
2062 }
2063
2064 pub fn open_status_log(&mut self) {
2066 if let Some(path) = self.status_log_path.clone() {
2067 match self.open_local_file(&path) {
2069 Ok(buffer_id) => {
2070 self.mark_buffer_read_only(buffer_id, true);
2071 }
2072 Err(e) => {
2073 tracing::error!("Failed to open status log: {}", e);
2074 }
2075 }
2076 } else {
2077 self.set_status_message("Status log not available".to_string());
2078 }
2079 }
2080
2081 pub fn check_warning_log(&mut self) -> bool {
2086 let Some((receiver, path)) = &self.warning_log else {
2087 return false;
2088 };
2089
2090 let mut new_warning_count = 0usize;
2092 while receiver.try_recv().is_ok() {
2093 new_warning_count += 1;
2094 }
2095
2096 if new_warning_count > 0 {
2097 self.warning_domains.general.add_warnings(new_warning_count);
2099 self.warning_domains.general.set_log_path(path.clone());
2100 }
2101
2102 new_warning_count > 0
2103 }
2104
2105 pub fn get_warning_domains(&self) -> &WarningDomainRegistry {
2107 &self.warning_domains
2108 }
2109
2110 pub fn get_warning_log_path(&self) -> Option<&PathBuf> {
2112 self.warning_domains.general.log_path.as_ref()
2113 }
2114
2115 pub fn open_warning_log(&mut self) {
2117 if let Some(path) = self.warning_domains.general.log_path.clone() {
2118 match self.open_local_file(&path) {
2120 Ok(buffer_id) => {
2121 self.mark_buffer_read_only(buffer_id, true);
2122 }
2123 Err(e) => {
2124 tracing::error!("Failed to open warning log: {}", e);
2125 }
2126 }
2127 }
2128 }
2129
2130 pub fn clear_warning_indicator(&mut self) {
2132 self.warning_domains.general.clear();
2133 }
2134
2135 pub fn clear_warnings(&mut self) {
2137 self.warning_domains.general.clear();
2138 self.warning_domains.lsp.clear();
2139 self.status_message = Some("Warnings cleared".to_string());
2140 }
2141
2142 pub fn has_lsp_error(&self) -> bool {
2144 self.warning_domains.lsp.level() == WarningLevel::Error
2145 }
2146
2147 pub fn get_effective_warning_level(&self) -> WarningLevel {
2150 self.warning_domains.lsp.level()
2151 }
2152
2153 pub fn get_general_warning_level(&self) -> WarningLevel {
2155 self.warning_domains.general.level()
2156 }
2157
2158 pub fn get_general_warning_count(&self) -> usize {
2160 self.warning_domains.general.count
2161 }
2162
2163 pub fn update_lsp_warning_domain(&mut self) {
2165 self.warning_domains
2166 .lsp
2167 .update_from_statuses(&self.lsp_server_statuses);
2168 }
2169
2170 pub fn check_mouse_hover_timer(&mut self) -> bool {
2176 if !self.config.editor.mouse_hover_enabled {
2178 return false;
2179 }
2180
2181 let hover_delay = std::time::Duration::from_millis(self.config.editor.mouse_hover_delay_ms);
2182
2183 let hover_info = match self.mouse_state.lsp_hover_state {
2185 Some((byte_pos, start_time, screen_x, screen_y)) => {
2186 if self.mouse_state.lsp_hover_request_sent {
2187 return false; }
2189 if start_time.elapsed() < hover_delay {
2190 return false; }
2192 Some((byte_pos, screen_x, screen_y))
2193 }
2194 None => return false,
2195 };
2196
2197 let Some((byte_pos, screen_x, screen_y)) = hover_info else {
2198 return false;
2199 };
2200
2201 self.mouse_hover_screen_position = Some((screen_x, screen_y));
2203
2204 match self.request_hover_at_position(byte_pos) {
2206 Ok(true) => {
2207 self.mouse_state.lsp_hover_request_sent = true;
2208 true
2209 }
2210 Ok(false) => false, Err(e) => {
2212 tracing::debug!("Failed to request hover: {}", e);
2213 false
2214 }
2215 }
2216 }
2217
2218 pub fn check_semantic_highlight_timer(&self) -> bool {
2223 for state in self.buffers.values() {
2225 if let Some(remaining) = state.reference_highlight_overlay.needs_redraw() {
2226 if remaining.is_zero() {
2227 return true;
2228 }
2229 }
2230 }
2231 false
2232 }
2233
2234 pub fn check_diagnostic_pull_timer(&mut self) -> bool {
2239 let Some((buffer_id, trigger_time)) = self.scheduled_diagnostic_pull else {
2240 return false;
2241 };
2242
2243 if Instant::now() < trigger_time {
2244 return false;
2245 }
2246
2247 self.scheduled_diagnostic_pull = None;
2248
2249 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
2251 return false;
2252 };
2253 let Some(uri) = metadata.file_uri().cloned() else {
2254 return false;
2255 };
2256 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
2257 return false;
2258 };
2259
2260 let Some(lsp) = self.lsp.as_mut() else {
2261 return false;
2262 };
2263 let Some(sh) = lsp.handle_for_feature_mut(&language, crate::types::LspFeature::Diagnostics)
2264 else {
2265 return false;
2266 };
2267 let client = &mut sh.handle;
2268
2269 let request_id = self.next_lsp_request_id;
2270 self.next_lsp_request_id += 1;
2271 let previous_result_id = self.diagnostic_result_ids.get(uri.as_str()).cloned();
2272 if let Err(e) = client.document_diagnostic(request_id, uri.clone(), previous_result_id) {
2273 tracing::debug!(
2274 "Failed to pull diagnostics after edit for {}: {}",
2275 uri.as_str(),
2276 e
2277 );
2278 } else {
2279 tracing::debug!(
2280 "Pulling diagnostics after edit for {} (request_id={})",
2281 uri.as_str(),
2282 request_id
2283 );
2284 }
2285
2286 false }
2288
2289 pub fn check_completion_trigger_timer(&mut self) -> bool {
2295 let Some(trigger_time) = self.scheduled_completion_trigger else {
2297 return false;
2298 };
2299
2300 if Instant::now() < trigger_time {
2302 return false;
2303 }
2304
2305 self.scheduled_completion_trigger = None;
2307
2308 if self.active_state().popups.is_visible() {
2310 return false;
2311 }
2312
2313 self.request_completion();
2315
2316 true
2317 }
2318
2319 fn load_ansi_background(&mut self, input: &str) -> AnyhowResult<()> {
2321 let trimmed = input.trim();
2322
2323 if trimmed.is_empty() {
2324 self.ansi_background = None;
2325 self.ansi_background_path = None;
2326 self.set_status_message(t!("status.background_cleared").to_string());
2327 return Ok(());
2328 }
2329
2330 let input_path = Path::new(trimmed);
2331 let resolved = if input_path.is_absolute() {
2332 input_path.to_path_buf()
2333 } else {
2334 self.working_dir.join(input_path)
2335 };
2336
2337 let canonical = resolved.canonicalize().unwrap_or_else(|_| resolved.clone());
2338
2339 let parsed = crate::primitives::ansi_background::AnsiBackground::from_file(&canonical)?;
2340
2341 self.ansi_background = Some(parsed);
2342 self.ansi_background_path = Some(canonical.clone());
2343 self.set_status_message(
2344 t!(
2345 "view.background_set",
2346 path = canonical.display().to_string()
2347 )
2348 .to_string(),
2349 );
2350
2351 Ok(())
2352 }
2353
2354 fn effective_tabs_width(&self) -> u16 {
2359 if self.file_explorer_visible && self.file_explorer.is_some() {
2360 let editor_percent = 1.0 - self.file_explorer_width_percent;
2362 (self.terminal_width as f32 * editor_percent) as u16
2363 } else {
2364 self.terminal_width
2365 }
2366 }
2367
2368 fn set_active_buffer(&mut self, buffer_id: BufferId) {
2378 if self.active_buffer() == buffer_id {
2379 return; }
2381
2382 self.on_editor_focus_lost();
2384
2385 self.cancel_search_prompt_if_active();
2388
2389 let previous = self.active_buffer();
2391
2392 if self.terminal_mode && self.is_terminal_buffer(previous) {
2394 self.terminal_mode_resume.insert(previous);
2395 self.terminal_mode = false;
2396 self.key_context = crate::input::keybindings::KeyContext::Normal;
2397 }
2398
2399 self.split_manager.set_active_buffer_id(buffer_id);
2401
2402 let active_split = self.split_manager.active_split();
2404 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
2405 view_state.switch_buffer(buffer_id);
2406 view_state.add_buffer(buffer_id);
2407 view_state.push_focus(previous);
2409 }
2410
2411 if self.terminal_mode_resume.contains(&buffer_id) && self.is_terminal_buffer(buffer_id) {
2413 self.terminal_mode = true;
2414 self.key_context = crate::input::keybindings::KeyContext::Terminal;
2415 } else if self.is_terminal_buffer(buffer_id) {
2416 self.sync_terminal_to_buffer(buffer_id);
2419 }
2420
2421 self.ensure_active_tab_visible(active_split, buffer_id, self.effective_tabs_width());
2423
2424 #[cfg(feature = "plugins")]
2430 self.update_plugin_state_snapshot();
2431
2432 self.plugin_manager.run_hook(
2434 "buffer_activated",
2435 crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
2436 );
2437 }
2438
2439 pub(super) fn focus_split(&mut self, split_id: LeafId, buffer_id: BufferId) {
2450 let previous_split = self.split_manager.active_split();
2451 let previous_buffer = self.active_buffer(); let split_changed = previous_split != split_id;
2453
2454 if split_changed {
2455 if self.terminal_mode && self.is_terminal_buffer(previous_buffer) {
2457 self.terminal_mode = false;
2458 self.key_context = crate::input::keybindings::KeyContext::Normal;
2459 }
2460
2461 self.split_manager.set_active_split(split_id);
2463
2464 self.split_manager.set_active_buffer_id(buffer_id);
2466
2467 if self.is_terminal_buffer(buffer_id) {
2469 self.terminal_mode = true;
2470 self.key_context = crate::input::keybindings::KeyContext::Terminal;
2471 } else {
2472 self.key_context = crate::input::keybindings::KeyContext::Normal;
2475 }
2476
2477 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2480 view_state.switch_buffer(buffer_id);
2481 }
2482
2483 if previous_buffer != buffer_id {
2485 self.position_history.commit_pending_movement();
2486 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2487 view_state.add_buffer(buffer_id);
2488 view_state.push_focus(previous_buffer);
2489 }
2490 }
2493 } else {
2494 self.set_active_buffer(buffer_id);
2496 }
2497 }
2498
2499 pub fn active_state(&self) -> &EditorState {
2501 self.buffers.get(&self.active_buffer()).unwrap()
2502 }
2503
2504 pub fn active_state_mut(&mut self) -> &mut EditorState {
2506 self.buffers.get_mut(&self.active_buffer()).unwrap()
2507 }
2508
2509 pub fn active_cursors(&self) -> &Cursors {
2511 let split_id = self.split_manager.active_split();
2512 &self.split_view_states.get(&split_id).unwrap().cursors
2513 }
2514
2515 pub fn active_cursors_mut(&mut self) -> &mut Cursors {
2517 let split_id = self.split_manager.active_split();
2518 &mut self.split_view_states.get_mut(&split_id).unwrap().cursors
2519 }
2520
2521 pub fn set_completion_items(&mut self, items: Vec<lsp_types::CompletionItem>) {
2523 self.completion_items = Some(items);
2524 }
2525
2526 pub fn active_viewport(&self) -> &crate::view::viewport::Viewport {
2528 let active_split = self.split_manager.active_split();
2529 &self.split_view_states.get(&active_split).unwrap().viewport
2530 }
2531
2532 pub fn active_viewport_mut(&mut self) -> &mut crate::view::viewport::Viewport {
2534 let active_split = self.split_manager.active_split();
2535 &mut self
2536 .split_view_states
2537 .get_mut(&active_split)
2538 .unwrap()
2539 .viewport
2540 }
2541
2542 pub fn get_buffer_display_name(&self, buffer_id: BufferId) -> String {
2544 if let Some(composite) = self.composite_buffers.get(&buffer_id) {
2546 return composite.name.clone();
2547 }
2548
2549 self.buffer_metadata
2550 .get(&buffer_id)
2551 .map(|m| m.display_name.clone())
2552 .or_else(|| {
2553 self.buffers.get(&buffer_id).and_then(|state| {
2554 state
2555 .buffer
2556 .file_path()
2557 .and_then(|p| p.file_name())
2558 .and_then(|n| n.to_str())
2559 .map(|s| s.to_string())
2560 })
2561 })
2562 .unwrap_or_else(|| "[No Name]".to_string())
2563 }
2564
2565 pub fn log_and_apply_event(&mut self, event: &Event) {
2577 if let Event::Delete { range, .. } = event {
2579 let displaced = self.active_state().capture_displaced_markers(range);
2580 self.active_event_log_mut().append(event.clone());
2581 if !displaced.is_empty() {
2582 self.active_event_log_mut()
2583 .set_displaced_markers_on_last(displaced);
2584 }
2585 } else {
2586 self.active_event_log_mut().append(event.clone());
2587 }
2588 self.apply_event_to_active_buffer(event);
2589 }
2590
2591 pub fn apply_event_to_active_buffer(&mut self, event: &Event) {
2592 match event {
2595 Event::Scroll { line_offset } => {
2596 self.handle_scroll_event(*line_offset);
2597 return;
2598 }
2599 Event::SetViewport { top_line } => {
2600 self.handle_set_viewport_event(*top_line);
2601 return;
2602 }
2603 Event::Recenter => {
2604 self.handle_recenter_event();
2605 return;
2606 }
2607 _ => {}
2608 }
2609
2610 let lsp_changes = self.collect_lsp_changes(event);
2614
2615 let line_info = self.calculate_event_line_info(event);
2617
2618 {
2621 let split_id = self.split_manager.active_split();
2622 let active_buf = self.active_buffer();
2623 let cursors = &mut self
2624 .split_view_states
2625 .get_mut(&split_id)
2626 .unwrap()
2627 .keyed_states
2628 .get_mut(&active_buf)
2629 .unwrap()
2630 .cursors;
2631 let state = self.buffers.get_mut(&active_buf).unwrap();
2632 state.apply(cursors, event);
2633 }
2634
2635 match event {
2638 Event::Insert { .. } | Event::Delete { .. } | Event::BulkEdit { .. } => {
2639 self.invalidate_layouts_for_buffer(self.active_buffer());
2640 self.schedule_semantic_tokens_full_refresh(self.active_buffer());
2641 self.schedule_folding_ranges_refresh(self.active_buffer());
2642 }
2643 Event::Batch { events, .. } => {
2644 let has_edits = events
2645 .iter()
2646 .any(|e| matches!(e, Event::Insert { .. } | Event::Delete { .. }));
2647 if has_edits {
2648 self.invalidate_layouts_for_buffer(self.active_buffer());
2649 self.schedule_semantic_tokens_full_refresh(self.active_buffer());
2650 self.schedule_folding_ranges_refresh(self.active_buffer());
2651 }
2652 }
2653 _ => {}
2654 }
2655
2656 self.adjust_other_split_cursors_for_event(event);
2658
2659 let in_interactive_replace = self.interactive_replace_state.is_some();
2663
2664 let _ = in_interactive_replace; self.trigger_plugin_hooks_for_event(event, line_info);
2673
2674 if lsp_changes.is_empty() && event.modifies_buffer() {
2680 if let Some(full_text) = self.active_state().buffer.to_string() {
2681 let full_change = vec![TextDocumentContentChangeEvent {
2682 range: None,
2683 range_length: None,
2684 text: full_text,
2685 }];
2686 self.send_lsp_changes_for_buffer(self.active_buffer(), full_change);
2687 }
2688 } else {
2689 self.send_lsp_changes_for_buffer(self.active_buffer(), lsp_changes);
2690 }
2691 }
2692
2693 pub fn apply_events_as_bulk_edit(
2707 &mut self,
2708 events: Vec<Event>,
2709 description: String,
2710 ) -> Option<Event> {
2711 use crate::model::event::CursorId;
2712
2713 let has_buffer_mods = events
2715 .iter()
2716 .any(|e| matches!(e, Event::Insert { .. } | Event::Delete { .. }));
2717
2718 if !has_buffer_mods {
2719 return None;
2721 }
2722
2723 let active_buf = self.active_buffer();
2724 let split_id = self.split_manager.active_split();
2725
2726 let old_cursors: Vec<(CursorId, usize, Option<usize>)> = self
2728 .split_view_states
2729 .get(&split_id)
2730 .unwrap()
2731 .keyed_states
2732 .get(&active_buf)
2733 .unwrap()
2734 .cursors
2735 .iter()
2736 .map(|(id, c)| (id, c.position, c.anchor))
2737 .collect();
2738
2739 let state = self.buffers.get_mut(&active_buf).unwrap();
2740
2741 let old_snapshot = state.buffer.snapshot_buffer_state();
2743
2744 let mut edits: Vec<(usize, usize, String)> = Vec::new();
2748
2749 for event in &events {
2750 match event {
2751 Event::Insert { position, text, .. } => {
2752 edits.push((*position, 0, text.clone()));
2753 }
2754 Event::Delete { range, .. } => {
2755 edits.push((range.start, range.len(), String::new()));
2756 }
2757 _ => {}
2758 }
2759 }
2760
2761 edits.sort_by(|a, b| b.0.cmp(&a.0));
2763
2764 let edit_refs: Vec<(usize, usize, &str)> = edits
2766 .iter()
2767 .map(|(pos, del, text)| (*pos, *del, text.as_str()))
2768 .collect();
2769
2770 let displaced_markers = state.capture_displaced_markers_bulk(&edits);
2772
2773 let _delta = state.buffer.apply_bulk_edits(&edit_refs);
2775
2776 let edit_lengths: Vec<(usize, usize, usize)> = {
2782 let mut lengths: Vec<(usize, usize, usize)> = Vec::new();
2783 for (pos, del_len, text) in &edits {
2784 if let Some(last) = lengths.last_mut() {
2785 if last.0 == *pos {
2786 last.1 += del_len;
2788 last.2 += text.len();
2789 continue;
2790 }
2791 }
2792 lengths.push((*pos, *del_len, text.len()));
2793 }
2794 lengths
2795 };
2796
2797 for &(pos, del_len, ins_len) in &edit_lengths {
2802 if del_len > 0 && ins_len > 0 {
2803 if ins_len > del_len {
2805 state.marker_list.adjust_for_insert(pos, ins_len - del_len);
2806 state.margins.adjust_for_insert(pos, ins_len - del_len);
2807 } else if del_len > ins_len {
2808 state.marker_list.adjust_for_delete(pos, del_len - ins_len);
2809 state.margins.adjust_for_delete(pos, del_len - ins_len);
2810 }
2811 } else if del_len > 0 {
2813 state.marker_list.adjust_for_delete(pos, del_len);
2814 state.margins.adjust_for_delete(pos, del_len);
2815 } else if ins_len > 0 {
2816 state.marker_list.adjust_for_insert(pos, ins_len);
2817 state.margins.adjust_for_insert(pos, ins_len);
2818 }
2819 }
2820
2821 let new_snapshot = state.buffer.snapshot_buffer_state();
2823
2824 let mut new_cursors: Vec<(CursorId, usize, Option<usize>)> = old_cursors.clone();
2827
2828 let mut position_deltas: Vec<(usize, isize)> = Vec::new();
2831 for (pos, del_len, text) in &edits {
2832 let delta = text.len() as isize - *del_len as isize;
2833 position_deltas.push((*pos, delta));
2834 }
2835 position_deltas.sort_by_key(|(pos, _)| *pos);
2836
2837 let calc_shift = |original_pos: usize| -> isize {
2839 let mut shift: isize = 0;
2840 for (edit_pos, delta) in &position_deltas {
2841 if *edit_pos < original_pos {
2842 shift += delta;
2843 }
2844 }
2845 shift
2846 };
2847
2848 for (cursor_id, ref mut pos, ref mut anchor) in &mut new_cursors {
2852 let mut found_move_cursor = false;
2853 let original_pos = *pos;
2855
2856 let insert_at_cursor_pos = events.iter().any(|e| {
2860 matches!(e, Event::Insert { position, cursor_id: c, .. }
2861 if *c == *cursor_id && *position == original_pos)
2862 });
2863
2864 for event in &events {
2866 if let Event::MoveCursor {
2867 cursor_id: event_cursor,
2868 new_position,
2869 new_anchor,
2870 ..
2871 } = event
2872 {
2873 if event_cursor == cursor_id {
2874 let shift = if insert_at_cursor_pos {
2878 calc_shift(original_pos)
2879 } else {
2880 0
2881 };
2882 *pos = (*new_position as isize + shift).max(0) as usize;
2883 *anchor = *new_anchor;
2884 found_move_cursor = true;
2885 }
2886 }
2887 }
2888
2889 if !found_move_cursor {
2891 let mut found_edit = false;
2892 for event in &events {
2893 match event {
2894 Event::Insert {
2895 position,
2896 text,
2897 cursor_id: event_cursor,
2898 } if event_cursor == cursor_id => {
2899 let shift = calc_shift(*position);
2902 let adjusted_pos = (*position as isize + shift).max(0) as usize;
2903 *pos = adjusted_pos.saturating_add(text.len());
2904 *anchor = None;
2905 found_edit = true;
2906 }
2907 Event::Delete {
2908 range,
2909 cursor_id: event_cursor,
2910 ..
2911 } if event_cursor == cursor_id => {
2912 let shift = calc_shift(range.start);
2915 *pos = (range.start as isize + shift).max(0) as usize;
2916 *anchor = None;
2917 found_edit = true;
2918 }
2919 _ => {}
2920 }
2921 }
2922
2923 if !found_edit {
2927 let shift = calc_shift(original_pos);
2928 *pos = (original_pos as isize + shift).max(0) as usize;
2929 }
2930 }
2931 }
2932
2933 {
2935 let cursors = &mut self
2936 .split_view_states
2937 .get_mut(&split_id)
2938 .unwrap()
2939 .keyed_states
2940 .get_mut(&active_buf)
2941 .unwrap()
2942 .cursors;
2943 for (cursor_id, position, anchor) in &new_cursors {
2944 if let Some(cursor) = cursors.get_mut(*cursor_id) {
2945 cursor.position = *position;
2946 cursor.anchor = *anchor;
2947 }
2948 }
2949 }
2950
2951 self.buffers
2953 .get_mut(&active_buf)
2954 .unwrap()
2955 .highlighter
2956 .invalidate_all();
2957
2958 let bulk_edit = Event::BulkEdit {
2960 old_snapshot: Some(old_snapshot),
2961 new_snapshot: Some(new_snapshot),
2962 old_cursors,
2963 new_cursors,
2964 description,
2965 edits: edit_lengths,
2966 displaced_markers,
2967 };
2968
2969 self.invalidate_layouts_for_buffer(self.active_buffer());
2971 self.adjust_other_split_cursors_for_event(&bulk_edit);
2972 let buffer_id = self.active_buffer();
2979 let full_content_change = self
2980 .buffers
2981 .get(&buffer_id)
2982 .and_then(|s| s.buffer.to_string())
2983 .map(|text| {
2984 vec![TextDocumentContentChangeEvent {
2985 range: None,
2986 range_length: None,
2987 text,
2988 }]
2989 })
2990 .unwrap_or_default();
2991 if !full_content_change.is_empty() {
2992 self.send_lsp_changes_for_buffer(buffer_id, full_content_change);
2993 }
2994
2995 Some(bulk_edit)
2996 }
2997
2998 fn trigger_plugin_hooks_for_event(&mut self, event: &Event, line_info: EventLineInfo) {
3001 let buffer_id = self.active_buffer();
3002
3003 let mut cursor_changed_lines = false;
3005 let hook_args = match event {
3006 Event::Insert { position, text, .. } => {
3007 let insert_position = *position;
3008 let insert_len = text.len();
3009
3010 if let Some(seen) = self.seen_byte_ranges.get_mut(&buffer_id) {
3012 let adjusted: std::collections::HashSet<(usize, usize)> = seen
3017 .iter()
3018 .filter_map(|&(start, end)| {
3019 if end <= insert_position {
3020 Some((start, end))
3022 } else if start >= insert_position {
3023 Some((start + insert_len, end + insert_len))
3025 } else {
3026 None
3028 }
3029 })
3030 .collect();
3031 *seen = adjusted;
3032 }
3033
3034 Some((
3035 "after_insert",
3036 crate::services::plugins::hooks::HookArgs::AfterInsert {
3037 buffer_id,
3038 position: *position,
3039 text: text.clone(),
3040 affected_start: insert_position,
3042 affected_end: insert_position + insert_len,
3043 start_line: line_info.start_line,
3045 end_line: line_info.end_line,
3046 lines_added: line_info.line_delta.max(0) as usize,
3047 },
3048 ))
3049 }
3050 Event::Delete {
3051 range,
3052 deleted_text,
3053 ..
3054 } => {
3055 let delete_start = range.start;
3056
3057 let delete_end = range.end;
3059 let delete_len = delete_end - delete_start;
3060 if let Some(seen) = self.seen_byte_ranges.get_mut(&buffer_id) {
3061 let adjusted: std::collections::HashSet<(usize, usize)> = seen
3066 .iter()
3067 .filter_map(|&(start, end)| {
3068 if end <= delete_start {
3069 Some((start, end))
3071 } else if start >= delete_end {
3072 Some((start - delete_len, end - delete_len))
3074 } else {
3075 None
3077 }
3078 })
3079 .collect();
3080 *seen = adjusted;
3081 }
3082
3083 Some((
3084 "after_delete",
3085 crate::services::plugins::hooks::HookArgs::AfterDelete {
3086 buffer_id,
3087 range: range.clone(),
3088 deleted_text: deleted_text.clone(),
3089 affected_start: delete_start,
3091 deleted_len: deleted_text.len(),
3092 start_line: line_info.start_line,
3094 end_line: line_info.end_line,
3095 lines_removed: (-line_info.line_delta).max(0) as usize,
3096 },
3097 ))
3098 }
3099 Event::Batch { events, .. } => {
3100 for e in events {
3104 let sub_line_info = self.calculate_event_line_info(e);
3107 self.trigger_plugin_hooks_for_event(e, sub_line_info);
3108 }
3109 None
3110 }
3111 Event::MoveCursor {
3112 cursor_id,
3113 old_position,
3114 new_position,
3115 ..
3116 } => {
3117 let old_line = self.active_state().buffer.get_line_number(*old_position) + 1;
3119 let line = self.active_state().buffer.get_line_number(*new_position) + 1;
3120 cursor_changed_lines = old_line != line;
3121 let text_props = self
3122 .active_state()
3123 .text_properties
3124 .get_at(*new_position)
3125 .into_iter()
3126 .map(|tp| tp.properties.clone())
3127 .collect();
3128 Some((
3129 "cursor_moved",
3130 crate::services::plugins::hooks::HookArgs::CursorMoved {
3131 buffer_id,
3132 cursor_id: *cursor_id,
3133 old_position: *old_position,
3134 new_position: *new_position,
3135 line,
3136 text_properties: text_props,
3137 },
3138 ))
3139 }
3140 _ => None,
3141 };
3142
3143 if let Some((hook_name, ref args)) = hook_args {
3145 #[cfg(feature = "plugins")]
3149 self.update_plugin_state_snapshot();
3150
3151 self.plugin_manager.run_hook(hook_name, args.clone());
3152 }
3153
3154 if cursor_changed_lines {
3165 self.handle_refresh_lines(buffer_id);
3166 }
3167 }
3168
3169 fn handle_scroll_event(&mut self, line_offset: isize) {
3175 use crate::view::ui::view_pipeline::ViewLineIterator;
3176
3177 let active_split = self.split_manager.active_split();
3178
3179 if let Some(group) = self
3183 .scroll_sync_manager
3184 .find_group_for_split(active_split.into())
3185 {
3186 let left = group.left_split;
3187 let right = group.right_split;
3188 if let Some(vs) = self.split_view_states.get_mut(&LeafId(left)) {
3189 vs.viewport.set_skip_ensure_visible();
3190 }
3191 if let Some(vs) = self.split_view_states.get_mut(&LeafId(right)) {
3192 vs.viewport.set_skip_ensure_visible();
3193 }
3194 }
3196
3197 let sync_group = self
3199 .split_view_states
3200 .get(&active_split)
3201 .and_then(|vs| vs.sync_group);
3202 let splits_to_scroll = if let Some(group_id) = sync_group {
3203 self.split_manager
3204 .get_splits_in_group(group_id, &self.split_view_states)
3205 } else {
3206 vec![active_split]
3207 };
3208
3209 for split_id in splits_to_scroll {
3210 let buffer_id = if let Some(id) = self.split_manager.buffer_for_split(split_id) {
3211 id
3212 } else {
3213 continue;
3214 };
3215 let tab_size = self.config.editor.tab_size;
3216
3217 let view_transform_tokens = self
3219 .split_view_states
3220 .get(&split_id)
3221 .and_then(|vs| vs.view_transform.as_ref())
3222 .map(|vt| vt.tokens.clone());
3223
3224 if let Some(state) = self.buffers.get_mut(&buffer_id) {
3226 let buffer = &mut state.buffer;
3227 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
3228 if let Some(tokens) = view_transform_tokens {
3229 let view_lines: Vec<_> =
3231 ViewLineIterator::new(&tokens, false, false, tab_size, false).collect();
3232 view_state
3233 .viewport
3234 .scroll_view_lines(&view_lines, line_offset);
3235 } else {
3236 if line_offset > 0 {
3238 view_state
3239 .viewport
3240 .scroll_down(buffer, line_offset as usize);
3241 } else {
3242 view_state
3243 .viewport
3244 .scroll_up(buffer, line_offset.unsigned_abs());
3245 }
3246 }
3247 view_state.viewport.set_skip_ensure_visible();
3249 }
3250 }
3251 }
3252 }
3253
3254 fn handle_set_viewport_event(&mut self, top_line: usize) {
3256 let active_split = self.split_manager.active_split();
3257
3258 if self
3261 .scroll_sync_manager
3262 .is_split_synced(active_split.into())
3263 {
3264 if let Some(group) = self
3265 .scroll_sync_manager
3266 .find_group_for_split_mut(active_split.into())
3267 {
3268 let scroll_line = if group.is_left_split(active_split.into()) {
3270 top_line
3271 } else {
3272 group.right_to_left_line(top_line)
3273 };
3274 group.set_scroll_line(scroll_line);
3275 }
3276
3277 if let Some(group) = self
3279 .scroll_sync_manager
3280 .find_group_for_split(active_split.into())
3281 {
3282 let left = group.left_split;
3283 let right = group.right_split;
3284 if let Some(vs) = self.split_view_states.get_mut(&LeafId(left)) {
3285 vs.viewport.set_skip_ensure_visible();
3286 }
3287 if let Some(vs) = self.split_view_states.get_mut(&LeafId(right)) {
3288 vs.viewport.set_skip_ensure_visible();
3289 }
3290 }
3291 return;
3292 }
3293
3294 let sync_group = self
3296 .split_view_states
3297 .get(&active_split)
3298 .and_then(|vs| vs.sync_group);
3299 let splits_to_scroll = if let Some(group_id) = sync_group {
3300 self.split_manager
3301 .get_splits_in_group(group_id, &self.split_view_states)
3302 } else {
3303 vec![active_split]
3304 };
3305
3306 for split_id in splits_to_scroll {
3307 let buffer_id = if let Some(id) = self.split_manager.buffer_for_split(split_id) {
3308 id
3309 } else {
3310 continue;
3311 };
3312
3313 if let Some(state) = self.buffers.get_mut(&buffer_id) {
3314 let buffer = &mut state.buffer;
3315 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
3316 view_state.viewport.scroll_to(buffer, top_line);
3317 view_state.viewport.set_skip_ensure_visible();
3319 }
3320 }
3321 }
3322 }
3323
3324 fn handle_recenter_event(&mut self) {
3326 let active_split = self.split_manager.active_split();
3327
3328 let sync_group = self
3330 .split_view_states
3331 .get(&active_split)
3332 .and_then(|vs| vs.sync_group);
3333 let splits_to_recenter = if let Some(group_id) = sync_group {
3334 self.split_manager
3335 .get_splits_in_group(group_id, &self.split_view_states)
3336 } else {
3337 vec![active_split]
3338 };
3339
3340 for split_id in splits_to_recenter {
3341 let buffer_id = if let Some(id) = self.split_manager.buffer_for_split(split_id) {
3342 id
3343 } else {
3344 continue;
3345 };
3346
3347 if let Some(state) = self.buffers.get_mut(&buffer_id) {
3348 let buffer = &mut state.buffer;
3349 let view_state = self.split_view_states.get_mut(&split_id);
3350
3351 if let Some(view_state) = view_state {
3352 let cursor = *view_state.cursors.primary();
3354 let viewport_height = view_state.viewport.visible_line_count();
3355 let target_rows_from_top = viewport_height / 2;
3356
3357 let mut iter = buffer.line_iterator(cursor.position, 80);
3359 for _ in 0..target_rows_from_top {
3360 if iter.prev().is_none() {
3361 break;
3362 }
3363 }
3364 let new_top_byte = iter.current_position();
3365 view_state.viewport.top_byte = new_top_byte;
3366 view_state.viewport.set_skip_ensure_visible();
3368 }
3369 }
3370 }
3371 }
3372
3373 fn invalidate_layouts_for_buffer(&mut self, buffer_id: BufferId) {
3380 let splits_for_buffer = self.split_manager.splits_for_buffer(buffer_id);
3382
3383 for split_id in splits_for_buffer {
3385 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
3386 view_state.invalidate_layout();
3387 view_state.view_transform = None;
3391 view_state.view_transform_stale = true;
3394 }
3395 }
3396 }
3397
3398 pub fn active_event_log(&self) -> &EventLog {
3400 self.event_logs.get(&self.active_buffer()).unwrap()
3401 }
3402
3403 pub fn active_event_log_mut(&mut self) -> &mut EventLog {
3405 self.event_logs.get_mut(&self.active_buffer()).unwrap()
3406 }
3407
3408 pub(super) fn update_modified_from_event_log(&mut self) {
3412 let is_at_saved = self
3413 .event_logs
3414 .get(&self.active_buffer())
3415 .map(|log| log.is_at_saved_position())
3416 .unwrap_or(false);
3417
3418 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
3419 state.buffer.set_modified(!is_at_saved);
3420 }
3421 }
3422
3423 pub fn should_quit(&self) -> bool {
3425 self.should_quit
3426 }
3427
3428 pub fn should_detach(&self) -> bool {
3430 self.should_detach
3431 }
3432
3433 pub fn clear_detach(&mut self) {
3435 self.should_detach = false;
3436 }
3437
3438 pub fn set_session_mode(&mut self, session_mode: bool) {
3440 self.session_mode = session_mode;
3441 self.clipboard.set_session_mode(session_mode);
3442 if session_mode {
3444 self.active_custom_contexts
3445 .insert(crate::types::context_keys::SESSION_MODE.to_string());
3446 } else {
3447 self.active_custom_contexts
3448 .remove(crate::types::context_keys::SESSION_MODE);
3449 }
3450 }
3451
3452 pub fn is_session_mode(&self) -> bool {
3454 self.session_mode
3455 }
3456
3457 pub fn set_software_cursor_only(&mut self, enabled: bool) {
3460 self.software_cursor_only = enabled;
3461 }
3462
3463 pub fn set_session_name(&mut self, name: Option<String>) {
3469 if let Some(ref session_name) = name {
3470 let base_recovery_dir = self.dir_context.recovery_dir();
3471 let scope = crate::services::recovery::RecoveryScope::Session {
3472 name: session_name.clone(),
3473 };
3474 let recovery_config = RecoveryConfig {
3475 enabled: self.recovery_service.is_enabled(),
3476 ..RecoveryConfig::default()
3477 };
3478 self.recovery_service =
3479 RecoveryService::with_scope(recovery_config, &base_recovery_dir, &scope);
3480 }
3481 self.session_name = name;
3482 }
3483
3484 pub fn session_name(&self) -> Option<&str> {
3486 self.session_name.as_deref()
3487 }
3488
3489 pub fn queue_escape_sequences(&mut self, sequences: &[u8]) {
3491 self.pending_escape_sequences.extend_from_slice(sequences);
3492 }
3493
3494 pub fn take_pending_escape_sequences(&mut self) -> Vec<u8> {
3496 std::mem::take(&mut self.pending_escape_sequences)
3497 }
3498
3499 pub fn take_pending_clipboard(
3501 &mut self,
3502 ) -> Option<crate::services::clipboard::PendingClipboard> {
3503 self.clipboard.take_pending_clipboard()
3504 }
3505
3506 pub fn should_restart(&self) -> bool {
3508 self.restart_with_dir.is_some()
3509 }
3510
3511 pub fn take_restart_dir(&mut self) -> Option<PathBuf> {
3514 self.restart_with_dir.take()
3515 }
3516
3517 pub fn request_full_redraw(&mut self) {
3522 self.full_redraw_requested = true;
3523 }
3524
3525 pub fn take_full_redraw_request(&mut self) -> bool {
3527 let requested = self.full_redraw_requested;
3528 self.full_redraw_requested = false;
3529 requested
3530 }
3531
3532 pub fn request_restart(&mut self, new_working_dir: PathBuf) {
3533 tracing::info!(
3534 "Restart requested with new working directory: {}",
3535 new_working_dir.display()
3536 );
3537 self.restart_with_dir = Some(new_working_dir);
3538 self.should_quit = true;
3540 }
3541
3542 pub fn theme(&self) -> &crate::view::theme::Theme {
3544 &self.theme
3545 }
3546
3547 pub fn is_settings_open(&self) -> bool {
3549 self.settings_state.as_ref().is_some_and(|s| s.visible)
3550 }
3551
3552 pub fn quit(&mut self) {
3554 let modified_count = self.count_modified_buffers_needing_prompt();
3556 if modified_count > 0 {
3557 let save_key = t!("prompt.key.save").to_string();
3558 let cancel_key = t!("prompt.key.cancel").to_string();
3559 let hot_exit = self.config.editor.hot_exit;
3560
3561 let msg = if hot_exit {
3562 let quit_key = t!("prompt.key.quit").to_string();
3564 if modified_count == 1 {
3565 t!(
3566 "prompt.quit_modified_hot_one",
3567 save_key = save_key,
3568 quit_key = quit_key,
3569 cancel_key = cancel_key
3570 )
3571 .to_string()
3572 } else {
3573 t!(
3574 "prompt.quit_modified_hot_many",
3575 count = modified_count,
3576 save_key = save_key,
3577 quit_key = quit_key,
3578 cancel_key = cancel_key
3579 )
3580 .to_string()
3581 }
3582 } else {
3583 let discard_key = t!("prompt.key.discard").to_string();
3585 if modified_count == 1 {
3586 t!(
3587 "prompt.quit_modified_one",
3588 save_key = save_key,
3589 discard_key = discard_key,
3590 cancel_key = cancel_key
3591 )
3592 .to_string()
3593 } else {
3594 t!(
3595 "prompt.quit_modified_many",
3596 count = modified_count,
3597 save_key = save_key,
3598 discard_key = discard_key,
3599 cancel_key = cancel_key
3600 )
3601 .to_string()
3602 }
3603 };
3604 self.start_prompt(msg, PromptType::ConfirmQuitWithModified);
3605 } else {
3606 self.should_quit = true;
3607 }
3608 }
3609
3610 fn count_modified_buffers_needing_prompt(&self) -> usize {
3618 let hot_exit = self.config.editor.hot_exit;
3619 let auto_save = self.config.editor.auto_save_enabled;
3620
3621 self.buffers
3622 .iter()
3623 .filter(|(buffer_id, state)| {
3624 if !state.buffer.is_modified() {
3625 return false;
3626 }
3627 if let Some(meta) = self.buffer_metadata.get(buffer_id) {
3628 if let Some(path) = meta.file_path() {
3629 let is_unnamed = path.as_os_str().is_empty();
3630 if is_unnamed && hot_exit {
3631 return false; }
3633 if !is_unnamed && auto_save {
3634 return false; }
3636 }
3637 }
3638 true
3639 })
3640 .count()
3641 }
3642
3643 pub fn focus_gained(&mut self) {
3645 self.plugin_manager.run_hook(
3646 "focus_gained",
3647 crate::services::plugins::hooks::HookArgs::FocusGained,
3648 );
3649 }
3650
3651 pub fn resize(&mut self, width: u16, height: u16) {
3653 self.terminal_width = width;
3655 self.terminal_height = height;
3656
3657 for view_state in self.split_view_states.values_mut() {
3659 view_state.viewport.resize(width, height);
3660 }
3661
3662 self.resize_visible_terminals();
3664
3665 self.plugin_manager.run_hook(
3667 "resize",
3668 fresh_core::hooks::HookArgs::Resize { width, height },
3669 );
3670 }
3671
3672 pub fn start_prompt(&mut self, message: String, prompt_type: PromptType) {
3676 self.start_prompt_with_suggestions(message, prompt_type, Vec::new());
3677 }
3678
3679 fn start_search_prompt(
3684 &mut self,
3685 message: String,
3686 prompt_type: PromptType,
3687 use_selection_range: bool,
3688 ) {
3689 self.pending_search_range = None;
3691
3692 let selection_range = self.active_cursors().primary().selection_range();
3693
3694 let selected_text = if let Some(range) = selection_range.clone() {
3695 let state = self.active_state_mut();
3696 let text = state.get_text_range(range.start, range.end);
3697 if !text.contains('\n') && !text.is_empty() {
3698 Some(text)
3699 } else {
3700 None
3701 }
3702 } else {
3703 None
3704 };
3705
3706 if use_selection_range {
3707 self.pending_search_range = selection_range;
3708 }
3709
3710 let from_history = selected_text.is_none();
3712 let default_text = selected_text.or_else(|| {
3713 self.get_prompt_history("search")
3714 .and_then(|h| h.last().map(|s| s.to_string()))
3715 });
3716
3717 self.start_prompt(message, prompt_type);
3719
3720 if let Some(text) = default_text {
3722 if let Some(ref mut prompt) = self.prompt {
3723 prompt.set_input(text.clone());
3724 prompt.selection_anchor = Some(0);
3725 prompt.cursor_pos = text.len();
3726 }
3727 if from_history {
3728 self.get_or_create_prompt_history("search").init_at_last();
3729 }
3730 self.update_search_highlights(&text);
3731 }
3732 }
3733
3734 pub fn start_prompt_with_suggestions(
3736 &mut self,
3737 message: String,
3738 prompt_type: PromptType,
3739 suggestions: Vec<Suggestion>,
3740 ) {
3741 self.on_editor_focus_lost();
3743
3744 match prompt_type {
3747 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
3748 self.clear_search_highlights();
3749 }
3750 _ => {}
3751 }
3752
3753 let needs_suggestions = matches!(
3755 prompt_type,
3756 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
3757 );
3758
3759 self.prompt = Some(Prompt::with_suggestions(message, prompt_type, suggestions));
3760
3761 if needs_suggestions {
3763 self.update_prompt_suggestions();
3764 }
3765 }
3766
3767 pub fn start_prompt_with_initial_text(
3769 &mut self,
3770 message: String,
3771 prompt_type: PromptType,
3772 initial_text: String,
3773 ) {
3774 self.on_editor_focus_lost();
3776
3777 self.prompt = Some(Prompt::with_initial_text(
3778 message,
3779 prompt_type,
3780 initial_text,
3781 ));
3782 }
3783
3784 pub fn start_quick_open(&mut self) {
3786 self.on_editor_focus_lost();
3788
3789 self.status_message = None;
3791
3792 let mut prompt = Prompt::with_suggestions(String::new(), PromptType::QuickOpen, vec![]);
3794 prompt.input = ">".to_string();
3795 prompt.cursor_pos = 1;
3796 self.prompt = Some(prompt);
3797
3798 self.update_quick_open_suggestions(">");
3800 }
3801
3802 fn build_quick_open_context(&self) -> QuickOpenContext {
3804 let open_buffers = self
3805 .buffers
3806 .iter()
3807 .filter_map(|(buffer_id, state)| {
3808 let path = state.buffer.file_path()?;
3809 let name = path
3810 .file_name()
3811 .map(|n| n.to_string_lossy().to_string())
3812 .unwrap_or_else(|| format!("Buffer {}", buffer_id.0));
3813 Some(BufferInfo {
3814 id: buffer_id.0,
3815 path: path.display().to_string(),
3816 name,
3817 modified: state.buffer.is_modified(),
3818 })
3819 })
3820 .collect();
3821
3822 let has_lsp_config = {
3823 let language = self
3824 .buffers
3825 .get(&self.active_buffer())
3826 .map(|s| s.language.as_str());
3827 language
3828 .and_then(|lang| self.lsp.as_ref().and_then(|lsp| lsp.get_config(lang)))
3829 .is_some()
3830 };
3831
3832 QuickOpenContext {
3833 cwd: self.working_dir.display().to_string(),
3834 open_buffers,
3835 active_buffer_id: self.active_buffer().0,
3836 active_buffer_path: self
3837 .active_state()
3838 .buffer
3839 .file_path()
3840 .map(|p| p.display().to_string()),
3841 has_selection: self.has_active_selection(),
3842 key_context: self.key_context.clone(),
3843 custom_contexts: self.active_custom_contexts.clone(),
3844 buffer_mode: self
3845 .buffer_metadata
3846 .get(&self.active_buffer())
3847 .and_then(|m| m.virtual_mode())
3848 .map(|s| s.to_string()),
3849 has_lsp_config,
3850 }
3851 }
3852
3853 fn update_quick_open_suggestions(&mut self, input: &str) {
3855 let context = self.build_quick_open_context();
3856 let suggestions = if let Some((provider, query)) =
3857 self.quick_open_registry.get_provider_for_input(input)
3858 {
3859 provider.suggestions(query, &context)
3860 } else {
3861 vec![]
3862 };
3863
3864 if let Some(prompt) = &mut self.prompt {
3865 prompt.suggestions = suggestions;
3866 prompt.selected_suggestion = if prompt.suggestions.is_empty() {
3867 None
3868 } else {
3869 Some(0)
3870 };
3871 }
3872 }
3873
3874 fn cancel_search_prompt_if_active(&mut self) {
3877 if let Some(ref prompt) = self.prompt {
3878 if matches!(
3879 prompt.prompt_type,
3880 PromptType::Search
3881 | PromptType::ReplaceSearch
3882 | PromptType::Replace { .. }
3883 | PromptType::QueryReplaceSearch
3884 | PromptType::QueryReplace { .. }
3885 | PromptType::QueryReplaceConfirm
3886 ) {
3887 self.prompt = None;
3888 self.interactive_replace_state = None;
3890 let ns = self.search_namespace.clone();
3892 let state = self.active_state_mut();
3893 state.overlays.clear_namespace(&ns, &mut state.marker_list);
3894 }
3895 }
3896 }
3897
3898 fn prefill_open_file_prompt(&mut self) {
3900 if let Some(prompt) = self.prompt.as_mut() {
3904 if prompt.prompt_type == PromptType::OpenFile {
3905 prompt.input.clear();
3906 prompt.cursor_pos = 0;
3907 prompt.selection_anchor = None;
3908 }
3909 }
3910 }
3911
3912 fn init_file_open_state(&mut self) {
3918 let buffer_id = self.active_buffer();
3920
3921 let initial_dir = if self.is_terminal_buffer(buffer_id) {
3924 self.get_terminal_id(buffer_id)
3925 .and_then(|tid| self.terminal_manager.get(tid))
3926 .and_then(|handle| handle.cwd())
3927 .unwrap_or_else(|| self.working_dir.clone())
3928 } else {
3929 self.active_state()
3930 .buffer
3931 .file_path()
3932 .and_then(|path| path.parent())
3933 .map(|p| p.to_path_buf())
3934 .unwrap_or_else(|| self.working_dir.clone())
3935 };
3936
3937 let show_hidden = self.config.file_browser.show_hidden;
3939 self.file_open_state = Some(file_open::FileOpenState::new(
3940 initial_dir.clone(),
3941 show_hidden,
3942 self.filesystem.clone(),
3943 ));
3944
3945 self.load_file_open_directory(initial_dir);
3947 self.load_file_open_shortcuts_async();
3948 }
3949
3950 fn init_folder_open_state(&mut self) {
3955 let initial_dir = self.working_dir.clone();
3957
3958 let show_hidden = self.config.file_browser.show_hidden;
3960 self.file_open_state = Some(file_open::FileOpenState::new(
3961 initial_dir.clone(),
3962 show_hidden,
3963 self.filesystem.clone(),
3964 ));
3965
3966 self.load_file_open_directory(initial_dir);
3968 self.load_file_open_shortcuts_async();
3969 }
3970
3971 pub fn change_working_dir(&mut self, new_path: PathBuf) {
3981 let new_path = new_path.canonicalize().unwrap_or(new_path);
3983
3984 self.request_restart(new_path);
3987 }
3988
3989 fn load_file_open_directory(&mut self, path: PathBuf) {
3991 if let Some(state) = &mut self.file_open_state {
3993 state.current_dir = path.clone();
3994 state.loading = true;
3995 state.error = None;
3996 state.update_shortcuts();
3997 }
3998
3999 if let Some(ref runtime) = self.tokio_runtime {
4001 let fs_manager = self.fs_manager.clone();
4002 let sender = self.async_bridge.as_ref().map(|b| b.sender());
4003
4004 runtime.spawn(async move {
4005 let result = fs_manager.list_dir_with_metadata(path).await;
4006 if let Some(sender) = sender {
4007 #[allow(clippy::let_underscore_must_use)]
4009 let _ = sender.send(AsyncMessage::FileOpenDirectoryLoaded(result));
4010 }
4011 });
4012 } else {
4013 if let Some(state) = &mut self.file_open_state {
4015 state.set_error("Async runtime not available".to_string());
4016 }
4017 }
4018 }
4019
4020 pub(super) fn handle_file_open_directory_loaded(
4022 &mut self,
4023 result: std::io::Result<Vec<crate::services::fs::DirEntry>>,
4024 ) {
4025 match result {
4026 Ok(entries) => {
4027 if let Some(state) = &mut self.file_open_state {
4028 state.set_entries(entries);
4029 }
4030 let filter = self
4032 .prompt
4033 .as_ref()
4034 .map(|p| p.input.clone())
4035 .unwrap_or_default();
4036 if !filter.is_empty() {
4037 if let Some(state) = &mut self.file_open_state {
4038 state.apply_filter(&filter);
4039 }
4040 }
4041 }
4042 Err(e) => {
4043 if let Some(state) = &mut self.file_open_state {
4044 state.set_error(e.to_string());
4045 }
4046 }
4047 }
4048 }
4049
4050 fn load_file_open_shortcuts_async(&mut self) {
4054 if let Some(ref runtime) = self.tokio_runtime {
4055 let filesystem = self.filesystem.clone();
4056 let sender = self.async_bridge.as_ref().map(|b| b.sender());
4057
4058 runtime.spawn(async move {
4059 let shortcuts = tokio::task::spawn_blocking(move || {
4061 file_open::FileOpenState::build_shortcuts_async(&*filesystem)
4062 })
4063 .await
4064 .unwrap_or_default();
4065
4066 if let Some(sender) = sender {
4067 #[allow(clippy::let_underscore_must_use)]
4069 let _ = sender.send(AsyncMessage::FileOpenShortcutsLoaded(shortcuts));
4070 }
4071 });
4072 }
4073 }
4074
4075 pub(super) fn handle_file_open_shortcuts_loaded(
4077 &mut self,
4078 shortcuts: Vec<file_open::NavigationShortcut>,
4079 ) {
4080 if let Some(state) = &mut self.file_open_state {
4081 state.merge_async_shortcuts(shortcuts);
4082 }
4083 }
4084
4085 pub fn cancel_prompt(&mut self) {
4087 let theme_to_restore = if let Some(ref prompt) = self.prompt {
4089 if let PromptType::SelectTheme { original_theme } = &prompt.prompt_type {
4090 Some(original_theme.clone())
4091 } else {
4092 None
4093 }
4094 } else {
4095 None
4096 };
4097
4098 if let Some(ref prompt) = self.prompt {
4100 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
4102 if let Some(history) = self.prompt_histories.get_mut(&key) {
4103 history.reset_navigation();
4104 }
4105 }
4106 match &prompt.prompt_type {
4107 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
4108 self.clear_search_highlights();
4109 }
4110 PromptType::Plugin { custom_type } => {
4111 use crate::services::plugins::hooks::HookArgs;
4113 self.plugin_manager.run_hook(
4114 "prompt_cancelled",
4115 HookArgs::PromptCancelled {
4116 prompt_type: custom_type.clone(),
4117 input: prompt.input.clone(),
4118 },
4119 );
4120 }
4121 PromptType::LspRename { overlay_handle, .. } => {
4122 let remove_overlay_event = crate::model::event::Event::RemoveOverlay {
4124 handle: overlay_handle.clone(),
4125 };
4126 self.apply_event_to_active_buffer(&remove_overlay_event);
4127 }
4128 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs => {
4129 self.file_open_state = None;
4131 self.file_browser_layout = None;
4132 }
4133 PromptType::AsyncPrompt => {
4134 if let Some(callback_id) = self.pending_async_prompt_callback.take() {
4136 self.plugin_manager
4137 .resolve_callback(callback_id, "null".to_string());
4138 }
4139 }
4140 _ => {}
4141 }
4142 }
4143
4144 self.prompt = None;
4145 self.pending_search_range = None;
4146 self.status_message = Some(t!("search.cancelled").to_string());
4147
4148 if let Some(original_theme) = theme_to_restore {
4150 self.preview_theme(&original_theme);
4151 }
4152 }
4153
4154 pub fn handle_prompt_scroll(&mut self, delta: i32) -> bool {
4157 if let Some(ref mut prompt) = self.prompt {
4158 if prompt.suggestions.is_empty() {
4159 return false;
4160 }
4161
4162 let current = prompt.selected_suggestion.unwrap_or(0);
4163 let len = prompt.suggestions.len();
4164
4165 let new_selected = if delta < 0 {
4168 current.saturating_sub((-delta) as usize)
4170 } else {
4171 (current + delta as usize).min(len.saturating_sub(1))
4173 };
4174
4175 prompt.selected_suggestion = Some(new_selected);
4176
4177 if !matches!(prompt.prompt_type, PromptType::Plugin { .. }) {
4179 if let Some(suggestion) = prompt.suggestions.get(new_selected) {
4180 prompt.input = suggestion.get_value().to_string();
4181 prompt.cursor_pos = prompt.input.len();
4182 }
4183 }
4184
4185 return true;
4186 }
4187 false
4188 }
4189
4190 pub fn confirm_prompt(&mut self) -> Option<(String, PromptType, Option<usize>)> {
4195 if let Some(prompt) = self.prompt.take() {
4196 let selected_index = prompt.selected_suggestion;
4197 let mut final_input = if prompt.sync_input_on_navigate {
4199 prompt.input.clone()
4202 } else if matches!(
4203 prompt.prompt_type,
4204 PromptType::OpenFile
4205 | PromptType::SwitchProject
4206 | PromptType::SaveFileAs
4207 | PromptType::StopLspServer
4208 | PromptType::RestartLspServer
4209 | PromptType::SelectTheme { .. }
4210 | PromptType::SelectLocale
4211 | PromptType::SwitchToTab
4212 | PromptType::SetLanguage
4213 | PromptType::SetEncoding
4214 | PromptType::SetLineEnding
4215 | PromptType::Plugin { .. }
4216 ) {
4217 if let Some(selected_idx) = prompt.selected_suggestion {
4219 if let Some(suggestion) = prompt.suggestions.get(selected_idx) {
4220 if suggestion.disabled {
4222 self.set_status_message(
4223 t!(
4224 "error.command_not_available",
4225 command = suggestion.text.clone()
4226 )
4227 .to_string(),
4228 );
4229 return None;
4230 }
4231 suggestion.get_value().to_string()
4233 } else {
4234 prompt.input.clone()
4235 }
4236 } else {
4237 prompt.input.clone()
4238 }
4239 } else {
4240 prompt.input.clone()
4241 };
4242
4243 if matches!(
4245 prompt.prompt_type,
4246 PromptType::StopLspServer | PromptType::RestartLspServer
4247 ) {
4248 let is_valid = prompt
4249 .suggestions
4250 .iter()
4251 .any(|s| s.text == final_input || s.get_value() == final_input);
4252 if !is_valid {
4253 self.prompt = Some(prompt);
4255 self.set_status_message(
4256 t!("error.no_lsp_match", input = final_input.clone()).to_string(),
4257 );
4258 return None;
4259 }
4260 }
4261
4262 if matches!(prompt.prompt_type, PromptType::RemoveRuler) {
4266 if prompt.input.is_empty() {
4267 if let Some(selected_idx) = prompt.selected_suggestion {
4269 if let Some(suggestion) = prompt.suggestions.get(selected_idx) {
4270 final_input = suggestion.get_value().to_string();
4271 }
4272 } else {
4273 self.prompt = Some(prompt);
4274 return None;
4275 }
4276 } else {
4277 let typed = prompt.input.trim().to_string();
4279 let matched = prompt.suggestions.iter().find(|s| s.get_value() == typed);
4280 if let Some(suggestion) = matched {
4281 final_input = suggestion.get_value().to_string();
4282 } else {
4283 self.prompt = Some(prompt);
4285 return None;
4286 }
4287 }
4288 }
4289
4290 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
4292 let history = self.get_or_create_prompt_history(&key);
4293 history.push(final_input.clone());
4294 history.reset_navigation();
4295 }
4296
4297 Some((final_input, prompt.prompt_type, selected_index))
4298 } else {
4299 None
4300 }
4301 }
4302
4303 pub fn is_prompting(&self) -> bool {
4305 self.prompt.is_some()
4306 }
4307
4308 fn get_or_create_prompt_history(
4310 &mut self,
4311 key: &str,
4312 ) -> &mut crate::input::input_history::InputHistory {
4313 self.prompt_histories.entry(key.to_string()).or_default()
4314 }
4315
4316 fn get_prompt_history(&self, key: &str) -> Option<&crate::input::input_history::InputHistory> {
4318 self.prompt_histories.get(key)
4319 }
4320
4321 fn prompt_type_to_history_key(prompt_type: &crate::view::prompt::PromptType) -> Option<String> {
4323 use crate::view::prompt::PromptType;
4324 match prompt_type {
4325 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
4326 Some("search".to_string())
4327 }
4328 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
4329 Some("replace".to_string())
4330 }
4331 PromptType::GotoLine => Some("goto_line".to_string()),
4332 PromptType::Plugin { custom_type } => Some(format!("plugin:{}", custom_type)),
4333 _ => None,
4334 }
4335 }
4336
4337 pub fn editor_mode(&self) -> Option<String> {
4340 self.editor_mode.clone()
4341 }
4342
4343 pub fn command_registry(&self) -> &Arc<RwLock<CommandRegistry>> {
4345 &self.command_registry
4346 }
4347
4348 pub fn plugin_manager(&self) -> &PluginManager {
4350 &self.plugin_manager
4351 }
4352
4353 pub fn plugin_manager_mut(&mut self) -> &mut PluginManager {
4355 &mut self.plugin_manager
4356 }
4357
4358 pub fn file_explorer_is_focused(&self) -> bool {
4360 self.key_context == KeyContext::FileExplorer
4361 }
4362
4363 pub fn prompt_input(&self) -> Option<&str> {
4365 self.prompt.as_ref().map(|p| p.input.as_str())
4366 }
4367
4368 pub fn has_active_selection(&self) -> bool {
4370 self.active_cursors().primary().selection_range().is_some()
4371 }
4372
4373 pub fn prompt_mut(&mut self) -> Option<&mut Prompt> {
4375 self.prompt.as_mut()
4376 }
4377
4378 pub fn set_status_message(&mut self, message: String) {
4380 tracing::info!(target: "status", "{}", message);
4381 self.plugin_status_message = None;
4382 self.status_message = Some(message);
4383 }
4384
4385 pub fn get_status_message(&self) -> Option<&String> {
4387 self.plugin_status_message
4388 .as_ref()
4389 .or(self.status_message.as_ref())
4390 }
4391
4392 pub fn get_plugin_errors(&self) -> &[String] {
4395 &self.plugin_errors
4396 }
4397
4398 pub fn clear_plugin_errors(&mut self) {
4400 self.plugin_errors.clear();
4401 }
4402
4403 pub fn update_prompt_suggestions(&mut self) {
4405 let (prompt_type, input) = if let Some(prompt) = &self.prompt {
4407 (prompt.prompt_type.clone(), prompt.input.clone())
4408 } else {
4409 return;
4410 };
4411
4412 match prompt_type {
4413 PromptType::QuickOpen => {
4414 self.update_quick_open_suggestions(&input);
4416 }
4417 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
4418 self.update_search_highlights(&input);
4420 if let Some(history) = self.prompt_histories.get_mut("search") {
4422 history.reset_navigation();
4423 }
4424 }
4425 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
4426 if let Some(history) = self.prompt_histories.get_mut("replace") {
4428 history.reset_navigation();
4429 }
4430 }
4431 PromptType::GotoLine => {
4432 if let Some(history) = self.prompt_histories.get_mut("goto_line") {
4434 history.reset_navigation();
4435 }
4436 }
4437 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs => {
4438 self.update_file_open_filter();
4440 }
4441 PromptType::Plugin { custom_type } => {
4442 let key = format!("plugin:{}", custom_type);
4444 if let Some(history) = self.prompt_histories.get_mut(&key) {
4445 history.reset_navigation();
4446 }
4447 use crate::services::plugins::hooks::HookArgs;
4449 self.plugin_manager.run_hook(
4450 "prompt_changed",
4451 HookArgs::PromptChanged {
4452 prompt_type: custom_type,
4453 input,
4454 },
4455 );
4456 if let Some(prompt) = &mut self.prompt {
4461 prompt.filter_suggestions(false);
4462 }
4463 }
4464 PromptType::SwitchToTab
4465 | PromptType::SelectTheme { .. }
4466 | PromptType::StopLspServer
4467 | PromptType::RestartLspServer
4468 | PromptType::SetLanguage
4469 | PromptType::SetEncoding
4470 | PromptType::SetLineEnding => {
4471 if let Some(prompt) = &mut self.prompt {
4472 prompt.filter_suggestions(false);
4473 }
4474 }
4475 PromptType::SelectLocale => {
4476 if let Some(prompt) = &mut self.prompt {
4478 prompt.filter_suggestions(true);
4479 }
4480 }
4481 _ => {}
4482 }
4483 }
4484
4485 pub fn process_async_messages(&mut self) -> bool {
4493 self.plugin_manager.check_thread_health();
4496
4497 let Some(bridge) = &self.async_bridge else {
4498 return false;
4499 };
4500
4501 let messages = {
4502 let _s = tracing::info_span!("try_recv_all").entered();
4503 bridge.try_recv_all()
4504 };
4505 let needs_render = !messages.is_empty();
4506 tracing::trace!(
4507 async_message_count = messages.len(),
4508 "received async messages"
4509 );
4510
4511 for message in messages {
4512 match message {
4513 AsyncMessage::LspDiagnostics {
4514 uri,
4515 diagnostics,
4516 server_name,
4517 } => {
4518 self.handle_lsp_diagnostics(uri, diagnostics, server_name);
4519 }
4520 AsyncMessage::LspInitialized {
4521 language,
4522 server_name,
4523 capabilities,
4524 } => {
4525 tracing::info!(
4526 "LSP server '{}' initialized for language: {}",
4527 server_name,
4528 language
4529 );
4530 self.status_message = Some(format!("LSP ({}) ready", language));
4531
4532 if let Some(lsp) = &mut self.lsp {
4534 lsp.set_server_capabilities(&language, &server_name, capabilities);
4535 }
4536
4537 self.resend_did_open_for_language(&language);
4539 self.request_semantic_tokens_for_language(&language);
4540 self.request_folding_ranges_for_language(&language);
4541 }
4542 AsyncMessage::LspError {
4543 language,
4544 error,
4545 stderr_log_path,
4546 } => {
4547 tracing::error!("LSP error for {}: {}", language, error);
4548 self.status_message = Some(format!("LSP error ({}): {}", language, error));
4549
4550 let server_command = self
4552 .config
4553 .lsp
4554 .get(&language)
4555 .and_then(|configs| configs.as_slice().first())
4556 .map(|c| c.command.clone())
4557 .unwrap_or_else(|| "unknown".to_string());
4558
4559 let error_type = if error.contains("not found") || error.contains("NotFound") {
4561 "not_found"
4562 } else if error.contains("permission") || error.contains("PermissionDenied") {
4563 "spawn_failed"
4564 } else if error.contains("timeout") {
4565 "timeout"
4566 } else {
4567 "spawn_failed"
4568 }
4569 .to_string();
4570
4571 self.plugin_manager.run_hook(
4573 "lsp_server_error",
4574 crate::services::plugins::hooks::HookArgs::LspServerError {
4575 language: language.clone(),
4576 server_command,
4577 error_type,
4578 message: error.clone(),
4579 },
4580 );
4581
4582 if let Some(log_path) = stderr_log_path {
4585 let has_content = log_path.metadata().map(|m| m.len() > 0).unwrap_or(false);
4586 if has_content {
4587 tracing::info!("Opening LSP stderr log in background: {:?}", log_path);
4588 match self.open_file_no_focus(&log_path) {
4589 Ok(buffer_id) => {
4590 self.mark_buffer_read_only(buffer_id, true);
4591 self.status_message = Some(format!(
4592 "LSP error ({}): {} - See stderr log",
4593 language, error
4594 ));
4595 }
4596 Err(e) => {
4597 tracing::error!("Failed to open LSP stderr log: {}", e);
4598 }
4599 }
4600 }
4601 }
4602 }
4603 AsyncMessage::LspCompletion { request_id, items } => {
4604 if let Err(e) = self.handle_completion_response(request_id, items) {
4605 tracing::error!("Error handling completion response: {}", e);
4606 }
4607 }
4608 AsyncMessage::LspGotoDefinition {
4609 request_id,
4610 locations,
4611 } => {
4612 if let Err(e) = self.handle_goto_definition_response(request_id, locations) {
4613 tracing::error!("Error handling goto definition response: {}", e);
4614 }
4615 }
4616 AsyncMessage::LspRename { request_id, result } => {
4617 if let Err(e) = self.handle_rename_response(request_id, result) {
4618 tracing::error!("Error handling rename response: {}", e);
4619 }
4620 }
4621 AsyncMessage::LspHover {
4622 request_id,
4623 contents,
4624 is_markdown,
4625 range,
4626 } => {
4627 self.handle_hover_response(request_id, contents, is_markdown, range);
4628 }
4629 AsyncMessage::LspReferences {
4630 request_id,
4631 locations,
4632 } => {
4633 if let Err(e) = self.handle_references_response(request_id, locations) {
4634 tracing::error!("Error handling references response: {}", e);
4635 }
4636 }
4637 AsyncMessage::LspSignatureHelp {
4638 request_id,
4639 signature_help,
4640 } => {
4641 self.handle_signature_help_response(request_id, signature_help);
4642 }
4643 AsyncMessage::LspCodeActions {
4644 request_id,
4645 actions,
4646 } => {
4647 self.handle_code_actions_response(request_id, actions);
4648 }
4649 AsyncMessage::LspApplyEdit { edit, label } => {
4650 tracing::info!("Applying workspace edit from server (label: {:?})", label);
4651 match self.apply_workspace_edit(edit) {
4652 Ok(n) => {
4653 if let Some(label) = label {
4654 self.set_status_message(
4655 t!("lsp.code_action_applied", title = &label, count = n)
4656 .to_string(),
4657 );
4658 }
4659 }
4660 Err(e) => {
4661 tracing::error!("Failed to apply workspace edit: {}", e);
4662 }
4663 }
4664 }
4665 AsyncMessage::LspCodeActionResolved {
4666 request_id: _,
4667 action,
4668 } => match action {
4669 Ok(resolved) => {
4670 self.execute_resolved_code_action(resolved);
4671 }
4672 Err(e) => {
4673 tracing::warn!("codeAction/resolve failed: {}", e);
4674 self.set_status_message(format!("Code action resolve failed: {e}"));
4675 }
4676 },
4677 AsyncMessage::LspCompletionResolved {
4678 request_id: _,
4679 item,
4680 } => {
4681 if let Ok(resolved) = item {
4682 self.handle_completion_resolved(resolved);
4683 }
4684 }
4685 AsyncMessage::LspFormatting {
4686 request_id: _,
4687 uri,
4688 edits,
4689 } => {
4690 if !edits.is_empty() {
4691 if let Err(e) = self.apply_formatting_edits(&uri, edits) {
4692 tracing::error!("Failed to apply formatting: {}", e);
4693 }
4694 }
4695 }
4696 AsyncMessage::LspPrepareRename {
4697 request_id: _,
4698 result,
4699 } => {
4700 self.handle_prepare_rename_response(result);
4701 }
4702 AsyncMessage::LspPulledDiagnostics {
4703 request_id: _,
4704 uri,
4705 result_id,
4706 diagnostics,
4707 unchanged,
4708 } => {
4709 self.handle_lsp_pulled_diagnostics(uri, result_id, diagnostics, unchanged);
4710 }
4711 AsyncMessage::LspInlayHints {
4712 request_id,
4713 uri,
4714 hints,
4715 } => {
4716 self.handle_lsp_inlay_hints(request_id, uri, hints);
4717 }
4718 AsyncMessage::LspFoldingRanges {
4719 request_id,
4720 uri,
4721 ranges,
4722 } => {
4723 self.handle_lsp_folding_ranges(request_id, uri, ranges);
4724 }
4725 AsyncMessage::LspSemanticTokens {
4726 request_id,
4727 uri,
4728 response,
4729 } => {
4730 self.handle_lsp_semantic_tokens(request_id, uri, response);
4731 }
4732 AsyncMessage::LspServerQuiescent { language } => {
4733 self.handle_lsp_server_quiescent(language);
4734 }
4735 AsyncMessage::LspDiagnosticRefresh { language } => {
4736 self.handle_lsp_diagnostic_refresh(language);
4737 }
4738 AsyncMessage::FileChanged { path } => {
4739 self.handle_async_file_changed(path);
4740 }
4741 AsyncMessage::GitStatusChanged { status } => {
4742 tracing::info!("Git status changed: {}", status);
4743 }
4745 AsyncMessage::FileExplorerInitialized(view) => {
4746 self.handle_file_explorer_initialized(view);
4747 }
4748 AsyncMessage::FileExplorerToggleNode(node_id) => {
4749 self.handle_file_explorer_toggle_node(node_id);
4750 }
4751 AsyncMessage::FileExplorerRefreshNode(node_id) => {
4752 self.handle_file_explorer_refresh_node(node_id);
4753 }
4754 AsyncMessage::FileExplorerExpandedToPath(view) => {
4755 self.handle_file_explorer_expanded_to_path(view);
4756 }
4757 AsyncMessage::Plugin(plugin_msg) => {
4758 use fresh_core::api::{JsCallbackId, PluginAsyncMessage};
4759 match plugin_msg {
4760 PluginAsyncMessage::ProcessOutput {
4761 process_id,
4762 stdout,
4763 stderr,
4764 exit_code,
4765 } => {
4766 self.handle_plugin_process_output(
4767 JsCallbackId::from(process_id),
4768 stdout,
4769 stderr,
4770 exit_code,
4771 );
4772 }
4773 PluginAsyncMessage::DelayComplete { callback_id } => {
4774 self.plugin_manager.resolve_callback(
4775 JsCallbackId::from(callback_id),
4776 "null".to_string(),
4777 );
4778 }
4779 PluginAsyncMessage::ProcessStdout { process_id, data } => {
4780 self.plugin_manager.run_hook(
4781 "onProcessStdout",
4782 crate::services::plugins::hooks::HookArgs::ProcessOutput {
4783 process_id,
4784 data,
4785 },
4786 );
4787 }
4788 PluginAsyncMessage::ProcessStderr { process_id, data } => {
4789 self.plugin_manager.run_hook(
4790 "onProcessStderr",
4791 crate::services::plugins::hooks::HookArgs::ProcessOutput {
4792 process_id,
4793 data,
4794 },
4795 );
4796 }
4797 PluginAsyncMessage::ProcessExit {
4798 process_id,
4799 callback_id,
4800 exit_code,
4801 } => {
4802 self.background_process_handles.remove(&process_id);
4803 let result = fresh_core::api::BackgroundProcessResult {
4804 process_id,
4805 exit_code,
4806 };
4807 self.plugin_manager.resolve_callback(
4808 JsCallbackId::from(callback_id),
4809 serde_json::to_string(&result).unwrap(),
4810 );
4811 }
4812 PluginAsyncMessage::LspResponse {
4813 language: _,
4814 request_id,
4815 result,
4816 } => {
4817 self.handle_plugin_lsp_response(request_id, result);
4818 }
4819 PluginAsyncMessage::PluginResponse(response) => {
4820 self.handle_plugin_response(response);
4821 }
4822 PluginAsyncMessage::GrepStreamingProgress {
4823 search_id,
4824 matches_json,
4825 } => {
4826 tracing::info!(
4827 "GrepStreamingProgress: search_id={} json_len={}",
4828 search_id,
4829 matches_json.len()
4830 );
4831 self.plugin_manager.call_streaming_callback(
4832 JsCallbackId::from(search_id),
4833 matches_json,
4834 false,
4835 );
4836 }
4837 PluginAsyncMessage::GrepStreamingComplete {
4838 search_id: _,
4839 callback_id,
4840 total_matches,
4841 truncated,
4842 } => {
4843 self.streaming_grep_cancellation = None;
4844 self.plugin_manager.resolve_callback(
4845 JsCallbackId::from(callback_id),
4846 format!(
4847 r#"{{"totalMatches":{},"truncated":{}}}"#,
4848 total_matches, truncated
4849 ),
4850 );
4851 }
4852 }
4853 }
4854 AsyncMessage::LspProgress {
4855 language,
4856 token,
4857 value,
4858 } => {
4859 self.handle_lsp_progress(language, token, value);
4860 }
4861 AsyncMessage::LspWindowMessage {
4862 language,
4863 message_type,
4864 message,
4865 } => {
4866 self.handle_lsp_window_message(language, message_type, message);
4867 }
4868 AsyncMessage::LspLogMessage {
4869 language,
4870 message_type,
4871 message,
4872 } => {
4873 self.handle_lsp_log_message(language, message_type, message);
4874 }
4875 AsyncMessage::LspStatusUpdate {
4876 language,
4877 server_name,
4878 status,
4879 message: _,
4880 } => {
4881 self.handle_lsp_status_update(language, server_name, status);
4882 }
4883 AsyncMessage::FileOpenDirectoryLoaded(result) => {
4884 self.handle_file_open_directory_loaded(result);
4885 }
4886 AsyncMessage::FileOpenShortcutsLoaded(shortcuts) => {
4887 self.handle_file_open_shortcuts_loaded(shortcuts);
4888 }
4889 AsyncMessage::TerminalOutput { terminal_id } => {
4890 tracing::trace!("Terminal output received for {:?}", terminal_id);
4892
4893 if self.config.terminal.jump_to_end_on_output && !self.terminal_mode {
4896 if let Some(&active_terminal_id) =
4898 self.terminal_buffers.get(&self.active_buffer())
4899 {
4900 if active_terminal_id == terminal_id {
4901 self.enter_terminal_mode();
4902 }
4903 }
4904 }
4905
4906 if self.terminal_mode {
4908 if let Some(handle) = self.terminal_manager.get(terminal_id) {
4909 if let Ok(mut state) = handle.state.lock() {
4910 state.scroll_to_bottom();
4911 }
4912 }
4913 }
4914 }
4915 AsyncMessage::TerminalExited { terminal_id } => {
4916 tracing::info!("Terminal {:?} exited", terminal_id);
4917 if let Some((&buffer_id, _)) = self
4919 .terminal_buffers
4920 .iter()
4921 .find(|(_, &tid)| tid == terminal_id)
4922 {
4923 if self.active_buffer() == buffer_id && self.terminal_mode {
4925 self.terminal_mode = false;
4926 self.key_context = crate::input::keybindings::KeyContext::Normal;
4927 }
4928
4929 self.sync_terminal_to_buffer(buffer_id);
4931
4932 let exit_msg = "\n[Terminal process exited]\n";
4934
4935 if let Some(backing_path) =
4936 self.terminal_backing_files.get(&terminal_id).cloned()
4937 {
4938 if let Ok(mut file) =
4939 self.filesystem.open_file_for_append(&backing_path)
4940 {
4941 use std::io::Write;
4942 if let Err(e) = file.write_all(exit_msg.as_bytes()) {
4943 tracing::warn!("Failed to write terminal exit message: {}", e);
4944 }
4945 }
4946
4947 if let Err(e) = self.revert_buffer_by_id(buffer_id, &backing_path) {
4949 tracing::warn!("Failed to revert terminal buffer: {}", e);
4950 }
4951 }
4952
4953 if let Some(state) = self.buffers.get_mut(&buffer_id) {
4955 state.editing_disabled = true;
4956 state.margins.configure_for_line_numbers(false);
4957 state.buffer.set_modified(false);
4958 }
4959
4960 self.terminal_buffers.remove(&buffer_id);
4962
4963 self.set_status_message(
4964 t!("terminal.exited", id = terminal_id.0).to_string(),
4965 );
4966 }
4967 self.terminal_manager.close(terminal_id);
4968 }
4969
4970 AsyncMessage::LspServerRequest {
4971 language,
4972 server_command,
4973 method,
4974 params,
4975 } => {
4976 self.handle_lsp_server_request(language, server_command, method, params);
4977 }
4978 AsyncMessage::PluginLspResponse {
4979 language: _,
4980 request_id,
4981 result,
4982 } => {
4983 self.handle_plugin_lsp_response(request_id, result);
4984 }
4985 AsyncMessage::PluginProcessOutput {
4986 process_id,
4987 stdout,
4988 stderr,
4989 exit_code,
4990 } => {
4991 self.handle_plugin_process_output(
4992 fresh_core::api::JsCallbackId::from(process_id),
4993 stdout,
4994 stderr,
4995 exit_code,
4996 );
4997 }
4998 AsyncMessage::GrammarRegistryBuilt {
4999 registry,
5000 callback_ids,
5001 } => {
5002 tracing::info!(
5003 "Background grammar build completed ({} syntaxes)",
5004 registry.available_syntaxes().len()
5005 );
5006 self.grammar_registry = registry;
5007 self.grammar_build_in_progress = false;
5008
5009 let buffers_to_update: Vec<_> = self
5011 .buffer_metadata
5012 .iter()
5013 .filter_map(|(id, meta)| meta.file_path().map(|p| (*id, p.to_path_buf())))
5014 .collect();
5015
5016 for (buf_id, path) in buffers_to_update {
5017 if let Some(state) = self.buffers.get_mut(&buf_id) {
5018 let detected =
5019 crate::primitives::detected_language::DetectedLanguage::from_path(
5020 &path,
5021 &self.grammar_registry,
5022 &self.config.languages,
5023 );
5024
5025 if detected.highlighter.has_highlighting()
5026 || !state.highlighter.has_highlighting()
5027 {
5028 state.apply_language(detected);
5029 }
5030 }
5031 }
5032
5033 #[cfg(feature = "plugins")]
5035 for cb_id in callback_ids {
5036 self.plugin_manager
5037 .resolve_callback(cb_id, "null".to_string());
5038 }
5039
5040 self.flush_pending_grammars();
5042 }
5043 }
5044 }
5045
5046 #[cfg(feature = "plugins")]
5049 {
5050 let _s = tracing::info_span!("update_plugin_state_snapshot").entered();
5051 self.update_plugin_state_snapshot();
5052 }
5053
5054 let processed_any_commands = {
5056 let _s = tracing::info_span!("process_plugin_commands").entered();
5057 self.process_plugin_commands()
5058 };
5059
5060 #[cfg(feature = "plugins")]
5064 if processed_any_commands {
5065 let _s = tracing::info_span!("update_plugin_state_snapshot_post").entered();
5066 self.update_plugin_state_snapshot();
5067 }
5068
5069 #[cfg(feature = "plugins")]
5071 {
5072 let _s = tracing::info_span!("process_pending_plugin_actions").entered();
5073 self.process_pending_plugin_actions();
5074 }
5075
5076 {
5078 let _s = tracing::info_span!("process_pending_lsp_restarts").entered();
5079 self.process_pending_lsp_restarts();
5080 }
5081
5082 #[cfg(feature = "plugins")]
5084 let plugin_render = {
5085 let render = self.plugin_render_requested;
5086 self.plugin_render_requested = false;
5087 render
5088 };
5089 #[cfg(not(feature = "plugins"))]
5090 let plugin_render = false;
5091
5092 if let Some(ref mut checker) = self.update_checker {
5094 let _ = checker.poll_result();
5096 }
5097
5098 let file_changes = {
5100 let _s = tracing::info_span!("poll_file_changes").entered();
5101 self.poll_file_changes()
5102 };
5103 let tree_changes = {
5104 let _s = tracing::info_span!("poll_file_tree_changes").entered();
5105 self.poll_file_tree_changes()
5106 };
5107
5108 needs_render || processed_any_commands || plugin_render || file_changes || tree_changes
5110 }
5111
5112 fn update_lsp_status_from_progress(&mut self) {
5114 if self.lsp_progress.is_empty() {
5115 self.update_lsp_status_from_server_statuses();
5117 return;
5118 }
5119
5120 if let Some((_, info)) = self.lsp_progress.iter().next() {
5122 let mut status = format!("LSP ({}): {}", info.language, info.title);
5123 if let Some(ref msg) = info.message {
5124 status.push_str(&format!(" - {}", msg));
5125 }
5126 if let Some(pct) = info.percentage {
5127 status.push_str(&format!(" ({}%)", pct));
5128 }
5129 self.lsp_status = status;
5130 }
5131 }
5132
5133 fn update_lsp_status_from_server_statuses(&mut self) {
5135 use crate::services::async_bridge::LspServerStatus;
5136
5137 let mut statuses: Vec<((String, String), LspServerStatus)> = self
5139 .lsp_server_statuses
5140 .iter()
5141 .map(|((lang, name), status)| ((lang.clone(), name.clone()), *status))
5142 .collect();
5143
5144 if statuses.is_empty() {
5145 self.lsp_status = String::new();
5146 return;
5147 }
5148
5149 statuses.sort_by(|a, b| a.0.cmp(&b.0));
5151
5152 let mut lang_counts: std::collections::HashMap<&str, usize> =
5154 std::collections::HashMap::new();
5155 for ((lang, _), _) in &statuses {
5156 *lang_counts.entry(lang.as_str()).or_default() += 1;
5157 }
5158
5159 let status_parts: Vec<String> = statuses
5161 .iter()
5162 .map(|((lang, name), status)| {
5163 let status_str = match status {
5164 LspServerStatus::Starting => "starting",
5165 LspServerStatus::Initializing => "initializing",
5166 LspServerStatus::Running => "ready",
5167 LspServerStatus::Error => "error",
5168 LspServerStatus::Shutdown => "shutdown",
5169 };
5170 if lang_counts.get(lang.as_str()).copied().unwrap_or(0) > 1 {
5172 format!("{}/{}: {}", lang, name, status_str)
5173 } else {
5174 format!("{}: {}", lang, status_str)
5175 }
5176 })
5177 .collect();
5178
5179 self.lsp_status = format!("LSP [{}]", status_parts.join(", "));
5180 }
5181
5182 #[cfg(feature = "plugins")]
5184 fn update_plugin_state_snapshot(&mut self) {
5185 if let Some(snapshot_handle) = self.plugin_manager.state_snapshot_handle() {
5187 use fresh_core::api::{BufferInfo, CursorInfo, ViewportInfo};
5188 let mut snapshot = snapshot_handle.write().unwrap();
5189
5190 let grammar_count = self.grammar_registry.available_syntaxes().len();
5192 if snapshot.available_grammars.len() != grammar_count {
5193 snapshot.available_grammars = self
5194 .grammar_registry
5195 .available_grammar_info()
5196 .into_iter()
5197 .map(|g| fresh_core::api::GrammarInfoSnapshot {
5198 name: g.name,
5199 source: g.source.to_string(),
5200 file_extensions: g.file_extensions,
5201 short_name: g.short_name,
5202 })
5203 .collect();
5204 }
5205
5206 snapshot.active_buffer_id = self.active_buffer();
5208
5209 snapshot.active_split_id = self.split_manager.active_split().0 .0;
5211
5212 snapshot.buffers.clear();
5214 snapshot.buffer_saved_diffs.clear();
5215 snapshot.buffer_cursor_positions.clear();
5216 snapshot.buffer_text_properties.clear();
5217
5218 for (buffer_id, state) in &self.buffers {
5219 let is_virtual = self
5220 .buffer_metadata
5221 .get(buffer_id)
5222 .map(|m| m.is_virtual())
5223 .unwrap_or(false);
5224 let active_split = self.split_manager.active_split();
5229 let active_vs = self.split_view_states.get(&active_split);
5230 let view_mode = active_vs
5231 .and_then(|vs| vs.buffer_state(*buffer_id))
5232 .map(|bs| match bs.view_mode {
5233 crate::state::ViewMode::Source => "source",
5234 crate::state::ViewMode::PageView => "compose",
5235 })
5236 .unwrap_or("source");
5237 let compose_width = active_vs
5238 .and_then(|vs| vs.buffer_state(*buffer_id))
5239 .and_then(|bs| bs.compose_width);
5240 let is_composing_in_any_split = self.split_view_states.values().any(|vs| {
5241 vs.buffer_state(*buffer_id)
5242 .map(|bs| matches!(bs.view_mode, crate::state::ViewMode::PageView))
5243 .unwrap_or(false)
5244 });
5245 let buffer_info = BufferInfo {
5246 id: *buffer_id,
5247 path: state.buffer.file_path().map(|p| p.to_path_buf()),
5248 modified: state.buffer.is_modified(),
5249 length: state.buffer.len(),
5250 is_virtual,
5251 view_mode: view_mode.to_string(),
5252 is_composing_in_any_split,
5253 compose_width,
5254 language: state.language.clone(),
5255 };
5256 snapshot.buffers.insert(*buffer_id, buffer_info);
5257
5258 let diff = {
5259 let diff = state.buffer.diff_since_saved();
5260 BufferSavedDiff {
5261 equal: diff.equal,
5262 byte_ranges: diff.byte_ranges.clone(),
5263 }
5264 };
5265 snapshot.buffer_saved_diffs.insert(*buffer_id, diff);
5266
5267 let cursor_pos = self
5269 .split_view_states
5270 .values()
5271 .find_map(|vs| vs.buffer_state(*buffer_id))
5272 .map(|bs| bs.cursors.primary().position)
5273 .unwrap_or(0);
5274 snapshot
5275 .buffer_cursor_positions
5276 .insert(*buffer_id, cursor_pos);
5277
5278 if !state.text_properties.is_empty() {
5280 snapshot
5281 .buffer_text_properties
5282 .insert(*buffer_id, state.text_properties.all().to_vec());
5283 }
5284 }
5285
5286 if let Some(active_vs) = self
5288 .split_view_states
5289 .get(&self.split_manager.active_split())
5290 {
5291 let active_cursors = &active_vs.cursors;
5293 let primary = active_cursors.primary();
5294 let primary_position = primary.position;
5295 let primary_selection = primary.selection_range();
5296
5297 snapshot.primary_cursor = Some(CursorInfo {
5298 position: primary_position,
5299 selection: primary_selection.clone(),
5300 });
5301
5302 snapshot.all_cursors = active_cursors
5304 .iter()
5305 .map(|(_, cursor)| CursorInfo {
5306 position: cursor.position,
5307 selection: cursor.selection_range(),
5308 })
5309 .collect();
5310
5311 if let Some(range) = primary_selection {
5313 if let Some(active_state) = self.buffers.get_mut(&self.active_buffer()) {
5314 snapshot.selected_text =
5315 Some(active_state.get_text_range(range.start, range.end));
5316 }
5317 }
5318
5319 let top_line = self.buffers.get(&self.active_buffer()).and_then(|state| {
5321 if state.buffer.line_count().is_some() {
5322 Some(state.buffer.get_line_number(active_vs.viewport.top_byte))
5323 } else {
5324 None
5325 }
5326 });
5327 snapshot.viewport = Some(ViewportInfo {
5328 top_byte: active_vs.viewport.top_byte,
5329 top_line,
5330 left_column: active_vs.viewport.left_column,
5331 width: active_vs.viewport.width,
5332 height: active_vs.viewport.height,
5333 });
5334 } else {
5335 snapshot.primary_cursor = None;
5336 snapshot.all_cursors.clear();
5337 snapshot.viewport = None;
5338 snapshot.selected_text = None;
5339 }
5340
5341 snapshot.clipboard = self.clipboard.get_internal().to_string();
5343
5344 snapshot.working_dir = self.working_dir.clone();
5346
5347 snapshot.diagnostics = self.stored_diagnostics.clone();
5349
5350 snapshot.folding_ranges = self.stored_folding_ranges.clone();
5352
5353 snapshot.config = serde_json::to_value(&self.config).unwrap_or(serde_json::Value::Null);
5355
5356 snapshot.user_config = self.user_config_raw.clone();
5359
5360 snapshot.editor_mode = self.editor_mode.clone();
5362
5363 for (plugin_name, state_map) in &self.plugin_global_state {
5366 let entry = snapshot
5367 .plugin_global_states
5368 .entry(plugin_name.clone())
5369 .or_default();
5370 for (key, value) in state_map {
5371 entry.entry(key.clone()).or_insert_with(|| value.clone());
5372 }
5373 }
5374
5375 let active_split_id = self.split_manager.active_split().0 .0;
5380 let split_changed = snapshot.plugin_view_states_split != active_split_id;
5381 if split_changed {
5382 snapshot.plugin_view_states.clear();
5383 snapshot.plugin_view_states_split = active_split_id;
5384 }
5385
5386 {
5388 let open_bids: Vec<_> = snapshot.buffers.keys().copied().collect();
5389 snapshot
5390 .plugin_view_states
5391 .retain(|bid, _| open_bids.contains(bid));
5392 }
5393
5394 if let Some(active_vs) = self
5396 .split_view_states
5397 .get(&self.split_manager.active_split())
5398 {
5399 for (buffer_id, buf_state) in &active_vs.keyed_states {
5400 if !buf_state.plugin_state.is_empty() {
5401 let entry = snapshot.plugin_view_states.entry(*buffer_id).or_default();
5402 for (key, value) in &buf_state.plugin_state {
5403 entry.entry(key.clone()).or_insert_with(|| value.clone());
5405 }
5406 }
5407 }
5408 }
5409 }
5410 }
5411
5412 pub fn handle_plugin_command(&mut self, command: PluginCommand) -> AnyhowResult<()> {
5414 match command {
5415 PluginCommand::InsertText {
5417 buffer_id,
5418 position,
5419 text,
5420 } => {
5421 self.handle_insert_text(buffer_id, position, text);
5422 }
5423 PluginCommand::DeleteRange { buffer_id, range } => {
5424 self.handle_delete_range(buffer_id, range);
5425 }
5426 PluginCommand::InsertAtCursor { text } => {
5427 self.handle_insert_at_cursor(text);
5428 }
5429 PluginCommand::DeleteSelection => {
5430 self.handle_delete_selection();
5431 }
5432
5433 PluginCommand::AddOverlay {
5435 buffer_id,
5436 namespace,
5437 range,
5438 options,
5439 } => {
5440 self.handle_add_overlay(buffer_id, namespace, range, options);
5441 }
5442 PluginCommand::RemoveOverlay { buffer_id, handle } => {
5443 self.handle_remove_overlay(buffer_id, handle);
5444 }
5445 PluginCommand::ClearAllOverlays { buffer_id } => {
5446 self.handle_clear_all_overlays(buffer_id);
5447 }
5448 PluginCommand::ClearNamespace {
5449 buffer_id,
5450 namespace,
5451 } => {
5452 self.handle_clear_namespace(buffer_id, namespace);
5453 }
5454 PluginCommand::ClearOverlaysInRange {
5455 buffer_id,
5456 start,
5457 end,
5458 } => {
5459 self.handle_clear_overlays_in_range(buffer_id, start, end);
5460 }
5461
5462 PluginCommand::AddVirtualText {
5464 buffer_id,
5465 virtual_text_id,
5466 position,
5467 text,
5468 color,
5469 use_bg,
5470 before,
5471 } => {
5472 self.handle_add_virtual_text(
5473 buffer_id,
5474 virtual_text_id,
5475 position,
5476 text,
5477 color,
5478 use_bg,
5479 before,
5480 );
5481 }
5482 PluginCommand::RemoveVirtualText {
5483 buffer_id,
5484 virtual_text_id,
5485 } => {
5486 self.handle_remove_virtual_text(buffer_id, virtual_text_id);
5487 }
5488 PluginCommand::RemoveVirtualTextsByPrefix { buffer_id, prefix } => {
5489 self.handle_remove_virtual_texts_by_prefix(buffer_id, prefix);
5490 }
5491 PluginCommand::ClearVirtualTexts { buffer_id } => {
5492 self.handle_clear_virtual_texts(buffer_id);
5493 }
5494 PluginCommand::AddVirtualLine {
5495 buffer_id,
5496 position,
5497 text,
5498 fg_color,
5499 bg_color,
5500 above,
5501 namespace,
5502 priority,
5503 } => {
5504 self.handle_add_virtual_line(
5505 buffer_id, position, text, fg_color, bg_color, above, namespace, priority,
5506 );
5507 }
5508 PluginCommand::ClearVirtualTextNamespace {
5509 buffer_id,
5510 namespace,
5511 } => {
5512 self.handle_clear_virtual_text_namespace(buffer_id, namespace);
5513 }
5514
5515 PluginCommand::AddConceal {
5517 buffer_id,
5518 namespace,
5519 start,
5520 end,
5521 replacement,
5522 } => {
5523 self.handle_add_conceal(buffer_id, namespace, start, end, replacement);
5524 }
5525 PluginCommand::ClearConcealNamespace {
5526 buffer_id,
5527 namespace,
5528 } => {
5529 self.handle_clear_conceal_namespace(buffer_id, namespace);
5530 }
5531 PluginCommand::ClearConcealsInRange {
5532 buffer_id,
5533 start,
5534 end,
5535 } => {
5536 self.handle_clear_conceals_in_range(buffer_id, start, end);
5537 }
5538
5539 PluginCommand::AddSoftBreak {
5541 buffer_id,
5542 namespace,
5543 position,
5544 indent,
5545 } => {
5546 self.handle_add_soft_break(buffer_id, namespace, position, indent);
5547 }
5548 PluginCommand::ClearSoftBreakNamespace {
5549 buffer_id,
5550 namespace,
5551 } => {
5552 self.handle_clear_soft_break_namespace(buffer_id, namespace);
5553 }
5554 PluginCommand::ClearSoftBreaksInRange {
5555 buffer_id,
5556 start,
5557 end,
5558 } => {
5559 self.handle_clear_soft_breaks_in_range(buffer_id, start, end);
5560 }
5561
5562 PluginCommand::AddMenuItem {
5564 menu_label,
5565 item,
5566 position,
5567 } => {
5568 self.handle_add_menu_item(menu_label, item, position);
5569 }
5570 PluginCommand::AddMenu { menu, position } => {
5571 self.handle_add_menu(menu, position);
5572 }
5573 PluginCommand::RemoveMenuItem {
5574 menu_label,
5575 item_label,
5576 } => {
5577 self.handle_remove_menu_item(menu_label, item_label);
5578 }
5579 PluginCommand::RemoveMenu { menu_label } => {
5580 self.handle_remove_menu(menu_label);
5581 }
5582
5583 PluginCommand::FocusSplit { split_id } => {
5585 self.handle_focus_split(split_id);
5586 }
5587 PluginCommand::SetSplitBuffer {
5588 split_id,
5589 buffer_id,
5590 } => {
5591 self.handle_set_split_buffer(split_id, buffer_id);
5592 }
5593 PluginCommand::SetSplitScroll { split_id, top_byte } => {
5594 self.handle_set_split_scroll(split_id, top_byte);
5595 }
5596 PluginCommand::RequestHighlights {
5597 buffer_id,
5598 range,
5599 request_id,
5600 } => {
5601 self.handle_request_highlights(buffer_id, range, request_id);
5602 }
5603 PluginCommand::CloseSplit { split_id } => {
5604 self.handle_close_split(split_id);
5605 }
5606 PluginCommand::SetSplitRatio { split_id, ratio } => {
5607 self.handle_set_split_ratio(split_id, ratio);
5608 }
5609 PluginCommand::SetSplitLabel { split_id, label } => {
5610 self.split_manager.set_label(LeafId(split_id), label);
5611 }
5612 PluginCommand::ClearSplitLabel { split_id } => {
5613 self.split_manager.clear_label(split_id);
5614 }
5615 PluginCommand::GetSplitByLabel { label, request_id } => {
5616 let split_id = self.split_manager.find_split_by_label(&label);
5617 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
5618 let json = serde_json::to_string(&split_id.map(|s| s.0 .0))
5619 .unwrap_or_else(|_| "null".to_string());
5620 self.plugin_manager.resolve_callback(callback_id, json);
5621 }
5622 PluginCommand::DistributeSplitsEvenly { split_ids: _ } => {
5623 self.handle_distribute_splits_evenly();
5624 }
5625 PluginCommand::SetBufferCursor {
5626 buffer_id,
5627 position,
5628 } => {
5629 self.handle_set_buffer_cursor(buffer_id, position);
5630 }
5631
5632 PluginCommand::SetLayoutHints {
5634 buffer_id,
5635 split_id,
5636 range: _,
5637 hints,
5638 } => {
5639 self.handle_set_layout_hints(buffer_id, split_id, hints);
5640 }
5641 PluginCommand::SetLineNumbers { buffer_id, enabled } => {
5642 self.handle_set_line_numbers(buffer_id, enabled);
5643 }
5644 PluginCommand::SetViewMode { buffer_id, mode } => {
5645 self.handle_set_view_mode(buffer_id, &mode);
5646 }
5647 PluginCommand::SetLineWrap {
5648 buffer_id,
5649 split_id,
5650 enabled,
5651 } => {
5652 self.handle_set_line_wrap(buffer_id, split_id, enabled);
5653 }
5654 PluginCommand::SubmitViewTransform {
5655 buffer_id,
5656 split_id,
5657 payload,
5658 } => {
5659 self.handle_submit_view_transform(buffer_id, split_id, payload);
5660 }
5661 PluginCommand::ClearViewTransform {
5662 buffer_id: _,
5663 split_id,
5664 } => {
5665 self.handle_clear_view_transform(split_id);
5666 }
5667 PluginCommand::SetViewState {
5668 buffer_id,
5669 key,
5670 value,
5671 } => {
5672 self.handle_set_view_state(buffer_id, key, value);
5673 }
5674 PluginCommand::SetGlobalState {
5675 plugin_name,
5676 key,
5677 value,
5678 } => {
5679 self.handle_set_global_state(plugin_name, key, value);
5680 }
5681 PluginCommand::RefreshLines { buffer_id } => {
5682 self.handle_refresh_lines(buffer_id);
5683 }
5684 PluginCommand::RefreshAllLines => {
5685 self.handle_refresh_all_lines();
5686 }
5687 PluginCommand::HookCompleted { .. } => {
5688 }
5690 PluginCommand::SetLineIndicator {
5691 buffer_id,
5692 line,
5693 namespace,
5694 symbol,
5695 color,
5696 priority,
5697 } => {
5698 self.handle_set_line_indicator(buffer_id, line, namespace, symbol, color, priority);
5699 }
5700 PluginCommand::SetLineIndicators {
5701 buffer_id,
5702 lines,
5703 namespace,
5704 symbol,
5705 color,
5706 priority,
5707 } => {
5708 self.handle_set_line_indicators(
5709 buffer_id, lines, namespace, symbol, color, priority,
5710 );
5711 }
5712 PluginCommand::ClearLineIndicators {
5713 buffer_id,
5714 namespace,
5715 } => {
5716 self.handle_clear_line_indicators(buffer_id, namespace);
5717 }
5718 PluginCommand::SetFileExplorerDecorations {
5719 namespace,
5720 decorations,
5721 } => {
5722 self.handle_set_file_explorer_decorations(namespace, decorations);
5723 }
5724 PluginCommand::ClearFileExplorerDecorations { namespace } => {
5725 self.handle_clear_file_explorer_decorations(&namespace);
5726 }
5727
5728 PluginCommand::SetStatus { message } => {
5730 self.handle_set_status(message);
5731 }
5732 PluginCommand::ApplyTheme { theme_name } => {
5733 self.apply_theme(&theme_name);
5734 }
5735 PluginCommand::ReloadConfig => {
5736 self.reload_config();
5737 }
5738 PluginCommand::ReloadThemes { apply_theme } => {
5739 self.reload_themes();
5740 if let Some(theme_name) = apply_theme {
5741 self.apply_theme(&theme_name);
5742 }
5743 }
5744 PluginCommand::RegisterGrammar {
5745 language,
5746 grammar_path,
5747 extensions,
5748 } => {
5749 self.handle_register_grammar(language, grammar_path, extensions);
5750 }
5751 PluginCommand::RegisterLanguageConfig { language, config } => {
5752 self.handle_register_language_config(language, config);
5753 }
5754 PluginCommand::RegisterLspServer { language, config } => {
5755 self.handle_register_lsp_server(language, config);
5756 }
5757 PluginCommand::ReloadGrammars { callback_id } => {
5758 self.handle_reload_grammars(callback_id);
5759 }
5760 PluginCommand::StartPrompt { label, prompt_type } => {
5761 self.handle_start_prompt(label, prompt_type);
5762 }
5763 PluginCommand::StartPromptWithInitial {
5764 label,
5765 prompt_type,
5766 initial_value,
5767 } => {
5768 self.handle_start_prompt_with_initial(label, prompt_type, initial_value);
5769 }
5770 PluginCommand::StartPromptAsync {
5771 label,
5772 initial_value,
5773 callback_id,
5774 } => {
5775 self.handle_start_prompt_async(label, initial_value, callback_id);
5776 }
5777 PluginCommand::SetPromptSuggestions { suggestions } => {
5778 self.handle_set_prompt_suggestions(suggestions);
5779 }
5780 PluginCommand::SetPromptInputSync { sync } => {
5781 if let Some(prompt) = &mut self.prompt {
5782 prompt.sync_input_on_navigate = sync;
5783 }
5784 }
5785
5786 PluginCommand::RegisterCommand { command } => {
5788 self.handle_register_command(command);
5789 }
5790 PluginCommand::UnregisterCommand { name } => {
5791 self.handle_unregister_command(name);
5792 }
5793 PluginCommand::DefineMode {
5794 name,
5795 bindings,
5796 read_only,
5797 allow_text_input,
5798 plugin_name,
5799 } => {
5800 self.handle_define_mode(name, bindings, read_only, allow_text_input, plugin_name);
5801 }
5802
5803 PluginCommand::OpenFileInBackground { path } => {
5805 self.handle_open_file_in_background(path);
5806 }
5807 PluginCommand::OpenFileAtLocation { path, line, column } => {
5808 return self.handle_open_file_at_location(path, line, column);
5809 }
5810 PluginCommand::OpenFileInSplit {
5811 split_id,
5812 path,
5813 line,
5814 column,
5815 } => {
5816 return self.handle_open_file_in_split(split_id, path, line, column);
5817 }
5818 PluginCommand::ShowBuffer { buffer_id } => {
5819 self.handle_show_buffer(buffer_id);
5820 }
5821 PluginCommand::CloseBuffer { buffer_id } => {
5822 self.handle_close_buffer(buffer_id);
5823 }
5824
5825 PluginCommand::SendLspRequest {
5827 language,
5828 method,
5829 params,
5830 request_id,
5831 } => {
5832 self.handle_send_lsp_request(language, method, params, request_id);
5833 }
5834
5835 PluginCommand::SetClipboard { text } => {
5837 self.handle_set_clipboard(text);
5838 }
5839
5840 PluginCommand::SpawnProcess {
5842 command,
5843 args,
5844 cwd,
5845 callback_id,
5846 } => {
5847 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
5850 let effective_cwd = cwd.or_else(|| {
5851 std::env::current_dir()
5852 .map(|p| p.to_string_lossy().to_string())
5853 .ok()
5854 });
5855 let sender = bridge.sender();
5856 let spawner = self.process_spawner.clone();
5857
5858 runtime.spawn(async move {
5859 #[allow(clippy::let_underscore_must_use)]
5861 match spawner.spawn(command, args, effective_cwd).await {
5862 Ok(result) => {
5863 let _ = sender.send(AsyncMessage::PluginProcessOutput {
5864 process_id: callback_id.as_u64(),
5865 stdout: result.stdout,
5866 stderr: result.stderr,
5867 exit_code: result.exit_code,
5868 });
5869 }
5870 Err(e) => {
5871 let _ = sender.send(AsyncMessage::PluginProcessOutput {
5872 process_id: callback_id.as_u64(),
5873 stdout: String::new(),
5874 stderr: e.to_string(),
5875 exit_code: -1,
5876 });
5877 }
5878 }
5879 });
5880 } else {
5881 self.plugin_manager
5883 .reject_callback(callback_id, "Async runtime not available".to_string());
5884 }
5885 }
5886
5887 PluginCommand::SpawnProcessWait {
5888 process_id,
5889 callback_id,
5890 } => {
5891 tracing::warn!(
5894 "SpawnProcessWait not fully implemented - process_id={}",
5895 process_id
5896 );
5897 self.plugin_manager.reject_callback(
5898 callback_id,
5899 format!(
5900 "SpawnProcessWait not yet fully implemented for process_id={}",
5901 process_id
5902 ),
5903 );
5904 }
5905
5906 PluginCommand::Delay {
5907 callback_id,
5908 duration_ms,
5909 } => {
5910 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
5912 let sender = bridge.sender();
5913 let callback_id_u64 = callback_id.as_u64();
5914 runtime.spawn(async move {
5915 tokio::time::sleep(tokio::time::Duration::from_millis(duration_ms)).await;
5916 #[allow(clippy::let_underscore_must_use)]
5918 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
5919 fresh_core::api::PluginAsyncMessage::DelayComplete {
5920 callback_id: callback_id_u64,
5921 },
5922 ));
5923 });
5924 } else {
5925 std::thread::sleep(std::time::Duration::from_millis(duration_ms));
5927 self.plugin_manager
5928 .resolve_callback(callback_id, "null".to_string());
5929 }
5930 }
5931
5932 PluginCommand::SpawnBackgroundProcess {
5933 process_id,
5934 command,
5935 args,
5936 cwd,
5937 callback_id,
5938 } => {
5939 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
5941 use tokio::io::{AsyncBufReadExt, BufReader};
5942 use tokio::process::Command as TokioCommand;
5943
5944 let effective_cwd = cwd.unwrap_or_else(|| {
5945 std::env::current_dir()
5946 .map(|p| p.to_string_lossy().to_string())
5947 .unwrap_or_else(|_| ".".to_string())
5948 });
5949
5950 let sender = bridge.sender();
5951 let sender_stdout = sender.clone();
5952 let sender_stderr = sender.clone();
5953 let callback_id_u64 = callback_id.as_u64();
5954
5955 #[allow(clippy::let_underscore_must_use)]
5957 let handle = runtime.spawn(async move {
5958 let mut child = match TokioCommand::new(&command)
5959 .args(&args)
5960 .current_dir(&effective_cwd)
5961 .stdout(std::process::Stdio::piped())
5962 .stderr(std::process::Stdio::piped())
5963 .spawn()
5964 {
5965 Ok(child) => child,
5966 Err(e) => {
5967 let _ = sender.send(
5968 crate::services::async_bridge::AsyncMessage::Plugin(
5969 fresh_core::api::PluginAsyncMessage::ProcessExit {
5970 process_id,
5971 callback_id: callback_id_u64,
5972 exit_code: -1,
5973 },
5974 ),
5975 );
5976 tracing::error!("Failed to spawn background process: {}", e);
5977 return;
5978 }
5979 };
5980
5981 let stdout = child.stdout.take();
5983 let stderr = child.stderr.take();
5984 let pid = process_id;
5985
5986 if let Some(stdout) = stdout {
5988 let sender = sender_stdout;
5989 tokio::spawn(async move {
5990 let reader = BufReader::new(stdout);
5991 let mut lines = reader.lines();
5992 while let Ok(Some(line)) = lines.next_line().await {
5993 let _ = sender.send(
5994 crate::services::async_bridge::AsyncMessage::Plugin(
5995 fresh_core::api::PluginAsyncMessage::ProcessStdout {
5996 process_id: pid,
5997 data: line + "\n",
5998 },
5999 ),
6000 );
6001 }
6002 });
6003 }
6004
6005 if let Some(stderr) = stderr {
6007 let sender = sender_stderr;
6008 tokio::spawn(async move {
6009 let reader = BufReader::new(stderr);
6010 let mut lines = reader.lines();
6011 while let Ok(Some(line)) = lines.next_line().await {
6012 let _ = sender.send(
6013 crate::services::async_bridge::AsyncMessage::Plugin(
6014 fresh_core::api::PluginAsyncMessage::ProcessStderr {
6015 process_id: pid,
6016 data: line + "\n",
6017 },
6018 ),
6019 );
6020 }
6021 });
6022 }
6023
6024 let exit_code = match child.wait().await {
6026 Ok(status) => status.code().unwrap_or(-1),
6027 Err(_) => -1,
6028 };
6029
6030 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
6031 fresh_core::api::PluginAsyncMessage::ProcessExit {
6032 process_id,
6033 callback_id: callback_id_u64,
6034 exit_code,
6035 },
6036 ));
6037 });
6038
6039 self.background_process_handles
6041 .insert(process_id, handle.abort_handle());
6042 } else {
6043 self.plugin_manager
6045 .reject_callback(callback_id, "Async runtime not available".to_string());
6046 }
6047 }
6048
6049 PluginCommand::KillBackgroundProcess { process_id } => {
6050 if let Some(handle) = self.background_process_handles.remove(&process_id) {
6051 handle.abort();
6052 tracing::debug!("Killed background process {}", process_id);
6053 }
6054 }
6055
6056 PluginCommand::CreateVirtualBuffer {
6058 name,
6059 mode,
6060 read_only,
6061 } => {
6062 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
6063 tracing::info!(
6064 "Created virtual buffer '{}' with mode '{}' (id={:?})",
6065 name,
6066 mode,
6067 buffer_id
6068 );
6069 }
6071 PluginCommand::CreateVirtualBufferWithContent {
6072 name,
6073 mode,
6074 read_only,
6075 entries,
6076 show_line_numbers,
6077 show_cursors,
6078 editing_disabled,
6079 hidden_from_tabs,
6080 request_id,
6081 } => {
6082 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
6083 tracing::info!(
6084 "Created virtual buffer '{}' with mode '{}' (id={:?})",
6085 name,
6086 mode,
6087 buffer_id
6088 );
6089
6090 if let Some(state) = self.buffers.get_mut(&buffer_id) {
6097 state.margins.configure_for_line_numbers(show_line_numbers);
6098 state.show_cursors = show_cursors;
6099 state.editing_disabled = editing_disabled;
6100 tracing::debug!(
6101 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
6102 buffer_id,
6103 show_line_numbers,
6104 show_cursors,
6105 editing_disabled
6106 );
6107 }
6108 let active_split = self.split_manager.active_split();
6109 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
6110 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
6111 }
6112
6113 if hidden_from_tabs {
6115 if let Some(meta) = self.buffer_metadata.get_mut(&buffer_id) {
6116 meta.hidden_from_tabs = true;
6117 }
6118 }
6119
6120 match self.set_virtual_buffer_content(buffer_id, entries) {
6122 Ok(()) => {
6123 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
6124 self.set_active_buffer(buffer_id);
6126 tracing::debug!("Switched to virtual buffer {:?}", buffer_id);
6127
6128 if let Some(req_id) = request_id {
6130 tracing::info!(
6131 "CreateVirtualBufferWithContent: resolving callback for request_id={}, buffer_id={:?}",
6132 req_id,
6133 buffer_id
6134 );
6135 let result = fresh_core::api::VirtualBufferResult {
6137 buffer_id: buffer_id.0 as u64,
6138 split_id: None,
6139 };
6140 self.plugin_manager.resolve_callback(
6141 fresh_core::api::JsCallbackId::from(req_id),
6142 serde_json::to_string(&result).unwrap_or_default(),
6143 );
6144 tracing::info!("CreateVirtualBufferWithContent: resolve_callback sent for request_id={}", req_id);
6145 }
6146 }
6147 Err(e) => {
6148 tracing::error!("Failed to set virtual buffer content: {}", e);
6149 }
6150 }
6151 }
6152 PluginCommand::CreateVirtualBufferInSplit {
6153 name,
6154 mode,
6155 read_only,
6156 entries,
6157 ratio,
6158 direction,
6159 panel_id,
6160 show_line_numbers,
6161 show_cursors,
6162 editing_disabled,
6163 line_wrap,
6164 before,
6165 request_id,
6166 } => {
6167 if let Some(pid) = &panel_id {
6169 if let Some(&existing_buffer_id) = self.panel_ids.get(pid) {
6170 if self.buffers.contains_key(&existing_buffer_id) {
6172 if let Err(e) =
6174 self.set_virtual_buffer_content(existing_buffer_id, entries)
6175 {
6176 tracing::error!("Failed to update panel content: {}", e);
6177 } else {
6178 tracing::info!("Updated existing panel '{}' content", pid);
6179 }
6180
6181 let splits = self.split_manager.splits_for_buffer(existing_buffer_id);
6183 if let Some(&split_id) = splits.first() {
6184 self.split_manager.set_active_split(split_id);
6185 self.split_manager.set_active_buffer_id(existing_buffer_id);
6188 tracing::debug!(
6189 "Focused split {:?} containing panel buffer",
6190 split_id
6191 );
6192 }
6193
6194 if let Some(req_id) = request_id {
6196 let result = fresh_core::api::VirtualBufferResult {
6197 buffer_id: existing_buffer_id.0 as u64,
6198 split_id: splits.first().map(|s| s.0 .0 as u64),
6199 };
6200 self.plugin_manager.resolve_callback(
6201 fresh_core::api::JsCallbackId::from(req_id),
6202 serde_json::to_string(&result).unwrap_or_default(),
6203 );
6204 }
6205 return Ok(());
6206 } else {
6207 tracing::warn!(
6209 "Removing stale panel_id '{}' pointing to non-existent buffer {:?}",
6210 pid,
6211 existing_buffer_id
6212 );
6213 self.panel_ids.remove(pid);
6214 }
6216 }
6217 }
6218
6219 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
6221 tracing::info!(
6222 "Created virtual buffer '{}' with mode '{}' in split (id={:?})",
6223 name,
6224 mode,
6225 buffer_id
6226 );
6227
6228 if let Some(state) = self.buffers.get_mut(&buffer_id) {
6230 state.margins.configure_for_line_numbers(show_line_numbers);
6231 state.show_cursors = show_cursors;
6232 state.editing_disabled = editing_disabled;
6233 tracing::debug!(
6234 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
6235 buffer_id,
6236 show_line_numbers,
6237 show_cursors,
6238 editing_disabled
6239 );
6240 }
6241
6242 if let Some(pid) = panel_id {
6244 self.panel_ids.insert(pid, buffer_id);
6245 }
6246
6247 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
6249 tracing::error!("Failed to set virtual buffer content: {}", e);
6250 return Ok(());
6251 }
6252
6253 let split_dir = match direction.as_deref() {
6255 Some("vertical") => crate::model::event::SplitDirection::Vertical,
6256 _ => crate::model::event::SplitDirection::Horizontal,
6257 };
6258
6259 let created_split_id = match self
6261 .split_manager
6262 .split_active_positioned(split_dir, buffer_id, ratio, before)
6263 {
6264 Ok(new_split_id) => {
6265 let mut view_state = SplitViewState::with_buffer(
6267 self.terminal_width,
6268 self.terminal_height,
6269 buffer_id,
6270 );
6271 view_state.apply_config_defaults(
6272 self.config.editor.line_numbers,
6273 self.config.editor.highlight_current_line,
6274 line_wrap
6275 .unwrap_or_else(|| self.resolve_line_wrap_for_buffer(buffer_id)),
6276 self.config.editor.wrap_indent,
6277 self.resolve_wrap_column_for_buffer(buffer_id),
6278 self.config.editor.rulers.clone(),
6279 );
6280 view_state.ensure_buffer_state(buffer_id).show_line_numbers =
6282 show_line_numbers;
6283 self.split_view_states.insert(new_split_id, view_state);
6284
6285 self.split_manager.set_active_split(new_split_id);
6287 tracing::info!(
6290 "Created {:?} split with virtual buffer {:?}",
6291 split_dir,
6292 buffer_id
6293 );
6294 Some(new_split_id)
6295 }
6296 Err(e) => {
6297 tracing::error!("Failed to create split: {}", e);
6298 self.set_active_buffer(buffer_id);
6300 None
6301 }
6302 };
6303
6304 if let Some(req_id) = request_id {
6307 tracing::trace!("CreateVirtualBufferInSplit: resolving callback for request_id={}, buffer_id={:?}, split_id={:?}", req_id, buffer_id, created_split_id);
6308 let result = fresh_core::api::VirtualBufferResult {
6309 buffer_id: buffer_id.0 as u64,
6310 split_id: created_split_id.map(|s| s.0 .0 as u64),
6311 };
6312 self.plugin_manager.resolve_callback(
6313 fresh_core::api::JsCallbackId::from(req_id),
6314 serde_json::to_string(&result).unwrap_or_default(),
6315 );
6316 }
6317 }
6318 PluginCommand::SetVirtualBufferContent { buffer_id, entries } => {
6319 match self.set_virtual_buffer_content(buffer_id, entries) {
6320 Ok(()) => {
6321 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
6322 }
6323 Err(e) => {
6324 tracing::error!("Failed to set virtual buffer content: {}", e);
6325 }
6326 }
6327 }
6328 PluginCommand::GetTextPropertiesAtCursor { buffer_id } => {
6329 if let Some(state) = self.buffers.get(&buffer_id) {
6331 let cursor_pos = self
6332 .split_view_states
6333 .values()
6334 .find_map(|vs| vs.buffer_state(buffer_id))
6335 .map(|bs| bs.cursors.primary().position)
6336 .unwrap_or(0);
6337 let properties = state.text_properties.get_at(cursor_pos);
6338 tracing::debug!(
6339 "Text properties at cursor in {:?}: {} properties found",
6340 buffer_id,
6341 properties.len()
6342 );
6343 }
6345 }
6346 PluginCommand::CreateVirtualBufferInExistingSplit {
6347 name,
6348 mode,
6349 read_only,
6350 entries,
6351 split_id,
6352 show_line_numbers,
6353 show_cursors,
6354 editing_disabled,
6355 line_wrap,
6356 request_id,
6357 } => {
6358 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
6360 tracing::info!(
6361 "Created virtual buffer '{}' with mode '{}' for existing split {:?} (id={:?})",
6362 name,
6363 mode,
6364 split_id,
6365 buffer_id
6366 );
6367
6368 if let Some(state) = self.buffers.get_mut(&buffer_id) {
6370 state.margins.configure_for_line_numbers(show_line_numbers);
6371 state.show_cursors = show_cursors;
6372 state.editing_disabled = editing_disabled;
6373 }
6374
6375 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
6377 tracing::error!("Failed to set virtual buffer content: {}", e);
6378 return Ok(());
6379 }
6380
6381 let leaf_id = LeafId(split_id);
6383 self.split_manager.set_split_buffer(leaf_id, buffer_id);
6384
6385 self.split_manager.set_active_split(leaf_id);
6387 self.split_manager.set_active_buffer_id(buffer_id);
6388
6389 if let Some(view_state) = self.split_view_states.get_mut(&leaf_id) {
6391 view_state.switch_buffer(buffer_id);
6392 view_state.add_buffer(buffer_id);
6393 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
6394
6395 if let Some(wrap) = line_wrap {
6397 view_state.active_state_mut().viewport.line_wrap_enabled = wrap;
6398 }
6399 }
6400
6401 tracing::info!(
6402 "Displayed virtual buffer {:?} in split {:?}",
6403 buffer_id,
6404 split_id
6405 );
6406
6407 if let Some(req_id) = request_id {
6409 let result = fresh_core::api::VirtualBufferResult {
6410 buffer_id: buffer_id.0 as u64,
6411 split_id: Some(split_id.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
6420 PluginCommand::SetContext { name, active } => {
6422 if active {
6423 self.active_custom_contexts.insert(name.clone());
6424 tracing::debug!("Set custom context: {}", name);
6425 } else {
6426 self.active_custom_contexts.remove(&name);
6427 tracing::debug!("Unset custom context: {}", name);
6428 }
6429 }
6430
6431 PluginCommand::SetReviewDiffHunks { hunks } => {
6433 self.review_hunks = hunks;
6434 tracing::debug!("Set {} review hunks", self.review_hunks.len());
6435 }
6436
6437 PluginCommand::ExecuteAction { action_name } => {
6439 self.handle_execute_action(action_name);
6440 }
6441 PluginCommand::ExecuteActions { actions } => {
6442 self.handle_execute_actions(actions);
6443 }
6444 PluginCommand::GetBufferText {
6445 buffer_id,
6446 start,
6447 end,
6448 request_id,
6449 } => {
6450 self.handle_get_buffer_text(buffer_id, start, end, request_id);
6451 }
6452 PluginCommand::GetLineStartPosition {
6453 buffer_id,
6454 line,
6455 request_id,
6456 } => {
6457 self.handle_get_line_start_position(buffer_id, line, request_id);
6458 }
6459 PluginCommand::GetLineEndPosition {
6460 buffer_id,
6461 line,
6462 request_id,
6463 } => {
6464 self.handle_get_line_end_position(buffer_id, line, request_id);
6465 }
6466 PluginCommand::GetBufferLineCount {
6467 buffer_id,
6468 request_id,
6469 } => {
6470 self.handle_get_buffer_line_count(buffer_id, request_id);
6471 }
6472 PluginCommand::ScrollToLineCenter {
6473 split_id,
6474 buffer_id,
6475 line,
6476 } => {
6477 self.handle_scroll_to_line_center(split_id, buffer_id, line);
6478 }
6479 PluginCommand::SetEditorMode { mode } => {
6480 self.handle_set_editor_mode(mode);
6481 }
6482
6483 PluginCommand::ShowActionPopup {
6485 popup_id,
6486 title,
6487 message,
6488 actions,
6489 } => {
6490 tracing::info!(
6491 "Action popup requested: id={}, title={}, actions={}",
6492 popup_id,
6493 title,
6494 actions.len()
6495 );
6496
6497 let items: Vec<crate::model::event::PopupListItemData> = actions
6499 .iter()
6500 .map(|action| crate::model::event::PopupListItemData {
6501 text: action.label.clone(),
6502 detail: None,
6503 icon: None,
6504 data: Some(action.id.clone()),
6505 })
6506 .collect();
6507
6508 let action_ids: Vec<(String, String)> =
6510 actions.into_iter().map(|a| (a.id, a.label)).collect();
6511 self.active_action_popup = Some((popup_id.clone(), action_ids));
6512
6513 let popup = crate::model::event::PopupData {
6515 kind: crate::model::event::PopupKindHint::List,
6516 title: Some(title),
6517 description: Some(message),
6518 transient: false,
6519 content: crate::model::event::PopupContentData::List { items, selected: 0 },
6520 position: crate::model::event::PopupPositionData::BottomRight,
6521 width: 60,
6522 max_height: 15,
6523 bordered: true,
6524 };
6525
6526 self.show_popup(popup);
6527 tracing::info!(
6528 "Action popup shown: id={}, active_action_popup={:?}",
6529 popup_id,
6530 self.active_action_popup.as_ref().map(|(id, _)| id)
6531 );
6532 }
6533
6534 PluginCommand::DisableLspForLanguage { language } => {
6535 tracing::info!("Disabling LSP for language: {}", language);
6536
6537 if let Some(ref mut lsp) = self.lsp {
6539 lsp.shutdown_server(&language);
6540 tracing::info!("Stopped LSP server for {}", language);
6541 }
6542
6543 if let Some(lsp_configs) = self.config.lsp.get_mut(&language) {
6545 for c in lsp_configs.as_mut_slice() {
6546 c.enabled = false;
6547 c.auto_start = false;
6548 }
6549 tracing::info!("Disabled LSP config for {}", language);
6550 }
6551
6552 if let Err(e) = self.save_config() {
6554 tracing::error!("Failed to save config: {}", e);
6555 self.status_message = Some(format!(
6556 "LSP disabled for {} (config save failed)",
6557 language
6558 ));
6559 } else {
6560 self.status_message = Some(format!("LSP disabled for {}", language));
6561 }
6562
6563 self.warning_domains.lsp.clear();
6565 }
6566
6567 PluginCommand::RestartLspForLanguage { language } => {
6568 tracing::info!("Plugin restarting LSP for language: {}", language);
6569
6570 let file_path = self
6571 .buffer_metadata
6572 .get(&self.active_buffer())
6573 .and_then(|meta| meta.file_path().cloned());
6574 let success = if let Some(ref mut lsp) = self.lsp {
6575 let (ok, msg) = lsp.manual_restart(&language, file_path.as_deref());
6576 self.status_message = Some(msg);
6577 ok
6578 } else {
6579 self.status_message = Some("No LSP manager available".to_string());
6580 false
6581 };
6582
6583 if success {
6584 self.reopen_buffers_for_language(&language);
6585 }
6586 }
6587
6588 PluginCommand::SetLspRootUri { language, uri } => {
6589 tracing::info!("Plugin setting LSP root URI for {}: {}", language, uri);
6590
6591 match uri.parse::<lsp_types::Uri>() {
6593 Ok(parsed_uri) => {
6594 if let Some(ref mut lsp) = self.lsp {
6595 let restarted = lsp.set_language_root_uri(&language, parsed_uri);
6596 if restarted {
6597 self.status_message = Some(format!(
6598 "LSP root updated for {} (restarting server)",
6599 language
6600 ));
6601 } else {
6602 self.status_message =
6603 Some(format!("LSP root set for {}", language));
6604 }
6605 }
6606 }
6607 Err(e) => {
6608 tracing::error!("Invalid LSP root URI '{}': {}", uri, e);
6609 self.status_message = Some(format!("Invalid LSP root URI: {}", e));
6610 }
6611 }
6612 }
6613
6614 PluginCommand::CreateScrollSyncGroup {
6616 group_id,
6617 left_split,
6618 right_split,
6619 } => {
6620 let success = self.scroll_sync_manager.create_group_with_id(
6621 group_id,
6622 left_split,
6623 right_split,
6624 );
6625 if success {
6626 tracing::debug!(
6627 "Created scroll sync group {} for splits {:?} and {:?}",
6628 group_id,
6629 left_split,
6630 right_split
6631 );
6632 } else {
6633 tracing::warn!(
6634 "Failed to create scroll sync group {} (ID already exists)",
6635 group_id
6636 );
6637 }
6638 }
6639 PluginCommand::SetScrollSyncAnchors { group_id, anchors } => {
6640 use crate::view::scroll_sync::SyncAnchor;
6641 let anchor_count = anchors.len();
6642 let sync_anchors: Vec<SyncAnchor> = anchors
6643 .into_iter()
6644 .map(|(left_line, right_line)| SyncAnchor {
6645 left_line,
6646 right_line,
6647 })
6648 .collect();
6649 self.scroll_sync_manager.set_anchors(group_id, sync_anchors);
6650 tracing::debug!(
6651 "Set {} anchors for scroll sync group {}",
6652 anchor_count,
6653 group_id
6654 );
6655 }
6656 PluginCommand::RemoveScrollSyncGroup { group_id } => {
6657 if self.scroll_sync_manager.remove_group(group_id) {
6658 tracing::debug!("Removed scroll sync group {}", group_id);
6659 } else {
6660 tracing::warn!("Scroll sync group {} not found", group_id);
6661 }
6662 }
6663
6664 PluginCommand::CreateCompositeBuffer {
6666 name,
6667 mode,
6668 layout,
6669 sources,
6670 hunks,
6671 initial_focus_hunk,
6672 request_id,
6673 } => {
6674 self.handle_create_composite_buffer(
6675 name,
6676 mode,
6677 layout,
6678 sources,
6679 hunks,
6680 initial_focus_hunk,
6681 request_id,
6682 );
6683 }
6684 PluginCommand::UpdateCompositeAlignment { buffer_id, hunks } => {
6685 self.handle_update_composite_alignment(buffer_id, hunks);
6686 }
6687 PluginCommand::CloseCompositeBuffer { buffer_id } => {
6688 self.close_composite_buffer(buffer_id);
6689 }
6690 PluginCommand::FlushLayout => {
6691 self.flush_layout();
6692 }
6693 PluginCommand::CompositeNextHunk { buffer_id } => {
6694 let split_id = self.split_manager.active_split();
6695 self.composite_next_hunk(split_id, buffer_id);
6696 }
6697 PluginCommand::CompositePrevHunk { buffer_id } => {
6698 let split_id = self.split_manager.active_split();
6699 self.composite_prev_hunk(split_id, buffer_id);
6700 }
6701
6702 PluginCommand::SaveBufferToPath { buffer_id, path } => {
6704 self.handle_save_buffer_to_path(buffer_id, path);
6705 }
6706
6707 #[cfg(feature = "plugins")]
6709 PluginCommand::LoadPlugin { path, callback_id } => {
6710 self.handle_load_plugin(path, callback_id);
6711 }
6712 #[cfg(feature = "plugins")]
6713 PluginCommand::UnloadPlugin { name, callback_id } => {
6714 self.handle_unload_plugin(name, callback_id);
6715 }
6716 #[cfg(feature = "plugins")]
6717 PluginCommand::ReloadPlugin { name, callback_id } => {
6718 self.handle_reload_plugin(name, callback_id);
6719 }
6720 #[cfg(feature = "plugins")]
6721 PluginCommand::ListPlugins { callback_id } => {
6722 self.handle_list_plugins(callback_id);
6723 }
6724 #[cfg(not(feature = "plugins"))]
6726 PluginCommand::LoadPlugin { .. }
6727 | PluginCommand::UnloadPlugin { .. }
6728 | PluginCommand::ReloadPlugin { .. }
6729 | PluginCommand::ListPlugins { .. } => {
6730 tracing::warn!("Plugin management commands require the 'plugins' feature");
6731 }
6732
6733 PluginCommand::CreateTerminal {
6735 cwd,
6736 direction,
6737 ratio,
6738 focus,
6739 request_id,
6740 } => {
6741 let (cols, rows) = self.get_terminal_dimensions();
6742
6743 if let Some(ref bridge) = self.async_bridge {
6745 self.terminal_manager.set_async_bridge(bridge.clone());
6746 }
6747
6748 let working_dir = cwd
6750 .map(std::path::PathBuf::from)
6751 .unwrap_or_else(|| self.working_dir.clone());
6752
6753 let terminal_root = self.dir_context.terminal_dir_for(&working_dir);
6755 if let Err(e) = self.filesystem.create_dir_all(&terminal_root) {
6756 tracing::warn!("Failed to create terminal directory: {}", e);
6757 }
6758 let predicted_terminal_id = self.terminal_manager.next_terminal_id();
6759 let log_path =
6760 terminal_root.join(format!("fresh-terminal-{}.log", predicted_terminal_id.0));
6761 let backing_path =
6762 terminal_root.join(format!("fresh-terminal-{}.txt", predicted_terminal_id.0));
6763 self.terminal_backing_files
6764 .insert(predicted_terminal_id, backing_path);
6765 let backing_path_for_spawn = self
6766 .terminal_backing_files
6767 .get(&predicted_terminal_id)
6768 .cloned();
6769
6770 match self.terminal_manager.spawn(
6771 cols,
6772 rows,
6773 Some(working_dir),
6774 Some(log_path.clone()),
6775 backing_path_for_spawn,
6776 ) {
6777 Ok(terminal_id) => {
6778 self.terminal_log_files
6780 .insert(terminal_id, log_path.clone());
6781 if terminal_id != predicted_terminal_id {
6783 self.terminal_backing_files.remove(&predicted_terminal_id);
6784 let backing_path =
6785 terminal_root.join(format!("fresh-terminal-{}.txt", terminal_id.0));
6786 self.terminal_backing_files
6787 .insert(terminal_id, backing_path);
6788 }
6789
6790 let active_split = self.split_manager.active_split();
6792 let buffer_id =
6793 self.create_terminal_buffer_attached(terminal_id, active_split);
6794
6795 let created_split_id = if let Some(dir_str) = direction.as_deref() {
6799 let split_dir = match dir_str {
6800 "horizontal" => crate::model::event::SplitDirection::Horizontal,
6801 _ => crate::model::event::SplitDirection::Vertical,
6802 };
6803
6804 let split_ratio = ratio.unwrap_or(0.5);
6805 match self
6806 .split_manager
6807 .split_active(split_dir, buffer_id, split_ratio)
6808 {
6809 Ok(new_split_id) => {
6810 let mut view_state = SplitViewState::with_buffer(
6811 self.terminal_width,
6812 self.terminal_height,
6813 buffer_id,
6814 );
6815 view_state.apply_config_defaults(
6816 self.config.editor.line_numbers,
6817 self.config.editor.highlight_current_line,
6818 false,
6819 false,
6820 None,
6821 self.config.editor.rulers.clone(),
6822 );
6823 self.split_view_states.insert(new_split_id, view_state);
6824
6825 if focus.unwrap_or(true) {
6826 self.split_manager.set_active_split(new_split_id);
6827 }
6828
6829 tracing::info!(
6830 "Created {:?} split for terminal {:?} with buffer {:?}",
6831 split_dir,
6832 terminal_id,
6833 buffer_id
6834 );
6835 Some(new_split_id)
6836 }
6837 Err(e) => {
6838 tracing::error!("Failed to create split for terminal: {}", e);
6839 self.set_active_buffer(buffer_id);
6840 None
6841 }
6842 }
6843 } else {
6844 self.set_active_buffer(buffer_id);
6846 None
6847 };
6848
6849 self.resize_visible_terminals();
6851
6852 let result = fresh_core::api::TerminalResult {
6854 buffer_id: buffer_id.0 as u64,
6855 terminal_id: terminal_id.0 as u64,
6856 split_id: created_split_id.map(|s| s.0 .0 as u64),
6857 };
6858 self.plugin_manager.resolve_callback(
6859 fresh_core::api::JsCallbackId::from(request_id),
6860 serde_json::to_string(&result).unwrap_or_default(),
6861 );
6862
6863 tracing::info!(
6864 "Plugin created terminal {:?} with buffer {:?}",
6865 terminal_id,
6866 buffer_id
6867 );
6868 }
6869 Err(e) => {
6870 tracing::error!("Failed to create terminal for plugin: {}", e);
6871 self.plugin_manager.reject_callback(
6872 fresh_core::api::JsCallbackId::from(request_id),
6873 format!("Failed to create terminal: {}", e),
6874 );
6875 }
6876 }
6877 }
6878
6879 PluginCommand::SendTerminalInput { terminal_id, data } => {
6880 if let Some(handle) = self.terminal_manager.get(terminal_id) {
6881 handle.write(data.as_bytes());
6882 tracing::trace!(
6883 "Plugin sent {} bytes to terminal {:?}",
6884 data.len(),
6885 terminal_id
6886 );
6887 } else {
6888 tracing::warn!(
6889 "Plugin tried to send input to non-existent terminal {:?}",
6890 terminal_id
6891 );
6892 }
6893 }
6894
6895 PluginCommand::CloseTerminal { terminal_id } => {
6896 let buffer_to_close = self
6898 .terminal_buffers
6899 .iter()
6900 .find(|(_, &tid)| tid == terminal_id)
6901 .map(|(&bid, _)| bid);
6902
6903 if let Some(buffer_id) = buffer_to_close {
6904 if let Err(e) = self.close_buffer(buffer_id) {
6905 tracing::warn!("Failed to close terminal buffer: {}", e);
6906 }
6907 tracing::info!("Plugin closed terminal {:?}", terminal_id);
6908 } else {
6909 self.terminal_manager.close(terminal_id);
6911 tracing::info!("Plugin closed terminal {:?} (no buffer found)", terminal_id);
6912 }
6913 }
6914
6915 PluginCommand::GrepProject {
6916 pattern,
6917 fixed_string,
6918 case_sensitive,
6919 max_results,
6920 whole_words,
6921 callback_id,
6922 } => {
6923 self.handle_grep_project(
6924 pattern,
6925 fixed_string,
6926 case_sensitive,
6927 max_results,
6928 whole_words,
6929 callback_id,
6930 );
6931 }
6932
6933 PluginCommand::GrepProjectStreaming {
6934 pattern,
6935 fixed_string,
6936 case_sensitive,
6937 max_results,
6938 whole_words,
6939 search_id,
6940 callback_id,
6941 } => {
6942 self.handle_grep_project_streaming(
6943 pattern,
6944 fixed_string,
6945 case_sensitive,
6946 max_results,
6947 whole_words,
6948 search_id,
6949 callback_id,
6950 );
6951 }
6952
6953 PluginCommand::ReplaceInBuffer {
6954 file_path,
6955 matches,
6956 replacement,
6957 callback_id,
6958 } => {
6959 self.handle_replace_in_buffer(file_path, matches, replacement, callback_id);
6960 }
6961 }
6962 Ok(())
6963 }
6964
6965 fn handle_save_buffer_to_path(&mut self, buffer_id: BufferId, path: std::path::PathBuf) {
6967 if let Some(state) = self.buffers.get_mut(&buffer_id) {
6968 match state.buffer.save_to_file(&path) {
6970 Ok(()) => {
6971 if let Err(e) = self.finalize_save(Some(path)) {
6974 tracing::warn!("Failed to finalize save: {}", e);
6975 }
6976 tracing::debug!("Saved buffer {:?} to path", buffer_id);
6977 }
6978 Err(e) => {
6979 self.handle_set_status(format!("Error saving: {}", e));
6980 tracing::error!("Failed to save buffer to path: {}", e);
6981 }
6982 }
6983 } else {
6984 self.handle_set_status(format!("Buffer {:?} not found", buffer_id));
6985 tracing::warn!("SaveBufferToPath: buffer {:?} not found", buffer_id);
6986 }
6987 }
6988
6989 #[cfg(feature = "plugins")]
6991 fn handle_load_plugin(&mut self, path: std::path::PathBuf, callback_id: JsCallbackId) {
6992 match self.plugin_manager.load_plugin(&path) {
6993 Ok(()) => {
6994 tracing::info!("Loaded plugin from {:?}", path);
6995 self.plugin_manager
6996 .resolve_callback(callback_id, "true".to_string());
6997 }
6998 Err(e) => {
6999 tracing::error!("Failed to load plugin from {:?}: {}", path, e);
7000 self.plugin_manager
7001 .reject_callback(callback_id, format!("{}", e));
7002 }
7003 }
7004 }
7005
7006 #[cfg(feature = "plugins")]
7008 fn handle_unload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
7009 match self.plugin_manager.unload_plugin(&name) {
7010 Ok(()) => {
7011 tracing::info!("Unloaded plugin: {}", name);
7012 self.plugin_manager
7013 .resolve_callback(callback_id, "true".to_string());
7014 }
7015 Err(e) => {
7016 tracing::error!("Failed to unload plugin '{}': {}", name, e);
7017 self.plugin_manager
7018 .reject_callback(callback_id, format!("{}", e));
7019 }
7020 }
7021 }
7022
7023 #[cfg(feature = "plugins")]
7025 fn handle_reload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
7026 match self.plugin_manager.reload_plugin(&name) {
7027 Ok(()) => {
7028 tracing::info!("Reloaded plugin: {}", name);
7029 self.plugin_manager
7030 .resolve_callback(callback_id, "true".to_string());
7031 }
7032 Err(e) => {
7033 tracing::error!("Failed to reload plugin '{}': {}", name, e);
7034 self.plugin_manager
7035 .reject_callback(callback_id, format!("{}", e));
7036 }
7037 }
7038 }
7039
7040 #[cfg(feature = "plugins")]
7042 fn handle_list_plugins(&mut self, callback_id: JsCallbackId) {
7043 let plugins = self.plugin_manager.list_plugins();
7044 let json_array: Vec<serde_json::Value> = plugins
7046 .iter()
7047 .map(|p| {
7048 serde_json::json!({
7049 "name": p.name,
7050 "path": p.path.to_string_lossy(),
7051 "enabled": p.enabled
7052 })
7053 })
7054 .collect();
7055 let json_str = serde_json::to_string(&json_array).unwrap_or_else(|_| "[]".to_string());
7056 self.plugin_manager.resolve_callback(callback_id, json_str);
7057 }
7058
7059 fn handle_execute_action(&mut self, action_name: String) {
7061 use crate::input::keybindings::Action;
7062 use std::collections::HashMap;
7063
7064 if let Some(action) = Action::from_str(&action_name, &HashMap::new()) {
7066 if let Err(e) = self.handle_action(action) {
7068 tracing::warn!("Failed to execute action '{}': {}", action_name, e);
7069 } else {
7070 tracing::debug!("Executed action: {}", action_name);
7071 }
7072 } else {
7073 tracing::warn!("Unknown action: {}", action_name);
7074 }
7075 }
7076
7077 fn handle_execute_actions(&mut self, actions: Vec<fresh_core::api::ActionSpec>) {
7080 use crate::input::keybindings::Action;
7081 use std::collections::HashMap;
7082
7083 for action_spec in actions {
7084 if let Some(action) = Action::from_str(&action_spec.action, &HashMap::new()) {
7085 for _ in 0..action_spec.count {
7087 if let Err(e) = self.handle_action(action.clone()) {
7088 tracing::warn!("Failed to execute action '{}': {}", action_spec.action, e);
7089 return; }
7091 }
7092 tracing::debug!(
7093 "Executed action '{}' {} time(s)",
7094 action_spec.action,
7095 action_spec.count
7096 );
7097 } else {
7098 tracing::warn!("Unknown action: {}", action_spec.action);
7099 return; }
7101 }
7102 }
7103
7104 fn handle_get_buffer_text(
7106 &mut self,
7107 buffer_id: BufferId,
7108 start: usize,
7109 end: usize,
7110 request_id: u64,
7111 ) {
7112 let result = if let Some(state) = self.buffers.get_mut(&buffer_id) {
7113 let len = state.buffer.len();
7115 if start <= end && end <= len {
7116 Ok(state.get_text_range(start, end))
7117 } else {
7118 Err(format!(
7119 "Invalid range {}..{} for buffer of length {}",
7120 start, end, len
7121 ))
7122 }
7123 } else {
7124 Err(format!("Buffer {:?} not found", buffer_id))
7125 };
7126
7127 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
7129 match result {
7130 Ok(text) => {
7131 let json = serde_json::to_string(&text).unwrap_or_else(|_| "null".to_string());
7133 self.plugin_manager.resolve_callback(callback_id, json);
7134 }
7135 Err(error) => {
7136 self.plugin_manager.reject_callback(callback_id, error);
7137 }
7138 }
7139 }
7140
7141 fn handle_set_editor_mode(&mut self, mode: Option<String>) {
7143 self.editor_mode = mode.clone();
7144 tracing::debug!("Set editor mode: {:?}", mode);
7145 }
7146
7147 fn handle_get_line_start_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
7149 let actual_buffer_id = if buffer_id.0 == 0 {
7151 self.active_buffer_id()
7152 } else {
7153 buffer_id
7154 };
7155
7156 let result = if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
7157 let line_number = line as usize;
7159 let buffer_len = state.buffer.len();
7160
7161 if line_number == 0 {
7162 Some(0)
7164 } else {
7165 let mut current_line = 0;
7167 let mut line_start = None;
7168
7169 let content = state.get_text_range(0, buffer_len);
7171 for (byte_idx, c) in content.char_indices() {
7172 if c == '\n' {
7173 current_line += 1;
7174 if current_line == line_number {
7175 line_start = Some(byte_idx + 1);
7177 break;
7178 }
7179 }
7180 }
7181 line_start
7182 }
7183 } else {
7184 None
7185 };
7186
7187 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
7189 let json = serde_json::to_string(&result).unwrap_or_else(|_| "null".to_string());
7191 self.plugin_manager.resolve_callback(callback_id, json);
7192 }
7193
7194 fn handle_get_line_end_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
7197 let actual_buffer_id = if buffer_id.0 == 0 {
7199 self.active_buffer_id()
7200 } else {
7201 buffer_id
7202 };
7203
7204 let result = if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
7205 let line_number = line as usize;
7206 let buffer_len = state.buffer.len();
7207
7208 let content = state.get_text_range(0, buffer_len);
7210 let mut current_line = 0;
7211 let mut line_end = None;
7212
7213 for (byte_idx, c) in content.char_indices() {
7214 if c == '\n' {
7215 if current_line == line_number {
7216 line_end = Some(byte_idx);
7218 break;
7219 }
7220 current_line += 1;
7221 }
7222 }
7223
7224 if line_end.is_none() && current_line == line_number {
7226 line_end = Some(buffer_len);
7227 }
7228
7229 line_end
7230 } else {
7231 None
7232 };
7233
7234 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
7235 let json = serde_json::to_string(&result).unwrap_or_else(|_| "null".to_string());
7236 self.plugin_manager.resolve_callback(callback_id, json);
7237 }
7238
7239 fn handle_get_buffer_line_count(&mut self, buffer_id: BufferId, request_id: u64) {
7241 let actual_buffer_id = if buffer_id.0 == 0 {
7243 self.active_buffer_id()
7244 } else {
7245 buffer_id
7246 };
7247
7248 let result = if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
7249 let buffer_len = state.buffer.len();
7250 let content = state.get_text_range(0, buffer_len);
7251
7252 if content.is_empty() {
7254 Some(1) } else {
7256 let newline_count = content.chars().filter(|&c| c == '\n').count();
7257 let ends_with_newline = content.ends_with('\n');
7259 if ends_with_newline {
7260 Some(newline_count)
7261 } else {
7262 Some(newline_count + 1)
7263 }
7264 }
7265 } else {
7266 None
7267 };
7268
7269 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
7270 let json = serde_json::to_string(&result).unwrap_or_else(|_| "null".to_string());
7271 self.plugin_manager.resolve_callback(callback_id, json);
7272 }
7273
7274 fn handle_scroll_to_line_center(
7276 &mut self,
7277 split_id: SplitId,
7278 buffer_id: BufferId,
7279 line: usize,
7280 ) {
7281 let actual_split_id = if split_id.0 == 0 {
7283 self.split_manager.active_split()
7284 } else {
7285 LeafId(split_id)
7286 };
7287
7288 let actual_buffer_id = if buffer_id.0 == 0 {
7290 self.active_buffer()
7291 } else {
7292 buffer_id
7293 };
7294
7295 let viewport_height = if let Some(view_state) = self.split_view_states.get(&actual_split_id)
7297 {
7298 view_state.viewport.height as usize
7299 } else {
7300 return;
7301 };
7302
7303 let lines_above = viewport_height / 2;
7305 let target_line = line.saturating_sub(lines_above);
7306
7307 if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
7309 let buffer = &mut state.buffer;
7310 if let Some(view_state) = self.split_view_states.get_mut(&actual_split_id) {
7311 view_state.viewport.scroll_to(buffer, target_line);
7312 view_state.viewport.set_skip_ensure_visible();
7314 }
7315 }
7316 }
7317}
7318
7319fn parse_key_string(key_str: &str) -> Option<(KeyCode, KeyModifiers)> {
7328 use crossterm::event::{KeyCode, KeyModifiers};
7329
7330 let mut modifiers = KeyModifiers::NONE;
7331 let mut remaining = key_str;
7332
7333 loop {
7335 if remaining.starts_with("C-") {
7336 modifiers |= KeyModifiers::CONTROL;
7337 remaining = &remaining[2..];
7338 } else if remaining.starts_with("M-") {
7339 modifiers |= KeyModifiers::ALT;
7340 remaining = &remaining[2..];
7341 } else if remaining.starts_with("S-") {
7342 modifiers |= KeyModifiers::SHIFT;
7343 remaining = &remaining[2..];
7344 } else {
7345 break;
7346 }
7347 }
7348
7349 let upper = remaining.to_uppercase();
7352 let code = match upper.as_str() {
7353 "RET" | "RETURN" | "ENTER" => KeyCode::Enter,
7354 "TAB" => KeyCode::Tab,
7355 "BACKTAB" => KeyCode::BackTab,
7356 "ESC" | "ESCAPE" => KeyCode::Esc,
7357 "SPC" | "SPACE" => KeyCode::Char(' '),
7358 "DEL" | "DELETE" => KeyCode::Delete,
7359 "BS" | "BACKSPACE" => KeyCode::Backspace,
7360 "UP" => KeyCode::Up,
7361 "DOWN" => KeyCode::Down,
7362 "LEFT" => KeyCode::Left,
7363 "RIGHT" => KeyCode::Right,
7364 "HOME" => KeyCode::Home,
7365 "END" => KeyCode::End,
7366 "PAGEUP" | "PGUP" => KeyCode::PageUp,
7367 "PAGEDOWN" | "PGDN" => KeyCode::PageDown,
7368 s if s.starts_with('F') && s.len() > 1 => {
7369 if let Ok(n) = s[1..].parse::<u8>() {
7371 KeyCode::F(n)
7372 } else {
7373 return None;
7374 }
7375 }
7376 _ if remaining.len() == 1 => {
7377 let c = remaining.chars().next()?;
7380 if c.is_ascii_uppercase() {
7381 modifiers |= KeyModifiers::SHIFT;
7382 }
7383 KeyCode::Char(c.to_ascii_lowercase())
7384 }
7385 _ => return None,
7386 };
7387
7388 Some((code, modifiers))
7389}
7390
7391#[cfg(test)]
7392mod tests {
7393 use super::*;
7394 use tempfile::TempDir;
7395
7396 fn test_dir_context() -> (DirectoryContext, TempDir) {
7398 let temp_dir = TempDir::new().unwrap();
7399 let dir_context = DirectoryContext::for_testing(temp_dir.path());
7400 (dir_context, temp_dir)
7401 }
7402
7403 fn test_filesystem() -> Arc<dyn FileSystem + Send + Sync> {
7405 Arc::new(crate::model::filesystem::StdFileSystem)
7406 }
7407
7408 #[test]
7409 fn test_editor_new() {
7410 let config = Config::default();
7411 let (dir_context, _temp) = test_dir_context();
7412 let editor = Editor::new(
7413 config,
7414 80,
7415 24,
7416 dir_context,
7417 crate::view::color_support::ColorCapability::TrueColor,
7418 test_filesystem(),
7419 )
7420 .unwrap();
7421
7422 assert_eq!(editor.buffers.len(), 1);
7423 assert!(!editor.should_quit());
7424 }
7425
7426 #[test]
7427 fn test_new_buffer() {
7428 let config = Config::default();
7429 let (dir_context, _temp) = test_dir_context();
7430 let mut editor = Editor::new(
7431 config,
7432 80,
7433 24,
7434 dir_context,
7435 crate::view::color_support::ColorCapability::TrueColor,
7436 test_filesystem(),
7437 )
7438 .unwrap();
7439
7440 let id = editor.new_buffer();
7441 assert_eq!(editor.buffers.len(), 2);
7442 assert_eq!(editor.active_buffer(), id);
7443 }
7444
7445 #[test]
7446 #[ignore]
7447 fn test_clipboard() {
7448 let config = Config::default();
7449 let (dir_context, _temp) = test_dir_context();
7450 let mut editor = Editor::new(
7451 config,
7452 80,
7453 24,
7454 dir_context,
7455 crate::view::color_support::ColorCapability::TrueColor,
7456 test_filesystem(),
7457 )
7458 .unwrap();
7459
7460 editor.clipboard.set_internal("test".to_string());
7462
7463 editor.paste();
7465
7466 let content = editor.active_state().buffer.to_string().unwrap();
7467 assert_eq!(content, "test");
7468 }
7469
7470 #[test]
7471 fn test_action_to_events_insert_char() {
7472 let config = Config::default();
7473 let (dir_context, _temp) = test_dir_context();
7474 let mut editor = Editor::new(
7475 config,
7476 80,
7477 24,
7478 dir_context,
7479 crate::view::color_support::ColorCapability::TrueColor,
7480 test_filesystem(),
7481 )
7482 .unwrap();
7483
7484 let events = editor.action_to_events(Action::InsertChar('a'));
7485 assert!(events.is_some());
7486
7487 let events = events.unwrap();
7488 assert_eq!(events.len(), 1);
7489
7490 match &events[0] {
7491 Event::Insert { position, text, .. } => {
7492 assert_eq!(*position, 0);
7493 assert_eq!(text, "a");
7494 }
7495 _ => panic!("Expected Insert event"),
7496 }
7497 }
7498
7499 #[test]
7500 fn test_action_to_events_move_right() {
7501 let config = Config::default();
7502 let (dir_context, _temp) = test_dir_context();
7503 let mut editor = Editor::new(
7504 config,
7505 80,
7506 24,
7507 dir_context,
7508 crate::view::color_support::ColorCapability::TrueColor,
7509 test_filesystem(),
7510 )
7511 .unwrap();
7512
7513 let cursor_id = editor.active_cursors().primary_id();
7515 editor.apply_event_to_active_buffer(&Event::Insert {
7516 position: 0,
7517 text: "hello".to_string(),
7518 cursor_id,
7519 });
7520
7521 let events = editor.action_to_events(Action::MoveRight);
7522 assert!(events.is_some());
7523
7524 let events = events.unwrap();
7525 assert_eq!(events.len(), 1);
7526
7527 match &events[0] {
7528 Event::MoveCursor {
7529 new_position,
7530 new_anchor,
7531 ..
7532 } => {
7533 assert_eq!(*new_position, 5);
7535 assert_eq!(*new_anchor, None); }
7537 _ => panic!("Expected MoveCursor event"),
7538 }
7539 }
7540
7541 #[test]
7542 fn test_action_to_events_move_up_down() {
7543 let config = Config::default();
7544 let (dir_context, _temp) = test_dir_context();
7545 let mut editor = Editor::new(
7546 config,
7547 80,
7548 24,
7549 dir_context,
7550 crate::view::color_support::ColorCapability::TrueColor,
7551 test_filesystem(),
7552 )
7553 .unwrap();
7554
7555 let cursor_id = editor.active_cursors().primary_id();
7557 editor.apply_event_to_active_buffer(&Event::Insert {
7558 position: 0,
7559 text: "line1\nline2\nline3".to_string(),
7560 cursor_id,
7561 });
7562
7563 editor.apply_event_to_active_buffer(&Event::MoveCursor {
7565 cursor_id,
7566 old_position: 0, new_position: 6,
7568 old_anchor: None, new_anchor: None,
7570 old_sticky_column: 0,
7571 new_sticky_column: 0,
7572 });
7573
7574 let events = editor.action_to_events(Action::MoveUp);
7576 assert!(events.is_some());
7577 let events = events.unwrap();
7578 assert_eq!(events.len(), 1);
7579
7580 match &events[0] {
7581 Event::MoveCursor { new_position, .. } => {
7582 assert_eq!(*new_position, 0); }
7584 _ => panic!("Expected MoveCursor event"),
7585 }
7586 }
7587
7588 #[test]
7589 fn test_action_to_events_insert_newline() {
7590 let config = Config::default();
7591 let (dir_context, _temp) = test_dir_context();
7592 let mut editor = Editor::new(
7593 config,
7594 80,
7595 24,
7596 dir_context,
7597 crate::view::color_support::ColorCapability::TrueColor,
7598 test_filesystem(),
7599 )
7600 .unwrap();
7601
7602 let events = editor.action_to_events(Action::InsertNewline);
7603 assert!(events.is_some());
7604
7605 let events = events.unwrap();
7606 assert_eq!(events.len(), 1);
7607
7608 match &events[0] {
7609 Event::Insert { text, .. } => {
7610 assert_eq!(text, "\n");
7611 }
7612 _ => panic!("Expected Insert event"),
7613 }
7614 }
7615
7616 #[test]
7617 fn test_action_to_events_unimplemented() {
7618 let config = Config::default();
7619 let (dir_context, _temp) = test_dir_context();
7620 let mut editor = Editor::new(
7621 config,
7622 80,
7623 24,
7624 dir_context,
7625 crate::view::color_support::ColorCapability::TrueColor,
7626 test_filesystem(),
7627 )
7628 .unwrap();
7629
7630 assert!(editor.action_to_events(Action::Save).is_none());
7632 assert!(editor.action_to_events(Action::Quit).is_none());
7633 assert!(editor.action_to_events(Action::Undo).is_none());
7634 }
7635
7636 #[test]
7637 fn test_action_to_events_delete_backward() {
7638 let config = Config::default();
7639 let (dir_context, _temp) = test_dir_context();
7640 let mut editor = Editor::new(
7641 config,
7642 80,
7643 24,
7644 dir_context,
7645 crate::view::color_support::ColorCapability::TrueColor,
7646 test_filesystem(),
7647 )
7648 .unwrap();
7649
7650 let cursor_id = editor.active_cursors().primary_id();
7652 editor.apply_event_to_active_buffer(&Event::Insert {
7653 position: 0,
7654 text: "hello".to_string(),
7655 cursor_id,
7656 });
7657
7658 let events = editor.action_to_events(Action::DeleteBackward);
7659 assert!(events.is_some());
7660
7661 let events = events.unwrap();
7662 assert_eq!(events.len(), 1);
7663
7664 match &events[0] {
7665 Event::Delete {
7666 range,
7667 deleted_text,
7668 ..
7669 } => {
7670 assert_eq!(range.clone(), 4..5); assert_eq!(deleted_text, "o");
7672 }
7673 _ => panic!("Expected Delete event"),
7674 }
7675 }
7676
7677 #[test]
7678 fn test_action_to_events_delete_forward() {
7679 let config = Config::default();
7680 let (dir_context, _temp) = test_dir_context();
7681 let mut editor = Editor::new(
7682 config,
7683 80,
7684 24,
7685 dir_context,
7686 crate::view::color_support::ColorCapability::TrueColor,
7687 test_filesystem(),
7688 )
7689 .unwrap();
7690
7691 let cursor_id = editor.active_cursors().primary_id();
7693 editor.apply_event_to_active_buffer(&Event::Insert {
7694 position: 0,
7695 text: "hello".to_string(),
7696 cursor_id,
7697 });
7698
7699 editor.apply_event_to_active_buffer(&Event::MoveCursor {
7701 cursor_id,
7702 old_position: 0, new_position: 0,
7704 old_anchor: None, new_anchor: None,
7706 old_sticky_column: 0,
7707 new_sticky_column: 0,
7708 });
7709
7710 let events = editor.action_to_events(Action::DeleteForward);
7711 assert!(events.is_some());
7712
7713 let events = events.unwrap();
7714 assert_eq!(events.len(), 1);
7715
7716 match &events[0] {
7717 Event::Delete {
7718 range,
7719 deleted_text,
7720 ..
7721 } => {
7722 assert_eq!(range.clone(), 0..1); assert_eq!(deleted_text, "h");
7724 }
7725 _ => panic!("Expected Delete event"),
7726 }
7727 }
7728
7729 #[test]
7730 fn test_action_to_events_select_right() {
7731 let config = Config::default();
7732 let (dir_context, _temp) = test_dir_context();
7733 let mut editor = Editor::new(
7734 config,
7735 80,
7736 24,
7737 dir_context,
7738 crate::view::color_support::ColorCapability::TrueColor,
7739 test_filesystem(),
7740 )
7741 .unwrap();
7742
7743 let cursor_id = editor.active_cursors().primary_id();
7745 editor.apply_event_to_active_buffer(&Event::Insert {
7746 position: 0,
7747 text: "hello".to_string(),
7748 cursor_id,
7749 });
7750
7751 editor.apply_event_to_active_buffer(&Event::MoveCursor {
7753 cursor_id,
7754 old_position: 0, new_position: 0,
7756 old_anchor: None, new_anchor: None,
7758 old_sticky_column: 0,
7759 new_sticky_column: 0,
7760 });
7761
7762 let events = editor.action_to_events(Action::SelectRight);
7763 assert!(events.is_some());
7764
7765 let events = events.unwrap();
7766 assert_eq!(events.len(), 1);
7767
7768 match &events[0] {
7769 Event::MoveCursor {
7770 new_position,
7771 new_anchor,
7772 ..
7773 } => {
7774 assert_eq!(*new_position, 1); assert_eq!(*new_anchor, Some(0)); }
7777 _ => panic!("Expected MoveCursor event"),
7778 }
7779 }
7780
7781 #[test]
7782 fn test_action_to_events_select_all() {
7783 let config = Config::default();
7784 let (dir_context, _temp) = test_dir_context();
7785 let mut editor = Editor::new(
7786 config,
7787 80,
7788 24,
7789 dir_context,
7790 crate::view::color_support::ColorCapability::TrueColor,
7791 test_filesystem(),
7792 )
7793 .unwrap();
7794
7795 let cursor_id = editor.active_cursors().primary_id();
7797 editor.apply_event_to_active_buffer(&Event::Insert {
7798 position: 0,
7799 text: "hello world".to_string(),
7800 cursor_id,
7801 });
7802
7803 let events = editor.action_to_events(Action::SelectAll);
7804 assert!(events.is_some());
7805
7806 let events = events.unwrap();
7807 assert_eq!(events.len(), 1);
7808
7809 match &events[0] {
7810 Event::MoveCursor {
7811 new_position,
7812 new_anchor,
7813 ..
7814 } => {
7815 assert_eq!(*new_position, 11); assert_eq!(*new_anchor, Some(0)); }
7818 _ => panic!("Expected MoveCursor event"),
7819 }
7820 }
7821
7822 #[test]
7823 fn test_action_to_events_document_nav() {
7824 let config = Config::default();
7825 let (dir_context, _temp) = test_dir_context();
7826 let mut editor = Editor::new(
7827 config,
7828 80,
7829 24,
7830 dir_context,
7831 crate::view::color_support::ColorCapability::TrueColor,
7832 test_filesystem(),
7833 )
7834 .unwrap();
7835
7836 let cursor_id = editor.active_cursors().primary_id();
7838 editor.apply_event_to_active_buffer(&Event::Insert {
7839 position: 0,
7840 text: "line1\nline2\nline3".to_string(),
7841 cursor_id,
7842 });
7843
7844 let events = editor.action_to_events(Action::MoveDocumentStart);
7846 assert!(events.is_some());
7847 let events = events.unwrap();
7848 match &events[0] {
7849 Event::MoveCursor { new_position, .. } => {
7850 assert_eq!(*new_position, 0);
7851 }
7852 _ => panic!("Expected MoveCursor event"),
7853 }
7854
7855 let events = editor.action_to_events(Action::MoveDocumentEnd);
7857 assert!(events.is_some());
7858 let events = events.unwrap();
7859 match &events[0] {
7860 Event::MoveCursor { new_position, .. } => {
7861 assert_eq!(*new_position, 17); }
7863 _ => panic!("Expected MoveCursor event"),
7864 }
7865 }
7866
7867 #[test]
7868 fn test_action_to_events_remove_secondary_cursors() {
7869 use crate::model::event::CursorId;
7870
7871 let config = Config::default();
7872 let (dir_context, _temp) = test_dir_context();
7873 let mut editor = Editor::new(
7874 config,
7875 80,
7876 24,
7877 dir_context,
7878 crate::view::color_support::ColorCapability::TrueColor,
7879 test_filesystem(),
7880 )
7881 .unwrap();
7882
7883 let cursor_id = editor.active_cursors().primary_id();
7885 editor.apply_event_to_active_buffer(&Event::Insert {
7886 position: 0,
7887 text: "hello world test".to_string(),
7888 cursor_id,
7889 });
7890
7891 editor.apply_event_to_active_buffer(&Event::AddCursor {
7893 cursor_id: CursorId(1),
7894 position: 5,
7895 anchor: None,
7896 });
7897 editor.apply_event_to_active_buffer(&Event::AddCursor {
7898 cursor_id: CursorId(2),
7899 position: 10,
7900 anchor: None,
7901 });
7902
7903 assert_eq!(editor.active_cursors().count(), 3);
7904
7905 let first_id = editor
7907 .active_cursors()
7908 .iter()
7909 .map(|(id, _)| id)
7910 .min_by_key(|id| id.0)
7911 .expect("Should have at least one cursor");
7912
7913 let events = editor.action_to_events(Action::RemoveSecondaryCursors);
7915 assert!(events.is_some());
7916
7917 let events = events.unwrap();
7918 let remove_cursor_events: Vec<_> = events
7921 .iter()
7922 .filter_map(|e| match e {
7923 Event::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
7924 _ => None,
7925 })
7926 .collect();
7927
7928 assert_eq!(remove_cursor_events.len(), 2);
7930
7931 for cursor_id in &remove_cursor_events {
7932 assert_ne!(*cursor_id, first_id);
7934 }
7935 }
7936
7937 #[test]
7938 fn test_action_to_events_scroll() {
7939 let config = Config::default();
7940 let (dir_context, _temp) = test_dir_context();
7941 let mut editor = Editor::new(
7942 config,
7943 80,
7944 24,
7945 dir_context,
7946 crate::view::color_support::ColorCapability::TrueColor,
7947 test_filesystem(),
7948 )
7949 .unwrap();
7950
7951 let events = editor.action_to_events(Action::ScrollUp);
7953 assert!(events.is_some());
7954 let events = events.unwrap();
7955 assert_eq!(events.len(), 1);
7956 match &events[0] {
7957 Event::Scroll { line_offset } => {
7958 assert_eq!(*line_offset, -1);
7959 }
7960 _ => panic!("Expected Scroll event"),
7961 }
7962
7963 let events = editor.action_to_events(Action::ScrollDown);
7965 assert!(events.is_some());
7966 let events = events.unwrap();
7967 assert_eq!(events.len(), 1);
7968 match &events[0] {
7969 Event::Scroll { line_offset } => {
7970 assert_eq!(*line_offset, 1);
7971 }
7972 _ => panic!("Expected Scroll event"),
7973 }
7974 }
7975
7976 #[test]
7977 fn test_action_to_events_none() {
7978 let config = Config::default();
7979 let (dir_context, _temp) = test_dir_context();
7980 let mut editor = Editor::new(
7981 config,
7982 80,
7983 24,
7984 dir_context,
7985 crate::view::color_support::ColorCapability::TrueColor,
7986 test_filesystem(),
7987 )
7988 .unwrap();
7989
7990 let events = editor.action_to_events(Action::None);
7992 assert!(events.is_none());
7993 }
7994
7995 #[test]
7996 fn test_lsp_incremental_insert_generates_correct_range() {
7997 use crate::model::buffer::Buffer;
8000
8001 let buffer = Buffer::from_str_test("hello\nworld");
8002
8003 let position = 0;
8006 let (line, character) = buffer.position_to_lsp_position(position);
8007
8008 assert_eq!(line, 0, "Insertion at start should be line 0");
8009 assert_eq!(character, 0, "Insertion at start should be char 0");
8010
8011 let lsp_pos = Position::new(line as u32, character as u32);
8013 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
8014
8015 assert_eq!(lsp_range.start.line, 0);
8016 assert_eq!(lsp_range.start.character, 0);
8017 assert_eq!(lsp_range.end.line, 0);
8018 assert_eq!(lsp_range.end.character, 0);
8019 assert_eq!(
8020 lsp_range.start, lsp_range.end,
8021 "Insert should have zero-width range"
8022 );
8023
8024 let position = 3;
8026 let (line, character) = buffer.position_to_lsp_position(position);
8027
8028 assert_eq!(line, 0);
8029 assert_eq!(character, 3);
8030
8031 let position = 6;
8033 let (line, character) = buffer.position_to_lsp_position(position);
8034
8035 assert_eq!(line, 1, "Position after newline should be line 1");
8036 assert_eq!(character, 0, "Position at start of line 2 should be char 0");
8037 }
8038
8039 #[test]
8040 fn test_lsp_incremental_delete_generates_correct_range() {
8041 use crate::model::buffer::Buffer;
8044
8045 let buffer = Buffer::from_str_test("hello\nworld");
8046
8047 let range_start = 1;
8049 let range_end = 5;
8050
8051 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
8052 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
8053
8054 assert_eq!(start_line, 0);
8055 assert_eq!(start_char, 1);
8056 assert_eq!(end_line, 0);
8057 assert_eq!(end_char, 5);
8058
8059 let lsp_range = LspRange::new(
8060 Position::new(start_line as u32, start_char as u32),
8061 Position::new(end_line as u32, end_char as u32),
8062 );
8063
8064 assert_eq!(lsp_range.start.line, 0);
8065 assert_eq!(lsp_range.start.character, 1);
8066 assert_eq!(lsp_range.end.line, 0);
8067 assert_eq!(lsp_range.end.character, 5);
8068 assert_ne!(
8069 lsp_range.start, lsp_range.end,
8070 "Delete should have non-zero range"
8071 );
8072
8073 let range_start = 4;
8075 let range_end = 8;
8076
8077 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
8078 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
8079
8080 assert_eq!(start_line, 0, "Delete start on line 0");
8081 assert_eq!(start_char, 4, "Delete start at char 4");
8082 assert_eq!(end_line, 1, "Delete end on line 1");
8083 assert_eq!(end_char, 2, "Delete end at char 2 of line 1");
8084 }
8085
8086 #[test]
8087 fn test_lsp_incremental_utf16_encoding() {
8088 use crate::model::buffer::Buffer;
8091
8092 let buffer = Buffer::from_str_test("😀hello");
8094
8095 let (line, character) = buffer.position_to_lsp_position(4);
8097
8098 assert_eq!(line, 0);
8099 assert_eq!(character, 2, "Emoji should count as 2 UTF-16 code units");
8100
8101 let (line, character) = buffer.position_to_lsp_position(9);
8103
8104 assert_eq!(line, 0);
8105 assert_eq!(
8106 character, 7,
8107 "Should be 2 (emoji) + 5 (text) = 7 UTF-16 code units"
8108 );
8109
8110 let buffer = Buffer::from_str_test("café");
8112
8113 let (line, character) = buffer.position_to_lsp_position(3);
8115
8116 assert_eq!(line, 0);
8117 assert_eq!(character, 3);
8118
8119 let (line, character) = buffer.position_to_lsp_position(5);
8121
8122 assert_eq!(line, 0);
8123 assert_eq!(character, 4, "é should count as 1 UTF-16 code unit");
8124 }
8125
8126 #[test]
8127 fn test_lsp_content_change_event_structure() {
8128 let insert_change = TextDocumentContentChangeEvent {
8132 range: Some(LspRange::new(Position::new(0, 5), Position::new(0, 5))),
8133 range_length: None,
8134 text: "NEW".to_string(),
8135 };
8136
8137 assert!(insert_change.range.is_some());
8138 assert_eq!(insert_change.text, "NEW");
8139 let range = insert_change.range.unwrap();
8140 assert_eq!(
8141 range.start, range.end,
8142 "Insert should have zero-width range"
8143 );
8144
8145 let delete_change = TextDocumentContentChangeEvent {
8147 range: Some(LspRange::new(Position::new(0, 2), Position::new(0, 7))),
8148 range_length: None,
8149 text: String::new(),
8150 };
8151
8152 assert!(delete_change.range.is_some());
8153 assert_eq!(delete_change.text, "");
8154 let range = delete_change.range.unwrap();
8155 assert_ne!(range.start, range.end, "Delete should have non-zero range");
8156 assert_eq!(range.start.line, 0);
8157 assert_eq!(range.start.character, 2);
8158 assert_eq!(range.end.line, 0);
8159 assert_eq!(range.end.character, 7);
8160 }
8161
8162 #[test]
8163 fn test_goto_matching_bracket_forward() {
8164 let config = Config::default();
8165 let (dir_context, _temp) = test_dir_context();
8166 let mut editor = Editor::new(
8167 config,
8168 80,
8169 24,
8170 dir_context,
8171 crate::view::color_support::ColorCapability::TrueColor,
8172 test_filesystem(),
8173 )
8174 .unwrap();
8175
8176 let cursor_id = editor.active_cursors().primary_id();
8178 editor.apply_event_to_active_buffer(&Event::Insert {
8179 position: 0,
8180 text: "fn main() { let x = (1 + 2); }".to_string(),
8181 cursor_id,
8182 });
8183
8184 editor.apply_event_to_active_buffer(&Event::MoveCursor {
8186 cursor_id,
8187 old_position: 31,
8188 new_position: 10,
8189 old_anchor: None,
8190 new_anchor: None,
8191 old_sticky_column: 0,
8192 new_sticky_column: 0,
8193 });
8194
8195 assert_eq!(editor.active_cursors().primary().position, 10);
8196
8197 editor.goto_matching_bracket();
8199
8200 assert_eq!(editor.active_cursors().primary().position, 29);
8205 }
8206
8207 #[test]
8208 fn test_goto_matching_bracket_backward() {
8209 let config = Config::default();
8210 let (dir_context, _temp) = test_dir_context();
8211 let mut editor = Editor::new(
8212 config,
8213 80,
8214 24,
8215 dir_context,
8216 crate::view::color_support::ColorCapability::TrueColor,
8217 test_filesystem(),
8218 )
8219 .unwrap();
8220
8221 let cursor_id = editor.active_cursors().primary_id();
8223 editor.apply_event_to_active_buffer(&Event::Insert {
8224 position: 0,
8225 text: "fn main() { let x = (1 + 2); }".to_string(),
8226 cursor_id,
8227 });
8228
8229 editor.apply_event_to_active_buffer(&Event::MoveCursor {
8231 cursor_id,
8232 old_position: 31,
8233 new_position: 26,
8234 old_anchor: None,
8235 new_anchor: None,
8236 old_sticky_column: 0,
8237 new_sticky_column: 0,
8238 });
8239
8240 editor.goto_matching_bracket();
8242
8243 assert_eq!(editor.active_cursors().primary().position, 20);
8245 }
8246
8247 #[test]
8248 fn test_goto_matching_bracket_nested() {
8249 let config = Config::default();
8250 let (dir_context, _temp) = test_dir_context();
8251 let mut editor = Editor::new(
8252 config,
8253 80,
8254 24,
8255 dir_context,
8256 crate::view::color_support::ColorCapability::TrueColor,
8257 test_filesystem(),
8258 )
8259 .unwrap();
8260
8261 let cursor_id = editor.active_cursors().primary_id();
8263 editor.apply_event_to_active_buffer(&Event::Insert {
8264 position: 0,
8265 text: "{a{b{c}d}e}".to_string(),
8266 cursor_id,
8267 });
8268
8269 editor.apply_event_to_active_buffer(&Event::MoveCursor {
8271 cursor_id,
8272 old_position: 11,
8273 new_position: 0,
8274 old_anchor: None,
8275 new_anchor: None,
8276 old_sticky_column: 0,
8277 new_sticky_column: 0,
8278 });
8279
8280 editor.goto_matching_bracket();
8282
8283 assert_eq!(editor.active_cursors().primary().position, 10);
8285 }
8286
8287 #[test]
8288 fn test_search_case_sensitive() {
8289 let config = Config::default();
8290 let (dir_context, _temp) = test_dir_context();
8291 let mut editor = Editor::new(
8292 config,
8293 80,
8294 24,
8295 dir_context,
8296 crate::view::color_support::ColorCapability::TrueColor,
8297 test_filesystem(),
8298 )
8299 .unwrap();
8300
8301 let cursor_id = editor.active_cursors().primary_id();
8303 editor.apply_event_to_active_buffer(&Event::Insert {
8304 position: 0,
8305 text: "Hello hello HELLO".to_string(),
8306 cursor_id,
8307 });
8308
8309 editor.search_case_sensitive = false;
8311 editor.perform_search("hello");
8312
8313 let search_state = editor.search_state.as_ref().unwrap();
8314 assert_eq!(
8315 search_state.matches.len(),
8316 3,
8317 "Should find all 3 matches case-insensitively"
8318 );
8319
8320 editor.search_case_sensitive = true;
8322 editor.perform_search("hello");
8323
8324 let search_state = editor.search_state.as_ref().unwrap();
8325 assert_eq!(
8326 search_state.matches.len(),
8327 1,
8328 "Should find only 1 exact match"
8329 );
8330 assert_eq!(
8331 search_state.matches[0], 6,
8332 "Should find 'hello' at position 6"
8333 );
8334 }
8335
8336 #[test]
8337 fn test_search_whole_word() {
8338 let config = Config::default();
8339 let (dir_context, _temp) = test_dir_context();
8340 let mut editor = Editor::new(
8341 config,
8342 80,
8343 24,
8344 dir_context,
8345 crate::view::color_support::ColorCapability::TrueColor,
8346 test_filesystem(),
8347 )
8348 .unwrap();
8349
8350 let cursor_id = editor.active_cursors().primary_id();
8352 editor.apply_event_to_active_buffer(&Event::Insert {
8353 position: 0,
8354 text: "test testing tested attest test".to_string(),
8355 cursor_id,
8356 });
8357
8358 editor.search_whole_word = false;
8360 editor.search_case_sensitive = true;
8361 editor.perform_search("test");
8362
8363 let search_state = editor.search_state.as_ref().unwrap();
8364 assert_eq!(
8365 search_state.matches.len(),
8366 5,
8367 "Should find 'test' in all occurrences"
8368 );
8369
8370 editor.search_whole_word = true;
8372 editor.perform_search("test");
8373
8374 let search_state = editor.search_state.as_ref().unwrap();
8375 assert_eq!(
8376 search_state.matches.len(),
8377 2,
8378 "Should find only whole word 'test'"
8379 );
8380 assert_eq!(search_state.matches[0], 0, "First match at position 0");
8381 assert_eq!(search_state.matches[1], 27, "Second match at position 27");
8382 }
8383
8384 #[test]
8385 fn test_search_scan_completes_when_capped() {
8386 let config = Config::default();
8392 let (dir_context, _temp) = test_dir_context();
8393 let mut editor = Editor::new(
8394 config,
8395 80,
8396 24,
8397 dir_context,
8398 crate::view::color_support::ColorCapability::TrueColor,
8399 test_filesystem(),
8400 )
8401 .unwrap();
8402
8403 let buffer_id = editor.active_buffer();
8406 let regex = regex::bytes::Regex::new("test").unwrap();
8407 let fake_chunks = vec![
8408 crate::model::buffer::LineScanChunk {
8409 leaf_index: 0,
8410 byte_len: 100,
8411 already_known: true,
8412 },
8413 crate::model::buffer::LineScanChunk {
8414 leaf_index: 1,
8415 byte_len: 100,
8416 already_known: true,
8417 },
8418 ];
8419
8420 editor.search_scan_state = Some(SearchScanState {
8421 buffer_id,
8422 leaves: Vec::new(),
8423 scan: crate::model::buffer::ChunkedSearchState {
8424 chunks: fake_chunks,
8425 next_chunk: 1, next_doc_offset: 100,
8427 total_bytes: 200,
8428 scanned_bytes: 100,
8429 regex,
8430 matches: vec![
8431 crate::model::buffer::SearchMatch {
8432 byte_offset: 10,
8433 length: 4,
8434 line: 1,
8435 column: 11,
8436 context: String::new(),
8437 },
8438 crate::model::buffer::SearchMatch {
8439 byte_offset: 50,
8440 length: 4,
8441 line: 1,
8442 column: 51,
8443 context: String::new(),
8444 },
8445 ],
8446 overlap_tail: Vec::new(),
8447 overlap_doc_offset: 0,
8448 max_matches: 10_000,
8449 capped: true, query_len: 4,
8451 running_line: 1,
8452 },
8453 query: "test".to_string(),
8454 search_range: None,
8455 case_sensitive: false,
8456 whole_word: false,
8457 use_regex: false,
8458 });
8459
8460 let result = editor.process_search_scan();
8462 assert!(
8463 result,
8464 "process_search_scan should return true (needs render)"
8465 );
8466
8467 assert!(
8469 editor.search_scan_state.is_none(),
8470 "search_scan_state should be None after capped scan completes"
8471 );
8472
8473 let search_state = editor
8475 .search_state
8476 .as_ref()
8477 .expect("search_state should be set after scan finishes");
8478 assert_eq!(search_state.matches.len(), 2, "Should have 2 matches");
8479 assert_eq!(search_state.query, "test");
8480 assert!(
8481 search_state.capped,
8482 "search_state should be marked as capped"
8483 );
8484 }
8485
8486 #[test]
8487 fn test_bookmarks() {
8488 let config = Config::default();
8489 let (dir_context, _temp) = test_dir_context();
8490 let mut editor = Editor::new(
8491 config,
8492 80,
8493 24,
8494 dir_context,
8495 crate::view::color_support::ColorCapability::TrueColor,
8496 test_filesystem(),
8497 )
8498 .unwrap();
8499
8500 let cursor_id = editor.active_cursors().primary_id();
8502 editor.apply_event_to_active_buffer(&Event::Insert {
8503 position: 0,
8504 text: "Line 1\nLine 2\nLine 3".to_string(),
8505 cursor_id,
8506 });
8507
8508 editor.apply_event_to_active_buffer(&Event::MoveCursor {
8510 cursor_id,
8511 old_position: 21,
8512 new_position: 7,
8513 old_anchor: None,
8514 new_anchor: None,
8515 old_sticky_column: 0,
8516 new_sticky_column: 0,
8517 });
8518
8519 editor.set_bookmark('1');
8521 assert!(editor.bookmarks.contains_key(&'1'));
8522 assert_eq!(editor.bookmarks.get(&'1').unwrap().position, 7);
8523
8524 editor.apply_event_to_active_buffer(&Event::MoveCursor {
8526 cursor_id,
8527 old_position: 7,
8528 new_position: 14,
8529 old_anchor: None,
8530 new_anchor: None,
8531 old_sticky_column: 0,
8532 new_sticky_column: 0,
8533 });
8534
8535 editor.jump_to_bookmark('1');
8537 assert_eq!(editor.active_cursors().primary().position, 7);
8538
8539 editor.clear_bookmark('1');
8541 assert!(!editor.bookmarks.contains_key(&'1'));
8542 }
8543
8544 #[test]
8545 fn test_action_enum_new_variants() {
8546 use serde_json::json;
8548
8549 let args = HashMap::new();
8550 assert_eq!(
8551 Action::from_str("smart_home", &args),
8552 Some(Action::SmartHome)
8553 );
8554 assert_eq!(
8555 Action::from_str("dedent_selection", &args),
8556 Some(Action::DedentSelection)
8557 );
8558 assert_eq!(
8559 Action::from_str("toggle_comment", &args),
8560 Some(Action::ToggleComment)
8561 );
8562 assert_eq!(
8563 Action::from_str("goto_matching_bracket", &args),
8564 Some(Action::GoToMatchingBracket)
8565 );
8566 assert_eq!(
8567 Action::from_str("list_bookmarks", &args),
8568 Some(Action::ListBookmarks)
8569 );
8570 assert_eq!(
8571 Action::from_str("toggle_search_case_sensitive", &args),
8572 Some(Action::ToggleSearchCaseSensitive)
8573 );
8574 assert_eq!(
8575 Action::from_str("toggle_search_whole_word", &args),
8576 Some(Action::ToggleSearchWholeWord)
8577 );
8578
8579 let mut args_with_char = HashMap::new();
8581 args_with_char.insert("char".to_string(), json!("5"));
8582 assert_eq!(
8583 Action::from_str("set_bookmark", &args_with_char),
8584 Some(Action::SetBookmark('5'))
8585 );
8586 assert_eq!(
8587 Action::from_str("jump_to_bookmark", &args_with_char),
8588 Some(Action::JumpToBookmark('5'))
8589 );
8590 assert_eq!(
8591 Action::from_str("clear_bookmark", &args_with_char),
8592 Some(Action::ClearBookmark('5'))
8593 );
8594 }
8595
8596 #[test]
8597 fn test_keybinding_new_defaults() {
8598 use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
8599
8600 let mut config = Config::default();
8604 config.active_keybinding_map = crate::config::KeybindingMapName("default".to_string());
8605 let resolver = KeybindingResolver::new(&config);
8606
8607 let event = KeyEvent {
8609 code: KeyCode::Char('/'),
8610 modifiers: KeyModifiers::CONTROL,
8611 kind: KeyEventKind::Press,
8612 state: KeyEventState::NONE,
8613 };
8614 let action = resolver.resolve(&event, KeyContext::Normal);
8615 assert_eq!(action, Action::ToggleComment);
8616
8617 let event = KeyEvent {
8619 code: KeyCode::Char(']'),
8620 modifiers: KeyModifiers::CONTROL,
8621 kind: KeyEventKind::Press,
8622 state: KeyEventState::NONE,
8623 };
8624 let action = resolver.resolve(&event, KeyContext::Normal);
8625 assert_eq!(action, Action::GoToMatchingBracket);
8626
8627 let event = KeyEvent {
8629 code: KeyCode::Tab,
8630 modifiers: KeyModifiers::SHIFT,
8631 kind: KeyEventKind::Press,
8632 state: KeyEventState::NONE,
8633 };
8634 let action = resolver.resolve(&event, KeyContext::Normal);
8635 assert_eq!(action, Action::DedentSelection);
8636
8637 let event = KeyEvent {
8639 code: KeyCode::Char('g'),
8640 modifiers: KeyModifiers::CONTROL,
8641 kind: KeyEventKind::Press,
8642 state: KeyEventState::NONE,
8643 };
8644 let action = resolver.resolve(&event, KeyContext::Normal);
8645 assert_eq!(action, Action::GotoLine);
8646
8647 let event = KeyEvent {
8649 code: KeyCode::Char('5'),
8650 modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
8651 kind: KeyEventKind::Press,
8652 state: KeyEventState::NONE,
8653 };
8654 let action = resolver.resolve(&event, KeyContext::Normal);
8655 assert_eq!(action, Action::SetBookmark('5'));
8656
8657 let event = KeyEvent {
8658 code: KeyCode::Char('5'),
8659 modifiers: KeyModifiers::ALT,
8660 kind: KeyEventKind::Press,
8661 state: KeyEventState::NONE,
8662 };
8663 let action = resolver.resolve(&event, KeyContext::Normal);
8664 assert_eq!(action, Action::JumpToBookmark('5'));
8665 }
8666
8667 #[test]
8679 fn test_lsp_rename_didchange_positions_bug() {
8680 use crate::model::buffer::Buffer;
8681
8682 let config = Config::default();
8683 let (dir_context, _temp) = test_dir_context();
8684 let mut editor = Editor::new(
8685 config,
8686 80,
8687 24,
8688 dir_context,
8689 crate::view::color_support::ColorCapability::TrueColor,
8690 test_filesystem(),
8691 )
8692 .unwrap();
8693
8694 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
8698 editor.active_state_mut().buffer =
8699 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
8700
8701 let cursor_id = editor.active_cursors().primary_id();
8706
8707 let batch = Event::Batch {
8708 events: vec![
8709 Event::Delete {
8711 range: 23..26, deleted_text: "val".to_string(),
8713 cursor_id,
8714 },
8715 Event::Insert {
8716 position: 23,
8717 text: "value".to_string(),
8718 cursor_id,
8719 },
8720 Event::Delete {
8722 range: 7..10, deleted_text: "val".to_string(),
8724 cursor_id,
8725 },
8726 Event::Insert {
8727 position: 7,
8728 text: "value".to_string(),
8729 cursor_id,
8730 },
8731 ],
8732 description: "LSP Rename".to_string(),
8733 };
8734
8735 let lsp_changes_before = editor.collect_lsp_changes(&batch);
8737
8738 editor.apply_event_to_active_buffer(&batch);
8740
8741 let lsp_changes_after = editor.collect_lsp_changes(&batch);
8744
8745 let final_content = editor.active_state().buffer.to_string().unwrap();
8747 assert_eq!(
8748 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
8749 "Buffer should have 'value' in both places"
8750 );
8751
8752 assert_eq!(lsp_changes_before.len(), 4, "Should have 4 changes");
8758
8759 let first_delete = &lsp_changes_before[0];
8760 let first_del_range = first_delete.range.unwrap();
8761 assert_eq!(
8762 first_del_range.start.line, 1,
8763 "First delete should be on line 1 (BEFORE)"
8764 );
8765 assert_eq!(
8766 first_del_range.start.character, 4,
8767 "First delete start should be at char 4 (BEFORE)"
8768 );
8769
8770 assert_eq!(lsp_changes_after.len(), 4, "Should have 4 changes");
8776
8777 let first_delete_after = &lsp_changes_after[0];
8778 let first_del_range_after = first_delete_after.range.unwrap();
8779
8780 eprintln!("BEFORE modification:");
8783 eprintln!(
8784 " Delete at line {}, char {}-{}",
8785 first_del_range.start.line,
8786 first_del_range.start.character,
8787 first_del_range.end.character
8788 );
8789 eprintln!("AFTER modification:");
8790 eprintln!(
8791 " Delete at line {}, char {}-{}",
8792 first_del_range_after.start.line,
8793 first_del_range_after.start.character,
8794 first_del_range_after.end.character
8795 );
8796
8797 assert_ne!(
8815 first_del_range_after.end.character, first_del_range.end.character,
8816 "BUG CONFIRMED: LSP positions are different when calculated after buffer modification!"
8817 );
8818
8819 eprintln!("\n=== BUG DEMONSTRATED ===");
8820 eprintln!("When collect_lsp_changes() is called AFTER buffer modification,");
8821 eprintln!("the positions are WRONG because they're calculated from the");
8822 eprintln!("modified buffer, not the original buffer.");
8823 eprintln!("This causes the second rename to fail with 'content modified' error.");
8824 eprintln!("========================\n");
8825 }
8826
8827 #[test]
8828 fn test_lsp_rename_preserves_cursor_position() {
8829 use crate::model::buffer::Buffer;
8830
8831 let config = Config::default();
8832 let (dir_context, _temp) = test_dir_context();
8833 let mut editor = Editor::new(
8834 config,
8835 80,
8836 24,
8837 dir_context,
8838 crate::view::color_support::ColorCapability::TrueColor,
8839 test_filesystem(),
8840 )
8841 .unwrap();
8842
8843 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
8847 editor.active_state_mut().buffer =
8848 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
8849
8850 let original_cursor_pos = 23;
8852 editor.active_cursors_mut().primary_mut().position = original_cursor_pos;
8853
8854 let buffer_text = editor.active_state().buffer.to_string().unwrap();
8856 let text_at_cursor = buffer_text[original_cursor_pos..original_cursor_pos + 3].to_string();
8857 assert_eq!(text_at_cursor, "val", "Cursor should be at 'val'");
8858
8859 let cursor_id = editor.active_cursors().primary_id();
8862 let buffer_id = editor.active_buffer();
8863
8864 let events = vec![
8865 Event::Delete {
8867 range: 23..26, deleted_text: "val".to_string(),
8869 cursor_id,
8870 },
8871 Event::Insert {
8872 position: 23,
8873 text: "value".to_string(),
8874 cursor_id,
8875 },
8876 Event::Delete {
8878 range: 7..10, deleted_text: "val".to_string(),
8880 cursor_id,
8881 },
8882 Event::Insert {
8883 position: 7,
8884 text: "value".to_string(),
8885 cursor_id,
8886 },
8887 ];
8888
8889 editor
8891 .apply_events_to_buffer_as_bulk_edit(buffer_id, events, "LSP Rename".to_string())
8892 .unwrap();
8893
8894 let final_content = editor.active_state().buffer.to_string().unwrap();
8896 assert_eq!(
8897 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
8898 "Buffer should have 'value' in both places"
8899 );
8900
8901 let final_cursor_pos = editor.active_cursors().primary().position;
8909 let expected_cursor_pos = 25; assert_eq!(
8912 final_cursor_pos, expected_cursor_pos,
8913 "Cursor should be at position {} (start of 'value' on line 1), but was at {}. \
8914 Original pos: {}, expected adjustment: +2 for first rename",
8915 expected_cursor_pos, final_cursor_pos, original_cursor_pos
8916 );
8917
8918 let text_at_new_cursor = &final_content[final_cursor_pos..final_cursor_pos + 5];
8920 assert_eq!(
8921 text_at_new_cursor, "value",
8922 "Cursor should be at the start of 'value' after rename"
8923 );
8924 }
8925
8926 #[test]
8927 fn test_lsp_rename_twice_consecutive() {
8928 use crate::model::buffer::Buffer;
8931
8932 let config = Config::default();
8933 let (dir_context, _temp) = test_dir_context();
8934 let mut editor = Editor::new(
8935 config,
8936 80,
8937 24,
8938 dir_context,
8939 crate::view::color_support::ColorCapability::TrueColor,
8940 test_filesystem(),
8941 )
8942 .unwrap();
8943
8944 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
8946 editor.active_state_mut().buffer =
8947 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
8948
8949 let cursor_id = editor.active_cursors().primary_id();
8950 let buffer_id = editor.active_buffer();
8951
8952 let events1 = vec![
8955 Event::Delete {
8957 range: 23..26,
8958 deleted_text: "val".to_string(),
8959 cursor_id,
8960 },
8961 Event::Insert {
8962 position: 23,
8963 text: "value".to_string(),
8964 cursor_id,
8965 },
8966 Event::Delete {
8968 range: 7..10,
8969 deleted_text: "val".to_string(),
8970 cursor_id,
8971 },
8972 Event::Insert {
8973 position: 7,
8974 text: "value".to_string(),
8975 cursor_id,
8976 },
8977 ];
8978
8979 let batch1 = Event::Batch {
8981 events: events1.clone(),
8982 description: "LSP Rename 1".to_string(),
8983 };
8984
8985 let lsp_changes1 = editor.collect_lsp_changes(&batch1);
8987
8988 assert_eq!(
8990 lsp_changes1.len(),
8991 4,
8992 "First rename should have 4 LSP changes"
8993 );
8994
8995 let first_del = &lsp_changes1[0];
8997 let first_del_range = first_del.range.unwrap();
8998 assert_eq!(first_del_range.start.line, 1, "First delete line");
8999 assert_eq!(
9000 first_del_range.start.character, 4,
9001 "First delete start char"
9002 );
9003 assert_eq!(first_del_range.end.character, 7, "First delete end char");
9004
9005 editor
9007 .apply_events_to_buffer_as_bulk_edit(buffer_id, events1, "LSP Rename 1".to_string())
9008 .unwrap();
9009
9010 let after_first = editor.active_state().buffer.to_string().unwrap();
9012 assert_eq!(
9013 after_first, "fn foo(value: i32) {\n value + 1\n}\n",
9014 "After first rename"
9015 );
9016
9017 let events2 = vec![
9027 Event::Delete {
9029 range: 25..30,
9030 deleted_text: "value".to_string(),
9031 cursor_id,
9032 },
9033 Event::Insert {
9034 position: 25,
9035 text: "x".to_string(),
9036 cursor_id,
9037 },
9038 Event::Delete {
9040 range: 7..12,
9041 deleted_text: "value".to_string(),
9042 cursor_id,
9043 },
9044 Event::Insert {
9045 position: 7,
9046 text: "x".to_string(),
9047 cursor_id,
9048 },
9049 ];
9050
9051 let batch2 = Event::Batch {
9053 events: events2.clone(),
9054 description: "LSP Rename 2".to_string(),
9055 };
9056
9057 let lsp_changes2 = editor.collect_lsp_changes(&batch2);
9059
9060 assert_eq!(
9064 lsp_changes2.len(),
9065 4,
9066 "Second rename should have 4 LSP changes"
9067 );
9068
9069 let second_first_del = &lsp_changes2[0];
9071 let second_first_del_range = second_first_del.range.unwrap();
9072 assert_eq!(
9073 second_first_del_range.start.line, 1,
9074 "Second rename first delete should be on line 1"
9075 );
9076 assert_eq!(
9077 second_first_del_range.start.character, 4,
9078 "Second rename first delete start should be at char 4"
9079 );
9080 assert_eq!(
9081 second_first_del_range.end.character, 9,
9082 "Second rename first delete end should be at char 9 (4 + 5 for 'value')"
9083 );
9084
9085 let second_third_del = &lsp_changes2[2];
9087 let second_third_del_range = second_third_del.range.unwrap();
9088 assert_eq!(
9089 second_third_del_range.start.line, 0,
9090 "Second rename third delete should be on line 0"
9091 );
9092 assert_eq!(
9093 second_third_del_range.start.character, 7,
9094 "Second rename third delete start should be at char 7"
9095 );
9096 assert_eq!(
9097 second_third_del_range.end.character, 12,
9098 "Second rename third delete end should be at char 12 (7 + 5 for 'value')"
9099 );
9100
9101 editor
9103 .apply_events_to_buffer_as_bulk_edit(buffer_id, events2, "LSP Rename 2".to_string())
9104 .unwrap();
9105
9106 let after_second = editor.active_state().buffer.to_string().unwrap();
9108 assert_eq!(
9109 after_second, "fn foo(x: i32) {\n x + 1\n}\n",
9110 "After second rename"
9111 );
9112 }
9113
9114 #[test]
9115 fn test_ensure_active_tab_visible_static_offset() {
9116 let config = Config::default();
9117 let (dir_context, _temp) = test_dir_context();
9118 let mut editor = Editor::new(
9119 config,
9120 80,
9121 24,
9122 dir_context,
9123 crate::view::color_support::ColorCapability::TrueColor,
9124 test_filesystem(),
9125 )
9126 .unwrap();
9127 let split_id = editor.split_manager.active_split();
9128
9129 let buf1 = editor.new_buffer();
9131 editor
9132 .buffers
9133 .get_mut(&buf1)
9134 .unwrap()
9135 .buffer
9136 .rename_file_path(std::path::PathBuf::from("aaa_long_name_01.txt"));
9137 let buf2 = editor.new_buffer();
9138 editor
9139 .buffers
9140 .get_mut(&buf2)
9141 .unwrap()
9142 .buffer
9143 .rename_file_path(std::path::PathBuf::from("bbb_long_name_02.txt"));
9144 let buf3 = editor.new_buffer();
9145 editor
9146 .buffers
9147 .get_mut(&buf3)
9148 .unwrap()
9149 .buffer
9150 .rename_file_path(std::path::PathBuf::from("ccc_long_name_03.txt"));
9151
9152 {
9153 let view_state = editor.split_view_states.get_mut(&split_id).unwrap();
9154 view_state.open_buffers = vec![buf1, buf2, buf3];
9155 view_state.tab_scroll_offset = 50;
9156 }
9157
9158 editor.ensure_active_tab_visible(split_id, buf1, 25);
9162 assert_eq!(
9163 editor
9164 .split_view_states
9165 .get(&split_id)
9166 .unwrap()
9167 .tab_scroll_offset,
9168 0
9169 );
9170
9171 editor.ensure_active_tab_visible(split_id, buf3, 25);
9173 let view_state = editor.split_view_states.get(&split_id).unwrap();
9174 assert!(view_state.tab_scroll_offset > 0);
9175 let total_width: usize = view_state
9176 .open_buffers
9177 .iter()
9178 .enumerate()
9179 .map(|(idx, id)| {
9180 let state = editor.buffers.get(id).unwrap();
9181 let name_len = state
9182 .buffer
9183 .file_path()
9184 .and_then(|p| p.file_name())
9185 .and_then(|n| n.to_str())
9186 .map(|s| s.chars().count())
9187 .unwrap_or(0);
9188 let tab_width = 2 + name_len;
9189 if idx < view_state.open_buffers.len() - 1 {
9190 tab_width + 1 } else {
9192 tab_width
9193 }
9194 })
9195 .sum();
9196 assert!(view_state.tab_scroll_offset <= total_width);
9197 }
9198}