1mod async_messages;
2mod buffer_groups;
3mod buffer_management;
4mod calibration_actions;
5pub mod calibration_wizard;
6mod clipboard;
7mod composite_buffer_actions;
8mod dabbrev_actions;
9pub mod event_debug;
10mod event_debug_actions;
11mod file_explorer;
12pub mod file_open;
13mod file_open_input;
14mod file_operations;
15mod help;
16mod input;
17mod input_dispatch;
18pub mod keybinding_editor;
19mod keybinding_editor_actions;
20mod lsp_actions;
21mod lsp_requests;
22mod menu_actions;
23mod menu_context;
24mod mouse_input;
25mod on_save_actions;
26mod plugin_commands;
27mod popup_actions;
28mod prompt_actions;
29mod recovery_actions;
30mod regex_replace;
31mod render;
32mod settings_actions;
33mod shell_command;
34mod split_actions;
35mod tab_drag;
36mod terminal;
37mod terminal_input;
38mod terminal_mouse;
39mod theme_inspect;
40mod toggle_actions;
41pub mod types;
42mod undo_actions;
43mod view_actions;
44pub mod warning_domains;
45pub mod workspace;
46
47use anyhow::Result as AnyhowResult;
48use rust_i18n::t;
49use std::path::Component;
50
51pub fn editor_tick(
56 editor: &mut Editor,
57 mut clear_terminal: impl FnMut() -> AnyhowResult<()>,
58) -> AnyhowResult<bool> {
59 let mut needs_render = false;
60
61 let async_messages = {
62 let _s = tracing::info_span!("process_async_messages").entered();
63 editor.process_async_messages()
64 };
65 if async_messages {
66 needs_render = true;
67 }
68 let pending_file_opens = {
69 let _s = tracing::info_span!("process_pending_file_opens").entered();
70 editor.process_pending_file_opens()
71 };
72 if pending_file_opens {
73 needs_render = true;
74 }
75 if editor.process_line_scan() {
76 needs_render = true;
77 }
78 let search_scan = {
79 let _s = tracing::info_span!("process_search_scan").entered();
80 editor.process_search_scan()
81 };
82 if search_scan {
83 needs_render = true;
84 }
85 let search_overlay_refresh = {
86 let _s = tracing::info_span!("check_search_overlay_refresh").entered();
87 editor.check_search_overlay_refresh()
88 };
89 if search_overlay_refresh {
90 needs_render = true;
91 }
92 if editor.check_mouse_hover_timer() {
93 needs_render = true;
94 }
95 if editor.check_semantic_highlight_timer() {
96 needs_render = true;
97 }
98 if editor.check_completion_trigger_timer() {
99 needs_render = true;
100 }
101 editor.check_diagnostic_pull_timer();
102 if editor.check_warning_log() {
103 needs_render = true;
104 }
105 if editor.poll_stdin_streaming() {
106 needs_render = true;
107 }
108
109 if let Err(e) = editor.auto_recovery_save_dirty_buffers() {
110 tracing::debug!("Auto-recovery-save error: {}", e);
111 }
112 if let Err(e) = editor.auto_save_persistent_buffers() {
113 tracing::debug!("Auto-save (disk) error: {}", e);
114 }
115
116 if editor.take_full_redraw_request() {
117 clear_terminal()?;
118 needs_render = true;
119 }
120
121 Ok(needs_render)
122}
123
124pub(crate) fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
127 let mut components = Vec::new();
128
129 for component in path.components() {
130 match component {
131 Component::CurDir => {
132 }
134 Component::ParentDir => {
135 if let Some(Component::Normal(_)) = components.last() {
137 components.pop();
138 } else {
139 components.push(component);
141 }
142 }
143 _ => {
144 components.push(component);
145 }
146 }
147 }
148
149 if components.is_empty() {
150 std::path::PathBuf::from(".")
151 } else {
152 components.iter().collect()
153 }
154}
155
156use self::types::{
157 Bookmark, CachedLayout, EventLineInfo, InteractiveReplaceState, LspMessageEntry,
158 LspProgressInfo, MacroRecordingState, MouseState, SearchState, TabContextMenu,
159 DEFAULT_BACKGROUND_FILE,
160};
161use crate::config::Config;
162use crate::config_io::{ConfigLayer, ConfigResolver, DirectoryContext};
163use crate::input::actions::action_to_events as convert_action_to_events;
164use crate::input::buffer_mode::ModeRegistry;
165use crate::input::command_registry::CommandRegistry;
166use crate::input::commands::Suggestion;
167use crate::input::keybindings::{Action, KeyContext, KeybindingResolver};
168use crate::input::position_history::PositionHistory;
169use crate::input::quick_open::{
170 BufferInfo, BufferProvider, CommandProvider, FileProvider, GotoLineProvider, QuickOpenContext,
171 QuickOpenRegistry,
172};
173use crate::model::cursor::Cursors;
174use crate::model::event::{Event, EventLog, LeafId, SplitDirection, SplitId};
175use crate::model::filesystem::FileSystem;
176use crate::services::async_bridge::{AsyncBridge, AsyncMessage};
177use crate::services::fs::FsManager;
178use crate::services::lsp::manager::LspManager;
179use crate::services::plugins::PluginManager;
180use crate::services::recovery::{RecoveryConfig, RecoveryService};
181use crate::services::time_source::{RealTimeSource, SharedTimeSource};
182use crate::state::EditorState;
183use crate::types::{LspLanguageConfig, LspServerConfig, ProcessLimits};
184use crate::view::file_tree::{FileTree, FileTreeView};
185use crate::view::prompt::{Prompt, PromptType};
186use crate::view::scroll_sync::ScrollSyncManager;
187use crate::view::split::{SplitManager, SplitViewState};
188use crate::view::ui::{
189 FileExplorerRenderer, SplitRenderer, StatusBarRenderer, SuggestionsRenderer,
190};
191use crossterm::event::{KeyCode, KeyModifiers};
192#[cfg(feature = "plugins")]
193use fresh_core::api::BufferSavedDiff;
194#[cfg(feature = "plugins")]
195use fresh_core::api::JsCallbackId;
196use fresh_core::api::PluginCommand;
197use lsp_types::{Position, Range as LspRange, TextDocumentContentChangeEvent};
198use ratatui::{
199 layout::{Constraint, Direction, Layout},
200 Frame,
201};
202use std::collections::{HashMap, HashSet};
203use std::ops::Range;
204use std::path::{Path, PathBuf};
205use std::sync::{Arc, RwLock};
206use std::time::Instant;
207
208pub use self::types::{BufferKind, BufferMetadata, HoverTarget};
210pub use self::warning_domains::{
211 GeneralWarningDomain, LspWarningDomain, WarningAction, WarningActionId, WarningDomain,
212 WarningDomainRegistry, WarningLevel, WarningPopupContent,
213};
214pub use crate::model::event::BufferId;
215
216fn uri_to_path(uri: &lsp_types::Uri) -> Result<PathBuf, String> {
218 fresh_core::file_uri::lsp_uri_to_path(uri).ok_or_else(|| "URI is not a file path".to_string())
219}
220
221#[derive(Clone, Debug)]
223pub struct PendingGrammar {
224 pub language: String,
226 pub grammar_path: String,
228 pub extensions: Vec<String>,
230}
231
232#[derive(Clone, Debug)]
234struct SemanticTokenRangeRequest {
235 buffer_id: BufferId,
236 version: u64,
237 range: Range<usize>,
238 start_line: usize,
239 end_line: usize,
240}
241
242#[derive(Clone, Copy, Debug)]
243enum SemanticTokensFullRequestKind {
244 Full,
245 FullDelta,
246}
247
248#[derive(Clone, Debug)]
249struct SemanticTokenFullRequest {
250 buffer_id: BufferId,
251 version: u64,
252 kind: SemanticTokensFullRequestKind,
253}
254
255#[derive(Clone, Debug)]
256struct FoldingRangeRequest {
257 buffer_id: BufferId,
258 version: u64,
259}
260
261#[derive(Debug, Clone)]
267pub struct DabbrevCycleState {
268 pub original_prefix: String,
270 pub word_start: usize,
272 pub candidates: Vec<String>,
274 pub index: usize,
276}
277
278pub struct Editor {
280 buffers: HashMap<BufferId, EditorState>,
282
283 event_logs: HashMap<BufferId, EventLog>,
288
289 next_buffer_id: usize,
291
292 config: Config,
294
295 user_config_raw: serde_json::Value,
297
298 dir_context: DirectoryContext,
300
301 grammar_registry: std::sync::Arc<crate::primitives::grammar::GrammarRegistry>,
303
304 pending_grammars: Vec<PendingGrammar>,
306
307 grammar_reload_pending: bool,
311
312 grammar_build_in_progress: bool,
315
316 needs_full_grammar_build: bool,
320
321 streaming_grep_cancellation: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
323
324 pending_grammar_callbacks: Vec<fresh_core::api::JsCallbackId>,
328
329 theme: crate::view::theme::Theme,
331
332 theme_registry: crate::view::theme::ThemeRegistry,
334
335 theme_cache: Arc<RwLock<HashMap<String, serde_json::Value>>>,
337
338 ansi_background: Option<crate::primitives::ansi_background::AnsiBackground>,
340
341 ansi_background_path: Option<PathBuf>,
343
344 background_fade: f32,
346
347 keybindings: Arc<RwLock<KeybindingResolver>>,
349
350 clipboard: crate::services::clipboard::Clipboard,
352
353 should_quit: bool,
355
356 should_detach: bool,
358
359 session_mode: bool,
361
362 software_cursor_only: bool,
364
365 session_name: Option<String>,
367
368 pending_escape_sequences: Vec<u8>,
371
372 restart_with_dir: Option<PathBuf>,
375
376 status_message: Option<String>,
378
379 plugin_status_message: Option<String>,
381
382 plugin_errors: Vec<String>,
385
386 prompt: Option<Prompt>,
388
389 terminal_width: u16,
391 terminal_height: u16,
392
393 lsp: Option<LspManager>,
395
396 buffer_metadata: HashMap<BufferId, BufferMetadata>,
398
399 mode_registry: ModeRegistry,
401
402 tokio_runtime: Option<tokio::runtime::Runtime>,
404
405 async_bridge: Option<AsyncBridge>,
407
408 split_manager: SplitManager,
410
411 split_view_states: HashMap<LeafId, SplitViewState>,
415
416 previous_viewports: HashMap<LeafId, (usize, u16, u16)>,
420
421 scroll_sync_manager: ScrollSyncManager,
424
425 file_explorer: Option<FileTreeView>,
427
428 fs_manager: Arc<FsManager>,
430
431 filesystem: Arc<dyn FileSystem + Send + Sync>,
433
434 local_filesystem: Arc<dyn FileSystem + Send + Sync>,
437
438 process_spawner: Arc<dyn crate::services::remote::ProcessSpawner>,
440
441 file_explorer_visible: bool,
443
444 file_explorer_sync_in_progress: bool,
447
448 file_explorer_width_percent: f32,
451
452 pending_file_explorer_show_hidden: Option<bool>,
454
455 pending_file_explorer_show_gitignored: Option<bool>,
457
458 file_explorer_decorations: HashMap<String, Vec<crate::view::file_tree::FileExplorerDecoration>>,
460
461 file_explorer_decoration_cache: crate::view::file_tree::FileExplorerDecorationCache,
463
464 menu_bar_visible: bool,
466
467 menu_bar_auto_shown: bool,
470
471 tab_bar_visible: bool,
473
474 status_bar_visible: bool,
476
477 prompt_line_visible: bool,
479
480 mouse_enabled: bool,
482
483 same_buffer_scroll_sync: bool,
485
486 mouse_cursor_position: Option<(u16, u16)>,
490
491 gpm_active: bool,
493
494 key_context: KeyContext,
496
497 menu_state: crate::view::ui::MenuState,
499
500 menus: crate::config::MenuConfig,
502
503 working_dir: PathBuf,
505
506 pub position_history: PositionHistory,
508
509 in_navigation: bool,
511
512 next_lsp_request_id: u64,
514
515 pending_completion_requests: HashSet<u64>,
517
518 completion_items: Option<Vec<lsp_types::CompletionItem>>,
521
522 scheduled_completion_trigger: Option<Instant>,
525
526 completion_service: crate::services::completion::CompletionService,
529
530 dabbrev_state: Option<DabbrevCycleState>,
534
535 pending_goto_definition_request: Option<u64>,
537
538 pending_hover_request: Option<u64>,
540
541 pending_references_request: Option<u64>,
543
544 pending_references_symbol: String,
546
547 pending_signature_help_request: Option<u64>,
549
550 pending_code_actions_requests: HashSet<u64>,
552
553 pending_code_actions_server_names: HashMap<u64, String>,
555
556 pending_code_actions: Option<Vec<(String, lsp_types::CodeActionOrCommand)>>,
560
561 pending_inlay_hints_request: Option<u64>,
563
564 pending_folding_range_requests: HashMap<u64, FoldingRangeRequest>,
566
567 folding_ranges_in_flight: HashMap<BufferId, (u64, u64)>,
569
570 folding_ranges_debounce: HashMap<BufferId, Instant>,
572
573 pending_semantic_token_requests: HashMap<u64, SemanticTokenFullRequest>,
575
576 semantic_tokens_in_flight: HashMap<BufferId, (u64, u64, SemanticTokensFullRequestKind)>,
578
579 pending_semantic_token_range_requests: HashMap<u64, SemanticTokenRangeRequest>,
581
582 semantic_tokens_range_in_flight: HashMap<BufferId, (u64, usize, usize, u64)>,
584
585 semantic_tokens_range_last_request: HashMap<BufferId, (usize, usize, u64, Instant)>,
587
588 semantic_tokens_range_applied: HashMap<BufferId, (usize, usize, u64)>,
590
591 semantic_tokens_full_debounce: HashMap<BufferId, Instant>,
593
594 hover_symbol_range: Option<(usize, usize)>,
597
598 hover_symbol_overlay: Option<crate::view::overlay::OverlayHandle>,
600
601 mouse_hover_screen_position: Option<(u16, u16)>,
604
605 search_state: Option<SearchState>,
607
608 search_namespace: crate::view::overlay::OverlayNamespace,
610
611 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace,
613
614 pending_search_range: Option<Range<usize>>,
616
617 interactive_replace_state: Option<InteractiveReplaceState>,
619
620 lsp_status: String,
622
623 mouse_state: MouseState,
625
626 tab_context_menu: Option<TabContextMenu>,
628
629 theme_info_popup: Option<types::ThemeInfoPopup>,
631
632 pub(crate) cached_layout: CachedLayout,
634
635 command_registry: Arc<RwLock<CommandRegistry>>,
637
638 quick_open_registry: QuickOpenRegistry,
640
641 plugin_manager: PluginManager,
643
644 plugin_dev_workspaces:
648 HashMap<BufferId, crate::services::plugins::plugin_dev_workspace::PluginDevWorkspace>,
649
650 seen_byte_ranges: HashMap<BufferId, std::collections::HashSet<(usize, usize)>>,
654
655 panel_ids: HashMap<String, BufferId>,
658
659 buffer_groups: HashMap<types::BufferGroupId, types::BufferGroup>,
661 buffer_to_group: HashMap<BufferId, types::BufferGroupId>,
663 next_buffer_group_id: usize,
665
666 pub(crate) grouped_subtrees:
674 HashMap<crate::model::event::LeafId, crate::view::split::SplitNode>,
675
676 background_process_handles: HashMap<u64, tokio::task::AbortHandle>,
679
680 prompt_histories: HashMap<String, crate::input::input_history::InputHistory>,
683
684 pending_async_prompt_callback: Option<fresh_core::api::JsCallbackId>,
688
689 lsp_progress: std::collections::HashMap<String, LspProgressInfo>,
691
692 lsp_server_statuses:
694 std::collections::HashMap<(String, String), crate::services::async_bridge::LspServerStatus>,
695
696 lsp_window_messages: Vec<LspMessageEntry>,
698
699 lsp_log_messages: Vec<LspMessageEntry>,
701
702 diagnostic_result_ids: HashMap<String, String>,
705
706 scheduled_diagnostic_pull: Option<(BufferId, Instant)>,
709
710 stored_push_diagnostics: HashMap<String, HashMap<String, Vec<lsp_types::Diagnostic>>>,
713
714 stored_pull_diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
716
717 stored_diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
719
720 stored_folding_ranges: HashMap<String, Vec<lsp_types::FoldingRange>>,
723
724 event_broadcaster: crate::model::control_event::EventBroadcaster,
726
727 bookmarks: HashMap<char, Bookmark>,
729
730 search_case_sensitive: bool,
732 search_whole_word: bool,
733 search_use_regex: bool,
734 search_confirm_each: bool,
736
737 macros: HashMap<char, Vec<Action>>,
739
740 macro_recording: Option<MacroRecordingState>,
742
743 last_macro_register: Option<char>,
745
746 macro_playing: bool,
748
749 #[cfg(feature = "plugins")]
751 pending_plugin_actions: Vec<(
752 String,
753 crate::services::plugins::thread::oneshot::Receiver<anyhow::Result<()>>,
754 )>,
755
756 #[cfg(feature = "plugins")]
758 plugin_render_requested: bool,
759
760 chord_state: Vec<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)>,
763
764 pending_lsp_confirmation: Option<String>,
767
768 pending_lsp_status_popup: Option<Vec<(String, String)>>,
772
773 pending_close_buffer: Option<BufferId>,
776
777 auto_revert_enabled: bool,
779
780 last_auto_revert_poll: std::time::Instant,
782
783 last_file_tree_poll: std::time::Instant,
785
786 git_index_resolved: bool,
788
789 file_mod_times: HashMap<PathBuf, std::time::SystemTime>,
792
793 dir_mod_times: HashMap<PathBuf, std::time::SystemTime>,
796
797 #[allow(clippy::type_complexity)]
801 pending_file_poll_rx:
802 Option<std::sync::mpsc::Receiver<Vec<(PathBuf, Option<std::time::SystemTime>)>>>,
803
804 #[allow(clippy::type_complexity)]
807 pending_dir_poll_rx: Option<
808 std::sync::mpsc::Receiver<(
809 Vec<(
810 crate::view::file_tree::NodeId,
811 PathBuf,
812 Option<std::time::SystemTime>,
813 )>,
814 Option<(PathBuf, std::time::SystemTime)>,
815 )>,
816 >,
817
818 file_rapid_change_counts: HashMap<PathBuf, (std::time::Instant, u32)>,
821
822 file_open_state: Option<file_open::FileOpenState>,
824
825 file_browser_layout: Option<crate::view::ui::FileBrowserLayout>,
827
828 recovery_service: RecoveryService,
830
831 full_redraw_requested: bool,
833
834 time_source: SharedTimeSource,
836
837 last_auto_recovery_save: std::time::Instant,
839
840 last_persistent_auto_save: std::time::Instant,
842
843 active_custom_contexts: HashSet<String>,
846
847 plugin_global_state: HashMap<String, HashMap<String, serde_json::Value>>,
850
851 editor_mode: Option<String>,
854
855 warning_log: Option<(std::sync::mpsc::Receiver<()>, PathBuf)>,
857
858 status_log_path: Option<PathBuf>,
860
861 warning_domains: WarningDomainRegistry,
864
865 update_checker: Option<crate::services::release_checker::PeriodicUpdateChecker>,
867
868 terminal_manager: crate::services::terminal::TerminalManager,
870
871 terminal_buffers: HashMap<BufferId, crate::services::terminal::TerminalId>,
873
874 terminal_backing_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
876
877 terminal_log_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
879
880 terminal_mode: bool,
882
883 keyboard_capture: bool,
887
888 terminal_mode_resume: std::collections::HashSet<BufferId>,
892
893 previous_click_time: Option<std::time::Instant>,
895
896 previous_click_position: Option<(u16, u16)>,
899
900 click_count: u8,
902
903 pub(crate) settings_state: Option<crate::view::settings::SettingsState>,
905
906 pub(crate) calibration_wizard: Option<calibration_wizard::CalibrationWizard>,
908
909 pub(crate) event_debug: Option<event_debug::EventDebug>,
911
912 pub(crate) keybinding_editor: Option<keybinding_editor::KeybindingEditor>,
914
915 pub(crate) key_translator: crate::input::key_translator::KeyTranslator,
917
918 color_capability: crate::view::color_support::ColorCapability,
920
921 review_hunks: Vec<fresh_core::api::ReviewHunk>,
923
924 active_action_popup: Option<(String, Vec<(String, String)>)>,
927
928 composite_buffers: HashMap<BufferId, crate::model::composite_buffer::CompositeBuffer>,
931
932 composite_view_states:
935 HashMap<(LeafId, BufferId), crate::view::composite_view::CompositeViewState>,
936
937 pending_file_opens: Vec<PendingFileOpen>,
941
942 pending_hot_exit_recovery: bool,
944
945 wait_tracking: HashMap<BufferId, (u64, bool)>,
947 completed_waits: Vec<u64>,
949
950 stdin_streaming: Option<StdinStreamingState>,
952
953 line_scan_state: Option<LineScanState>,
955
956 search_scan_state: Option<SearchScanState>,
958
959 search_overlay_top_byte: Option<usize>,
962}
963
964#[derive(Debug, Clone)]
966pub struct PendingFileOpen {
967 pub path: PathBuf,
969 pub line: Option<usize>,
971 pub column: Option<usize>,
973 pub end_line: Option<usize>,
975 pub end_column: Option<usize>,
977 pub message: Option<String>,
979 pub wait_id: Option<u64>,
981}
982
983#[allow(dead_code)] struct SearchScanState {
989 buffer_id: BufferId,
990 leaves: Vec<crate::model::piece_tree::LeafData>,
992 scan: crate::model::buffer::ChunkedSearchState,
994 query: String,
996 search_range: Option<std::ops::Range<usize>>,
998 case_sensitive: bool,
1000 whole_word: bool,
1001 use_regex: bool,
1002}
1003
1004struct LineScanState {
1006 buffer_id: BufferId,
1007 leaves: Vec<crate::model::piece_tree::LeafData>,
1009 chunks: Vec<crate::model::buffer::LineScanChunk>,
1011 next_chunk: usize,
1012 total_bytes: usize,
1013 scanned_bytes: usize,
1014 updates: Vec<(usize, usize)>,
1016 open_goto_line_on_complete: bool,
1019}
1020
1021pub struct StdinStreamingState {
1023 pub temp_path: PathBuf,
1025 pub buffer_id: BufferId,
1027 pub last_known_size: usize,
1029 pub complete: bool,
1031 pub thread_handle: Option<std::thread::JoinHandle<anyhow::Result<()>>>,
1033}
1034
1035impl Editor {
1036 pub fn new(
1039 config: Config,
1040 width: u16,
1041 height: u16,
1042 dir_context: DirectoryContext,
1043 color_capability: crate::view::color_support::ColorCapability,
1044 filesystem: Arc<dyn FileSystem + Send + Sync>,
1045 ) -> AnyhowResult<Self> {
1046 Self::with_working_dir(
1047 config,
1048 width,
1049 height,
1050 None,
1051 dir_context,
1052 true,
1053 color_capability,
1054 filesystem,
1055 )
1056 }
1057
1058 #[allow(clippy::too_many_arguments)]
1061 pub fn with_working_dir(
1062 config: Config,
1063 width: u16,
1064 height: u16,
1065 working_dir: Option<PathBuf>,
1066 dir_context: DirectoryContext,
1067 plugins_enabled: bool,
1068 color_capability: crate::view::color_support::ColorCapability,
1069 filesystem: Arc<dyn FileSystem + Send + Sync>,
1070 ) -> AnyhowResult<Self> {
1071 tracing::info!("Building default grammar registry...");
1072 let start = std::time::Instant::now();
1073 let grammar_registry = crate::primitives::grammar::GrammarRegistry::defaults_only();
1074 tracing::info!("Default grammar registry built in {:?}", start.elapsed());
1075 Self::with_options(
1079 config,
1080 width,
1081 height,
1082 working_dir,
1083 filesystem,
1084 plugins_enabled,
1085 dir_context,
1086 None,
1087 color_capability,
1088 grammar_registry,
1089 )
1090 }
1091
1092 #[allow(clippy::too_many_arguments)]
1097 pub fn for_test(
1098 config: Config,
1099 width: u16,
1100 height: u16,
1101 working_dir: Option<PathBuf>,
1102 dir_context: DirectoryContext,
1103 color_capability: crate::view::color_support::ColorCapability,
1104 filesystem: Arc<dyn FileSystem + Send + Sync>,
1105 time_source: Option<SharedTimeSource>,
1106 grammar_registry: Option<Arc<crate::primitives::grammar::GrammarRegistry>>,
1107 ) -> AnyhowResult<Self> {
1108 let grammar_registry =
1109 grammar_registry.unwrap_or_else(crate::primitives::grammar::GrammarRegistry::empty);
1110 let mut editor = Self::with_options(
1111 config,
1112 width,
1113 height,
1114 working_dir,
1115 filesystem,
1116 true,
1117 dir_context,
1118 time_source,
1119 color_capability,
1120 grammar_registry,
1121 )?;
1122 editor.needs_full_grammar_build = false;
1125 Ok(editor)
1126 }
1127
1128 #[allow(clippy::too_many_arguments)]
1132 fn with_options(
1133 mut config: Config,
1134 width: u16,
1135 height: u16,
1136 working_dir: Option<PathBuf>,
1137 filesystem: Arc<dyn FileSystem + Send + Sync>,
1138 enable_plugins: bool,
1139 dir_context: DirectoryContext,
1140 time_source: Option<SharedTimeSource>,
1141 color_capability: crate::view::color_support::ColorCapability,
1142 grammar_registry: Arc<crate::primitives::grammar::GrammarRegistry>,
1143 ) -> AnyhowResult<Self> {
1144 let time_source = time_source.unwrap_or_else(RealTimeSource::shared);
1146 tracing::info!("Editor::new called with width={}, height={}", width, height);
1147
1148 let working_dir = working_dir
1150 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
1151
1152 let working_dir = working_dir.canonicalize().unwrap_or(working_dir);
1155
1156 tracing::info!("Loading themes...");
1158 let theme_loader = crate::view::theme::ThemeLoader::new(dir_context.themes_dir());
1159 let scan_result =
1163 crate::services::packages::scan_installed_packages(&dir_context.config_dir);
1164
1165 for (lang_id, lang_config) in &scan_result.language_configs {
1167 config
1168 .languages
1169 .entry(lang_id.clone())
1170 .or_insert_with(|| lang_config.clone());
1171 }
1172
1173 for (lang_id, lsp_config) in &scan_result.lsp_configs {
1175 config
1176 .lsp
1177 .entry(lang_id.clone())
1178 .or_insert_with(|| LspLanguageConfig::Multi(vec![lsp_config.clone()]));
1179 }
1180
1181 let theme_registry = theme_loader.load_all(&scan_result.bundle_theme_dirs);
1182 tracing::info!("Themes loaded");
1183
1184 let theme = theme_registry.get_cloned(&config.theme).unwrap_or_else(|| {
1186 tracing::warn!(
1187 "Theme '{}' not found, falling back to default theme",
1188 config.theme.0
1189 );
1190 theme_registry
1191 .get_cloned(&crate::config::ThemeName(
1192 crate::view::theme::THEME_HIGH_CONTRAST.to_string(),
1193 ))
1194 .expect("Default theme must exist")
1195 });
1196
1197 theme.set_terminal_cursor_color();
1199
1200 let keybindings = Arc::new(RwLock::new(KeybindingResolver::new(&config)));
1201
1202 let mut buffers = HashMap::new();
1204 let mut event_logs = HashMap::new();
1205
1206 let buffer_id = BufferId(1);
1211 let mut state = EditorState::new(
1212 width,
1213 height,
1214 config.editor.large_file_threshold_bytes as usize,
1215 Arc::clone(&filesystem),
1216 );
1217 state
1219 .margins
1220 .configure_for_line_numbers(config.editor.line_numbers);
1221 state.buffer_settings.tab_size = config.editor.tab_size;
1222 state.buffer_settings.auto_close = config.editor.auto_close;
1223 tracing::info!("EditorState created for buffer {:?}", buffer_id);
1225 buffers.insert(buffer_id, state);
1226 event_logs.insert(buffer_id, EventLog::new());
1227
1228 let mut buffer_metadata = HashMap::new();
1230 buffer_metadata.insert(buffer_id, BufferMetadata::new());
1231
1232 let root_uri = types::file_path_to_lsp_uri(&working_dir);
1234
1235 let tokio_runtime = tokio::runtime::Builder::new_multi_thread()
1237 .worker_threads(2) .thread_name("editor-async")
1239 .enable_all()
1240 .build()
1241 .ok();
1242
1243 let async_bridge = AsyncBridge::new();
1245
1246 if tokio_runtime.is_none() {
1247 tracing::warn!("Failed to create Tokio runtime - async features disabled");
1248 }
1249
1250 let mut lsp = LspManager::new(root_uri);
1252
1253 if let Some(ref runtime) = tokio_runtime {
1255 lsp.set_runtime(runtime.handle().clone(), async_bridge.clone());
1256 }
1257
1258 for (language, lsp_configs) in &config.lsp {
1260 lsp.set_language_configs(language.clone(), lsp_configs.as_slice().to_vec());
1261 }
1262
1263 let universal_servers: Vec<LspServerConfig> = config
1265 .universal_lsp
1266 .values()
1267 .flat_map(|lc| lc.as_slice().to_vec())
1268 .filter(|c| c.enabled)
1269 .collect();
1270 lsp.set_universal_configs(universal_servers);
1271
1272 if working_dir.join("deno.json").exists() || working_dir.join("deno.jsonc").exists() {
1275 tracing::info!("Detected Deno project (deno.json found), using deno lsp for JS/TS");
1276 let deno_config = LspServerConfig {
1277 command: "deno".to_string(),
1278 args: vec!["lsp".to_string()],
1279 enabled: true,
1280 auto_start: false,
1281 process_limits: ProcessLimits::default(),
1282 initialization_options: Some(serde_json::json!({"enable": true})),
1283 ..Default::default()
1284 };
1285 lsp.set_language_config("javascript".to_string(), deno_config.clone());
1286 lsp.set_language_config("typescript".to_string(), deno_config);
1287 }
1288
1289 let split_manager = SplitManager::new(buffer_id);
1291
1292 let mut split_view_states = HashMap::new();
1294 let initial_split_id = split_manager.active_split();
1295 let mut initial_view_state = SplitViewState::with_buffer(width, height, buffer_id);
1296 initial_view_state.apply_config_defaults(
1297 config.editor.line_numbers,
1298 config.editor.highlight_current_line,
1299 config.editor.line_wrap,
1300 config.editor.wrap_indent,
1301 config.editor.wrap_column,
1302 config.editor.rulers.clone(),
1303 );
1304 split_view_states.insert(initial_split_id, initial_view_state);
1305
1306 let fs_manager = Arc::new(FsManager::new(Arc::clone(&filesystem)));
1308
1309 let command_registry = Arc::new(RwLock::new(CommandRegistry::new()));
1311
1312 let mut quick_open_registry = QuickOpenRegistry::new();
1314 let process_spawner: Arc<dyn crate::services::remote::ProcessSpawner> =
1315 Arc::new(crate::services::remote::LocalProcessSpawner);
1316 quick_open_registry.register(Box::new(FileProvider::new(
1317 Arc::clone(&filesystem),
1318 Arc::clone(&process_spawner),
1319 tokio_runtime.as_ref().map(|rt| rt.handle().clone()),
1320 Some(async_bridge.sender()),
1321 )));
1322 quick_open_registry.register(Box::new(CommandProvider::new(
1323 Arc::clone(&command_registry),
1324 Arc::clone(&keybindings),
1325 )));
1326 quick_open_registry.register(Box::new(BufferProvider::new()));
1327 quick_open_registry.register(Box::new(GotoLineProvider::new()));
1328
1329 let theme_cache = Arc::new(RwLock::new(theme_registry.to_json_map()));
1331
1332 let plugin_manager = PluginManager::new(
1334 enable_plugins,
1335 Arc::clone(&command_registry),
1336 dir_context.clone(),
1337 Arc::clone(&theme_cache),
1338 );
1339
1340 #[cfg(feature = "plugins")]
1343 if let Some(snapshot_handle) = plugin_manager.state_snapshot_handle() {
1344 let mut snapshot = snapshot_handle.write().unwrap();
1345 snapshot.working_dir = working_dir.clone();
1346 }
1347
1348 if plugin_manager.is_active() {
1355 let mut plugin_dirs: Vec<std::path::PathBuf> = vec![];
1356
1357 if let Ok(exe_path) = std::env::current_exe() {
1359 if let Some(exe_dir) = exe_path.parent() {
1360 let exe_plugin_dir = exe_dir.join("plugins");
1361 if exe_plugin_dir.exists() {
1362 plugin_dirs.push(exe_plugin_dir);
1363 }
1364 }
1365 }
1366
1367 let working_plugin_dir = working_dir.join("plugins");
1369 if working_plugin_dir.exists() && !plugin_dirs.contains(&working_plugin_dir) {
1370 plugin_dirs.push(working_plugin_dir);
1371 }
1372
1373 #[cfg(feature = "embed-plugins")]
1375 if plugin_dirs.is_empty() {
1376 if let Some(embedded_dir) =
1377 crate::services::plugins::embedded::get_embedded_plugins_dir()
1378 {
1379 tracing::info!("Using embedded plugins from: {:?}", embedded_dir);
1380 plugin_dirs.push(embedded_dir.clone());
1381 }
1382 }
1383
1384 let user_plugins_dir = dir_context.config_dir.join("plugins");
1386 if user_plugins_dir.exists() && !plugin_dirs.contains(&user_plugins_dir) {
1387 tracing::info!("Found user plugins directory: {:?}", user_plugins_dir);
1388 plugin_dirs.push(user_plugins_dir.clone());
1389 }
1390
1391 let packages_dir = dir_context.config_dir.join("plugins").join("packages");
1393 if packages_dir.exists() {
1394 if let Ok(entries) = std::fs::read_dir(&packages_dir) {
1395 for entry in entries.flatten() {
1396 let path = entry.path();
1397 if path.is_dir() {
1399 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
1400 if !name.starts_with('.') {
1401 tracing::info!("Found package manager plugin: {:?}", path);
1402 plugin_dirs.push(path);
1403 }
1404 }
1405 }
1406 }
1407 }
1408 }
1409
1410 for dir in &scan_result.bundle_plugin_dirs {
1412 tracing::info!("Found bundle plugin directory: {:?}", dir);
1413 plugin_dirs.push(dir.clone());
1414 }
1415
1416 if plugin_dirs.is_empty() {
1417 tracing::debug!(
1418 "No plugins directory found next to executable or in working dir: {:?}",
1419 working_dir
1420 );
1421 }
1422
1423 for plugin_dir in plugin_dirs {
1425 tracing::info!("Loading TypeScript plugins from: {:?}", plugin_dir);
1426 let (errors, discovered_plugins) =
1427 plugin_manager.load_plugins_from_dir_with_config(&plugin_dir, &config.plugins);
1428
1429 for (name, plugin_config) in discovered_plugins {
1432 config.plugins.insert(name, plugin_config);
1433 }
1434
1435 if !errors.is_empty() {
1436 for err in &errors {
1437 tracing::error!("TypeScript plugin load error: {}", err);
1438 }
1439 #[cfg(debug_assertions)]
1441 panic!(
1442 "TypeScript plugin loading failed with {} error(s): {}",
1443 errors.len(),
1444 errors.join("; ")
1445 );
1446 }
1447 }
1448 }
1449
1450 let file_explorer_width = config.file_explorer.width;
1452 let recovery_enabled = config.editor.recovery_enabled;
1453 let check_for_updates = config.check_for_updates;
1454 let show_menu_bar = config.editor.show_menu_bar;
1455 let show_tab_bar = config.editor.show_tab_bar;
1456 let show_status_bar = config.editor.show_status_bar;
1457 let show_prompt_line = config.editor.show_prompt_line;
1458
1459 let update_checker = if check_for_updates {
1461 tracing::debug!("Update checking enabled, starting periodic checker");
1462 Some(
1463 crate::services::release_checker::start_periodic_update_check(
1464 crate::services::release_checker::DEFAULT_RELEASES_URL,
1465 time_source.clone(),
1466 dir_context.data_dir.clone(),
1467 ),
1468 )
1469 } else {
1470 tracing::debug!("Update checking disabled by config");
1471 None
1472 };
1473
1474 let user_config_raw = Config::read_user_config_raw(&working_dir);
1476
1477 let mut editor = Editor {
1478 buffers,
1479 event_logs,
1480 next_buffer_id: 2,
1481 config,
1482 user_config_raw,
1483 dir_context: dir_context.clone(),
1484 grammar_registry,
1485 pending_grammars: scan_result
1486 .additional_grammars
1487 .iter()
1488 .map(|g| PendingGrammar {
1489 language: g.language.clone(),
1490 grammar_path: g.path.to_string_lossy().to_string(),
1491 extensions: g.extensions.clone(),
1492 })
1493 .collect(),
1494 grammar_reload_pending: false,
1495 grammar_build_in_progress: false,
1496 needs_full_grammar_build: true,
1497 streaming_grep_cancellation: None,
1498 pending_grammar_callbacks: Vec::new(),
1499 theme,
1500 theme_registry,
1501 theme_cache,
1502 ansi_background: None,
1503 ansi_background_path: None,
1504 background_fade: crate::primitives::ansi_background::DEFAULT_BACKGROUND_FADE,
1505 keybindings,
1506 clipboard: crate::services::clipboard::Clipboard::new(),
1507 should_quit: false,
1508 should_detach: false,
1509 session_mode: false,
1510 software_cursor_only: false,
1511 session_name: None,
1512 pending_escape_sequences: Vec::new(),
1513 restart_with_dir: None,
1514 status_message: None,
1515 plugin_status_message: None,
1516 plugin_errors: Vec::new(),
1517 prompt: None,
1518 terminal_width: width,
1519 terminal_height: height,
1520 lsp: Some(lsp),
1521 buffer_metadata,
1522 mode_registry: ModeRegistry::new(),
1523 tokio_runtime,
1524 async_bridge: Some(async_bridge),
1525 split_manager,
1526 split_view_states,
1527 previous_viewports: HashMap::new(),
1528 scroll_sync_manager: ScrollSyncManager::new(),
1529 file_explorer: None,
1530 fs_manager,
1531 filesystem,
1532 local_filesystem: Arc::new(crate::model::filesystem::StdFileSystem),
1533 process_spawner,
1534 file_explorer_visible: false,
1535 file_explorer_sync_in_progress: false,
1536 file_explorer_width_percent: file_explorer_width,
1537 pending_file_explorer_show_hidden: None,
1538 pending_file_explorer_show_gitignored: None,
1539 menu_bar_visible: show_menu_bar,
1540 file_explorer_decorations: HashMap::new(),
1541 file_explorer_decoration_cache:
1542 crate::view::file_tree::FileExplorerDecorationCache::default(),
1543 menu_bar_auto_shown: false,
1544 tab_bar_visible: show_tab_bar,
1545 status_bar_visible: show_status_bar,
1546 prompt_line_visible: show_prompt_line,
1547 mouse_enabled: true,
1548 same_buffer_scroll_sync: false,
1549 mouse_cursor_position: None,
1550 gpm_active: false,
1551 key_context: KeyContext::Normal,
1552 menu_state: crate::view::ui::MenuState::new(dir_context.themes_dir()),
1553 menus: crate::config::MenuConfig::translated(),
1554 working_dir,
1555 position_history: PositionHistory::new(),
1556 in_navigation: false,
1557 next_lsp_request_id: 0,
1558 pending_completion_requests: HashSet::new(),
1559 completion_items: None,
1560 scheduled_completion_trigger: None,
1561 completion_service: crate::services::completion::CompletionService::new(),
1562 dabbrev_state: None,
1563 pending_goto_definition_request: None,
1564 pending_hover_request: None,
1565 pending_references_request: None,
1566 pending_references_symbol: String::new(),
1567 pending_signature_help_request: None,
1568 pending_code_actions_requests: HashSet::new(),
1569 pending_code_actions_server_names: HashMap::new(),
1570 pending_code_actions: None,
1571 pending_inlay_hints_request: None,
1572 pending_folding_range_requests: HashMap::new(),
1573 folding_ranges_in_flight: HashMap::new(),
1574 folding_ranges_debounce: HashMap::new(),
1575 pending_semantic_token_requests: HashMap::new(),
1576 semantic_tokens_in_flight: HashMap::new(),
1577 pending_semantic_token_range_requests: HashMap::new(),
1578 semantic_tokens_range_in_flight: HashMap::new(),
1579 semantic_tokens_range_last_request: HashMap::new(),
1580 semantic_tokens_range_applied: HashMap::new(),
1581 semantic_tokens_full_debounce: HashMap::new(),
1582 hover_symbol_range: None,
1583 hover_symbol_overlay: None,
1584 mouse_hover_screen_position: None,
1585 search_state: None,
1586 search_namespace: crate::view::overlay::OverlayNamespace::from_string(
1587 "search".to_string(),
1588 ),
1589 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace::from_string(
1590 "lsp-diagnostic".to_string(),
1591 ),
1592 pending_search_range: None,
1593 interactive_replace_state: None,
1594 lsp_status: String::new(),
1595 mouse_state: MouseState::default(),
1596 tab_context_menu: None,
1597 theme_info_popup: None,
1598 cached_layout: CachedLayout::default(),
1599 command_registry,
1600 quick_open_registry,
1601 plugin_manager,
1602 plugin_dev_workspaces: HashMap::new(),
1603 seen_byte_ranges: HashMap::new(),
1604 panel_ids: HashMap::new(),
1605 buffer_groups: HashMap::new(),
1606 buffer_to_group: HashMap::new(),
1607 next_buffer_group_id: 0,
1608 grouped_subtrees: HashMap::new(),
1609 background_process_handles: HashMap::new(),
1610 prompt_histories: {
1611 let mut histories = HashMap::new();
1613 for history_name in ["search", "replace", "goto_line"] {
1614 let path = dir_context.prompt_history_path(history_name);
1615 let history = crate::input::input_history::InputHistory::load_from_file(&path)
1616 .unwrap_or_else(|e| {
1617 tracing::warn!("Failed to load {} history: {}", history_name, e);
1618 crate::input::input_history::InputHistory::new()
1619 });
1620 histories.insert(history_name.to_string(), history);
1621 }
1622 histories
1623 },
1624 pending_async_prompt_callback: None,
1625 lsp_progress: std::collections::HashMap::new(),
1626 lsp_server_statuses: std::collections::HashMap::new(),
1627 lsp_window_messages: Vec::new(),
1628 lsp_log_messages: Vec::new(),
1629 diagnostic_result_ids: HashMap::new(),
1630 scheduled_diagnostic_pull: None,
1631 stored_push_diagnostics: HashMap::new(),
1632 stored_pull_diagnostics: HashMap::new(),
1633 stored_diagnostics: HashMap::new(),
1634 stored_folding_ranges: HashMap::new(),
1635 event_broadcaster: crate::model::control_event::EventBroadcaster::default(),
1636 bookmarks: HashMap::new(),
1637 search_case_sensitive: true,
1638 search_whole_word: false,
1639 search_use_regex: false,
1640 search_confirm_each: false,
1641 macros: HashMap::new(),
1642 macro_recording: None,
1643 last_macro_register: None,
1644 macro_playing: false,
1645 #[cfg(feature = "plugins")]
1646 pending_plugin_actions: Vec::new(),
1647 #[cfg(feature = "plugins")]
1648 plugin_render_requested: false,
1649 chord_state: Vec::new(),
1650 pending_lsp_confirmation: None,
1651 pending_lsp_status_popup: None,
1652 pending_close_buffer: None,
1653 auto_revert_enabled: true,
1654 last_auto_revert_poll: time_source.now(),
1655 last_file_tree_poll: time_source.now(),
1656 git_index_resolved: false,
1657 file_mod_times: HashMap::new(),
1658 dir_mod_times: HashMap::new(),
1659 pending_file_poll_rx: None,
1660 pending_dir_poll_rx: None,
1661 file_rapid_change_counts: HashMap::new(),
1662 file_open_state: None,
1663 file_browser_layout: None,
1664 recovery_service: {
1665 let recovery_config = RecoveryConfig {
1666 enabled: recovery_enabled,
1667 ..RecoveryConfig::default()
1668 };
1669 RecoveryService::with_config_and_dir(recovery_config, dir_context.recovery_dir())
1670 },
1671 full_redraw_requested: false,
1672 time_source: time_source.clone(),
1673 last_auto_recovery_save: time_source.now(),
1674 last_persistent_auto_save: time_source.now(),
1675 active_custom_contexts: HashSet::new(),
1676 plugin_global_state: HashMap::new(),
1677 editor_mode: None,
1678 warning_log: None,
1679 status_log_path: None,
1680 warning_domains: WarningDomainRegistry::new(),
1681 update_checker,
1682 terminal_manager: crate::services::terminal::TerminalManager::new(),
1683 terminal_buffers: HashMap::new(),
1684 terminal_backing_files: HashMap::new(),
1685 terminal_log_files: HashMap::new(),
1686 terminal_mode: false,
1687 keyboard_capture: false,
1688 terminal_mode_resume: std::collections::HashSet::new(),
1689 previous_click_time: None,
1690 previous_click_position: None,
1691 click_count: 0,
1692 settings_state: None,
1693 calibration_wizard: None,
1694 event_debug: None,
1695 keybinding_editor: None,
1696 key_translator: crate::input::key_translator::KeyTranslator::load_from_config_dir(
1697 &dir_context.config_dir,
1698 )
1699 .unwrap_or_default(),
1700 color_capability,
1701 pending_file_opens: Vec::new(),
1702 pending_hot_exit_recovery: false,
1703 wait_tracking: HashMap::new(),
1704 completed_waits: Vec::new(),
1705 stdin_streaming: None,
1706 line_scan_state: None,
1707 search_scan_state: None,
1708 search_overlay_top_byte: None,
1709 review_hunks: Vec::new(),
1710 active_action_popup: None,
1711 composite_buffers: HashMap::new(),
1712 composite_view_states: HashMap::new(),
1713 };
1714
1715 editor.clipboard.apply_config(&editor.config.clipboard);
1717
1718 #[cfg(feature = "plugins")]
1719 {
1720 editor.update_plugin_state_snapshot();
1721 if editor.plugin_manager.is_active() {
1722 editor.plugin_manager.run_hook(
1723 "editor_initialized",
1724 crate::services::plugins::hooks::HookArgs::EditorInitialized,
1725 );
1726 }
1727 }
1728
1729 Ok(editor)
1730 }
1731
1732 pub fn event_broadcaster(&self) -> &crate::model::control_event::EventBroadcaster {
1734 &self.event_broadcaster
1735 }
1736
1737 fn start_background_grammar_build(
1742 &mut self,
1743 additional: Vec<crate::primitives::grammar::GrammarSpec>,
1744 callback_ids: Vec<fresh_core::api::JsCallbackId>,
1745 ) {
1746 let Some(bridge) = &self.async_bridge else {
1747 return;
1748 };
1749 self.grammar_build_in_progress = true;
1750 let sender = bridge.sender();
1751 let config_dir = self.dir_context.config_dir.clone();
1752 tracing::info!(
1753 "Spawning background grammar build thread ({} plugin grammars)...",
1754 additional.len()
1755 );
1756 std::thread::Builder::new()
1757 .name("grammar-build".to_string())
1758 .spawn(move || {
1759 tracing::info!("[grammar-build] Thread started");
1760 let start = std::time::Instant::now();
1761 let registry = if additional.is_empty() {
1762 crate::primitives::grammar::GrammarRegistry::for_editor(config_dir)
1763 } else {
1764 crate::primitives::grammar::GrammarRegistry::for_editor_with_additional(
1765 config_dir,
1766 &additional,
1767 )
1768 };
1769 tracing::info!("[grammar-build] Complete in {:?}", start.elapsed());
1770 drop(sender.send(
1771 crate::services::async_bridge::AsyncMessage::GrammarRegistryBuilt {
1772 registry,
1773 callback_ids,
1774 },
1775 ));
1776 })
1777 .ok();
1778 }
1779
1780 pub fn async_bridge(&self) -> Option<&AsyncBridge> {
1782 self.async_bridge.as_ref()
1783 }
1784
1785 pub fn config(&self) -> &Config {
1787 &self.config
1788 }
1789
1790 pub fn key_translator(&self) -> &crate::input::key_translator::KeyTranslator {
1792 &self.key_translator
1793 }
1794
1795 pub fn time_source(&self) -> &SharedTimeSource {
1797 &self.time_source
1798 }
1799
1800 pub fn emit_event(&self, name: impl Into<String>, data: serde_json::Value) {
1802 self.event_broadcaster.emit_named(name, data);
1803 }
1804
1805 fn send_plugin_response(&self, response: fresh_core::api::PluginResponse) {
1807 self.plugin_manager.deliver_response(response);
1808 }
1809
1810 fn take_pending_semantic_token_request(
1812 &mut self,
1813 request_id: u64,
1814 ) -> Option<SemanticTokenFullRequest> {
1815 if let Some(request) = self.pending_semantic_token_requests.remove(&request_id) {
1816 self.semantic_tokens_in_flight.remove(&request.buffer_id);
1817 Some(request)
1818 } else {
1819 None
1820 }
1821 }
1822
1823 fn take_pending_semantic_token_range_request(
1825 &mut self,
1826 request_id: u64,
1827 ) -> Option<SemanticTokenRangeRequest> {
1828 if let Some(request) = self
1829 .pending_semantic_token_range_requests
1830 .remove(&request_id)
1831 {
1832 self.semantic_tokens_range_in_flight
1833 .remove(&request.buffer_id);
1834 Some(request)
1835 } else {
1836 None
1837 }
1838 }
1839
1840 pub fn get_all_keybindings(&self) -> Vec<(String, String)> {
1842 self.keybindings.read().unwrap().get_all_bindings()
1843 }
1844
1845 pub fn get_keybinding_for_action(&self, action_name: &str) -> Option<String> {
1848 self.keybindings
1849 .read()
1850 .unwrap()
1851 .find_keybinding_for_action(action_name, self.key_context.clone())
1852 }
1853
1854 pub fn mode_registry_mut(&mut self) -> &mut ModeRegistry {
1856 &mut self.mode_registry
1857 }
1858
1859 pub fn mode_registry(&self) -> &ModeRegistry {
1861 &self.mode_registry
1862 }
1863
1864 #[inline]
1880 pub fn active_buffer(&self) -> BufferId {
1881 let active_split = self.split_manager.active_split();
1882 if let Some(vs) = self.split_view_states.get(&active_split) {
1883 if vs.active_group_tab.is_some() {
1884 if let Some(inner_leaf) = vs.focused_group_leaf {
1885 if let Some(inner_vs) = self.split_view_states.get(&inner_leaf) {
1886 let inner_buf = inner_vs.active_buffer;
1887 if self.buffers.contains_key(&inner_buf) {
1888 return inner_buf;
1889 }
1890 }
1891 }
1892 }
1893 }
1894 self.split_manager
1895 .active_buffer_id()
1896 .expect("Editor always has at least one buffer")
1897 }
1898
1899 #[inline]
1906 pub fn effective_active_split(&self) -> crate::model::event::LeafId {
1907 let active_split = self.split_manager.active_split();
1908 if let Some(vs) = self.split_view_states.get(&active_split) {
1909 if vs.active_group_tab.is_some() {
1910 if let Some(inner_leaf) = vs.focused_group_leaf {
1911 if self.split_view_states.contains_key(&inner_leaf) {
1912 return inner_leaf;
1913 }
1914 }
1915 }
1916 }
1917 active_split
1918 }
1919
1920 pub fn active_buffer_mode(&self) -> Option<&str> {
1922 self.buffer_metadata
1923 .get(&self.active_buffer())
1924 .and_then(|meta| meta.virtual_mode())
1925 }
1926
1927 pub fn is_active_buffer_read_only(&self) -> bool {
1929 if let Some(metadata) = self.buffer_metadata.get(&self.active_buffer()) {
1930 if metadata.read_only {
1931 return true;
1932 }
1933 if let Some(mode_name) = metadata.virtual_mode() {
1935 return self.mode_registry.is_read_only(mode_name);
1936 }
1937 }
1938 false
1939 }
1940
1941 pub fn is_editing_disabled(&self) -> bool {
1944 self.active_state().editing_disabled
1945 }
1946
1947 pub fn mark_buffer_read_only(&mut self, buffer_id: BufferId, read_only: bool) {
1950 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
1951 metadata.read_only = read_only;
1952 }
1953 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1954 state.editing_disabled = read_only;
1955 }
1956 }
1957
1958 pub fn effective_mode(&self) -> Option<&str> {
1964 self.active_buffer_mode().or(self.editor_mode.as_deref())
1965 }
1966
1967 pub fn has_active_lsp_progress(&self) -> bool {
1969 !self.lsp_progress.is_empty()
1970 }
1971
1972 pub fn get_lsp_progress(&self) -> Vec<(String, String, Option<String>)> {
1974 self.lsp_progress
1975 .iter()
1976 .map(|(token, info)| (token.clone(), info.title.clone(), info.message.clone()))
1977 .collect()
1978 }
1979
1980 pub fn is_lsp_server_ready(&self, language: &str) -> bool {
1982 use crate::services::async_bridge::LspServerStatus;
1983 self.lsp_server_statuses
1984 .iter()
1985 .any(|((lang, server_name), status)| {
1986 if !matches!(status, LspServerStatus::Running) {
1987 return false;
1988 }
1989 if lang == language {
1990 return true;
1991 }
1992 self.lsp
1994 .as_ref()
1995 .and_then(|lsp| lsp.server_scope(server_name))
1996 .map(|scope| scope.accepts(language))
1997 .unwrap_or(false)
1998 })
1999 }
2000
2001 pub fn get_lsp_status(&self) -> &str {
2003 &self.lsp_status
2004 }
2005
2006 pub fn get_stored_diagnostics(&self) -> &HashMap<String, Vec<lsp_types::Diagnostic>> {
2009 &self.stored_diagnostics
2010 }
2011
2012 pub fn is_update_available(&self) -> bool {
2014 self.update_checker
2015 .as_ref()
2016 .map(|c| c.is_update_available())
2017 .unwrap_or(false)
2018 }
2019
2020 pub fn latest_version(&self) -> Option<&str> {
2022 self.update_checker
2023 .as_ref()
2024 .and_then(|c| c.latest_version())
2025 }
2026
2027 pub fn get_update_result(
2029 &self,
2030 ) -> Option<&crate::services::release_checker::ReleaseCheckResult> {
2031 self.update_checker
2032 .as_ref()
2033 .and_then(|c| c.get_cached_result())
2034 }
2035
2036 #[doc(hidden)]
2041 pub fn set_update_checker(
2042 &mut self,
2043 checker: crate::services::release_checker::PeriodicUpdateChecker,
2044 ) {
2045 self.update_checker = Some(checker);
2046 }
2047
2048 pub fn set_lsp_config(&mut self, language: String, config: Vec<LspServerConfig>) {
2050 if let Some(ref mut lsp) = self.lsp {
2051 lsp.set_language_configs(language, config);
2052 }
2053 }
2054
2055 pub fn running_lsp_servers(&self) -> Vec<String> {
2057 self.lsp
2058 .as_ref()
2059 .map(|lsp| lsp.running_servers())
2060 .unwrap_or_default()
2061 }
2062
2063 pub fn pending_completion_requests_count(&self) -> usize {
2065 self.pending_completion_requests.len()
2066 }
2067
2068 pub fn completion_items_count(&self) -> usize {
2070 self.completion_items.as_ref().map_or(0, |v| v.len())
2071 }
2072
2073 pub fn initialized_lsp_server_count(&self, language: &str) -> usize {
2075 self.lsp
2076 .as_ref()
2077 .map(|lsp| {
2078 lsp.get_handles(language)
2079 .iter()
2080 .filter(|sh| sh.capabilities.initialized)
2081 .count()
2082 })
2083 .unwrap_or(0)
2084 }
2085
2086 pub fn shutdown_lsp_server(&mut self, language: &str) -> bool {
2090 if let Some(ref mut lsp) = self.lsp {
2091 lsp.shutdown_server(language)
2092 } else {
2093 false
2094 }
2095 }
2096
2097 pub fn enable_event_streaming<P: AsRef<Path>>(&mut self, path: P) -> AnyhowResult<()> {
2099 for event_log in self.event_logs.values_mut() {
2101 event_log.enable_streaming(&path)?;
2102 }
2103 Ok(())
2104 }
2105
2106 pub fn log_keystroke(&mut self, key_code: &str, modifiers: &str) {
2108 if let Some(event_log) = self.event_logs.get_mut(&self.active_buffer()) {
2109 event_log.log_keystroke(key_code, modifiers);
2110 }
2111 }
2112
2113 pub fn set_warning_log(&mut self, receiver: std::sync::mpsc::Receiver<()>, path: PathBuf) {
2118 self.warning_log = Some((receiver, path));
2119 }
2120
2121 pub fn set_status_log_path(&mut self, path: PathBuf) {
2123 self.status_log_path = Some(path);
2124 }
2125
2126 pub fn set_process_spawner(
2129 &mut self,
2130 spawner: Arc<dyn crate::services::remote::ProcessSpawner>,
2131 ) {
2132 self.process_spawner = spawner;
2133 }
2134
2135 pub fn remote_connection_info(&self) -> Option<&str> {
2139 self.filesystem.remote_connection_info()
2140 }
2141
2142 pub fn get_status_log_path(&self) -> Option<&PathBuf> {
2144 self.status_log_path.as_ref()
2145 }
2146
2147 pub fn open_status_log(&mut self) {
2149 if let Some(path) = self.status_log_path.clone() {
2150 match self.open_local_file(&path) {
2152 Ok(buffer_id) => {
2153 self.mark_buffer_read_only(buffer_id, true);
2154 }
2155 Err(e) => {
2156 tracing::error!("Failed to open status log: {}", e);
2157 }
2158 }
2159 } else {
2160 self.set_status_message("Status log not available".to_string());
2161 }
2162 }
2163
2164 pub fn check_warning_log(&mut self) -> bool {
2169 let Some((receiver, path)) = &self.warning_log else {
2170 return false;
2171 };
2172
2173 let mut new_warning_count = 0usize;
2175 while receiver.try_recv().is_ok() {
2176 new_warning_count += 1;
2177 }
2178
2179 if new_warning_count > 0 {
2180 self.warning_domains.general.add_warnings(new_warning_count);
2182 self.warning_domains.general.set_log_path(path.clone());
2183 }
2184
2185 new_warning_count > 0
2186 }
2187
2188 pub fn get_warning_domains(&self) -> &WarningDomainRegistry {
2190 &self.warning_domains
2191 }
2192
2193 pub fn get_warning_log_path(&self) -> Option<&PathBuf> {
2195 self.warning_domains.general.log_path.as_ref()
2196 }
2197
2198 pub fn open_warning_log(&mut self) {
2200 if let Some(path) = self.warning_domains.general.log_path.clone() {
2201 match self.open_local_file(&path) {
2203 Ok(buffer_id) => {
2204 self.mark_buffer_read_only(buffer_id, true);
2205 }
2206 Err(e) => {
2207 tracing::error!("Failed to open warning log: {}", e);
2208 }
2209 }
2210 }
2211 }
2212
2213 pub fn clear_warning_indicator(&mut self) {
2215 self.warning_domains.general.clear();
2216 }
2217
2218 pub fn clear_warnings(&mut self) {
2220 self.warning_domains.general.clear();
2221 self.warning_domains.lsp.clear();
2222 self.status_message = Some("Warnings cleared".to_string());
2223 }
2224
2225 pub fn has_lsp_error(&self) -> bool {
2227 self.warning_domains.lsp.level() == WarningLevel::Error
2228 }
2229
2230 pub fn get_effective_warning_level(&self) -> WarningLevel {
2233 self.warning_domains.lsp.level()
2234 }
2235
2236 pub fn get_general_warning_level(&self) -> WarningLevel {
2238 self.warning_domains.general.level()
2239 }
2240
2241 pub fn get_general_warning_count(&self) -> usize {
2243 self.warning_domains.general.count
2244 }
2245
2246 pub fn update_lsp_warning_domain(&mut self) {
2248 self.warning_domains
2249 .lsp
2250 .update_from_statuses(&self.lsp_server_statuses);
2251 }
2252
2253 pub fn check_mouse_hover_timer(&mut self) -> bool {
2259 if !self.config.editor.mouse_hover_enabled {
2261 return false;
2262 }
2263
2264 let hover_delay = std::time::Duration::from_millis(self.config.editor.mouse_hover_delay_ms);
2265
2266 let hover_info = match self.mouse_state.lsp_hover_state {
2268 Some((byte_pos, start_time, screen_x, screen_y)) => {
2269 if self.mouse_state.lsp_hover_request_sent {
2270 return false; }
2272 if start_time.elapsed() < hover_delay {
2273 return false; }
2275 Some((byte_pos, screen_x, screen_y))
2276 }
2277 None => return false,
2278 };
2279
2280 let Some((byte_pos, screen_x, screen_y)) = hover_info else {
2281 return false;
2282 };
2283
2284 self.mouse_hover_screen_position = Some((screen_x, screen_y));
2286
2287 match self.request_hover_at_position(byte_pos) {
2289 Ok(true) => {
2290 self.mouse_state.lsp_hover_request_sent = true;
2291 true
2292 }
2293 Ok(false) => false, Err(e) => {
2295 tracing::debug!("Failed to request hover: {}", e);
2296 false
2297 }
2298 }
2299 }
2300
2301 pub fn check_semantic_highlight_timer(&self) -> bool {
2306 for state in self.buffers.values() {
2308 if let Some(remaining) = state.reference_highlight_overlay.needs_redraw() {
2309 if remaining.is_zero() {
2310 return true;
2311 }
2312 }
2313 }
2314 false
2315 }
2316
2317 pub fn check_diagnostic_pull_timer(&mut self) -> bool {
2322 let Some((buffer_id, trigger_time)) = self.scheduled_diagnostic_pull else {
2323 return false;
2324 };
2325
2326 if Instant::now() < trigger_time {
2327 return false;
2328 }
2329
2330 self.scheduled_diagnostic_pull = None;
2331
2332 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
2334 return false;
2335 };
2336 let Some(uri) = metadata.file_uri().cloned() else {
2337 return false;
2338 };
2339 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
2340 return false;
2341 };
2342
2343 let Some(lsp) = self.lsp.as_mut() else {
2344 return false;
2345 };
2346 let Some(sh) = lsp.handle_for_feature_mut(&language, crate::types::LspFeature::Diagnostics)
2347 else {
2348 return false;
2349 };
2350 let client = &mut sh.handle;
2351
2352 let request_id = self.next_lsp_request_id;
2353 self.next_lsp_request_id += 1;
2354 let previous_result_id = self.diagnostic_result_ids.get(uri.as_str()).cloned();
2355 if let Err(e) = client.document_diagnostic(request_id, uri.clone(), previous_result_id) {
2356 tracing::debug!(
2357 "Failed to pull diagnostics after edit for {}: {}",
2358 uri.as_str(),
2359 e
2360 );
2361 } else {
2362 tracing::debug!(
2363 "Pulling diagnostics after edit for {} (request_id={})",
2364 uri.as_str(),
2365 request_id
2366 );
2367 }
2368
2369 false }
2371
2372 pub fn check_completion_trigger_timer(&mut self) -> bool {
2378 let Some(trigger_time) = self.scheduled_completion_trigger else {
2380 return false;
2381 };
2382
2383 if Instant::now() < trigger_time {
2385 return false;
2386 }
2387
2388 self.scheduled_completion_trigger = None;
2390
2391 if self.active_state().popups.is_visible() {
2393 return false;
2394 }
2395
2396 self.request_completion();
2398
2399 true
2400 }
2401
2402 fn load_ansi_background(&mut self, input: &str) -> AnyhowResult<()> {
2404 let trimmed = input.trim();
2405
2406 if trimmed.is_empty() {
2407 self.ansi_background = None;
2408 self.ansi_background_path = None;
2409 self.set_status_message(t!("status.background_cleared").to_string());
2410 return Ok(());
2411 }
2412
2413 let input_path = Path::new(trimmed);
2414 let resolved = if input_path.is_absolute() {
2415 input_path.to_path_buf()
2416 } else {
2417 self.working_dir.join(input_path)
2418 };
2419
2420 let canonical = resolved.canonicalize().unwrap_or_else(|_| resolved.clone());
2421
2422 let parsed = crate::primitives::ansi_background::AnsiBackground::from_file(&canonical)?;
2423
2424 self.ansi_background = Some(parsed);
2425 self.ansi_background_path = Some(canonical.clone());
2426 self.set_status_message(
2427 t!(
2428 "view.background_set",
2429 path = canonical.display().to_string()
2430 )
2431 .to_string(),
2432 );
2433
2434 Ok(())
2435 }
2436
2437 fn effective_tabs_width(&self) -> u16 {
2442 if self.file_explorer_visible && self.file_explorer.is_some() {
2443 let editor_percent = 1.0 - self.file_explorer_width_percent;
2445 (self.terminal_width as f32 * editor_percent) as u16
2446 } else {
2447 self.terminal_width
2448 }
2449 }
2450
2451 fn set_active_buffer(&mut self, buffer_id: BufferId) {
2461 if self.active_buffer() == buffer_id {
2462 return; }
2464
2465 self.on_editor_focus_lost();
2467
2468 self.cancel_search_prompt_if_active();
2471
2472 let previous = self.active_buffer();
2474
2475 if self.terminal_mode && self.is_terminal_buffer(previous) {
2477 self.terminal_mode_resume.insert(previous);
2478 self.terminal_mode = false;
2479 self.key_context = crate::input::keybindings::KeyContext::Normal;
2480 }
2481
2482 self.split_manager.set_active_buffer_id(buffer_id);
2484
2485 let active_split = self.split_manager.active_split();
2487 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
2488 let previous_target = view_state.active_target();
2491 view_state.switch_buffer(buffer_id);
2492 view_state.add_buffer(buffer_id);
2493 view_state.active_group_tab = None;
2494 view_state.focused_group_leaf = None;
2495 view_state.push_focus(previous_target);
2496 }
2497
2498 if self.terminal_mode_resume.contains(&buffer_id) && self.is_terminal_buffer(buffer_id) {
2500 self.terminal_mode = true;
2501 self.key_context = crate::input::keybindings::KeyContext::Terminal;
2502 } else if self.is_terminal_buffer(buffer_id) {
2503 self.sync_terminal_to_buffer(buffer_id);
2506 }
2507
2508 self.ensure_active_tab_visible(active_split, buffer_id, self.effective_tabs_width());
2510
2511 #[cfg(feature = "plugins")]
2517 self.update_plugin_state_snapshot();
2518
2519 self.plugin_manager.run_hook(
2521 "buffer_activated",
2522 crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
2523 );
2524 }
2525
2526 pub(super) fn focus_split(&mut self, split_id: LeafId, buffer_id: BufferId) {
2537 let previous_split = self.split_manager.active_split();
2538 let previous_buffer = self.active_buffer(); let split_changed = previous_split != split_id;
2540
2541 if !self
2548 .split_manager
2549 .root()
2550 .leaf_split_ids()
2551 .contains(&split_id)
2552 {
2553 let host_split = self
2555 .grouped_subtrees
2556 .iter()
2557 .find(|(_, node)| {
2558 if let crate::view::split::SplitNode::Grouped { layout, .. } = node {
2559 layout.find(split_id.into()).is_some()
2560 } else {
2561 false
2562 }
2563 })
2564 .map(|(group_leaf_id, _)| *group_leaf_id)
2565 .and_then(|group_leaf_id| {
2566 self.split_view_states
2568 .iter()
2569 .find(|(_, vs)| vs.has_group(group_leaf_id))
2570 .map(|(sid, _)| (*sid, group_leaf_id))
2571 });
2572
2573 if let Some((host, group_leaf_id)) = host_split {
2574 self.split_manager.set_active_split(host);
2575 if let Some(vs) = self.split_view_states.get_mut(&host) {
2576 vs.active_group_tab = Some(group_leaf_id);
2577 vs.focused_group_leaf = Some(split_id);
2578 }
2579 if let Some(inner_vs) = self.split_view_states.get_mut(&split_id) {
2580 inner_vs.switch_buffer(buffer_id);
2581 }
2582 self.key_context = crate::input::keybindings::KeyContext::Normal;
2583 return;
2584 }
2585 }
2588
2589 if split_changed {
2590 if self.terminal_mode && self.is_terminal_buffer(previous_buffer) {
2592 self.terminal_mode = false;
2593 self.key_context = crate::input::keybindings::KeyContext::Normal;
2594 }
2595
2596 self.split_manager.set_active_split(split_id);
2598
2599 self.split_manager.set_active_buffer_id(buffer_id);
2601
2602 if self.is_terminal_buffer(buffer_id) {
2604 self.terminal_mode = true;
2605 self.key_context = crate::input::keybindings::KeyContext::Terminal;
2606 } else {
2607 self.key_context = crate::input::keybindings::KeyContext::Normal;
2610 }
2611
2612 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2615 view_state.switch_buffer(buffer_id);
2616 }
2617
2618 if previous_buffer != buffer_id {
2620 self.position_history.commit_pending_movement();
2621 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2622 view_state.add_buffer(buffer_id);
2623 view_state.push_focus(crate::view::split::TabTarget::Buffer(previous_buffer));
2624 }
2625 }
2628 } else {
2629 self.set_active_buffer(buffer_id);
2631 }
2632 }
2633
2634 pub fn active_state(&self) -> &EditorState {
2636 self.buffers.get(&self.active_buffer()).unwrap()
2637 }
2638
2639 pub fn active_state_mut(&mut self) -> &mut EditorState {
2641 self.buffers.get_mut(&self.active_buffer()).unwrap()
2642 }
2643
2644 pub fn active_cursors(&self) -> &Cursors {
2646 let split_id = self.split_manager.active_split();
2647 &self.split_view_states.get(&split_id).unwrap().cursors
2648 }
2649
2650 pub fn active_cursors_mut(&mut self) -> &mut Cursors {
2652 let split_id = self.split_manager.active_split();
2653 &mut self.split_view_states.get_mut(&split_id).unwrap().cursors
2654 }
2655
2656 pub fn set_completion_items(&mut self, items: Vec<lsp_types::CompletionItem>) {
2658 self.completion_items = Some(items);
2659 }
2660
2661 pub fn active_viewport(&self) -> &crate::view::viewport::Viewport {
2663 let active_split = self.split_manager.active_split();
2664 &self.split_view_states.get(&active_split).unwrap().viewport
2665 }
2666
2667 pub fn active_viewport_mut(&mut self) -> &mut crate::view::viewport::Viewport {
2669 let active_split = self.split_manager.active_split();
2670 &mut self
2671 .split_view_states
2672 .get_mut(&active_split)
2673 .unwrap()
2674 .viewport
2675 }
2676
2677 pub fn get_buffer_display_name(&self, buffer_id: BufferId) -> String {
2679 if let Some(composite) = self.composite_buffers.get(&buffer_id) {
2681 return composite.name.clone();
2682 }
2683
2684 self.buffer_metadata
2685 .get(&buffer_id)
2686 .map(|m| m.display_name.clone())
2687 .or_else(|| {
2688 self.buffers.get(&buffer_id).and_then(|state| {
2689 state
2690 .buffer
2691 .file_path()
2692 .and_then(|p| p.file_name())
2693 .and_then(|n| n.to_str())
2694 .map(|s| s.to_string())
2695 })
2696 })
2697 .unwrap_or_else(|| "[No Name]".to_string())
2698 }
2699
2700 pub fn log_and_apply_event(&mut self, event: &Event) {
2712 if let Event::Delete { range, .. } = event {
2714 let displaced = self.active_state().capture_displaced_markers(range);
2715 self.active_event_log_mut().append(event.clone());
2716 if !displaced.is_empty() {
2717 self.active_event_log_mut()
2718 .set_displaced_markers_on_last(displaced);
2719 }
2720 } else {
2721 self.active_event_log_mut().append(event.clone());
2722 }
2723 self.apply_event_to_active_buffer(event);
2724 }
2725
2726 pub fn apply_event_to_active_buffer(&mut self, event: &Event) {
2727 match event {
2730 Event::Scroll { line_offset } => {
2731 self.handle_scroll_event(*line_offset);
2732 return;
2733 }
2734 Event::SetViewport { top_line } => {
2735 self.handle_set_viewport_event(*top_line);
2736 return;
2737 }
2738 Event::Recenter => {
2739 self.handle_recenter_event();
2740 return;
2741 }
2742 _ => {}
2743 }
2744
2745 let lsp_changes = self.collect_lsp_changes(event);
2749
2750 let line_info = self.calculate_event_line_info(event);
2752
2753 {
2762 let split_id = self.effective_active_split();
2763 let active_buf = self.active_buffer();
2764 let cursors = &mut self
2765 .split_view_states
2766 .get_mut(&split_id)
2767 .unwrap()
2768 .keyed_states
2769 .get_mut(&active_buf)
2770 .unwrap()
2771 .cursors;
2772 let state = self.buffers.get_mut(&active_buf).unwrap();
2773 state.apply(cursors, event);
2774 }
2775
2776 match event {
2779 Event::Insert { .. } | Event::Delete { .. } | Event::BulkEdit { .. } => {
2780 self.invalidate_layouts_for_buffer(self.active_buffer());
2781 self.schedule_semantic_tokens_full_refresh(self.active_buffer());
2782 self.schedule_folding_ranges_refresh(self.active_buffer());
2783 }
2784 Event::Batch { events, .. } => {
2785 let has_edits = events
2786 .iter()
2787 .any(|e| matches!(e, Event::Insert { .. } | Event::Delete { .. }));
2788 if has_edits {
2789 self.invalidate_layouts_for_buffer(self.active_buffer());
2790 self.schedule_semantic_tokens_full_refresh(self.active_buffer());
2791 self.schedule_folding_ranges_refresh(self.active_buffer());
2792 }
2793 }
2794 _ => {}
2795 }
2796
2797 self.adjust_other_split_cursors_for_event(event);
2799
2800 let in_interactive_replace = self.interactive_replace_state.is_some();
2804
2805 let _ = in_interactive_replace; self.trigger_plugin_hooks_for_event(event, line_info);
2814
2815 if lsp_changes.is_empty() && event.modifies_buffer() {
2821 if let Some(full_text) = self.active_state().buffer.to_string() {
2822 let full_change = vec![TextDocumentContentChangeEvent {
2823 range: None,
2824 range_length: None,
2825 text: full_text,
2826 }];
2827 self.send_lsp_changes_for_buffer(self.active_buffer(), full_change);
2828 }
2829 } else {
2830 self.send_lsp_changes_for_buffer(self.active_buffer(), lsp_changes);
2831 }
2832 }
2833
2834 pub fn apply_events_as_bulk_edit(
2848 &mut self,
2849 events: Vec<Event>,
2850 description: String,
2851 ) -> Option<Event> {
2852 use crate::model::event::CursorId;
2853
2854 let has_buffer_mods = events
2856 .iter()
2857 .any(|e| matches!(e, Event::Insert { .. } | Event::Delete { .. }));
2858
2859 if !has_buffer_mods {
2860 return None;
2862 }
2863
2864 let active_buf = self.active_buffer();
2865 let split_id = self.split_manager.active_split();
2866
2867 let old_cursors: Vec<(CursorId, usize, Option<usize>)> = self
2869 .split_view_states
2870 .get(&split_id)
2871 .unwrap()
2872 .keyed_states
2873 .get(&active_buf)
2874 .unwrap()
2875 .cursors
2876 .iter()
2877 .map(|(id, c)| (id, c.position, c.anchor))
2878 .collect();
2879
2880 let state = self.buffers.get_mut(&active_buf).unwrap();
2881
2882 let old_snapshot = state.buffer.snapshot_buffer_state();
2884
2885 let mut edits: Vec<(usize, usize, String)> = Vec::new();
2889
2890 for event in &events {
2891 match event {
2892 Event::Insert { position, text, .. } => {
2893 edits.push((*position, 0, text.clone()));
2894 }
2895 Event::Delete { range, .. } => {
2896 edits.push((range.start, range.len(), String::new()));
2897 }
2898 _ => {}
2899 }
2900 }
2901
2902 edits.sort_by(|a, b| b.0.cmp(&a.0));
2904
2905 let edit_refs: Vec<(usize, usize, &str)> = edits
2907 .iter()
2908 .map(|(pos, del, text)| (*pos, *del, text.as_str()))
2909 .collect();
2910
2911 let displaced_markers = state.capture_displaced_markers_bulk(&edits);
2913
2914 let _delta = state.buffer.apply_bulk_edits(&edit_refs);
2916
2917 let edit_lengths: Vec<(usize, usize, usize)> = {
2923 let mut lengths: Vec<(usize, usize, usize)> = Vec::new();
2924 for (pos, del_len, text) in &edits {
2925 if let Some(last) = lengths.last_mut() {
2926 if last.0 == *pos {
2927 last.1 += del_len;
2929 last.2 += text.len();
2930 continue;
2931 }
2932 }
2933 lengths.push((*pos, *del_len, text.len()));
2934 }
2935 lengths
2936 };
2937
2938 for &(pos, del_len, ins_len) in &edit_lengths {
2943 if del_len > 0 && ins_len > 0 {
2944 if ins_len > del_len {
2946 state.marker_list.adjust_for_insert(pos, ins_len - del_len);
2947 state.margins.adjust_for_insert(pos, ins_len - del_len);
2948 } else if del_len > ins_len {
2949 state.marker_list.adjust_for_delete(pos, del_len - ins_len);
2950 state.margins.adjust_for_delete(pos, del_len - ins_len);
2951 }
2952 } else if del_len > 0 {
2954 state.marker_list.adjust_for_delete(pos, del_len);
2955 state.margins.adjust_for_delete(pos, del_len);
2956 } else if ins_len > 0 {
2957 state.marker_list.adjust_for_insert(pos, ins_len);
2958 state.margins.adjust_for_insert(pos, ins_len);
2959 }
2960 }
2961
2962 let new_snapshot = state.buffer.snapshot_buffer_state();
2964
2965 let mut new_cursors: Vec<(CursorId, usize, Option<usize>)> = old_cursors.clone();
2968
2969 let mut position_deltas: Vec<(usize, isize)> = Vec::new();
2972 for (pos, del_len, text) in &edits {
2973 let delta = text.len() as isize - *del_len as isize;
2974 position_deltas.push((*pos, delta));
2975 }
2976 position_deltas.sort_by_key(|(pos, _)| *pos);
2977
2978 let calc_shift = |original_pos: usize| -> isize {
2980 let mut shift: isize = 0;
2981 for (edit_pos, delta) in &position_deltas {
2982 if *edit_pos < original_pos {
2983 shift += delta;
2984 }
2985 }
2986 shift
2987 };
2988
2989 for (cursor_id, ref mut pos, ref mut anchor) in &mut new_cursors {
2993 let mut found_move_cursor = false;
2994 let original_pos = *pos;
2996
2997 let insert_at_cursor_pos = events.iter().any(|e| {
3001 matches!(e, Event::Insert { position, cursor_id: c, .. }
3002 if *c == *cursor_id && *position == original_pos)
3003 });
3004
3005 for event in &events {
3007 if let Event::MoveCursor {
3008 cursor_id: event_cursor,
3009 new_position,
3010 new_anchor,
3011 ..
3012 } = event
3013 {
3014 if event_cursor == cursor_id {
3015 let shift = if insert_at_cursor_pos {
3019 calc_shift(original_pos)
3020 } else {
3021 0
3022 };
3023 *pos = (*new_position as isize + shift).max(0) as usize;
3024 *anchor = *new_anchor;
3025 found_move_cursor = true;
3026 }
3027 }
3028 }
3029
3030 if !found_move_cursor {
3032 let mut found_edit = false;
3033 for event in &events {
3034 match event {
3035 Event::Insert {
3036 position,
3037 text,
3038 cursor_id: event_cursor,
3039 } if event_cursor == cursor_id => {
3040 let shift = calc_shift(*position);
3043 let adjusted_pos = (*position as isize + shift).max(0) as usize;
3044 *pos = adjusted_pos.saturating_add(text.len());
3045 *anchor = None;
3046 found_edit = true;
3047 }
3048 Event::Delete {
3049 range,
3050 cursor_id: event_cursor,
3051 ..
3052 } if event_cursor == cursor_id => {
3053 let shift = calc_shift(range.start);
3056 *pos = (range.start as isize + shift).max(0) as usize;
3057 *anchor = None;
3058 found_edit = true;
3059 }
3060 _ => {}
3061 }
3062 }
3063
3064 if !found_edit {
3068 let shift = calc_shift(original_pos);
3069 *pos = (original_pos as isize + shift).max(0) as usize;
3070 }
3071 }
3072 }
3073
3074 {
3076 let cursors = &mut self
3077 .split_view_states
3078 .get_mut(&split_id)
3079 .unwrap()
3080 .keyed_states
3081 .get_mut(&active_buf)
3082 .unwrap()
3083 .cursors;
3084 for (cursor_id, position, anchor) in &new_cursors {
3085 if let Some(cursor) = cursors.get_mut(*cursor_id) {
3086 cursor.position = *position;
3087 cursor.anchor = *anchor;
3088 }
3089 }
3090 }
3091
3092 self.buffers
3094 .get_mut(&active_buf)
3095 .unwrap()
3096 .highlighter
3097 .invalidate_all();
3098
3099 let bulk_edit = Event::BulkEdit {
3101 old_snapshot: Some(old_snapshot),
3102 new_snapshot: Some(new_snapshot),
3103 old_cursors,
3104 new_cursors,
3105 description,
3106 edits: edit_lengths,
3107 displaced_markers,
3108 };
3109
3110 self.invalidate_layouts_for_buffer(self.active_buffer());
3112 self.adjust_other_split_cursors_for_event(&bulk_edit);
3113 let buffer_id = self.active_buffer();
3120 let full_content_change = self
3121 .buffers
3122 .get(&buffer_id)
3123 .and_then(|s| s.buffer.to_string())
3124 .map(|text| {
3125 vec![TextDocumentContentChangeEvent {
3126 range: None,
3127 range_length: None,
3128 text,
3129 }]
3130 })
3131 .unwrap_or_default();
3132 if !full_content_change.is_empty() {
3133 self.send_lsp_changes_for_buffer(buffer_id, full_content_change);
3134 }
3135
3136 Some(bulk_edit)
3137 }
3138
3139 fn trigger_plugin_hooks_for_event(&mut self, event: &Event, line_info: EventLineInfo) {
3142 let buffer_id = self.active_buffer();
3143
3144 let mut cursor_changed_lines = false;
3146 let hook_args = match event {
3147 Event::Insert { position, text, .. } => {
3148 let insert_position = *position;
3149 let insert_len = text.len();
3150
3151 if let Some(seen) = self.seen_byte_ranges.get_mut(&buffer_id) {
3153 let adjusted: std::collections::HashSet<(usize, usize)> = seen
3158 .iter()
3159 .filter_map(|&(start, end)| {
3160 if end <= insert_position {
3161 Some((start, end))
3163 } else if start >= insert_position {
3164 Some((start + insert_len, end + insert_len))
3166 } else {
3167 None
3169 }
3170 })
3171 .collect();
3172 *seen = adjusted;
3173 }
3174
3175 Some((
3176 "after_insert",
3177 crate::services::plugins::hooks::HookArgs::AfterInsert {
3178 buffer_id,
3179 position: *position,
3180 text: text.clone(),
3181 affected_start: insert_position,
3183 affected_end: insert_position + insert_len,
3184 start_line: line_info.start_line,
3186 end_line: line_info.end_line,
3187 lines_added: line_info.line_delta.max(0) as usize,
3188 },
3189 ))
3190 }
3191 Event::Delete {
3192 range,
3193 deleted_text,
3194 ..
3195 } => {
3196 let delete_start = range.start;
3197
3198 let delete_end = range.end;
3200 let delete_len = delete_end - delete_start;
3201 if let Some(seen) = self.seen_byte_ranges.get_mut(&buffer_id) {
3202 let adjusted: std::collections::HashSet<(usize, usize)> = seen
3207 .iter()
3208 .filter_map(|&(start, end)| {
3209 if end <= delete_start {
3210 Some((start, end))
3212 } else if start >= delete_end {
3213 Some((start - delete_len, end - delete_len))
3215 } else {
3216 None
3218 }
3219 })
3220 .collect();
3221 *seen = adjusted;
3222 }
3223
3224 Some((
3225 "after_delete",
3226 crate::services::plugins::hooks::HookArgs::AfterDelete {
3227 buffer_id,
3228 range: range.clone(),
3229 deleted_text: deleted_text.clone(),
3230 affected_start: delete_start,
3232 deleted_len: deleted_text.len(),
3233 start_line: line_info.start_line,
3235 end_line: line_info.end_line,
3236 lines_removed: (-line_info.line_delta).max(0) as usize,
3237 },
3238 ))
3239 }
3240 Event::Batch { events, .. } => {
3241 for e in events {
3245 let sub_line_info = self.calculate_event_line_info(e);
3248 self.trigger_plugin_hooks_for_event(e, sub_line_info);
3249 }
3250 None
3251 }
3252 Event::MoveCursor {
3253 cursor_id,
3254 old_position,
3255 new_position,
3256 ..
3257 } => {
3258 let old_line = self.active_state().buffer.get_line_number(*old_position) + 1;
3260 let line = self.active_state().buffer.get_line_number(*new_position) + 1;
3261 cursor_changed_lines = old_line != line;
3262 let text_props = self
3263 .active_state()
3264 .text_properties
3265 .get_at(*new_position)
3266 .into_iter()
3267 .map(|tp| tp.properties.clone())
3268 .collect();
3269 Some((
3270 "cursor_moved",
3271 crate::services::plugins::hooks::HookArgs::CursorMoved {
3272 buffer_id,
3273 cursor_id: *cursor_id,
3274 old_position: *old_position,
3275 new_position: *new_position,
3276 line,
3277 text_properties: text_props,
3278 },
3279 ))
3280 }
3281 _ => None,
3282 };
3283
3284 if let Some((hook_name, ref args)) = hook_args {
3286 #[cfg(feature = "plugins")]
3290 self.update_plugin_state_snapshot();
3291
3292 self.plugin_manager.run_hook(hook_name, args.clone());
3293 }
3294
3295 if cursor_changed_lines {
3306 self.handle_refresh_lines(buffer_id);
3307 }
3308 }
3309
3310 fn handle_scroll_event(&mut self, line_offset: isize) {
3316 use crate::view::ui::view_pipeline::ViewLineIterator;
3317
3318 let active_split = self.split_manager.active_split();
3319
3320 if let Some(group) = self
3324 .scroll_sync_manager
3325 .find_group_for_split(active_split.into())
3326 {
3327 let left = group.left_split;
3328 let right = group.right_split;
3329 if let Some(vs) = self.split_view_states.get_mut(&LeafId(left)) {
3330 vs.viewport.set_skip_ensure_visible();
3331 }
3332 if let Some(vs) = self.split_view_states.get_mut(&LeafId(right)) {
3333 vs.viewport.set_skip_ensure_visible();
3334 }
3335 }
3337
3338 let sync_group = self
3340 .split_view_states
3341 .get(&active_split)
3342 .and_then(|vs| vs.sync_group);
3343 let splits_to_scroll = if let Some(group_id) = sync_group {
3344 self.split_manager
3345 .get_splits_in_group(group_id, &self.split_view_states)
3346 } else {
3347 vec![active_split]
3348 };
3349
3350 for split_id in splits_to_scroll {
3351 let buffer_id = if let Some(id) = self.split_manager.buffer_for_split(split_id) {
3352 id
3353 } else {
3354 continue;
3355 };
3356 let tab_size = self.config.editor.tab_size;
3357
3358 let view_transform_tokens = self
3360 .split_view_states
3361 .get(&split_id)
3362 .and_then(|vs| vs.view_transform.as_ref())
3363 .map(|vt| vt.tokens.clone());
3364
3365 if let Some(state) = self.buffers.get_mut(&buffer_id) {
3367 let buffer = &mut state.buffer;
3368 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
3369 if let Some(tokens) = view_transform_tokens {
3370 let view_lines: Vec<_> =
3372 ViewLineIterator::new(&tokens, false, false, tab_size, false).collect();
3373 view_state
3374 .viewport
3375 .scroll_view_lines(&view_lines, line_offset);
3376 } else {
3377 if line_offset > 0 {
3379 view_state
3380 .viewport
3381 .scroll_down(buffer, line_offset as usize);
3382 } else {
3383 view_state
3384 .viewport
3385 .scroll_up(buffer, line_offset.unsigned_abs());
3386 }
3387 }
3388 view_state.viewport.set_skip_ensure_visible();
3390 }
3391 }
3392 }
3393 }
3394
3395 fn handle_set_viewport_event(&mut self, top_line: usize) {
3397 let active_split = self.split_manager.active_split();
3398
3399 if self
3402 .scroll_sync_manager
3403 .is_split_synced(active_split.into())
3404 {
3405 if let Some(group) = self
3406 .scroll_sync_manager
3407 .find_group_for_split_mut(active_split.into())
3408 {
3409 let scroll_line = if group.is_left_split(active_split.into()) {
3411 top_line
3412 } else {
3413 group.right_to_left_line(top_line)
3414 };
3415 group.set_scroll_line(scroll_line);
3416 }
3417
3418 if let Some(group) = self
3420 .scroll_sync_manager
3421 .find_group_for_split(active_split.into())
3422 {
3423 let left = group.left_split;
3424 let right = group.right_split;
3425 if let Some(vs) = self.split_view_states.get_mut(&LeafId(left)) {
3426 vs.viewport.set_skip_ensure_visible();
3427 }
3428 if let Some(vs) = self.split_view_states.get_mut(&LeafId(right)) {
3429 vs.viewport.set_skip_ensure_visible();
3430 }
3431 }
3432 return;
3433 }
3434
3435 let sync_group = self
3437 .split_view_states
3438 .get(&active_split)
3439 .and_then(|vs| vs.sync_group);
3440 let splits_to_scroll = if let Some(group_id) = sync_group {
3441 self.split_manager
3442 .get_splits_in_group(group_id, &self.split_view_states)
3443 } else {
3444 vec![active_split]
3445 };
3446
3447 for split_id in splits_to_scroll {
3448 let buffer_id = if let Some(id) = self.split_manager.buffer_for_split(split_id) {
3449 id
3450 } else {
3451 continue;
3452 };
3453
3454 if let Some(state) = self.buffers.get_mut(&buffer_id) {
3455 let buffer = &mut state.buffer;
3456 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
3457 view_state.viewport.scroll_to(buffer, top_line);
3458 view_state.viewport.set_skip_ensure_visible();
3460 }
3461 }
3462 }
3463 }
3464
3465 fn handle_recenter_event(&mut self) {
3467 let active_split = self.split_manager.active_split();
3468
3469 let sync_group = self
3471 .split_view_states
3472 .get(&active_split)
3473 .and_then(|vs| vs.sync_group);
3474 let splits_to_recenter = if let Some(group_id) = sync_group {
3475 self.split_manager
3476 .get_splits_in_group(group_id, &self.split_view_states)
3477 } else {
3478 vec![active_split]
3479 };
3480
3481 for split_id in splits_to_recenter {
3482 let buffer_id = if let Some(id) = self.split_manager.buffer_for_split(split_id) {
3483 id
3484 } else {
3485 continue;
3486 };
3487
3488 if let Some(state) = self.buffers.get_mut(&buffer_id) {
3489 let buffer = &mut state.buffer;
3490 let view_state = self.split_view_states.get_mut(&split_id);
3491
3492 if let Some(view_state) = view_state {
3493 let cursor = *view_state.cursors.primary();
3495 let viewport_height = view_state.viewport.visible_line_count();
3496 let target_rows_from_top = viewport_height / 2;
3497
3498 let mut iter = buffer.line_iterator(cursor.position, 80);
3500 for _ in 0..target_rows_from_top {
3501 if iter.prev().is_none() {
3502 break;
3503 }
3504 }
3505 let new_top_byte = iter.current_position();
3506 view_state.viewport.top_byte = new_top_byte;
3507 view_state.viewport.set_skip_ensure_visible();
3509 }
3510 }
3511 }
3512 }
3513
3514 fn invalidate_layouts_for_buffer(&mut self, buffer_id: BufferId) {
3521 let splits_for_buffer = self.split_manager.splits_for_buffer(buffer_id);
3523
3524 for split_id in splits_for_buffer {
3526 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
3527 view_state.invalidate_layout();
3528 view_state.view_transform = None;
3532 view_state.view_transform_stale = true;
3535 }
3536 }
3537 }
3538
3539 pub fn active_event_log(&self) -> &EventLog {
3541 self.event_logs.get(&self.active_buffer()).unwrap()
3542 }
3543
3544 pub fn active_event_log_mut(&mut self) -> &mut EventLog {
3546 self.event_logs.get_mut(&self.active_buffer()).unwrap()
3547 }
3548
3549 pub(super) fn update_modified_from_event_log(&mut self) {
3553 let is_at_saved = self
3554 .event_logs
3555 .get(&self.active_buffer())
3556 .map(|log| log.is_at_saved_position())
3557 .unwrap_or(false);
3558
3559 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
3560 state.buffer.set_modified(!is_at_saved);
3561 }
3562 }
3563
3564 pub fn should_quit(&self) -> bool {
3566 self.should_quit
3567 }
3568
3569 pub fn should_detach(&self) -> bool {
3571 self.should_detach
3572 }
3573
3574 pub fn clear_detach(&mut self) {
3576 self.should_detach = false;
3577 }
3578
3579 pub fn set_session_mode(&mut self, session_mode: bool) {
3581 self.session_mode = session_mode;
3582 self.clipboard.set_session_mode(session_mode);
3583 if session_mode {
3585 self.active_custom_contexts
3586 .insert(crate::types::context_keys::SESSION_MODE.to_string());
3587 } else {
3588 self.active_custom_contexts
3589 .remove(crate::types::context_keys::SESSION_MODE);
3590 }
3591 }
3592
3593 pub fn is_session_mode(&self) -> bool {
3595 self.session_mode
3596 }
3597
3598 pub fn set_software_cursor_only(&mut self, enabled: bool) {
3601 self.software_cursor_only = enabled;
3602 }
3603
3604 pub fn set_session_name(&mut self, name: Option<String>) {
3610 if let Some(ref session_name) = name {
3611 let base_recovery_dir = self.dir_context.recovery_dir();
3612 let scope = crate::services::recovery::RecoveryScope::Session {
3613 name: session_name.clone(),
3614 };
3615 let recovery_config = RecoveryConfig {
3616 enabled: self.recovery_service.is_enabled(),
3617 ..RecoveryConfig::default()
3618 };
3619 self.recovery_service =
3620 RecoveryService::with_scope(recovery_config, &base_recovery_dir, &scope);
3621 }
3622 self.session_name = name;
3623 }
3624
3625 pub fn session_name(&self) -> Option<&str> {
3627 self.session_name.as_deref()
3628 }
3629
3630 pub fn queue_escape_sequences(&mut self, sequences: &[u8]) {
3632 self.pending_escape_sequences.extend_from_slice(sequences);
3633 }
3634
3635 pub fn take_pending_escape_sequences(&mut self) -> Vec<u8> {
3637 std::mem::take(&mut self.pending_escape_sequences)
3638 }
3639
3640 pub fn take_pending_clipboard(
3642 &mut self,
3643 ) -> Option<crate::services::clipboard::PendingClipboard> {
3644 self.clipboard.take_pending_clipboard()
3645 }
3646
3647 pub fn should_restart(&self) -> bool {
3649 self.restart_with_dir.is_some()
3650 }
3651
3652 pub fn take_restart_dir(&mut self) -> Option<PathBuf> {
3655 self.restart_with_dir.take()
3656 }
3657
3658 pub fn request_full_redraw(&mut self) {
3663 self.full_redraw_requested = true;
3664 }
3665
3666 pub fn take_full_redraw_request(&mut self) -> bool {
3668 let requested = self.full_redraw_requested;
3669 self.full_redraw_requested = false;
3670 requested
3671 }
3672
3673 pub fn request_restart(&mut self, new_working_dir: PathBuf) {
3674 tracing::info!(
3675 "Restart requested with new working directory: {}",
3676 new_working_dir.display()
3677 );
3678 self.restart_with_dir = Some(new_working_dir);
3679 self.should_quit = true;
3681 }
3682
3683 pub fn theme(&self) -> &crate::view::theme::Theme {
3685 &self.theme
3686 }
3687
3688 pub fn is_settings_open(&self) -> bool {
3690 self.settings_state.as_ref().is_some_and(|s| s.visible)
3691 }
3692
3693 pub fn quit(&mut self) {
3695 let modified_count = self.count_modified_buffers_needing_prompt();
3697 if modified_count > 0 {
3698 let save_key = t!("prompt.key.save").to_string();
3699 let cancel_key = t!("prompt.key.cancel").to_string();
3700 let hot_exit = self.config.editor.hot_exit;
3701
3702 let msg = if hot_exit {
3703 let quit_key = t!("prompt.key.quit").to_string();
3705 if modified_count == 1 {
3706 t!(
3707 "prompt.quit_modified_hot_one",
3708 save_key = save_key,
3709 quit_key = quit_key,
3710 cancel_key = cancel_key
3711 )
3712 .to_string()
3713 } else {
3714 t!(
3715 "prompt.quit_modified_hot_many",
3716 count = modified_count,
3717 save_key = save_key,
3718 quit_key = quit_key,
3719 cancel_key = cancel_key
3720 )
3721 .to_string()
3722 }
3723 } else {
3724 let discard_key = t!("prompt.key.discard").to_string();
3726 if modified_count == 1 {
3727 t!(
3728 "prompt.quit_modified_one",
3729 save_key = save_key,
3730 discard_key = discard_key,
3731 cancel_key = cancel_key
3732 )
3733 .to_string()
3734 } else {
3735 t!(
3736 "prompt.quit_modified_many",
3737 count = modified_count,
3738 save_key = save_key,
3739 discard_key = discard_key,
3740 cancel_key = cancel_key
3741 )
3742 .to_string()
3743 }
3744 };
3745 self.start_prompt(msg, PromptType::ConfirmQuitWithModified);
3746 } else {
3747 self.should_quit = true;
3748 }
3749 }
3750
3751 fn count_modified_buffers_needing_prompt(&self) -> usize {
3759 let hot_exit = self.config.editor.hot_exit;
3760 let auto_save = self.config.editor.auto_save_enabled;
3761
3762 self.buffers
3763 .iter()
3764 .filter(|(buffer_id, state)| {
3765 if !state.buffer.is_modified() {
3766 return false;
3767 }
3768 if let Some(meta) = self.buffer_metadata.get(buffer_id) {
3769 if let Some(path) = meta.file_path() {
3770 let is_unnamed = path.as_os_str().is_empty();
3771 if is_unnamed && hot_exit {
3772 return false; }
3774 if !is_unnamed && auto_save {
3775 return false; }
3777 }
3778 }
3779 true
3780 })
3781 .count()
3782 }
3783
3784 pub fn focus_gained(&mut self) {
3786 self.plugin_manager.run_hook(
3787 "focus_gained",
3788 crate::services::plugins::hooks::HookArgs::FocusGained,
3789 );
3790 }
3791
3792 pub fn resize(&mut self, width: u16, height: u16) {
3794 self.terminal_width = width;
3796 self.terminal_height = height;
3797
3798 for view_state in self.split_view_states.values_mut() {
3800 view_state.viewport.resize(width, height);
3801 }
3802
3803 self.resize_visible_terminals();
3805
3806 self.plugin_manager.run_hook(
3808 "resize",
3809 fresh_core::hooks::HookArgs::Resize { width, height },
3810 );
3811 }
3812
3813 pub fn start_prompt(&mut self, message: String, prompt_type: PromptType) {
3817 self.start_prompt_with_suggestions(message, prompt_type, Vec::new());
3818 }
3819
3820 fn start_search_prompt(
3825 &mut self,
3826 message: String,
3827 prompt_type: PromptType,
3828 use_selection_range: bool,
3829 ) {
3830 self.pending_search_range = None;
3832
3833 let selection_range = self.active_cursors().primary().selection_range();
3834
3835 let selected_text = if let Some(range) = selection_range.clone() {
3836 let state = self.active_state_mut();
3837 let text = state.get_text_range(range.start, range.end);
3838 if !text.contains('\n') && !text.is_empty() {
3839 Some(text)
3840 } else {
3841 None
3842 }
3843 } else {
3844 None
3845 };
3846
3847 if use_selection_range {
3848 self.pending_search_range = selection_range;
3849 }
3850
3851 let from_history = selected_text.is_none();
3853 let default_text = selected_text.or_else(|| {
3854 self.get_prompt_history("search")
3855 .and_then(|h| h.last().map(|s| s.to_string()))
3856 });
3857
3858 self.start_prompt(message, prompt_type);
3860
3861 if let Some(text) = default_text {
3863 if let Some(ref mut prompt) = self.prompt {
3864 prompt.set_input(text.clone());
3865 prompt.selection_anchor = Some(0);
3866 prompt.cursor_pos = text.len();
3867 }
3868 if from_history {
3869 self.get_or_create_prompt_history("search").init_at_last();
3870 }
3871 self.update_search_highlights(&text);
3872 }
3873 }
3874
3875 pub fn start_prompt_with_suggestions(
3877 &mut self,
3878 message: String,
3879 prompt_type: PromptType,
3880 suggestions: Vec<Suggestion>,
3881 ) {
3882 self.on_editor_focus_lost();
3884
3885 match prompt_type {
3888 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
3889 self.clear_search_highlights();
3890 }
3891 _ => {}
3892 }
3893
3894 let needs_suggestions = matches!(
3896 prompt_type,
3897 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
3898 );
3899
3900 self.prompt = Some(Prompt::with_suggestions(message, prompt_type, suggestions));
3901
3902 if needs_suggestions {
3904 self.update_prompt_suggestions();
3905 }
3906 }
3907
3908 pub fn start_prompt_with_initial_text(
3910 &mut self,
3911 message: String,
3912 prompt_type: PromptType,
3913 initial_text: String,
3914 ) {
3915 self.on_editor_focus_lost();
3917
3918 self.prompt = Some(Prompt::with_initial_text(
3919 message,
3920 prompt_type,
3921 initial_text,
3922 ));
3923 }
3924
3925 pub fn start_quick_open(&mut self) {
3927 self.on_editor_focus_lost();
3929
3930 self.status_message = None;
3932
3933 let mut prompt = Prompt::with_suggestions(String::new(), PromptType::QuickOpen, vec![]);
3935 prompt.input = ">".to_string();
3936 prompt.cursor_pos = 1;
3937 self.prompt = Some(prompt);
3938
3939 self.update_quick_open_suggestions(">");
3941 }
3942
3943 fn build_quick_open_context(&self) -> QuickOpenContext {
3945 let open_buffers = self
3946 .buffers
3947 .iter()
3948 .filter_map(|(buffer_id, state)| {
3949 let path = state.buffer.file_path()?;
3950 let name = path
3951 .file_name()
3952 .map(|n| n.to_string_lossy().to_string())
3953 .unwrap_or_else(|| format!("Buffer {}", buffer_id.0));
3954 Some(BufferInfo {
3955 id: buffer_id.0,
3956 path: path.display().to_string(),
3957 name,
3958 modified: state.buffer.is_modified(),
3959 })
3960 })
3961 .collect();
3962
3963 let has_lsp_config = {
3964 let language = self
3965 .buffers
3966 .get(&self.active_buffer())
3967 .map(|s| s.language.as_str());
3968 language
3969 .and_then(|lang| self.lsp.as_ref().and_then(|lsp| lsp.get_config(lang)))
3970 .is_some()
3971 };
3972
3973 QuickOpenContext {
3974 cwd: self.working_dir.display().to_string(),
3975 open_buffers,
3976 active_buffer_id: self.active_buffer().0,
3977 active_buffer_path: self
3978 .active_state()
3979 .buffer
3980 .file_path()
3981 .map(|p| p.display().to_string()),
3982 has_selection: self.has_active_selection(),
3983 key_context: self.key_context.clone(),
3984 custom_contexts: self.active_custom_contexts.clone(),
3985 buffer_mode: self
3986 .buffer_metadata
3987 .get(&self.active_buffer())
3988 .and_then(|m| m.virtual_mode())
3989 .map(|s| s.to_string()),
3990 has_lsp_config,
3991 }
3992 }
3993
3994 fn update_quick_open_suggestions(&mut self, input: &str) {
3996 let context = self.build_quick_open_context();
3997 let suggestions = if let Some((provider, query)) =
3998 self.quick_open_registry.get_provider_for_input(input)
3999 {
4000 provider.suggestions(query, &context)
4001 } else {
4002 vec![]
4003 };
4004
4005 if let Some(prompt) = &mut self.prompt {
4006 prompt.suggestions = suggestions;
4007 prompt.selected_suggestion = if prompt.suggestions.is_empty() {
4008 None
4009 } else {
4010 Some(0)
4011 };
4012 }
4013 }
4014
4015 fn cancel_search_prompt_if_active(&mut self) {
4018 if let Some(ref prompt) = self.prompt {
4019 if matches!(
4020 prompt.prompt_type,
4021 PromptType::Search
4022 | PromptType::ReplaceSearch
4023 | PromptType::Replace { .. }
4024 | PromptType::QueryReplaceSearch
4025 | PromptType::QueryReplace { .. }
4026 | PromptType::QueryReplaceConfirm
4027 ) {
4028 self.prompt = None;
4029 self.interactive_replace_state = None;
4031 let ns = self.search_namespace.clone();
4033 let state = self.active_state_mut();
4034 state.overlays.clear_namespace(&ns, &mut state.marker_list);
4035 }
4036 }
4037 }
4038
4039 fn prefill_open_file_prompt(&mut self) {
4041 if let Some(prompt) = self.prompt.as_mut() {
4045 if prompt.prompt_type == PromptType::OpenFile {
4046 prompt.input.clear();
4047 prompt.cursor_pos = 0;
4048 prompt.selection_anchor = None;
4049 }
4050 }
4051 }
4052
4053 fn init_file_open_state(&mut self) {
4059 let buffer_id = self.active_buffer();
4061
4062 let initial_dir = if self.is_terminal_buffer(buffer_id) {
4065 self.get_terminal_id(buffer_id)
4066 .and_then(|tid| self.terminal_manager.get(tid))
4067 .and_then(|handle| handle.cwd())
4068 .unwrap_or_else(|| self.working_dir.clone())
4069 } else {
4070 self.active_state()
4071 .buffer
4072 .file_path()
4073 .and_then(|path| path.parent())
4074 .map(|p| p.to_path_buf())
4075 .unwrap_or_else(|| self.working_dir.clone())
4076 };
4077
4078 let show_hidden = self.config.file_browser.show_hidden;
4080 self.file_open_state = Some(file_open::FileOpenState::new(
4081 initial_dir.clone(),
4082 show_hidden,
4083 self.filesystem.clone(),
4084 ));
4085
4086 self.load_file_open_directory(initial_dir);
4088 self.load_file_open_shortcuts_async();
4089 }
4090
4091 fn init_folder_open_state(&mut self) {
4096 let initial_dir = self.working_dir.clone();
4098
4099 let show_hidden = self.config.file_browser.show_hidden;
4101 self.file_open_state = Some(file_open::FileOpenState::new(
4102 initial_dir.clone(),
4103 show_hidden,
4104 self.filesystem.clone(),
4105 ));
4106
4107 self.load_file_open_directory(initial_dir);
4109 self.load_file_open_shortcuts_async();
4110 }
4111
4112 pub fn change_working_dir(&mut self, new_path: PathBuf) {
4122 let new_path = new_path.canonicalize().unwrap_or(new_path);
4124
4125 self.request_restart(new_path);
4128 }
4129
4130 fn load_file_open_directory(&mut self, path: PathBuf) {
4132 if let Some(state) = &mut self.file_open_state {
4134 state.current_dir = path.clone();
4135 state.loading = true;
4136 state.error = None;
4137 state.update_shortcuts();
4138 }
4139
4140 if let Some(ref runtime) = self.tokio_runtime {
4142 let fs_manager = self.fs_manager.clone();
4143 let sender = self.async_bridge.as_ref().map(|b| b.sender());
4144
4145 runtime.spawn(async move {
4146 let result = fs_manager.list_dir_with_metadata(path).await;
4147 if let Some(sender) = sender {
4148 #[allow(clippy::let_underscore_must_use)]
4150 let _ = sender.send(AsyncMessage::FileOpenDirectoryLoaded(result));
4151 }
4152 });
4153 } else {
4154 if let Some(state) = &mut self.file_open_state {
4156 state.set_error("Async runtime not available".to_string());
4157 }
4158 }
4159 }
4160
4161 pub(super) fn handle_file_open_directory_loaded(
4163 &mut self,
4164 result: std::io::Result<Vec<crate::services::fs::DirEntry>>,
4165 ) {
4166 match result {
4167 Ok(entries) => {
4168 if let Some(state) = &mut self.file_open_state {
4169 state.set_entries(entries);
4170 }
4171 let filter = self
4173 .prompt
4174 .as_ref()
4175 .map(|p| p.input.clone())
4176 .unwrap_or_default();
4177 if !filter.is_empty() {
4178 if let Some(state) = &mut self.file_open_state {
4179 state.apply_filter(&filter);
4180 }
4181 }
4182 }
4183 Err(e) => {
4184 if let Some(state) = &mut self.file_open_state {
4185 state.set_error(e.to_string());
4186 }
4187 }
4188 }
4189 }
4190
4191 fn load_file_open_shortcuts_async(&mut self) {
4195 if let Some(ref runtime) = self.tokio_runtime {
4196 let filesystem = self.filesystem.clone();
4197 let sender = self.async_bridge.as_ref().map(|b| b.sender());
4198
4199 runtime.spawn(async move {
4200 let shortcuts = tokio::task::spawn_blocking(move || {
4202 file_open::FileOpenState::build_shortcuts_async(&*filesystem)
4203 })
4204 .await
4205 .unwrap_or_default();
4206
4207 if let Some(sender) = sender {
4208 #[allow(clippy::let_underscore_must_use)]
4210 let _ = sender.send(AsyncMessage::FileOpenShortcutsLoaded(shortcuts));
4211 }
4212 });
4213 }
4214 }
4215
4216 pub(super) fn handle_file_open_shortcuts_loaded(
4218 &mut self,
4219 shortcuts: Vec<file_open::NavigationShortcut>,
4220 ) {
4221 if let Some(state) = &mut self.file_open_state {
4222 state.merge_async_shortcuts(shortcuts);
4223 }
4224 }
4225
4226 pub fn cancel_prompt(&mut self) {
4228 let theme_to_restore = if let Some(ref prompt) = self.prompt {
4230 if let PromptType::SelectTheme { original_theme } = &prompt.prompt_type {
4231 Some(original_theme.clone())
4232 } else {
4233 None
4234 }
4235 } else {
4236 None
4237 };
4238
4239 if let Some(ref prompt) = self.prompt {
4241 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
4243 if let Some(history) = self.prompt_histories.get_mut(&key) {
4244 history.reset_navigation();
4245 }
4246 }
4247 match &prompt.prompt_type {
4248 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
4249 self.clear_search_highlights();
4250 }
4251 PromptType::Plugin { custom_type } => {
4252 use crate::services::plugins::hooks::HookArgs;
4254 self.plugin_manager.run_hook(
4255 "prompt_cancelled",
4256 HookArgs::PromptCancelled {
4257 prompt_type: custom_type.clone(),
4258 input: prompt.input.clone(),
4259 },
4260 );
4261 }
4262 PromptType::LspRename { overlay_handle, .. } => {
4263 let remove_overlay_event = crate::model::event::Event::RemoveOverlay {
4265 handle: overlay_handle.clone(),
4266 };
4267 self.apply_event_to_active_buffer(&remove_overlay_event);
4268 }
4269 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs => {
4270 self.file_open_state = None;
4272 self.file_browser_layout = None;
4273 }
4274 PromptType::AsyncPrompt => {
4275 if let Some(callback_id) = self.pending_async_prompt_callback.take() {
4277 self.plugin_manager
4278 .resolve_callback(callback_id, "null".to_string());
4279 }
4280 }
4281 PromptType::QuickOpen => {
4282 if let Some((provider, _)) = self.quick_open_registry.get_provider_for_input("")
4284 {
4285 if let Some(fp) = provider
4286 .as_any()
4287 .downcast_ref::<crate::input::quick_open::providers::FileProvider>(
4288 ) {
4289 fp.cancel_loading();
4290 }
4291 }
4292 }
4293 _ => {}
4294 }
4295 }
4296
4297 self.prompt = None;
4298 self.pending_search_range = None;
4299 self.status_message = Some(t!("search.cancelled").to_string());
4300
4301 if let Some(original_theme) = theme_to_restore {
4303 self.preview_theme(&original_theme);
4304 }
4305 }
4306
4307 pub fn handle_prompt_scroll(&mut self, delta: i32) -> bool {
4310 if let Some(ref mut prompt) = self.prompt {
4311 if prompt.suggestions.is_empty() {
4312 return false;
4313 }
4314
4315 let current = prompt.selected_suggestion.unwrap_or(0);
4316 let len = prompt.suggestions.len();
4317
4318 let new_selected = if delta < 0 {
4321 current.saturating_sub((-delta) as usize)
4323 } else {
4324 (current + delta as usize).min(len.saturating_sub(1))
4326 };
4327
4328 prompt.selected_suggestion = Some(new_selected);
4329
4330 if !matches!(prompt.prompt_type, PromptType::Plugin { .. }) {
4332 if let Some(suggestion) = prompt.suggestions.get(new_selected) {
4333 prompt.input = suggestion.get_value().to_string();
4334 prompt.cursor_pos = prompt.input.len();
4335 }
4336 }
4337
4338 return true;
4339 }
4340 false
4341 }
4342
4343 pub fn confirm_prompt(&mut self) -> Option<(String, PromptType, Option<usize>)> {
4348 if let Some(prompt) = self.prompt.take() {
4349 let selected_index = prompt.selected_suggestion;
4350 let mut final_input = if prompt.sync_input_on_navigate {
4352 prompt.input.clone()
4355 } else if matches!(
4356 prompt.prompt_type,
4357 PromptType::OpenFile
4358 | PromptType::SwitchProject
4359 | PromptType::SaveFileAs
4360 | PromptType::StopLspServer
4361 | PromptType::RestartLspServer
4362 | PromptType::SelectTheme { .. }
4363 | PromptType::SelectLocale
4364 | PromptType::SwitchToTab
4365 | PromptType::SetLanguage
4366 | PromptType::SetEncoding
4367 | PromptType::SetLineEnding
4368 | PromptType::Plugin { .. }
4369 ) {
4370 if let Some(selected_idx) = prompt.selected_suggestion {
4372 if let Some(suggestion) = prompt.suggestions.get(selected_idx) {
4373 if suggestion.disabled {
4375 self.set_status_message(
4376 t!(
4377 "error.command_not_available",
4378 command = suggestion.text.clone()
4379 )
4380 .to_string(),
4381 );
4382 return None;
4383 }
4384 suggestion.get_value().to_string()
4386 } else {
4387 prompt.input.clone()
4388 }
4389 } else {
4390 prompt.input.clone()
4391 }
4392 } else {
4393 prompt.input.clone()
4394 };
4395
4396 if matches!(
4398 prompt.prompt_type,
4399 PromptType::StopLspServer | PromptType::RestartLspServer
4400 ) {
4401 let is_valid = prompt
4402 .suggestions
4403 .iter()
4404 .any(|s| s.text == final_input || s.get_value() == final_input);
4405 if !is_valid {
4406 self.prompt = Some(prompt);
4408 self.set_status_message(
4409 t!("error.no_lsp_match", input = final_input.clone()).to_string(),
4410 );
4411 return None;
4412 }
4413 }
4414
4415 if matches!(prompt.prompt_type, PromptType::RemoveRuler) {
4419 if prompt.input.is_empty() {
4420 if let Some(selected_idx) = prompt.selected_suggestion {
4422 if let Some(suggestion) = prompt.suggestions.get(selected_idx) {
4423 final_input = suggestion.get_value().to_string();
4424 }
4425 } else {
4426 self.prompt = Some(prompt);
4427 return None;
4428 }
4429 } else {
4430 let typed = prompt.input.trim().to_string();
4432 let matched = prompt.suggestions.iter().find(|s| s.get_value() == typed);
4433 if let Some(suggestion) = matched {
4434 final_input = suggestion.get_value().to_string();
4435 } else {
4436 self.prompt = Some(prompt);
4438 return None;
4439 }
4440 }
4441 }
4442
4443 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
4445 let history = self.get_or_create_prompt_history(&key);
4446 history.push(final_input.clone());
4447 history.reset_navigation();
4448 }
4449
4450 Some((final_input, prompt.prompt_type, selected_index))
4451 } else {
4452 None
4453 }
4454 }
4455
4456 pub fn is_prompting(&self) -> bool {
4458 self.prompt.is_some()
4459 }
4460
4461 fn get_or_create_prompt_history(
4463 &mut self,
4464 key: &str,
4465 ) -> &mut crate::input::input_history::InputHistory {
4466 self.prompt_histories.entry(key.to_string()).or_default()
4467 }
4468
4469 fn get_prompt_history(&self, key: &str) -> Option<&crate::input::input_history::InputHistory> {
4471 self.prompt_histories.get(key)
4472 }
4473
4474 fn prompt_type_to_history_key(prompt_type: &crate::view::prompt::PromptType) -> Option<String> {
4476 use crate::view::prompt::PromptType;
4477 match prompt_type {
4478 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
4479 Some("search".to_string())
4480 }
4481 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
4482 Some("replace".to_string())
4483 }
4484 PromptType::GotoLine => Some("goto_line".to_string()),
4485 PromptType::Plugin { custom_type } => Some(format!("plugin:{}", custom_type)),
4486 _ => None,
4487 }
4488 }
4489
4490 pub fn editor_mode(&self) -> Option<String> {
4493 self.editor_mode.clone()
4494 }
4495
4496 pub fn command_registry(&self) -> &Arc<RwLock<CommandRegistry>> {
4498 &self.command_registry
4499 }
4500
4501 pub fn plugin_manager(&self) -> &PluginManager {
4503 &self.plugin_manager
4504 }
4505
4506 pub fn plugin_manager_mut(&mut self) -> &mut PluginManager {
4508 &mut self.plugin_manager
4509 }
4510
4511 pub fn file_explorer_is_focused(&self) -> bool {
4513 self.key_context == KeyContext::FileExplorer
4514 }
4515
4516 pub fn prompt_input(&self) -> Option<&str> {
4518 self.prompt.as_ref().map(|p| p.input.as_str())
4519 }
4520
4521 pub fn has_active_selection(&self) -> bool {
4523 self.active_cursors().primary().selection_range().is_some()
4524 }
4525
4526 pub fn prompt_mut(&mut self) -> Option<&mut Prompt> {
4528 self.prompt.as_mut()
4529 }
4530
4531 pub fn set_status_message(&mut self, message: String) {
4533 tracing::info!(target: "status", "{}", message);
4534 self.plugin_status_message = None;
4535 self.status_message = Some(message);
4536 }
4537
4538 pub fn get_status_message(&self) -> Option<&String> {
4540 self.plugin_status_message
4541 .as_ref()
4542 .or(self.status_message.as_ref())
4543 }
4544
4545 pub fn get_plugin_errors(&self) -> &[String] {
4548 &self.plugin_errors
4549 }
4550
4551 pub fn clear_plugin_errors(&mut self) {
4553 self.plugin_errors.clear();
4554 }
4555
4556 pub fn update_prompt_suggestions(&mut self) {
4558 let (prompt_type, input) = if let Some(prompt) = &self.prompt {
4560 (prompt.prompt_type.clone(), prompt.input.clone())
4561 } else {
4562 return;
4563 };
4564
4565 match prompt_type {
4566 PromptType::QuickOpen => {
4567 self.update_quick_open_suggestions(&input);
4569 }
4570 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
4571 self.update_search_highlights(&input);
4573 if let Some(history) = self.prompt_histories.get_mut("search") {
4575 history.reset_navigation();
4576 }
4577 }
4578 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
4579 if let Some(history) = self.prompt_histories.get_mut("replace") {
4581 history.reset_navigation();
4582 }
4583 }
4584 PromptType::GotoLine => {
4585 if let Some(history) = self.prompt_histories.get_mut("goto_line") {
4587 history.reset_navigation();
4588 }
4589 }
4590 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs => {
4591 self.update_file_open_filter();
4593 }
4594 PromptType::Plugin { custom_type } => {
4595 let key = format!("plugin:{}", custom_type);
4597 if let Some(history) = self.prompt_histories.get_mut(&key) {
4598 history.reset_navigation();
4599 }
4600 use crate::services::plugins::hooks::HookArgs;
4602 self.plugin_manager.run_hook(
4603 "prompt_changed",
4604 HookArgs::PromptChanged {
4605 prompt_type: custom_type,
4606 input,
4607 },
4608 );
4609 if let Some(prompt) = &mut self.prompt {
4614 prompt.filter_suggestions(false);
4615 }
4616 }
4617 PromptType::SwitchToTab
4618 | PromptType::SelectTheme { .. }
4619 | PromptType::StopLspServer
4620 | PromptType::RestartLspServer
4621 | PromptType::SetLanguage
4622 | PromptType::SetEncoding
4623 | PromptType::SetLineEnding => {
4624 if let Some(prompt) = &mut self.prompt {
4625 prompt.filter_suggestions(false);
4626 }
4627 }
4628 PromptType::SelectLocale => {
4629 if let Some(prompt) = &mut self.prompt {
4631 prompt.filter_suggestions(true);
4632 }
4633 }
4634 _ => {}
4635 }
4636 }
4637
4638 pub fn process_async_messages(&mut self) -> bool {
4646 self.plugin_manager.check_thread_health();
4649
4650 let Some(bridge) = &self.async_bridge else {
4651 return false;
4652 };
4653
4654 let messages = {
4655 let _s = tracing::info_span!("try_recv_all").entered();
4656 bridge.try_recv_all()
4657 };
4658 let needs_render = !messages.is_empty();
4659 tracing::trace!(
4660 async_message_count = messages.len(),
4661 "received async messages"
4662 );
4663
4664 for message in messages {
4665 match message {
4666 AsyncMessage::LspDiagnostics {
4667 uri,
4668 diagnostics,
4669 server_name,
4670 } => {
4671 self.handle_lsp_diagnostics(uri, diagnostics, server_name);
4672 }
4673 AsyncMessage::LspInitialized {
4674 language,
4675 server_name,
4676 capabilities,
4677 } => {
4678 tracing::info!(
4679 "LSP server '{}' initialized for language: {}",
4680 server_name,
4681 language
4682 );
4683 self.status_message = Some(format!("LSP ({}) ready", language));
4684
4685 if let Some(lsp) = &mut self.lsp {
4687 lsp.set_server_capabilities(&language, &server_name, capabilities);
4688 }
4689
4690 self.resend_did_open_for_language(&language);
4692 self.request_semantic_tokens_for_language(&language);
4693 self.request_folding_ranges_for_language(&language);
4694 }
4695 AsyncMessage::LspError {
4696 language,
4697 error,
4698 stderr_log_path,
4699 } => {
4700 tracing::error!("LSP error for {}: {}", language, error);
4701 self.status_message = Some(format!("LSP error ({}): {}", language, error));
4702
4703 let server_command = self
4705 .config
4706 .lsp
4707 .get(&language)
4708 .and_then(|configs| configs.as_slice().first())
4709 .map(|c| c.command.clone())
4710 .unwrap_or_else(|| "unknown".to_string());
4711
4712 let error_type = if error.contains("not found") || error.contains("NotFound") {
4714 "not_found"
4715 } else if error.contains("permission") || error.contains("PermissionDenied") {
4716 "spawn_failed"
4717 } else if error.contains("timeout") {
4718 "timeout"
4719 } else {
4720 "spawn_failed"
4721 }
4722 .to_string();
4723
4724 self.plugin_manager.run_hook(
4726 "lsp_server_error",
4727 crate::services::plugins::hooks::HookArgs::LspServerError {
4728 language: language.clone(),
4729 server_command,
4730 error_type,
4731 message: error.clone(),
4732 },
4733 );
4734
4735 if let Some(log_path) = stderr_log_path {
4738 let has_content = log_path.metadata().map(|m| m.len() > 0).unwrap_or(false);
4739 if has_content {
4740 tracing::info!("Opening LSP stderr log in background: {:?}", log_path);
4741 match self.open_file_no_focus(&log_path) {
4742 Ok(buffer_id) => {
4743 self.mark_buffer_read_only(buffer_id, true);
4744 self.status_message = Some(format!(
4745 "LSP error ({}): {} - See stderr log",
4746 language, error
4747 ));
4748 }
4749 Err(e) => {
4750 tracing::error!("Failed to open LSP stderr log: {}", e);
4751 }
4752 }
4753 }
4754 }
4755 }
4756 AsyncMessage::LspCompletion { request_id, items } => {
4757 if let Err(e) = self.handle_completion_response(request_id, items) {
4758 tracing::error!("Error handling completion response: {}", e);
4759 }
4760 }
4761 AsyncMessage::LspGotoDefinition {
4762 request_id,
4763 locations,
4764 } => {
4765 if let Err(e) = self.handle_goto_definition_response(request_id, locations) {
4766 tracing::error!("Error handling goto definition response: {}", e);
4767 }
4768 }
4769 AsyncMessage::LspRename { request_id, result } => {
4770 if let Err(e) = self.handle_rename_response(request_id, result) {
4771 tracing::error!("Error handling rename response: {}", e);
4772 }
4773 }
4774 AsyncMessage::LspHover {
4775 request_id,
4776 contents,
4777 is_markdown,
4778 range,
4779 } => {
4780 self.handle_hover_response(request_id, contents, is_markdown, range);
4781 }
4782 AsyncMessage::LspReferences {
4783 request_id,
4784 locations,
4785 } => {
4786 if let Err(e) = self.handle_references_response(request_id, locations) {
4787 tracing::error!("Error handling references response: {}", e);
4788 }
4789 }
4790 AsyncMessage::LspSignatureHelp {
4791 request_id,
4792 signature_help,
4793 } => {
4794 self.handle_signature_help_response(request_id, signature_help);
4795 }
4796 AsyncMessage::LspCodeActions {
4797 request_id,
4798 actions,
4799 } => {
4800 self.handle_code_actions_response(request_id, actions);
4801 }
4802 AsyncMessage::LspApplyEdit { edit, label } => {
4803 tracing::info!("Applying workspace edit from server (label: {:?})", label);
4804 match self.apply_workspace_edit(edit) {
4805 Ok(n) => {
4806 if let Some(label) = label {
4807 self.set_status_message(
4808 t!("lsp.code_action_applied", title = &label, count = n)
4809 .to_string(),
4810 );
4811 }
4812 }
4813 Err(e) => {
4814 tracing::error!("Failed to apply workspace edit: {}", e);
4815 }
4816 }
4817 }
4818 AsyncMessage::LspCodeActionResolved {
4819 request_id: _,
4820 action,
4821 } => match action {
4822 Ok(resolved) => {
4823 self.execute_resolved_code_action(resolved);
4824 }
4825 Err(e) => {
4826 tracing::warn!("codeAction/resolve failed: {}", e);
4827 self.set_status_message(format!("Code action resolve failed: {e}"));
4828 }
4829 },
4830 AsyncMessage::LspCompletionResolved {
4831 request_id: _,
4832 item,
4833 } => {
4834 if let Ok(resolved) = item {
4835 self.handle_completion_resolved(resolved);
4836 }
4837 }
4838 AsyncMessage::LspFormatting {
4839 request_id: _,
4840 uri,
4841 edits,
4842 } => {
4843 if !edits.is_empty() {
4844 if let Err(e) = self.apply_formatting_edits(&uri, edits) {
4845 tracing::error!("Failed to apply formatting: {}", e);
4846 }
4847 }
4848 }
4849 AsyncMessage::LspPrepareRename {
4850 request_id: _,
4851 result,
4852 } => {
4853 self.handle_prepare_rename_response(result);
4854 }
4855 AsyncMessage::LspPulledDiagnostics {
4856 request_id: _,
4857 uri,
4858 result_id,
4859 diagnostics,
4860 unchanged,
4861 } => {
4862 self.handle_lsp_pulled_diagnostics(uri, result_id, diagnostics, unchanged);
4863 }
4864 AsyncMessage::LspInlayHints {
4865 request_id,
4866 uri,
4867 hints,
4868 } => {
4869 self.handle_lsp_inlay_hints(request_id, uri, hints);
4870 }
4871 AsyncMessage::LspFoldingRanges {
4872 request_id,
4873 uri,
4874 ranges,
4875 } => {
4876 self.handle_lsp_folding_ranges(request_id, uri, ranges);
4877 }
4878 AsyncMessage::LspSemanticTokens {
4879 request_id,
4880 uri,
4881 response,
4882 } => {
4883 self.handle_lsp_semantic_tokens(request_id, uri, response);
4884 }
4885 AsyncMessage::LspServerQuiescent { language } => {
4886 self.handle_lsp_server_quiescent(language);
4887 }
4888 AsyncMessage::LspDiagnosticRefresh { language } => {
4889 self.handle_lsp_diagnostic_refresh(language);
4890 }
4891 AsyncMessage::FileChanged { path } => {
4892 self.handle_async_file_changed(path);
4893 }
4894 AsyncMessage::GitStatusChanged { status } => {
4895 tracing::info!("Git status changed: {}", status);
4896 }
4898 AsyncMessage::FileExplorerInitialized(view) => {
4899 self.handle_file_explorer_initialized(view);
4900 }
4901 AsyncMessage::FileExplorerToggleNode(node_id) => {
4902 self.handle_file_explorer_toggle_node(node_id);
4903 }
4904 AsyncMessage::FileExplorerRefreshNode(node_id) => {
4905 self.handle_file_explorer_refresh_node(node_id);
4906 }
4907 AsyncMessage::FileExplorerExpandedToPath(view) => {
4908 self.handle_file_explorer_expanded_to_path(view);
4909 }
4910 AsyncMessage::Plugin(plugin_msg) => {
4911 use fresh_core::api::{JsCallbackId, PluginAsyncMessage};
4912 match plugin_msg {
4913 PluginAsyncMessage::ProcessOutput {
4914 process_id,
4915 stdout,
4916 stderr,
4917 exit_code,
4918 } => {
4919 self.handle_plugin_process_output(
4920 JsCallbackId::from(process_id),
4921 stdout,
4922 stderr,
4923 exit_code,
4924 );
4925 }
4926 PluginAsyncMessage::DelayComplete { callback_id } => {
4927 self.plugin_manager.resolve_callback(
4928 JsCallbackId::from(callback_id),
4929 "null".to_string(),
4930 );
4931 }
4932 PluginAsyncMessage::ProcessStdout { process_id, data } => {
4933 self.plugin_manager.run_hook(
4934 "onProcessStdout",
4935 crate::services::plugins::hooks::HookArgs::ProcessOutput {
4936 process_id,
4937 data,
4938 },
4939 );
4940 }
4941 PluginAsyncMessage::ProcessStderr { process_id, data } => {
4942 self.plugin_manager.run_hook(
4943 "onProcessStderr",
4944 crate::services::plugins::hooks::HookArgs::ProcessOutput {
4945 process_id,
4946 data,
4947 },
4948 );
4949 }
4950 PluginAsyncMessage::ProcessExit {
4951 process_id,
4952 callback_id,
4953 exit_code,
4954 } => {
4955 self.background_process_handles.remove(&process_id);
4956 let result = fresh_core::api::BackgroundProcessResult {
4957 process_id,
4958 exit_code,
4959 };
4960 self.plugin_manager.resolve_callback(
4961 JsCallbackId::from(callback_id),
4962 serde_json::to_string(&result).unwrap(),
4963 );
4964 }
4965 PluginAsyncMessage::LspResponse {
4966 language: _,
4967 request_id,
4968 result,
4969 } => {
4970 self.handle_plugin_lsp_response(request_id, result);
4971 }
4972 PluginAsyncMessage::PluginResponse(response) => {
4973 self.handle_plugin_response(response);
4974 }
4975 PluginAsyncMessage::GrepStreamingProgress {
4976 search_id,
4977 matches_json,
4978 } => {
4979 tracing::info!(
4980 "GrepStreamingProgress: search_id={} json_len={}",
4981 search_id,
4982 matches_json.len()
4983 );
4984 self.plugin_manager.call_streaming_callback(
4985 JsCallbackId::from(search_id),
4986 matches_json,
4987 false,
4988 );
4989 }
4990 PluginAsyncMessage::GrepStreamingComplete {
4991 search_id: _,
4992 callback_id,
4993 total_matches,
4994 truncated,
4995 } => {
4996 self.streaming_grep_cancellation = None;
4997 self.plugin_manager.resolve_callback(
4998 JsCallbackId::from(callback_id),
4999 format!(
5000 r#"{{"totalMatches":{},"truncated":{}}}"#,
5001 total_matches, truncated
5002 ),
5003 );
5004 }
5005 }
5006 }
5007 AsyncMessage::LspProgress {
5008 language,
5009 token,
5010 value,
5011 } => {
5012 self.handle_lsp_progress(language, token, value);
5013 }
5014 AsyncMessage::LspWindowMessage {
5015 language,
5016 message_type,
5017 message,
5018 } => {
5019 self.handle_lsp_window_message(language, message_type, message);
5020 }
5021 AsyncMessage::LspLogMessage {
5022 language,
5023 message_type,
5024 message,
5025 } => {
5026 self.handle_lsp_log_message(language, message_type, message);
5027 }
5028 AsyncMessage::LspStatusUpdate {
5029 language,
5030 server_name,
5031 status,
5032 message: _,
5033 } => {
5034 self.handle_lsp_status_update(language, server_name, status);
5035 }
5036 AsyncMessage::FileOpenDirectoryLoaded(result) => {
5037 self.handle_file_open_directory_loaded(result);
5038 }
5039 AsyncMessage::FileOpenShortcutsLoaded(shortcuts) => {
5040 self.handle_file_open_shortcuts_loaded(shortcuts);
5041 }
5042 AsyncMessage::TerminalOutput { terminal_id } => {
5043 tracing::trace!("Terminal output received for {:?}", terminal_id);
5045
5046 if self.config.terminal.jump_to_end_on_output && !self.terminal_mode {
5049 if let Some(&active_terminal_id) =
5051 self.terminal_buffers.get(&self.active_buffer())
5052 {
5053 if active_terminal_id == terminal_id {
5054 self.enter_terminal_mode();
5055 }
5056 }
5057 }
5058
5059 if self.terminal_mode {
5061 if let Some(handle) = self.terminal_manager.get(terminal_id) {
5062 if let Ok(mut state) = handle.state.lock() {
5063 state.scroll_to_bottom();
5064 }
5065 }
5066 }
5067 }
5068 AsyncMessage::TerminalExited { terminal_id } => {
5069 tracing::info!("Terminal {:?} exited", terminal_id);
5070 if let Some((&buffer_id, _)) = self
5072 .terminal_buffers
5073 .iter()
5074 .find(|(_, &tid)| tid == terminal_id)
5075 {
5076 if self.active_buffer() == buffer_id && self.terminal_mode {
5078 self.terminal_mode = false;
5079 self.key_context = crate::input::keybindings::KeyContext::Normal;
5080 }
5081
5082 self.sync_terminal_to_buffer(buffer_id);
5084
5085 let exit_msg = "\n[Terminal process exited]\n";
5087
5088 if let Some(backing_path) =
5089 self.terminal_backing_files.get(&terminal_id).cloned()
5090 {
5091 if let Ok(mut file) =
5092 self.filesystem.open_file_for_append(&backing_path)
5093 {
5094 use std::io::Write;
5095 if let Err(e) = file.write_all(exit_msg.as_bytes()) {
5096 tracing::warn!("Failed to write terminal exit message: {}", e);
5097 }
5098 }
5099
5100 if let Err(e) = self.revert_buffer_by_id(buffer_id, &backing_path) {
5102 tracing::warn!("Failed to revert terminal buffer: {}", e);
5103 }
5104 }
5105
5106 if let Some(state) = self.buffers.get_mut(&buffer_id) {
5108 state.editing_disabled = true;
5109 state.margins.configure_for_line_numbers(false);
5110 state.buffer.set_modified(false);
5111 }
5112
5113 self.terminal_buffers.remove(&buffer_id);
5115
5116 self.set_status_message(
5117 t!("terminal.exited", id = terminal_id.0).to_string(),
5118 );
5119 }
5120 self.terminal_manager.close(terminal_id);
5121 }
5122
5123 AsyncMessage::LspServerRequest {
5124 language,
5125 server_command,
5126 method,
5127 params,
5128 } => {
5129 self.handle_lsp_server_request(language, server_command, method, params);
5130 }
5131 AsyncMessage::PluginLspResponse {
5132 language: _,
5133 request_id,
5134 result,
5135 } => {
5136 self.handle_plugin_lsp_response(request_id, result);
5137 }
5138 AsyncMessage::PluginProcessOutput {
5139 process_id,
5140 stdout,
5141 stderr,
5142 exit_code,
5143 } => {
5144 self.handle_plugin_process_output(
5145 fresh_core::api::JsCallbackId::from(process_id),
5146 stdout,
5147 stderr,
5148 exit_code,
5149 );
5150 }
5151 AsyncMessage::GrammarRegistryBuilt {
5152 registry,
5153 callback_ids,
5154 } => {
5155 tracing::info!(
5156 "Background grammar build completed ({} syntaxes)",
5157 registry.available_syntaxes().len()
5158 );
5159 self.grammar_registry = registry;
5160 self.grammar_build_in_progress = false;
5161
5162 let buffers_to_update: Vec<_> = self
5164 .buffer_metadata
5165 .iter()
5166 .filter_map(|(id, meta)| meta.file_path().map(|p| (*id, p.to_path_buf())))
5167 .collect();
5168
5169 for (buf_id, path) in buffers_to_update {
5170 if let Some(state) = self.buffers.get_mut(&buf_id) {
5171 let detected =
5172 crate::primitives::detected_language::DetectedLanguage::from_path(
5173 &path,
5174 &self.grammar_registry,
5175 &self.config.languages,
5176 );
5177
5178 if detected.highlighter.has_highlighting()
5179 || !state.highlighter.has_highlighting()
5180 {
5181 state.apply_language(detected);
5182 }
5183 }
5184 }
5185
5186 #[cfg(feature = "plugins")]
5188 for cb_id in callback_ids {
5189 self.plugin_manager
5190 .resolve_callback(cb_id, "null".to_string());
5191 }
5192
5193 self.flush_pending_grammars();
5195 }
5196 AsyncMessage::QuickOpenFilesLoaded { files, complete } => {
5197 if let Some((provider, _)) = self.quick_open_registry.get_provider_for_input("")
5200 {
5201 if let Some(fp) = provider
5202 .as_any()
5203 .downcast_ref::<crate::input::quick_open::providers::FileProvider>(
5204 ) {
5205 if complete {
5206 fp.set_cache(files);
5207 } else {
5208 fp.set_partial_cache(files);
5209 }
5210 }
5211 }
5212 if let Some(prompt) = &self.prompt {
5214 if prompt.prompt_type == PromptType::QuickOpen {
5215 let input = prompt.input.clone();
5216 self.update_quick_open_suggestions(&input);
5217 }
5218 }
5219 }
5220 }
5221 }
5222
5223 #[cfg(feature = "plugins")]
5226 {
5227 let _s = tracing::info_span!("update_plugin_state_snapshot").entered();
5228 self.update_plugin_state_snapshot();
5229 }
5230
5231 let processed_any_commands = {
5233 let _s = tracing::info_span!("process_plugin_commands").entered();
5234 self.process_plugin_commands()
5235 };
5236
5237 #[cfg(feature = "plugins")]
5241 if processed_any_commands {
5242 let _s = tracing::info_span!("update_plugin_state_snapshot_post").entered();
5243 self.update_plugin_state_snapshot();
5244 }
5245
5246 #[cfg(feature = "plugins")]
5248 {
5249 let _s = tracing::info_span!("process_pending_plugin_actions").entered();
5250 self.process_pending_plugin_actions();
5251 }
5252
5253 {
5255 let _s = tracing::info_span!("process_pending_lsp_restarts").entered();
5256 self.process_pending_lsp_restarts();
5257 }
5258
5259 #[cfg(feature = "plugins")]
5261 let plugin_render = {
5262 let render = self.plugin_render_requested;
5263 self.plugin_render_requested = false;
5264 render
5265 };
5266 #[cfg(not(feature = "plugins"))]
5267 let plugin_render = false;
5268
5269 if let Some(ref mut checker) = self.update_checker {
5271 let _ = checker.poll_result();
5273 }
5274
5275 let file_changes = {
5277 let _s = tracing::info_span!("poll_file_changes").entered();
5278 self.poll_file_changes()
5279 };
5280 let tree_changes = {
5281 let _s = tracing::info_span!("poll_file_tree_changes").entered();
5282 self.poll_file_tree_changes()
5283 };
5284
5285 needs_render || processed_any_commands || plugin_render || file_changes || tree_changes
5287 }
5288
5289 fn update_lsp_status_from_progress(&mut self) {
5291 if self.lsp_progress.is_empty() {
5292 self.update_lsp_status_from_server_statuses();
5294 return;
5295 }
5296
5297 if let Some((_, info)) = self.lsp_progress.iter().next() {
5299 let mut status = format!("LSP ({}): {}", info.language, info.title);
5300 if let Some(ref msg) = info.message {
5301 status.push_str(&format!(" - {}", msg));
5302 }
5303 if let Some(pct) = info.percentage {
5304 status.push_str(&format!(" ({}%)", pct));
5305 }
5306 self.lsp_status = status;
5307 }
5308 }
5309
5310 fn update_lsp_status_from_server_statuses(&mut self) {
5312 use crate::services::async_bridge::LspServerStatus;
5313
5314 let mut statuses: Vec<((String, String), LspServerStatus)> = self
5316 .lsp_server_statuses
5317 .iter()
5318 .map(|((lang, name), status)| ((lang.clone(), name.clone()), *status))
5319 .collect();
5320
5321 if statuses.is_empty() {
5322 self.lsp_status = String::new();
5323 return;
5324 }
5325
5326 statuses.sort_by(|a, b| a.0.cmp(&b.0));
5328
5329 let mut lang_counts: std::collections::HashMap<&str, usize> =
5331 std::collections::HashMap::new();
5332 for ((lang, _), _) in &statuses {
5333 *lang_counts.entry(lang.as_str()).or_default() += 1;
5334 }
5335
5336 let status_parts: Vec<String> = statuses
5338 .iter()
5339 .map(|((lang, name), status)| {
5340 let status_str = match status {
5341 LspServerStatus::Starting => "starting",
5342 LspServerStatus::Initializing => "initializing",
5343 LspServerStatus::Running => "ready",
5344 LspServerStatus::Error => "error",
5345 LspServerStatus::Shutdown => "shutdown",
5346 };
5347 if lang_counts.get(lang.as_str()).copied().unwrap_or(0) > 1 {
5349 format!("{}/{}: {}", lang, name, status_str)
5350 } else {
5351 format!("{}: {}", lang, status_str)
5352 }
5353 })
5354 .collect();
5355
5356 self.lsp_status = format!("LSP [{}]", status_parts.join(", "));
5357 }
5358
5359 #[cfg(feature = "plugins")]
5361 fn update_plugin_state_snapshot(&mut self) {
5362 if let Some(snapshot_handle) = self.plugin_manager.state_snapshot_handle() {
5364 use fresh_core::api::{BufferInfo, CursorInfo, ViewportInfo};
5365 let mut snapshot = snapshot_handle.write().unwrap();
5366
5367 let grammar_count = self.grammar_registry.available_syntaxes().len();
5369 if snapshot.available_grammars.len() != grammar_count {
5370 snapshot.available_grammars = self
5371 .grammar_registry
5372 .available_grammar_info()
5373 .into_iter()
5374 .map(|g| fresh_core::api::GrammarInfoSnapshot {
5375 name: g.name,
5376 source: g.source.to_string(),
5377 file_extensions: g.file_extensions,
5378 short_name: g.short_name,
5379 })
5380 .collect();
5381 }
5382
5383 snapshot.active_buffer_id = self.active_buffer();
5385
5386 snapshot.active_split_id = self.split_manager.active_split().0 .0;
5388
5389 snapshot.buffers.clear();
5391 snapshot.buffer_saved_diffs.clear();
5392 snapshot.buffer_cursor_positions.clear();
5393 snapshot.buffer_text_properties.clear();
5394
5395 for (buffer_id, state) in &self.buffers {
5396 let is_virtual = self
5397 .buffer_metadata
5398 .get(buffer_id)
5399 .map(|m| m.is_virtual())
5400 .unwrap_or(false);
5401 let active_split = self.split_manager.active_split();
5406 let active_vs = self.split_view_states.get(&active_split);
5407 let view_mode = active_vs
5408 .and_then(|vs| vs.buffer_state(*buffer_id))
5409 .map(|bs| match bs.view_mode {
5410 crate::state::ViewMode::Source => "source",
5411 crate::state::ViewMode::PageView => "compose",
5412 })
5413 .unwrap_or("source");
5414 let compose_width = active_vs
5415 .and_then(|vs| vs.buffer_state(*buffer_id))
5416 .and_then(|bs| bs.compose_width);
5417 let is_composing_in_any_split = self.split_view_states.values().any(|vs| {
5418 vs.buffer_state(*buffer_id)
5419 .map(|bs| matches!(bs.view_mode, crate::state::ViewMode::PageView))
5420 .unwrap_or(false)
5421 });
5422 let buffer_info = BufferInfo {
5423 id: *buffer_id,
5424 path: state.buffer.file_path().map(|p| p.to_path_buf()),
5425 modified: state.buffer.is_modified(),
5426 length: state.buffer.len(),
5427 is_virtual,
5428 view_mode: view_mode.to_string(),
5429 is_composing_in_any_split,
5430 compose_width,
5431 language: state.language.clone(),
5432 };
5433 snapshot.buffers.insert(*buffer_id, buffer_info);
5434
5435 let diff = {
5436 let diff = state.buffer.diff_since_saved();
5437 BufferSavedDiff {
5438 equal: diff.equal,
5439 byte_ranges: diff.byte_ranges.clone(),
5440 }
5441 };
5442 snapshot.buffer_saved_diffs.insert(*buffer_id, diff);
5443
5444 let cursor_pos = self
5446 .split_view_states
5447 .values()
5448 .find_map(|vs| vs.buffer_state(*buffer_id))
5449 .map(|bs| bs.cursors.primary().position)
5450 .unwrap_or(0);
5451 snapshot
5452 .buffer_cursor_positions
5453 .insert(*buffer_id, cursor_pos);
5454
5455 if !state.text_properties.is_empty() {
5457 snapshot
5458 .buffer_text_properties
5459 .insert(*buffer_id, state.text_properties.all().to_vec());
5460 }
5461 }
5462
5463 if let Some(active_vs) = self
5465 .split_view_states
5466 .get(&self.split_manager.active_split())
5467 {
5468 let active_cursors = &active_vs.cursors;
5470 let primary = active_cursors.primary();
5471 let primary_position = primary.position;
5472 let primary_selection = primary.selection_range();
5473
5474 snapshot.primary_cursor = Some(CursorInfo {
5475 position: primary_position,
5476 selection: primary_selection.clone(),
5477 });
5478
5479 snapshot.all_cursors = active_cursors
5481 .iter()
5482 .map(|(_, cursor)| CursorInfo {
5483 position: cursor.position,
5484 selection: cursor.selection_range(),
5485 })
5486 .collect();
5487
5488 if let Some(range) = primary_selection {
5490 if let Some(active_state) = self.buffers.get_mut(&self.active_buffer()) {
5491 snapshot.selected_text =
5492 Some(active_state.get_text_range(range.start, range.end));
5493 }
5494 }
5495
5496 let top_line = self.buffers.get(&self.active_buffer()).and_then(|state| {
5498 if state.buffer.line_count().is_some() {
5499 Some(state.buffer.get_line_number(active_vs.viewport.top_byte))
5500 } else {
5501 None
5502 }
5503 });
5504 snapshot.viewport = Some(ViewportInfo {
5505 top_byte: active_vs.viewport.top_byte,
5506 top_line,
5507 left_column: active_vs.viewport.left_column,
5508 width: active_vs.viewport.width,
5509 height: active_vs.viewport.height,
5510 });
5511 } else {
5512 snapshot.primary_cursor = None;
5513 snapshot.all_cursors.clear();
5514 snapshot.viewport = None;
5515 snapshot.selected_text = None;
5516 }
5517
5518 snapshot.clipboard = self.clipboard.get_internal().to_string();
5520
5521 snapshot.working_dir = self.working_dir.clone();
5523
5524 snapshot.diagnostics = self.stored_diagnostics.clone();
5526
5527 snapshot.folding_ranges = self.stored_folding_ranges.clone();
5529
5530 snapshot.config = serde_json::to_value(&self.config).unwrap_or(serde_json::Value::Null);
5532
5533 snapshot.user_config = self.user_config_raw.clone();
5536
5537 snapshot.editor_mode = self.editor_mode.clone();
5539
5540 for (plugin_name, state_map) in &self.plugin_global_state {
5543 let entry = snapshot
5544 .plugin_global_states
5545 .entry(plugin_name.clone())
5546 .or_default();
5547 for (key, value) in state_map {
5548 entry.entry(key.clone()).or_insert_with(|| value.clone());
5549 }
5550 }
5551
5552 let active_split_id = self.split_manager.active_split().0 .0;
5557 let split_changed = snapshot.plugin_view_states_split != active_split_id;
5558 if split_changed {
5559 snapshot.plugin_view_states.clear();
5560 snapshot.plugin_view_states_split = active_split_id;
5561 }
5562
5563 {
5565 let open_bids: Vec<_> = snapshot.buffers.keys().copied().collect();
5566 snapshot
5567 .plugin_view_states
5568 .retain(|bid, _| open_bids.contains(bid));
5569 }
5570
5571 if let Some(active_vs) = self
5573 .split_view_states
5574 .get(&self.split_manager.active_split())
5575 {
5576 for (buffer_id, buf_state) in &active_vs.keyed_states {
5577 if !buf_state.plugin_state.is_empty() {
5578 let entry = snapshot.plugin_view_states.entry(*buffer_id).or_default();
5579 for (key, value) in &buf_state.plugin_state {
5580 entry.entry(key.clone()).or_insert_with(|| value.clone());
5582 }
5583 }
5584 }
5585 }
5586 }
5587 }
5588
5589 pub fn handle_plugin_command(&mut self, command: PluginCommand) -> AnyhowResult<()> {
5591 match command {
5592 PluginCommand::InsertText {
5594 buffer_id,
5595 position,
5596 text,
5597 } => {
5598 self.handle_insert_text(buffer_id, position, text);
5599 }
5600 PluginCommand::DeleteRange { buffer_id, range } => {
5601 self.handle_delete_range(buffer_id, range);
5602 }
5603 PluginCommand::InsertAtCursor { text } => {
5604 self.handle_insert_at_cursor(text);
5605 }
5606 PluginCommand::DeleteSelection => {
5607 self.handle_delete_selection();
5608 }
5609
5610 PluginCommand::AddOverlay {
5612 buffer_id,
5613 namespace,
5614 range,
5615 options,
5616 } => {
5617 self.handle_add_overlay(buffer_id, namespace, range, options);
5618 }
5619 PluginCommand::RemoveOverlay { buffer_id, handle } => {
5620 self.handle_remove_overlay(buffer_id, handle);
5621 }
5622 PluginCommand::ClearAllOverlays { buffer_id } => {
5623 self.handle_clear_all_overlays(buffer_id);
5624 }
5625 PluginCommand::ClearNamespace {
5626 buffer_id,
5627 namespace,
5628 } => {
5629 self.handle_clear_namespace(buffer_id, namespace);
5630 }
5631 PluginCommand::ClearOverlaysInRange {
5632 buffer_id,
5633 start,
5634 end,
5635 } => {
5636 self.handle_clear_overlays_in_range(buffer_id, start, end);
5637 }
5638
5639 PluginCommand::AddVirtualText {
5641 buffer_id,
5642 virtual_text_id,
5643 position,
5644 text,
5645 color,
5646 use_bg,
5647 before,
5648 } => {
5649 self.handle_add_virtual_text(
5650 buffer_id,
5651 virtual_text_id,
5652 position,
5653 text,
5654 color,
5655 use_bg,
5656 before,
5657 );
5658 }
5659 PluginCommand::RemoveVirtualText {
5660 buffer_id,
5661 virtual_text_id,
5662 } => {
5663 self.handle_remove_virtual_text(buffer_id, virtual_text_id);
5664 }
5665 PluginCommand::RemoveVirtualTextsByPrefix { buffer_id, prefix } => {
5666 self.handle_remove_virtual_texts_by_prefix(buffer_id, prefix);
5667 }
5668 PluginCommand::ClearVirtualTexts { buffer_id } => {
5669 self.handle_clear_virtual_texts(buffer_id);
5670 }
5671 PluginCommand::AddVirtualLine {
5672 buffer_id,
5673 position,
5674 text,
5675 fg_color,
5676 bg_color,
5677 above,
5678 namespace,
5679 priority,
5680 } => {
5681 self.handle_add_virtual_line(
5682 buffer_id, position, text, fg_color, bg_color, above, namespace, priority,
5683 );
5684 }
5685 PluginCommand::ClearVirtualTextNamespace {
5686 buffer_id,
5687 namespace,
5688 } => {
5689 self.handle_clear_virtual_text_namespace(buffer_id, namespace);
5690 }
5691
5692 PluginCommand::AddConceal {
5694 buffer_id,
5695 namespace,
5696 start,
5697 end,
5698 replacement,
5699 } => {
5700 self.handle_add_conceal(buffer_id, namespace, start, end, replacement);
5701 }
5702 PluginCommand::ClearConcealNamespace {
5703 buffer_id,
5704 namespace,
5705 } => {
5706 self.handle_clear_conceal_namespace(buffer_id, namespace);
5707 }
5708 PluginCommand::ClearConcealsInRange {
5709 buffer_id,
5710 start,
5711 end,
5712 } => {
5713 self.handle_clear_conceals_in_range(buffer_id, start, end);
5714 }
5715
5716 PluginCommand::AddSoftBreak {
5718 buffer_id,
5719 namespace,
5720 position,
5721 indent,
5722 } => {
5723 self.handle_add_soft_break(buffer_id, namespace, position, indent);
5724 }
5725 PluginCommand::ClearSoftBreakNamespace {
5726 buffer_id,
5727 namespace,
5728 } => {
5729 self.handle_clear_soft_break_namespace(buffer_id, namespace);
5730 }
5731 PluginCommand::ClearSoftBreaksInRange {
5732 buffer_id,
5733 start,
5734 end,
5735 } => {
5736 self.handle_clear_soft_breaks_in_range(buffer_id, start, end);
5737 }
5738
5739 PluginCommand::AddMenuItem {
5741 menu_label,
5742 item,
5743 position,
5744 } => {
5745 self.handle_add_menu_item(menu_label, item, position);
5746 }
5747 PluginCommand::AddMenu { menu, position } => {
5748 self.handle_add_menu(menu, position);
5749 }
5750 PluginCommand::RemoveMenuItem {
5751 menu_label,
5752 item_label,
5753 } => {
5754 self.handle_remove_menu_item(menu_label, item_label);
5755 }
5756 PluginCommand::RemoveMenu { menu_label } => {
5757 self.handle_remove_menu(menu_label);
5758 }
5759
5760 PluginCommand::FocusSplit { split_id } => {
5762 self.handle_focus_split(split_id);
5763 }
5764 PluginCommand::SetSplitBuffer {
5765 split_id,
5766 buffer_id,
5767 } => {
5768 self.handle_set_split_buffer(split_id, buffer_id);
5769 }
5770 PluginCommand::SetSplitScroll { split_id, top_byte } => {
5771 self.handle_set_split_scroll(split_id, top_byte);
5772 }
5773 PluginCommand::RequestHighlights {
5774 buffer_id,
5775 range,
5776 request_id,
5777 } => {
5778 self.handle_request_highlights(buffer_id, range, request_id);
5779 }
5780 PluginCommand::CloseSplit { split_id } => {
5781 self.handle_close_split(split_id);
5782 }
5783 PluginCommand::SetSplitRatio { split_id, ratio } => {
5784 self.handle_set_split_ratio(split_id, ratio);
5785 }
5786 PluginCommand::SetSplitLabel { split_id, label } => {
5787 self.split_manager.set_label(LeafId(split_id), label);
5788 }
5789 PluginCommand::ClearSplitLabel { split_id } => {
5790 self.split_manager.clear_label(split_id);
5791 }
5792 PluginCommand::GetSplitByLabel { label, request_id } => {
5793 let split_id = self.split_manager.find_split_by_label(&label);
5794 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
5795 let json = serde_json::to_string(&split_id.map(|s| s.0 .0))
5796 .unwrap_or_else(|_| "null".to_string());
5797 self.plugin_manager.resolve_callback(callback_id, json);
5798 }
5799 PluginCommand::DistributeSplitsEvenly { split_ids: _ } => {
5800 self.handle_distribute_splits_evenly();
5801 }
5802 PluginCommand::SetBufferCursor {
5803 buffer_id,
5804 position,
5805 } => {
5806 self.handle_set_buffer_cursor(buffer_id, position);
5807 }
5808 PluginCommand::SetBufferShowCursors { buffer_id, show } => {
5809 if let Some(state) = self.buffers.get_mut(&buffer_id) {
5810 state.show_cursors = show;
5811 } else {
5812 tracing::warn!("SetBufferShowCursors: buffer {:?} not found", buffer_id);
5813 }
5814 }
5815
5816 PluginCommand::SetLayoutHints {
5818 buffer_id,
5819 split_id,
5820 range: _,
5821 hints,
5822 } => {
5823 self.handle_set_layout_hints(buffer_id, split_id, hints);
5824 }
5825 PluginCommand::SetLineNumbers { buffer_id, enabled } => {
5826 self.handle_set_line_numbers(buffer_id, enabled);
5827 }
5828 PluginCommand::SetViewMode { buffer_id, mode } => {
5829 self.handle_set_view_mode(buffer_id, &mode);
5830 }
5831 PluginCommand::SetLineWrap {
5832 buffer_id,
5833 split_id,
5834 enabled,
5835 } => {
5836 self.handle_set_line_wrap(buffer_id, split_id, enabled);
5837 }
5838 PluginCommand::SubmitViewTransform {
5839 buffer_id,
5840 split_id,
5841 payload,
5842 } => {
5843 self.handle_submit_view_transform(buffer_id, split_id, payload);
5844 }
5845 PluginCommand::ClearViewTransform {
5846 buffer_id: _,
5847 split_id,
5848 } => {
5849 self.handle_clear_view_transform(split_id);
5850 }
5851 PluginCommand::SetViewState {
5852 buffer_id,
5853 key,
5854 value,
5855 } => {
5856 self.handle_set_view_state(buffer_id, key, value);
5857 }
5858 PluginCommand::SetGlobalState {
5859 plugin_name,
5860 key,
5861 value,
5862 } => {
5863 self.handle_set_global_state(plugin_name, key, value);
5864 }
5865 PluginCommand::RefreshLines { buffer_id } => {
5866 self.handle_refresh_lines(buffer_id);
5867 }
5868 PluginCommand::RefreshAllLines => {
5869 self.handle_refresh_all_lines();
5870 }
5871 PluginCommand::HookCompleted { .. } => {
5872 }
5874 PluginCommand::SetLineIndicator {
5875 buffer_id,
5876 line,
5877 namespace,
5878 symbol,
5879 color,
5880 priority,
5881 } => {
5882 self.handle_set_line_indicator(buffer_id, line, namespace, symbol, color, priority);
5883 }
5884 PluginCommand::SetLineIndicators {
5885 buffer_id,
5886 lines,
5887 namespace,
5888 symbol,
5889 color,
5890 priority,
5891 } => {
5892 self.handle_set_line_indicators(
5893 buffer_id, lines, namespace, symbol, color, priority,
5894 );
5895 }
5896 PluginCommand::ClearLineIndicators {
5897 buffer_id,
5898 namespace,
5899 } => {
5900 self.handle_clear_line_indicators(buffer_id, namespace);
5901 }
5902 PluginCommand::SetFileExplorerDecorations {
5903 namespace,
5904 decorations,
5905 } => {
5906 self.handle_set_file_explorer_decorations(namespace, decorations);
5907 }
5908 PluginCommand::ClearFileExplorerDecorations { namespace } => {
5909 self.handle_clear_file_explorer_decorations(&namespace);
5910 }
5911
5912 PluginCommand::SetStatus { message } => {
5914 self.handle_set_status(message);
5915 }
5916 PluginCommand::ApplyTheme { theme_name } => {
5917 self.apply_theme(&theme_name);
5918 }
5919 PluginCommand::ReloadConfig => {
5920 self.reload_config();
5921 }
5922 PluginCommand::ReloadThemes { apply_theme } => {
5923 self.reload_themes();
5924 if let Some(theme_name) = apply_theme {
5925 self.apply_theme(&theme_name);
5926 }
5927 }
5928 PluginCommand::RegisterGrammar {
5929 language,
5930 grammar_path,
5931 extensions,
5932 } => {
5933 self.handle_register_grammar(language, grammar_path, extensions);
5934 }
5935 PluginCommand::RegisterLanguageConfig { language, config } => {
5936 self.handle_register_language_config(language, config);
5937 }
5938 PluginCommand::RegisterLspServer { language, config } => {
5939 self.handle_register_lsp_server(language, config);
5940 }
5941 PluginCommand::ReloadGrammars { callback_id } => {
5942 self.handle_reload_grammars(callback_id);
5943 }
5944 PluginCommand::StartPrompt { label, prompt_type } => {
5945 self.handle_start_prompt(label, prompt_type);
5946 }
5947 PluginCommand::StartPromptWithInitial {
5948 label,
5949 prompt_type,
5950 initial_value,
5951 } => {
5952 self.handle_start_prompt_with_initial(label, prompt_type, initial_value);
5953 }
5954 PluginCommand::StartPromptAsync {
5955 label,
5956 initial_value,
5957 callback_id,
5958 } => {
5959 self.handle_start_prompt_async(label, initial_value, callback_id);
5960 }
5961 PluginCommand::SetPromptSuggestions { suggestions } => {
5962 self.handle_set_prompt_suggestions(suggestions);
5963 }
5964 PluginCommand::SetPromptInputSync { sync } => {
5965 if let Some(prompt) = &mut self.prompt {
5966 prompt.sync_input_on_navigate = sync;
5967 }
5968 }
5969
5970 PluginCommand::RegisterCommand { command } => {
5972 self.handle_register_command(command);
5973 }
5974 PluginCommand::UnregisterCommand { name } => {
5975 self.handle_unregister_command(name);
5976 }
5977 PluginCommand::DefineMode {
5978 name,
5979 bindings,
5980 read_only,
5981 allow_text_input,
5982 plugin_name,
5983 } => {
5984 self.handle_define_mode(name, bindings, read_only, allow_text_input, plugin_name);
5985 }
5986
5987 PluginCommand::OpenFileInBackground { path } => {
5989 self.handle_open_file_in_background(path);
5990 }
5991 PluginCommand::OpenFileAtLocation { path, line, column } => {
5992 return self.handle_open_file_at_location(path, line, column);
5993 }
5994 PluginCommand::OpenFileInSplit {
5995 split_id,
5996 path,
5997 line,
5998 column,
5999 } => {
6000 return self.handle_open_file_in_split(split_id, path, line, column);
6001 }
6002 PluginCommand::ShowBuffer { buffer_id } => {
6003 self.handle_show_buffer(buffer_id);
6004 }
6005 PluginCommand::CloseBuffer { buffer_id } => {
6006 self.handle_close_buffer(buffer_id);
6007 }
6008
6009 PluginCommand::SendLspRequest {
6011 language,
6012 method,
6013 params,
6014 request_id,
6015 } => {
6016 self.handle_send_lsp_request(language, method, params, request_id);
6017 }
6018
6019 PluginCommand::SetClipboard { text } => {
6021 self.handle_set_clipboard(text);
6022 }
6023
6024 PluginCommand::SpawnProcess {
6026 command,
6027 args,
6028 cwd,
6029 callback_id,
6030 } => {
6031 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
6034 let effective_cwd = cwd.or_else(|| {
6035 std::env::current_dir()
6036 .map(|p| p.to_string_lossy().to_string())
6037 .ok()
6038 });
6039 let sender = bridge.sender();
6040 let spawner = self.process_spawner.clone();
6041
6042 runtime.spawn(async move {
6043 #[allow(clippy::let_underscore_must_use)]
6045 match spawner.spawn(command, args, effective_cwd).await {
6046 Ok(result) => {
6047 let _ = sender.send(AsyncMessage::PluginProcessOutput {
6048 process_id: callback_id.as_u64(),
6049 stdout: result.stdout,
6050 stderr: result.stderr,
6051 exit_code: result.exit_code,
6052 });
6053 }
6054 Err(e) => {
6055 let _ = sender.send(AsyncMessage::PluginProcessOutput {
6056 process_id: callback_id.as_u64(),
6057 stdout: String::new(),
6058 stderr: e.to_string(),
6059 exit_code: -1,
6060 });
6061 }
6062 }
6063 });
6064 } else {
6065 self.plugin_manager
6067 .reject_callback(callback_id, "Async runtime not available".to_string());
6068 }
6069 }
6070
6071 PluginCommand::SpawnProcessWait {
6072 process_id,
6073 callback_id,
6074 } => {
6075 tracing::warn!(
6078 "SpawnProcessWait not fully implemented - process_id={}",
6079 process_id
6080 );
6081 self.plugin_manager.reject_callback(
6082 callback_id,
6083 format!(
6084 "SpawnProcessWait not yet fully implemented for process_id={}",
6085 process_id
6086 ),
6087 );
6088 }
6089
6090 PluginCommand::Delay {
6091 callback_id,
6092 duration_ms,
6093 } => {
6094 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
6096 let sender = bridge.sender();
6097 let callback_id_u64 = callback_id.as_u64();
6098 runtime.spawn(async move {
6099 tokio::time::sleep(tokio::time::Duration::from_millis(duration_ms)).await;
6100 #[allow(clippy::let_underscore_must_use)]
6102 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
6103 fresh_core::api::PluginAsyncMessage::DelayComplete {
6104 callback_id: callback_id_u64,
6105 },
6106 ));
6107 });
6108 } else {
6109 std::thread::sleep(std::time::Duration::from_millis(duration_ms));
6111 self.plugin_manager
6112 .resolve_callback(callback_id, "null".to_string());
6113 }
6114 }
6115
6116 PluginCommand::SpawnBackgroundProcess {
6117 process_id,
6118 command,
6119 args,
6120 cwd,
6121 callback_id,
6122 } => {
6123 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
6125 use tokio::io::{AsyncBufReadExt, BufReader};
6126 use tokio::process::Command as TokioCommand;
6127
6128 let effective_cwd = cwd.unwrap_or_else(|| {
6129 std::env::current_dir()
6130 .map(|p| p.to_string_lossy().to_string())
6131 .unwrap_or_else(|_| ".".to_string())
6132 });
6133
6134 let sender = bridge.sender();
6135 let sender_stdout = sender.clone();
6136 let sender_stderr = sender.clone();
6137 let callback_id_u64 = callback_id.as_u64();
6138
6139 #[allow(clippy::let_underscore_must_use)]
6141 let handle = runtime.spawn(async move {
6142 let mut child = match TokioCommand::new(&command)
6143 .args(&args)
6144 .current_dir(&effective_cwd)
6145 .stdout(std::process::Stdio::piped())
6146 .stderr(std::process::Stdio::piped())
6147 .spawn()
6148 {
6149 Ok(child) => child,
6150 Err(e) => {
6151 let _ = sender.send(
6152 crate::services::async_bridge::AsyncMessage::Plugin(
6153 fresh_core::api::PluginAsyncMessage::ProcessExit {
6154 process_id,
6155 callback_id: callback_id_u64,
6156 exit_code: -1,
6157 },
6158 ),
6159 );
6160 tracing::error!("Failed to spawn background process: {}", e);
6161 return;
6162 }
6163 };
6164
6165 let stdout = child.stdout.take();
6167 let stderr = child.stderr.take();
6168 let pid = process_id;
6169
6170 if let Some(stdout) = stdout {
6172 let sender = sender_stdout;
6173 tokio::spawn(async move {
6174 let reader = BufReader::new(stdout);
6175 let mut lines = reader.lines();
6176 while let Ok(Some(line)) = lines.next_line().await {
6177 let _ = sender.send(
6178 crate::services::async_bridge::AsyncMessage::Plugin(
6179 fresh_core::api::PluginAsyncMessage::ProcessStdout {
6180 process_id: pid,
6181 data: line + "\n",
6182 },
6183 ),
6184 );
6185 }
6186 });
6187 }
6188
6189 if let Some(stderr) = stderr {
6191 let sender = sender_stderr;
6192 tokio::spawn(async move {
6193 let reader = BufReader::new(stderr);
6194 let mut lines = reader.lines();
6195 while let Ok(Some(line)) = lines.next_line().await {
6196 let _ = sender.send(
6197 crate::services::async_bridge::AsyncMessage::Plugin(
6198 fresh_core::api::PluginAsyncMessage::ProcessStderr {
6199 process_id: pid,
6200 data: line + "\n",
6201 },
6202 ),
6203 );
6204 }
6205 });
6206 }
6207
6208 let exit_code = match child.wait().await {
6210 Ok(status) => status.code().unwrap_or(-1),
6211 Err(_) => -1,
6212 };
6213
6214 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
6215 fresh_core::api::PluginAsyncMessage::ProcessExit {
6216 process_id,
6217 callback_id: callback_id_u64,
6218 exit_code,
6219 },
6220 ));
6221 });
6222
6223 self.background_process_handles
6225 .insert(process_id, handle.abort_handle());
6226 } else {
6227 self.plugin_manager
6229 .reject_callback(callback_id, "Async runtime not available".to_string());
6230 }
6231 }
6232
6233 PluginCommand::KillBackgroundProcess { process_id } => {
6234 if let Some(handle) = self.background_process_handles.remove(&process_id) {
6235 handle.abort();
6236 tracing::debug!("Killed background process {}", process_id);
6237 }
6238 }
6239
6240 PluginCommand::CreateVirtualBuffer {
6242 name,
6243 mode,
6244 read_only,
6245 } => {
6246 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
6247 tracing::info!(
6248 "Created virtual buffer '{}' with mode '{}' (id={:?})",
6249 name,
6250 mode,
6251 buffer_id
6252 );
6253 }
6255 PluginCommand::CreateVirtualBufferWithContent {
6256 name,
6257 mode,
6258 read_only,
6259 entries,
6260 show_line_numbers,
6261 show_cursors,
6262 editing_disabled,
6263 hidden_from_tabs,
6264 request_id,
6265 } => {
6266 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
6267 tracing::info!(
6268 "Created virtual buffer '{}' with mode '{}' (id={:?})",
6269 name,
6270 mode,
6271 buffer_id
6272 );
6273
6274 if let Some(state) = self.buffers.get_mut(&buffer_id) {
6281 state.margins.configure_for_line_numbers(show_line_numbers);
6282 state.show_cursors = show_cursors;
6283 state.editing_disabled = editing_disabled;
6284 tracing::debug!(
6285 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
6286 buffer_id,
6287 show_line_numbers,
6288 show_cursors,
6289 editing_disabled
6290 );
6291 }
6292 let active_split = self.split_manager.active_split();
6293 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
6294 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
6295 }
6296
6297 if hidden_from_tabs {
6299 if let Some(meta) = self.buffer_metadata.get_mut(&buffer_id) {
6300 meta.hidden_from_tabs = true;
6301 }
6302 }
6303
6304 match self.set_virtual_buffer_content(buffer_id, entries) {
6306 Ok(()) => {
6307 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
6308 self.set_active_buffer(buffer_id);
6310 tracing::debug!("Switched to virtual buffer {:?}", buffer_id);
6311
6312 if let Some(req_id) = request_id {
6314 tracing::info!(
6315 "CreateVirtualBufferWithContent: resolving callback for request_id={}, buffer_id={:?}",
6316 req_id,
6317 buffer_id
6318 );
6319 let result = fresh_core::api::VirtualBufferResult {
6321 buffer_id: buffer_id.0 as u64,
6322 split_id: None,
6323 };
6324 self.plugin_manager.resolve_callback(
6325 fresh_core::api::JsCallbackId::from(req_id),
6326 serde_json::to_string(&result).unwrap_or_default(),
6327 );
6328 tracing::info!("CreateVirtualBufferWithContent: resolve_callback sent for request_id={}", req_id);
6329 }
6330 }
6331 Err(e) => {
6332 tracing::error!("Failed to set virtual buffer content: {}", e);
6333 }
6334 }
6335 }
6336 PluginCommand::CreateVirtualBufferInSplit {
6337 name,
6338 mode,
6339 read_only,
6340 entries,
6341 ratio,
6342 direction,
6343 panel_id,
6344 show_line_numbers,
6345 show_cursors,
6346 editing_disabled,
6347 line_wrap,
6348 before,
6349 request_id,
6350 } => {
6351 if let Some(pid) = &panel_id {
6353 if let Some(&existing_buffer_id) = self.panel_ids.get(pid) {
6354 if self.buffers.contains_key(&existing_buffer_id) {
6356 if let Err(e) =
6358 self.set_virtual_buffer_content(existing_buffer_id, entries)
6359 {
6360 tracing::error!("Failed to update panel content: {}", e);
6361 } else {
6362 tracing::info!("Updated existing panel '{}' content", pid);
6363 }
6364
6365 let splits = self.split_manager.splits_for_buffer(existing_buffer_id);
6367 if let Some(&split_id) = splits.first() {
6368 self.split_manager.set_active_split(split_id);
6369 self.split_manager.set_active_buffer_id(existing_buffer_id);
6372 tracing::debug!(
6373 "Focused split {:?} containing panel buffer",
6374 split_id
6375 );
6376 }
6377
6378 if let Some(req_id) = request_id {
6380 let result = fresh_core::api::VirtualBufferResult {
6381 buffer_id: existing_buffer_id.0 as u64,
6382 split_id: splits.first().map(|s| s.0 .0 as u64),
6383 };
6384 self.plugin_manager.resolve_callback(
6385 fresh_core::api::JsCallbackId::from(req_id),
6386 serde_json::to_string(&result).unwrap_or_default(),
6387 );
6388 }
6389 return Ok(());
6390 } else {
6391 tracing::warn!(
6393 "Removing stale panel_id '{}' pointing to non-existent buffer {:?}",
6394 pid,
6395 existing_buffer_id
6396 );
6397 self.panel_ids.remove(pid);
6398 }
6400 }
6401 }
6402
6403 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
6405 tracing::info!(
6406 "Created virtual buffer '{}' with mode '{}' in split (id={:?})",
6407 name,
6408 mode,
6409 buffer_id
6410 );
6411
6412 if let Some(state) = self.buffers.get_mut(&buffer_id) {
6414 state.margins.configure_for_line_numbers(show_line_numbers);
6415 state.show_cursors = show_cursors;
6416 state.editing_disabled = editing_disabled;
6417 tracing::debug!(
6418 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
6419 buffer_id,
6420 show_line_numbers,
6421 show_cursors,
6422 editing_disabled
6423 );
6424 }
6425
6426 if let Some(pid) = panel_id {
6428 self.panel_ids.insert(pid, buffer_id);
6429 }
6430
6431 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
6433 tracing::error!("Failed to set virtual buffer content: {}", e);
6434 return Ok(());
6435 }
6436
6437 let split_dir = match direction.as_deref() {
6439 Some("vertical") => crate::model::event::SplitDirection::Vertical,
6440 _ => crate::model::event::SplitDirection::Horizontal,
6441 };
6442
6443 let created_split_id = match self
6445 .split_manager
6446 .split_active_positioned(split_dir, buffer_id, ratio, before)
6447 {
6448 Ok(new_split_id) => {
6449 let mut view_state = SplitViewState::with_buffer(
6451 self.terminal_width,
6452 self.terminal_height,
6453 buffer_id,
6454 );
6455 view_state.apply_config_defaults(
6456 self.config.editor.line_numbers,
6457 self.config.editor.highlight_current_line,
6458 line_wrap
6459 .unwrap_or_else(|| self.resolve_line_wrap_for_buffer(buffer_id)),
6460 self.config.editor.wrap_indent,
6461 self.resolve_wrap_column_for_buffer(buffer_id),
6462 self.config.editor.rulers.clone(),
6463 );
6464 view_state.ensure_buffer_state(buffer_id).show_line_numbers =
6466 show_line_numbers;
6467 self.split_view_states.insert(new_split_id, view_state);
6468
6469 self.split_manager.set_active_split(new_split_id);
6471 tracing::info!(
6474 "Created {:?} split with virtual buffer {:?}",
6475 split_dir,
6476 buffer_id
6477 );
6478 Some(new_split_id)
6479 }
6480 Err(e) => {
6481 tracing::error!("Failed to create split: {}", e);
6482 self.set_active_buffer(buffer_id);
6484 None
6485 }
6486 };
6487
6488 if let Some(req_id) = request_id {
6491 tracing::trace!("CreateVirtualBufferInSplit: resolving callback for request_id={}, buffer_id={:?}, split_id={:?}", req_id, buffer_id, created_split_id);
6492 let result = fresh_core::api::VirtualBufferResult {
6493 buffer_id: buffer_id.0 as u64,
6494 split_id: created_split_id.map(|s| s.0 .0 as u64),
6495 };
6496 self.plugin_manager.resolve_callback(
6497 fresh_core::api::JsCallbackId::from(req_id),
6498 serde_json::to_string(&result).unwrap_or_default(),
6499 );
6500 }
6501 }
6502 PluginCommand::SetVirtualBufferContent { buffer_id, entries } => {
6503 match self.set_virtual_buffer_content(buffer_id, entries) {
6504 Ok(()) => {
6505 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
6506 }
6507 Err(e) => {
6508 tracing::error!("Failed to set virtual buffer content: {}", e);
6509 }
6510 }
6511 }
6512 PluginCommand::GetTextPropertiesAtCursor { buffer_id } => {
6513 if let Some(state) = self.buffers.get(&buffer_id) {
6515 let cursor_pos = self
6516 .split_view_states
6517 .values()
6518 .find_map(|vs| vs.buffer_state(buffer_id))
6519 .map(|bs| bs.cursors.primary().position)
6520 .unwrap_or(0);
6521 let properties = state.text_properties.get_at(cursor_pos);
6522 tracing::debug!(
6523 "Text properties at cursor in {:?}: {} properties found",
6524 buffer_id,
6525 properties.len()
6526 );
6527 }
6529 }
6530 PluginCommand::CreateVirtualBufferInExistingSplit {
6531 name,
6532 mode,
6533 read_only,
6534 entries,
6535 split_id,
6536 show_line_numbers,
6537 show_cursors,
6538 editing_disabled,
6539 line_wrap,
6540 request_id,
6541 } => {
6542 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
6544 tracing::info!(
6545 "Created virtual buffer '{}' with mode '{}' for existing split {:?} (id={:?})",
6546 name,
6547 mode,
6548 split_id,
6549 buffer_id
6550 );
6551
6552 if let Some(state) = self.buffers.get_mut(&buffer_id) {
6554 state.margins.configure_for_line_numbers(show_line_numbers);
6555 state.show_cursors = show_cursors;
6556 state.editing_disabled = editing_disabled;
6557 }
6558
6559 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
6561 tracing::error!("Failed to set virtual buffer content: {}", e);
6562 return Ok(());
6563 }
6564
6565 let leaf_id = LeafId(split_id);
6567 self.split_manager.set_split_buffer(leaf_id, buffer_id);
6568
6569 self.split_manager.set_active_split(leaf_id);
6571 self.split_manager.set_active_buffer_id(buffer_id);
6572
6573 if let Some(view_state) = self.split_view_states.get_mut(&leaf_id) {
6575 view_state.switch_buffer(buffer_id);
6576 view_state.add_buffer(buffer_id);
6577 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
6578
6579 if let Some(wrap) = line_wrap {
6581 view_state.active_state_mut().viewport.line_wrap_enabled = wrap;
6582 }
6583 }
6584
6585 tracing::info!(
6586 "Displayed virtual buffer {:?} in split {:?}",
6587 buffer_id,
6588 split_id
6589 );
6590
6591 if let Some(req_id) = request_id {
6593 let result = fresh_core::api::VirtualBufferResult {
6594 buffer_id: buffer_id.0 as u64,
6595 split_id: Some(split_id.0 as u64),
6596 };
6597 self.plugin_manager.resolve_callback(
6598 fresh_core::api::JsCallbackId::from(req_id),
6599 serde_json::to_string(&result).unwrap_or_default(),
6600 );
6601 }
6602 }
6603
6604 PluginCommand::SetContext { name, active } => {
6606 if active {
6607 self.active_custom_contexts.insert(name.clone());
6608 tracing::debug!("Set custom context: {}", name);
6609 } else {
6610 self.active_custom_contexts.remove(&name);
6611 tracing::debug!("Unset custom context: {}", name);
6612 }
6613 }
6614
6615 PluginCommand::SetReviewDiffHunks { hunks } => {
6617 self.review_hunks = hunks;
6618 tracing::debug!("Set {} review hunks", self.review_hunks.len());
6619 }
6620
6621 PluginCommand::ExecuteAction { action_name } => {
6623 self.handle_execute_action(action_name);
6624 }
6625 PluginCommand::ExecuteActions { actions } => {
6626 self.handle_execute_actions(actions);
6627 }
6628 PluginCommand::GetBufferText {
6629 buffer_id,
6630 start,
6631 end,
6632 request_id,
6633 } => {
6634 self.handle_get_buffer_text(buffer_id, start, end, request_id);
6635 }
6636 PluginCommand::GetLineStartPosition {
6637 buffer_id,
6638 line,
6639 request_id,
6640 } => {
6641 self.handle_get_line_start_position(buffer_id, line, request_id);
6642 }
6643 PluginCommand::GetLineEndPosition {
6644 buffer_id,
6645 line,
6646 request_id,
6647 } => {
6648 self.handle_get_line_end_position(buffer_id, line, request_id);
6649 }
6650 PluginCommand::GetBufferLineCount {
6651 buffer_id,
6652 request_id,
6653 } => {
6654 self.handle_get_buffer_line_count(buffer_id, request_id);
6655 }
6656 PluginCommand::ScrollToLineCenter {
6657 split_id,
6658 buffer_id,
6659 line,
6660 } => {
6661 self.handle_scroll_to_line_center(split_id, buffer_id, line);
6662 }
6663 PluginCommand::ScrollBufferToLine { buffer_id, line } => {
6664 self.handle_scroll_buffer_to_line(buffer_id, line);
6665 }
6666 PluginCommand::SetEditorMode { mode } => {
6667 self.handle_set_editor_mode(mode);
6668 }
6669
6670 PluginCommand::ShowActionPopup {
6672 popup_id,
6673 title,
6674 message,
6675 actions,
6676 } => {
6677 tracing::info!(
6678 "Action popup requested: id={}, title={}, actions={}",
6679 popup_id,
6680 title,
6681 actions.len()
6682 );
6683
6684 let items: Vec<crate::model::event::PopupListItemData> = actions
6686 .iter()
6687 .map(|action| crate::model::event::PopupListItemData {
6688 text: action.label.clone(),
6689 detail: None,
6690 icon: None,
6691 data: Some(action.id.clone()),
6692 })
6693 .collect();
6694
6695 let action_ids: Vec<(String, String)> =
6697 actions.into_iter().map(|a| (a.id, a.label)).collect();
6698 self.active_action_popup = Some((popup_id.clone(), action_ids));
6699
6700 let popup = crate::model::event::PopupData {
6702 kind: crate::model::event::PopupKindHint::List,
6703 title: Some(title),
6704 description: Some(message),
6705 transient: false,
6706 content: crate::model::event::PopupContentData::List { items, selected: 0 },
6707 position: crate::model::event::PopupPositionData::BottomRight,
6708 width: 60,
6709 max_height: 15,
6710 bordered: true,
6711 };
6712
6713 self.show_popup(popup);
6714 tracing::info!(
6715 "Action popup shown: id={}, active_action_popup={:?}",
6716 popup_id,
6717 self.active_action_popup.as_ref().map(|(id, _)| id)
6718 );
6719 }
6720
6721 PluginCommand::DisableLspForLanguage { language } => {
6722 tracing::info!("Disabling LSP for language: {}", language);
6723
6724 if let Some(ref mut lsp) = self.lsp {
6726 lsp.shutdown_server(&language);
6727 tracing::info!("Stopped LSP server for {}", language);
6728 }
6729
6730 if let Some(lsp_configs) = self.config.lsp.get_mut(&language) {
6732 for c in lsp_configs.as_mut_slice() {
6733 c.enabled = false;
6734 c.auto_start = false;
6735 }
6736 tracing::info!("Disabled LSP config for {}", language);
6737 }
6738
6739 if let Err(e) = self.save_config() {
6741 tracing::error!("Failed to save config: {}", e);
6742 self.status_message = Some(format!(
6743 "LSP disabled for {} (config save failed)",
6744 language
6745 ));
6746 } else {
6747 self.status_message = Some(format!("LSP disabled for {}", language));
6748 }
6749
6750 self.warning_domains.lsp.clear();
6752 }
6753
6754 PluginCommand::RestartLspForLanguage { language } => {
6755 tracing::info!("Plugin restarting LSP for language: {}", language);
6756
6757 let file_path = self
6758 .buffer_metadata
6759 .get(&self.active_buffer())
6760 .and_then(|meta| meta.file_path().cloned());
6761 let success = if let Some(ref mut lsp) = self.lsp {
6762 let (ok, msg) = lsp.manual_restart(&language, file_path.as_deref());
6763 self.status_message = Some(msg);
6764 ok
6765 } else {
6766 self.status_message = Some("No LSP manager available".to_string());
6767 false
6768 };
6769
6770 if success {
6771 self.reopen_buffers_for_language(&language);
6772 }
6773 }
6774
6775 PluginCommand::SetLspRootUri { language, uri } => {
6776 tracing::info!("Plugin setting LSP root URI for {}: {}", language, uri);
6777
6778 match uri.parse::<lsp_types::Uri>() {
6780 Ok(parsed_uri) => {
6781 if let Some(ref mut lsp) = self.lsp {
6782 let restarted = lsp.set_language_root_uri(&language, parsed_uri);
6783 if restarted {
6784 self.status_message = Some(format!(
6785 "LSP root updated for {} (restarting server)",
6786 language
6787 ));
6788 } else {
6789 self.status_message =
6790 Some(format!("LSP root set for {}", language));
6791 }
6792 }
6793 }
6794 Err(e) => {
6795 tracing::error!("Invalid LSP root URI '{}': {}", uri, e);
6796 self.status_message = Some(format!("Invalid LSP root URI: {}", e));
6797 }
6798 }
6799 }
6800
6801 PluginCommand::CreateScrollSyncGroup {
6803 group_id,
6804 left_split,
6805 right_split,
6806 } => {
6807 let success = self.scroll_sync_manager.create_group_with_id(
6808 group_id,
6809 left_split,
6810 right_split,
6811 );
6812 if success {
6813 tracing::debug!(
6814 "Created scroll sync group {} for splits {:?} and {:?}",
6815 group_id,
6816 left_split,
6817 right_split
6818 );
6819 } else {
6820 tracing::warn!(
6821 "Failed to create scroll sync group {} (ID already exists)",
6822 group_id
6823 );
6824 }
6825 }
6826 PluginCommand::SetScrollSyncAnchors { group_id, anchors } => {
6827 use crate::view::scroll_sync::SyncAnchor;
6828 let anchor_count = anchors.len();
6829 let sync_anchors: Vec<SyncAnchor> = anchors
6830 .into_iter()
6831 .map(|(left_line, right_line)| SyncAnchor {
6832 left_line,
6833 right_line,
6834 })
6835 .collect();
6836 self.scroll_sync_manager.set_anchors(group_id, sync_anchors);
6837 tracing::debug!(
6838 "Set {} anchors for scroll sync group {}",
6839 anchor_count,
6840 group_id
6841 );
6842 }
6843 PluginCommand::RemoveScrollSyncGroup { group_id } => {
6844 if self.scroll_sync_manager.remove_group(group_id) {
6845 tracing::debug!("Removed scroll sync group {}", group_id);
6846 } else {
6847 tracing::warn!("Scroll sync group {} not found", group_id);
6848 }
6849 }
6850
6851 PluginCommand::CreateCompositeBuffer {
6853 name,
6854 mode,
6855 layout,
6856 sources,
6857 hunks,
6858 initial_focus_hunk,
6859 request_id,
6860 } => {
6861 self.handle_create_composite_buffer(
6862 name,
6863 mode,
6864 layout,
6865 sources,
6866 hunks,
6867 initial_focus_hunk,
6868 request_id,
6869 );
6870 }
6871 PluginCommand::UpdateCompositeAlignment { buffer_id, hunks } => {
6872 self.handle_update_composite_alignment(buffer_id, hunks);
6873 }
6874 PluginCommand::CloseCompositeBuffer { buffer_id } => {
6875 self.close_composite_buffer(buffer_id);
6876 }
6877 PluginCommand::FlushLayout => {
6878 self.flush_layout();
6879 }
6880 PluginCommand::CompositeNextHunk { buffer_id } => {
6881 let split_id = self.split_manager.active_split();
6882 self.composite_next_hunk(split_id, buffer_id);
6883 }
6884 PluginCommand::CompositePrevHunk { buffer_id } => {
6885 let split_id = self.split_manager.active_split();
6886 self.composite_prev_hunk(split_id, buffer_id);
6887 }
6888
6889 PluginCommand::CreateBufferGroup {
6891 name,
6892 mode,
6893 layout_json,
6894 request_id,
6895 } => match self.create_buffer_group(name, mode, layout_json) {
6896 Ok(result) => {
6897 if let Some(req_id) = request_id {
6898 let json = serde_json::to_string(&result).unwrap_or_default();
6899 self.plugin_manager
6900 .resolve_callback(fresh_core::api::JsCallbackId::from(req_id), json);
6901 }
6902 }
6903 Err(e) => {
6904 tracing::error!("Failed to create buffer group: {}", e);
6905 }
6906 },
6907 PluginCommand::SetPanelContent {
6908 group_id,
6909 panel_name,
6910 entries,
6911 } => {
6912 self.set_panel_content(group_id, panel_name, entries);
6913 }
6914 PluginCommand::CloseBufferGroup { group_id } => {
6915 self.close_buffer_group(group_id);
6916 }
6917 PluginCommand::FocusPanel {
6918 group_id,
6919 panel_name,
6920 } => {
6921 self.focus_panel(group_id, panel_name);
6922 }
6923
6924 PluginCommand::SaveBufferToPath { buffer_id, path } => {
6926 self.handle_save_buffer_to_path(buffer_id, path);
6927 }
6928
6929 #[cfg(feature = "plugins")]
6931 PluginCommand::LoadPlugin { path, callback_id } => {
6932 self.handle_load_plugin(path, callback_id);
6933 }
6934 #[cfg(feature = "plugins")]
6935 PluginCommand::UnloadPlugin { name, callback_id } => {
6936 self.handle_unload_plugin(name, callback_id);
6937 }
6938 #[cfg(feature = "plugins")]
6939 PluginCommand::ReloadPlugin { name, callback_id } => {
6940 self.handle_reload_plugin(name, callback_id);
6941 }
6942 #[cfg(feature = "plugins")]
6943 PluginCommand::ListPlugins { callback_id } => {
6944 self.handle_list_plugins(callback_id);
6945 }
6946 #[cfg(not(feature = "plugins"))]
6948 PluginCommand::LoadPlugin { .. }
6949 | PluginCommand::UnloadPlugin { .. }
6950 | PluginCommand::ReloadPlugin { .. }
6951 | PluginCommand::ListPlugins { .. } => {
6952 tracing::warn!("Plugin management commands require the 'plugins' feature");
6953 }
6954
6955 PluginCommand::CreateTerminal {
6957 cwd,
6958 direction,
6959 ratio,
6960 focus,
6961 request_id,
6962 } => {
6963 let (cols, rows) = self.get_terminal_dimensions();
6964
6965 if let Some(ref bridge) = self.async_bridge {
6967 self.terminal_manager.set_async_bridge(bridge.clone());
6968 }
6969
6970 let working_dir = cwd
6972 .map(std::path::PathBuf::from)
6973 .unwrap_or_else(|| self.working_dir.clone());
6974
6975 let terminal_root = self.dir_context.terminal_dir_for(&working_dir);
6977 if let Err(e) = self.filesystem.create_dir_all(&terminal_root) {
6978 tracing::warn!("Failed to create terminal directory: {}", e);
6979 }
6980 let predicted_terminal_id = self.terminal_manager.next_terminal_id();
6981 let log_path =
6982 terminal_root.join(format!("fresh-terminal-{}.log", predicted_terminal_id.0));
6983 let backing_path =
6984 terminal_root.join(format!("fresh-terminal-{}.txt", predicted_terminal_id.0));
6985 self.terminal_backing_files
6986 .insert(predicted_terminal_id, backing_path);
6987 let backing_path_for_spawn = self
6988 .terminal_backing_files
6989 .get(&predicted_terminal_id)
6990 .cloned();
6991
6992 match self.terminal_manager.spawn(
6993 cols,
6994 rows,
6995 Some(working_dir),
6996 Some(log_path.clone()),
6997 backing_path_for_spawn,
6998 ) {
6999 Ok(terminal_id) => {
7000 self.terminal_log_files
7002 .insert(terminal_id, log_path.clone());
7003 if terminal_id != predicted_terminal_id {
7005 self.terminal_backing_files.remove(&predicted_terminal_id);
7006 let backing_path =
7007 terminal_root.join(format!("fresh-terminal-{}.txt", terminal_id.0));
7008 self.terminal_backing_files
7009 .insert(terminal_id, backing_path);
7010 }
7011
7012 let active_split = self.split_manager.active_split();
7014 let buffer_id =
7015 self.create_terminal_buffer_attached(terminal_id, active_split);
7016
7017 let created_split_id = if let Some(dir_str) = direction.as_deref() {
7021 let split_dir = match dir_str {
7022 "horizontal" => crate::model::event::SplitDirection::Horizontal,
7023 _ => crate::model::event::SplitDirection::Vertical,
7024 };
7025
7026 let split_ratio = ratio.unwrap_or(0.5);
7027 match self
7028 .split_manager
7029 .split_active(split_dir, buffer_id, split_ratio)
7030 {
7031 Ok(new_split_id) => {
7032 let mut view_state = SplitViewState::with_buffer(
7033 self.terminal_width,
7034 self.terminal_height,
7035 buffer_id,
7036 );
7037 view_state.apply_config_defaults(
7038 self.config.editor.line_numbers,
7039 self.config.editor.highlight_current_line,
7040 false,
7041 false,
7042 None,
7043 self.config.editor.rulers.clone(),
7044 );
7045 self.split_view_states.insert(new_split_id, view_state);
7046
7047 if focus.unwrap_or(true) {
7048 self.split_manager.set_active_split(new_split_id);
7049 }
7050
7051 tracing::info!(
7052 "Created {:?} split for terminal {:?} with buffer {:?}",
7053 split_dir,
7054 terminal_id,
7055 buffer_id
7056 );
7057 Some(new_split_id)
7058 }
7059 Err(e) => {
7060 tracing::error!("Failed to create split for terminal: {}", e);
7061 self.set_active_buffer(buffer_id);
7062 None
7063 }
7064 }
7065 } else {
7066 self.set_active_buffer(buffer_id);
7068 None
7069 };
7070
7071 self.resize_visible_terminals();
7073
7074 let result = fresh_core::api::TerminalResult {
7076 buffer_id: buffer_id.0 as u64,
7077 terminal_id: terminal_id.0 as u64,
7078 split_id: created_split_id.map(|s| s.0 .0 as u64),
7079 };
7080 self.plugin_manager.resolve_callback(
7081 fresh_core::api::JsCallbackId::from(request_id),
7082 serde_json::to_string(&result).unwrap_or_default(),
7083 );
7084
7085 tracing::info!(
7086 "Plugin created terminal {:?} with buffer {:?}",
7087 terminal_id,
7088 buffer_id
7089 );
7090 }
7091 Err(e) => {
7092 tracing::error!("Failed to create terminal for plugin: {}", e);
7093 self.plugin_manager.reject_callback(
7094 fresh_core::api::JsCallbackId::from(request_id),
7095 format!("Failed to create terminal: {}", e),
7096 );
7097 }
7098 }
7099 }
7100
7101 PluginCommand::SendTerminalInput { terminal_id, data } => {
7102 if let Some(handle) = self.terminal_manager.get(terminal_id) {
7103 handle.write(data.as_bytes());
7104 tracing::trace!(
7105 "Plugin sent {} bytes to terminal {:?}",
7106 data.len(),
7107 terminal_id
7108 );
7109 } else {
7110 tracing::warn!(
7111 "Plugin tried to send input to non-existent terminal {:?}",
7112 terminal_id
7113 );
7114 }
7115 }
7116
7117 PluginCommand::CloseTerminal { terminal_id } => {
7118 let buffer_to_close = self
7120 .terminal_buffers
7121 .iter()
7122 .find(|(_, &tid)| tid == terminal_id)
7123 .map(|(&bid, _)| bid);
7124
7125 if let Some(buffer_id) = buffer_to_close {
7126 if let Err(e) = self.close_buffer(buffer_id) {
7127 tracing::warn!("Failed to close terminal buffer: {}", e);
7128 }
7129 tracing::info!("Plugin closed terminal {:?}", terminal_id);
7130 } else {
7131 self.terminal_manager.close(terminal_id);
7133 tracing::info!("Plugin closed terminal {:?} (no buffer found)", terminal_id);
7134 }
7135 }
7136
7137 PluginCommand::GrepProject {
7138 pattern,
7139 fixed_string,
7140 case_sensitive,
7141 max_results,
7142 whole_words,
7143 callback_id,
7144 } => {
7145 self.handle_grep_project(
7146 pattern,
7147 fixed_string,
7148 case_sensitive,
7149 max_results,
7150 whole_words,
7151 callback_id,
7152 );
7153 }
7154
7155 PluginCommand::GrepProjectStreaming {
7156 pattern,
7157 fixed_string,
7158 case_sensitive,
7159 max_results,
7160 whole_words,
7161 search_id,
7162 callback_id,
7163 } => {
7164 self.handle_grep_project_streaming(
7165 pattern,
7166 fixed_string,
7167 case_sensitive,
7168 max_results,
7169 whole_words,
7170 search_id,
7171 callback_id,
7172 );
7173 }
7174
7175 PluginCommand::ReplaceInBuffer {
7176 file_path,
7177 matches,
7178 replacement,
7179 callback_id,
7180 } => {
7181 self.handle_replace_in_buffer(file_path, matches, replacement, callback_id);
7182 }
7183 }
7184 Ok(())
7185 }
7186
7187 fn handle_save_buffer_to_path(&mut self, buffer_id: BufferId, path: std::path::PathBuf) {
7189 if let Some(state) = self.buffers.get_mut(&buffer_id) {
7190 match state.buffer.save_to_file(&path) {
7192 Ok(()) => {
7193 if let Err(e) = self.finalize_save(Some(path)) {
7196 tracing::warn!("Failed to finalize save: {}", e);
7197 }
7198 tracing::debug!("Saved buffer {:?} to path", buffer_id);
7199 }
7200 Err(e) => {
7201 self.handle_set_status(format!("Error saving: {}", e));
7202 tracing::error!("Failed to save buffer to path: {}", e);
7203 }
7204 }
7205 } else {
7206 self.handle_set_status(format!("Buffer {:?} not found", buffer_id));
7207 tracing::warn!("SaveBufferToPath: buffer {:?} not found", buffer_id);
7208 }
7209 }
7210
7211 #[cfg(feature = "plugins")]
7213 fn handle_load_plugin(&mut self, path: std::path::PathBuf, callback_id: JsCallbackId) {
7214 match self.plugin_manager.load_plugin(&path) {
7215 Ok(()) => {
7216 tracing::info!("Loaded plugin from {:?}", path);
7217 self.plugin_manager
7218 .resolve_callback(callback_id, "true".to_string());
7219 }
7220 Err(e) => {
7221 tracing::error!("Failed to load plugin from {:?}: {}", path, e);
7222 self.plugin_manager
7223 .reject_callback(callback_id, format!("{}", e));
7224 }
7225 }
7226 }
7227
7228 #[cfg(feature = "plugins")]
7230 fn handle_unload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
7231 match self.plugin_manager.unload_plugin(&name) {
7232 Ok(()) => {
7233 tracing::info!("Unloaded plugin: {}", name);
7234 self.plugin_manager
7235 .resolve_callback(callback_id, "true".to_string());
7236 }
7237 Err(e) => {
7238 tracing::error!("Failed to unload plugin '{}': {}", name, e);
7239 self.plugin_manager
7240 .reject_callback(callback_id, format!("{}", e));
7241 }
7242 }
7243 }
7244
7245 #[cfg(feature = "plugins")]
7247 fn handle_reload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
7248 match self.plugin_manager.reload_plugin(&name) {
7249 Ok(()) => {
7250 tracing::info!("Reloaded plugin: {}", name);
7251 self.plugin_manager
7252 .resolve_callback(callback_id, "true".to_string());
7253 }
7254 Err(e) => {
7255 tracing::error!("Failed to reload plugin '{}': {}", name, e);
7256 self.plugin_manager
7257 .reject_callback(callback_id, format!("{}", e));
7258 }
7259 }
7260 }
7261
7262 #[cfg(feature = "plugins")]
7264 fn handle_list_plugins(&mut self, callback_id: JsCallbackId) {
7265 let plugins = self.plugin_manager.list_plugins();
7266 let json_array: Vec<serde_json::Value> = plugins
7268 .iter()
7269 .map(|p| {
7270 serde_json::json!({
7271 "name": p.name,
7272 "path": p.path.to_string_lossy(),
7273 "enabled": p.enabled
7274 })
7275 })
7276 .collect();
7277 let json_str = serde_json::to_string(&json_array).unwrap_or_else(|_| "[]".to_string());
7278 self.plugin_manager.resolve_callback(callback_id, json_str);
7279 }
7280
7281 fn handle_execute_action(&mut self, action_name: String) {
7283 use crate::input::keybindings::Action;
7284 use std::collections::HashMap;
7285
7286 if let Some(action) = Action::from_str(&action_name, &HashMap::new()) {
7288 if let Err(e) = self.handle_action(action) {
7290 tracing::warn!("Failed to execute action '{}': {}", action_name, e);
7291 } else {
7292 tracing::debug!("Executed action: {}", action_name);
7293 }
7294 } else {
7295 tracing::warn!("Unknown action: {}", action_name);
7296 }
7297 }
7298
7299 fn handle_execute_actions(&mut self, actions: Vec<fresh_core::api::ActionSpec>) {
7302 use crate::input::keybindings::Action;
7303 use std::collections::HashMap;
7304
7305 for action_spec in actions {
7306 if let Some(action) = Action::from_str(&action_spec.action, &HashMap::new()) {
7307 for _ in 0..action_spec.count {
7309 if let Err(e) = self.handle_action(action.clone()) {
7310 tracing::warn!("Failed to execute action '{}': {}", action_spec.action, e);
7311 return; }
7313 }
7314 tracing::debug!(
7315 "Executed action '{}' {} time(s)",
7316 action_spec.action,
7317 action_spec.count
7318 );
7319 } else {
7320 tracing::warn!("Unknown action: {}", action_spec.action);
7321 return; }
7323 }
7324 }
7325
7326 fn handle_get_buffer_text(
7328 &mut self,
7329 buffer_id: BufferId,
7330 start: usize,
7331 end: usize,
7332 request_id: u64,
7333 ) {
7334 let result = if let Some(state) = self.buffers.get_mut(&buffer_id) {
7335 let len = state.buffer.len();
7337 if start <= end && end <= len {
7338 Ok(state.get_text_range(start, end))
7339 } else {
7340 Err(format!(
7341 "Invalid range {}..{} for buffer of length {}",
7342 start, end, len
7343 ))
7344 }
7345 } else {
7346 Err(format!("Buffer {:?} not found", buffer_id))
7347 };
7348
7349 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
7351 match result {
7352 Ok(text) => {
7353 let json = serde_json::to_string(&text).unwrap_or_else(|_| "null".to_string());
7355 self.plugin_manager.resolve_callback(callback_id, json);
7356 }
7357 Err(error) => {
7358 self.plugin_manager.reject_callback(callback_id, error);
7359 }
7360 }
7361 }
7362
7363 fn handle_set_editor_mode(&mut self, mode: Option<String>) {
7365 self.editor_mode = mode.clone();
7366 tracing::debug!("Set editor mode: {:?}", mode);
7367 }
7368
7369 fn handle_get_line_start_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
7371 let actual_buffer_id = if buffer_id.0 == 0 {
7373 self.active_buffer_id()
7374 } else {
7375 buffer_id
7376 };
7377
7378 let result = if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
7379 let line_number = line as usize;
7381 let buffer_len = state.buffer.len();
7382
7383 if line_number == 0 {
7384 Some(0)
7386 } else {
7387 let mut current_line = 0;
7389 let mut line_start = None;
7390
7391 let content = state.get_text_range(0, buffer_len);
7393 for (byte_idx, c) in content.char_indices() {
7394 if c == '\n' {
7395 current_line += 1;
7396 if current_line == line_number {
7397 line_start = Some(byte_idx + 1);
7399 break;
7400 }
7401 }
7402 }
7403 line_start
7404 }
7405 } else {
7406 None
7407 };
7408
7409 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
7411 let json = serde_json::to_string(&result).unwrap_or_else(|_| "null".to_string());
7413 self.plugin_manager.resolve_callback(callback_id, json);
7414 }
7415
7416 fn handle_get_line_end_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
7419 let actual_buffer_id = if buffer_id.0 == 0 {
7421 self.active_buffer_id()
7422 } else {
7423 buffer_id
7424 };
7425
7426 let result = if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
7427 let line_number = line as usize;
7428 let buffer_len = state.buffer.len();
7429
7430 let content = state.get_text_range(0, buffer_len);
7432 let mut current_line = 0;
7433 let mut line_end = None;
7434
7435 for (byte_idx, c) in content.char_indices() {
7436 if c == '\n' {
7437 if current_line == line_number {
7438 line_end = Some(byte_idx);
7440 break;
7441 }
7442 current_line += 1;
7443 }
7444 }
7445
7446 if line_end.is_none() && current_line == line_number {
7448 line_end = Some(buffer_len);
7449 }
7450
7451 line_end
7452 } else {
7453 None
7454 };
7455
7456 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
7457 let json = serde_json::to_string(&result).unwrap_or_else(|_| "null".to_string());
7458 self.plugin_manager.resolve_callback(callback_id, json);
7459 }
7460
7461 fn handle_get_buffer_line_count(&mut self, buffer_id: BufferId, request_id: u64) {
7463 let actual_buffer_id = if buffer_id.0 == 0 {
7465 self.active_buffer_id()
7466 } else {
7467 buffer_id
7468 };
7469
7470 let result = if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
7471 let buffer_len = state.buffer.len();
7472 let content = state.get_text_range(0, buffer_len);
7473
7474 if content.is_empty() {
7476 Some(1) } else {
7478 let newline_count = content.chars().filter(|&c| c == '\n').count();
7479 let ends_with_newline = content.ends_with('\n');
7481 if ends_with_newline {
7482 Some(newline_count)
7483 } else {
7484 Some(newline_count + 1)
7485 }
7486 }
7487 } else {
7488 None
7489 };
7490
7491 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
7492 let json = serde_json::to_string(&result).unwrap_or_else(|_| "null".to_string());
7493 self.plugin_manager.resolve_callback(callback_id, json);
7494 }
7495
7496 fn handle_scroll_to_line_center(
7498 &mut self,
7499 split_id: SplitId,
7500 buffer_id: BufferId,
7501 line: usize,
7502 ) {
7503 let actual_split_id = if split_id.0 == 0 {
7505 self.split_manager.active_split()
7506 } else {
7507 LeafId(split_id)
7508 };
7509
7510 let actual_buffer_id = if buffer_id.0 == 0 {
7512 self.active_buffer()
7513 } else {
7514 buffer_id
7515 };
7516
7517 let viewport_height = if let Some(view_state) = self.split_view_states.get(&actual_split_id)
7519 {
7520 view_state.viewport.height as usize
7521 } else {
7522 return;
7523 };
7524
7525 let lines_above = viewport_height / 2;
7527 let target_line = line.saturating_sub(lines_above);
7528
7529 if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
7531 let buffer = &mut state.buffer;
7532 if let Some(view_state) = self.split_view_states.get_mut(&actual_split_id) {
7533 view_state.viewport.scroll_to(buffer, target_line);
7534 view_state.viewport.set_skip_ensure_visible();
7536 }
7537 }
7538 }
7539
7540 fn handle_scroll_buffer_to_line(&mut self, buffer_id: BufferId, line: usize) {
7550 if !self.buffers.contains_key(&buffer_id) {
7551 return;
7552 }
7553
7554 let mut target_leaves: Vec<LeafId> = Vec::new();
7556
7557 for leaf_id in self.split_manager.root().leaf_split_ids() {
7559 if let Some(vs) = self.split_view_states.get(&leaf_id) {
7560 if vs.active_buffer == buffer_id {
7561 target_leaves.push(leaf_id);
7562 }
7563 }
7564 }
7565
7566 for (_group_leaf_id, node) in self.grouped_subtrees.iter() {
7568 if let crate::view::split::SplitNode::Grouped { layout, .. } = node {
7569 for inner_leaf in layout.leaf_split_ids() {
7570 if let Some(vs) = self.split_view_states.get(&inner_leaf) {
7571 if vs.active_buffer == buffer_id && !target_leaves.contains(&inner_leaf) {
7572 target_leaves.push(inner_leaf);
7573 }
7574 }
7575 }
7576 }
7577 }
7578
7579 if target_leaves.is_empty() {
7580 return;
7581 }
7582
7583 let state = match self.buffers.get_mut(&buffer_id) {
7584 Some(s) => s,
7585 None => return,
7586 };
7587
7588 for leaf_id in target_leaves {
7589 let Some(view_state) = self.split_view_states.get_mut(&leaf_id) else {
7590 continue;
7591 };
7592 let viewport_height = view_state.viewport.height as usize;
7593 let lines_above = viewport_height / 3;
7596 let target = line.saturating_sub(lines_above);
7597 view_state.viewport.scroll_to(&mut state.buffer, target);
7598 view_state.viewport.set_skip_ensure_visible();
7599 }
7600 }
7601}
7602
7603fn parse_key_string(key_str: &str) -> Option<(KeyCode, KeyModifiers)> {
7612 use crossterm::event::{KeyCode, KeyModifiers};
7613
7614 let mut modifiers = KeyModifiers::NONE;
7615 let mut remaining = key_str;
7616
7617 loop {
7619 if remaining.starts_with("C-") {
7620 modifiers |= KeyModifiers::CONTROL;
7621 remaining = &remaining[2..];
7622 } else if remaining.starts_with("M-") {
7623 modifiers |= KeyModifiers::ALT;
7624 remaining = &remaining[2..];
7625 } else if remaining.starts_with("S-") {
7626 modifiers |= KeyModifiers::SHIFT;
7627 remaining = &remaining[2..];
7628 } else {
7629 break;
7630 }
7631 }
7632
7633 let upper = remaining.to_uppercase();
7636 let code = match upper.as_str() {
7637 "RET" | "RETURN" | "ENTER" => KeyCode::Enter,
7638 "TAB" => KeyCode::Tab,
7639 "BACKTAB" => KeyCode::BackTab,
7640 "ESC" | "ESCAPE" => KeyCode::Esc,
7641 "SPC" | "SPACE" => KeyCode::Char(' '),
7642 "DEL" | "DELETE" => KeyCode::Delete,
7643 "BS" | "BACKSPACE" => KeyCode::Backspace,
7644 "UP" => KeyCode::Up,
7645 "DOWN" => KeyCode::Down,
7646 "LEFT" => KeyCode::Left,
7647 "RIGHT" => KeyCode::Right,
7648 "HOME" => KeyCode::Home,
7649 "END" => KeyCode::End,
7650 "PAGEUP" | "PGUP" => KeyCode::PageUp,
7651 "PAGEDOWN" | "PGDN" => KeyCode::PageDown,
7652 s if s.starts_with('F') && s.len() > 1 => {
7653 if let Ok(n) = s[1..].parse::<u8>() {
7655 KeyCode::F(n)
7656 } else {
7657 return None;
7658 }
7659 }
7660 _ if remaining.len() == 1 => {
7661 let c = remaining.chars().next()?;
7664 if c.is_ascii_uppercase() {
7665 modifiers |= KeyModifiers::SHIFT;
7666 }
7667 KeyCode::Char(c.to_ascii_lowercase())
7668 }
7669 _ => return None,
7670 };
7671
7672 Some((code, modifiers))
7673}
7674
7675#[cfg(test)]
7676mod tests {
7677 use super::*;
7678 use tempfile::TempDir;
7679
7680 fn test_dir_context() -> (DirectoryContext, TempDir) {
7682 let temp_dir = TempDir::new().unwrap();
7683 let dir_context = DirectoryContext::for_testing(temp_dir.path());
7684 (dir_context, temp_dir)
7685 }
7686
7687 fn test_filesystem() -> Arc<dyn FileSystem + Send + Sync> {
7689 Arc::new(crate::model::filesystem::StdFileSystem)
7690 }
7691
7692 #[test]
7693 fn test_editor_new() {
7694 let config = Config::default();
7695 let (dir_context, _temp) = test_dir_context();
7696 let editor = Editor::new(
7697 config,
7698 80,
7699 24,
7700 dir_context,
7701 crate::view::color_support::ColorCapability::TrueColor,
7702 test_filesystem(),
7703 )
7704 .unwrap();
7705
7706 assert_eq!(editor.buffers.len(), 1);
7707 assert!(!editor.should_quit());
7708 }
7709
7710 #[test]
7711 fn test_new_buffer() {
7712 let config = Config::default();
7713 let (dir_context, _temp) = test_dir_context();
7714 let mut editor = Editor::new(
7715 config,
7716 80,
7717 24,
7718 dir_context,
7719 crate::view::color_support::ColorCapability::TrueColor,
7720 test_filesystem(),
7721 )
7722 .unwrap();
7723
7724 let id = editor.new_buffer();
7725 assert_eq!(editor.buffers.len(), 2);
7726 assert_eq!(editor.active_buffer(), id);
7727 }
7728
7729 #[test]
7730 #[ignore]
7731 fn test_clipboard() {
7732 let config = Config::default();
7733 let (dir_context, _temp) = test_dir_context();
7734 let mut editor = Editor::new(
7735 config,
7736 80,
7737 24,
7738 dir_context,
7739 crate::view::color_support::ColorCapability::TrueColor,
7740 test_filesystem(),
7741 )
7742 .unwrap();
7743
7744 editor.clipboard.set_internal("test".to_string());
7746
7747 editor.paste();
7749
7750 let content = editor.active_state().buffer.to_string().unwrap();
7751 assert_eq!(content, "test");
7752 }
7753
7754 #[test]
7755 fn test_action_to_events_insert_char() {
7756 let config = Config::default();
7757 let (dir_context, _temp) = test_dir_context();
7758 let mut editor = Editor::new(
7759 config,
7760 80,
7761 24,
7762 dir_context,
7763 crate::view::color_support::ColorCapability::TrueColor,
7764 test_filesystem(),
7765 )
7766 .unwrap();
7767
7768 let events = editor.action_to_events(Action::InsertChar('a'));
7769 assert!(events.is_some());
7770
7771 let events = events.unwrap();
7772 assert_eq!(events.len(), 1);
7773
7774 match &events[0] {
7775 Event::Insert { position, text, .. } => {
7776 assert_eq!(*position, 0);
7777 assert_eq!(text, "a");
7778 }
7779 _ => panic!("Expected Insert event"),
7780 }
7781 }
7782
7783 #[test]
7784 fn test_action_to_events_move_right() {
7785 let config = Config::default();
7786 let (dir_context, _temp) = test_dir_context();
7787 let mut editor = Editor::new(
7788 config,
7789 80,
7790 24,
7791 dir_context,
7792 crate::view::color_support::ColorCapability::TrueColor,
7793 test_filesystem(),
7794 )
7795 .unwrap();
7796
7797 let cursor_id = editor.active_cursors().primary_id();
7799 editor.apply_event_to_active_buffer(&Event::Insert {
7800 position: 0,
7801 text: "hello".to_string(),
7802 cursor_id,
7803 });
7804
7805 let events = editor.action_to_events(Action::MoveRight);
7806 assert!(events.is_some());
7807
7808 let events = events.unwrap();
7809 assert_eq!(events.len(), 1);
7810
7811 match &events[0] {
7812 Event::MoveCursor {
7813 new_position,
7814 new_anchor,
7815 ..
7816 } => {
7817 assert_eq!(*new_position, 5);
7819 assert_eq!(*new_anchor, None); }
7821 _ => panic!("Expected MoveCursor event"),
7822 }
7823 }
7824
7825 #[test]
7826 fn test_action_to_events_move_up_down() {
7827 let config = Config::default();
7828 let (dir_context, _temp) = test_dir_context();
7829 let mut editor = Editor::new(
7830 config,
7831 80,
7832 24,
7833 dir_context,
7834 crate::view::color_support::ColorCapability::TrueColor,
7835 test_filesystem(),
7836 )
7837 .unwrap();
7838
7839 let cursor_id = editor.active_cursors().primary_id();
7841 editor.apply_event_to_active_buffer(&Event::Insert {
7842 position: 0,
7843 text: "line1\nline2\nline3".to_string(),
7844 cursor_id,
7845 });
7846
7847 editor.apply_event_to_active_buffer(&Event::MoveCursor {
7849 cursor_id,
7850 old_position: 0, new_position: 6,
7852 old_anchor: None, new_anchor: None,
7854 old_sticky_column: 0,
7855 new_sticky_column: 0,
7856 });
7857
7858 let events = editor.action_to_events(Action::MoveUp);
7860 assert!(events.is_some());
7861 let events = events.unwrap();
7862 assert_eq!(events.len(), 1);
7863
7864 match &events[0] {
7865 Event::MoveCursor { new_position, .. } => {
7866 assert_eq!(*new_position, 0); }
7868 _ => panic!("Expected MoveCursor event"),
7869 }
7870 }
7871
7872 #[test]
7873 fn test_action_to_events_insert_newline() {
7874 let config = Config::default();
7875 let (dir_context, _temp) = test_dir_context();
7876 let mut editor = Editor::new(
7877 config,
7878 80,
7879 24,
7880 dir_context,
7881 crate::view::color_support::ColorCapability::TrueColor,
7882 test_filesystem(),
7883 )
7884 .unwrap();
7885
7886 let events = editor.action_to_events(Action::InsertNewline);
7887 assert!(events.is_some());
7888
7889 let events = events.unwrap();
7890 assert_eq!(events.len(), 1);
7891
7892 match &events[0] {
7893 Event::Insert { text, .. } => {
7894 assert_eq!(text, "\n");
7895 }
7896 _ => panic!("Expected Insert event"),
7897 }
7898 }
7899
7900 #[test]
7901 fn test_action_to_events_unimplemented() {
7902 let config = Config::default();
7903 let (dir_context, _temp) = test_dir_context();
7904 let mut editor = Editor::new(
7905 config,
7906 80,
7907 24,
7908 dir_context,
7909 crate::view::color_support::ColorCapability::TrueColor,
7910 test_filesystem(),
7911 )
7912 .unwrap();
7913
7914 assert!(editor.action_to_events(Action::Save).is_none());
7916 assert!(editor.action_to_events(Action::Quit).is_none());
7917 assert!(editor.action_to_events(Action::Undo).is_none());
7918 }
7919
7920 #[test]
7921 fn test_action_to_events_delete_backward() {
7922 let config = Config::default();
7923 let (dir_context, _temp) = test_dir_context();
7924 let mut editor = Editor::new(
7925 config,
7926 80,
7927 24,
7928 dir_context,
7929 crate::view::color_support::ColorCapability::TrueColor,
7930 test_filesystem(),
7931 )
7932 .unwrap();
7933
7934 let cursor_id = editor.active_cursors().primary_id();
7936 editor.apply_event_to_active_buffer(&Event::Insert {
7937 position: 0,
7938 text: "hello".to_string(),
7939 cursor_id,
7940 });
7941
7942 let events = editor.action_to_events(Action::DeleteBackward);
7943 assert!(events.is_some());
7944
7945 let events = events.unwrap();
7946 assert_eq!(events.len(), 1);
7947
7948 match &events[0] {
7949 Event::Delete {
7950 range,
7951 deleted_text,
7952 ..
7953 } => {
7954 assert_eq!(range.clone(), 4..5); assert_eq!(deleted_text, "o");
7956 }
7957 _ => panic!("Expected Delete event"),
7958 }
7959 }
7960
7961 #[test]
7962 fn test_action_to_events_delete_forward() {
7963 let config = Config::default();
7964 let (dir_context, _temp) = test_dir_context();
7965 let mut editor = Editor::new(
7966 config,
7967 80,
7968 24,
7969 dir_context,
7970 crate::view::color_support::ColorCapability::TrueColor,
7971 test_filesystem(),
7972 )
7973 .unwrap();
7974
7975 let cursor_id = editor.active_cursors().primary_id();
7977 editor.apply_event_to_active_buffer(&Event::Insert {
7978 position: 0,
7979 text: "hello".to_string(),
7980 cursor_id,
7981 });
7982
7983 editor.apply_event_to_active_buffer(&Event::MoveCursor {
7985 cursor_id,
7986 old_position: 0, new_position: 0,
7988 old_anchor: None, new_anchor: None,
7990 old_sticky_column: 0,
7991 new_sticky_column: 0,
7992 });
7993
7994 let events = editor.action_to_events(Action::DeleteForward);
7995 assert!(events.is_some());
7996
7997 let events = events.unwrap();
7998 assert_eq!(events.len(), 1);
7999
8000 match &events[0] {
8001 Event::Delete {
8002 range,
8003 deleted_text,
8004 ..
8005 } => {
8006 assert_eq!(range.clone(), 0..1); assert_eq!(deleted_text, "h");
8008 }
8009 _ => panic!("Expected Delete event"),
8010 }
8011 }
8012
8013 #[test]
8014 fn test_action_to_events_select_right() {
8015 let config = Config::default();
8016 let (dir_context, _temp) = test_dir_context();
8017 let mut editor = Editor::new(
8018 config,
8019 80,
8020 24,
8021 dir_context,
8022 crate::view::color_support::ColorCapability::TrueColor,
8023 test_filesystem(),
8024 )
8025 .unwrap();
8026
8027 let cursor_id = editor.active_cursors().primary_id();
8029 editor.apply_event_to_active_buffer(&Event::Insert {
8030 position: 0,
8031 text: "hello".to_string(),
8032 cursor_id,
8033 });
8034
8035 editor.apply_event_to_active_buffer(&Event::MoveCursor {
8037 cursor_id,
8038 old_position: 0, new_position: 0,
8040 old_anchor: None, new_anchor: None,
8042 old_sticky_column: 0,
8043 new_sticky_column: 0,
8044 });
8045
8046 let events = editor.action_to_events(Action::SelectRight);
8047 assert!(events.is_some());
8048
8049 let events = events.unwrap();
8050 assert_eq!(events.len(), 1);
8051
8052 match &events[0] {
8053 Event::MoveCursor {
8054 new_position,
8055 new_anchor,
8056 ..
8057 } => {
8058 assert_eq!(*new_position, 1); assert_eq!(*new_anchor, Some(0)); }
8061 _ => panic!("Expected MoveCursor event"),
8062 }
8063 }
8064
8065 #[test]
8066 fn test_action_to_events_select_all() {
8067 let config = Config::default();
8068 let (dir_context, _temp) = test_dir_context();
8069 let mut editor = Editor::new(
8070 config,
8071 80,
8072 24,
8073 dir_context,
8074 crate::view::color_support::ColorCapability::TrueColor,
8075 test_filesystem(),
8076 )
8077 .unwrap();
8078
8079 let cursor_id = editor.active_cursors().primary_id();
8081 editor.apply_event_to_active_buffer(&Event::Insert {
8082 position: 0,
8083 text: "hello world".to_string(),
8084 cursor_id,
8085 });
8086
8087 let events = editor.action_to_events(Action::SelectAll);
8088 assert!(events.is_some());
8089
8090 let events = events.unwrap();
8091 assert_eq!(events.len(), 1);
8092
8093 match &events[0] {
8094 Event::MoveCursor {
8095 new_position,
8096 new_anchor,
8097 ..
8098 } => {
8099 assert_eq!(*new_position, 11); assert_eq!(*new_anchor, Some(0)); }
8102 _ => panic!("Expected MoveCursor event"),
8103 }
8104 }
8105
8106 #[test]
8107 fn test_action_to_events_document_nav() {
8108 let config = Config::default();
8109 let (dir_context, _temp) = test_dir_context();
8110 let mut editor = Editor::new(
8111 config,
8112 80,
8113 24,
8114 dir_context,
8115 crate::view::color_support::ColorCapability::TrueColor,
8116 test_filesystem(),
8117 )
8118 .unwrap();
8119
8120 let cursor_id = editor.active_cursors().primary_id();
8122 editor.apply_event_to_active_buffer(&Event::Insert {
8123 position: 0,
8124 text: "line1\nline2\nline3".to_string(),
8125 cursor_id,
8126 });
8127
8128 let events = editor.action_to_events(Action::MoveDocumentStart);
8130 assert!(events.is_some());
8131 let events = events.unwrap();
8132 match &events[0] {
8133 Event::MoveCursor { new_position, .. } => {
8134 assert_eq!(*new_position, 0);
8135 }
8136 _ => panic!("Expected MoveCursor event"),
8137 }
8138
8139 let events = editor.action_to_events(Action::MoveDocumentEnd);
8141 assert!(events.is_some());
8142 let events = events.unwrap();
8143 match &events[0] {
8144 Event::MoveCursor { new_position, .. } => {
8145 assert_eq!(*new_position, 17); }
8147 _ => panic!("Expected MoveCursor event"),
8148 }
8149 }
8150
8151 #[test]
8152 fn test_action_to_events_remove_secondary_cursors() {
8153 use crate::model::event::CursorId;
8154
8155 let config = Config::default();
8156 let (dir_context, _temp) = test_dir_context();
8157 let mut editor = Editor::new(
8158 config,
8159 80,
8160 24,
8161 dir_context,
8162 crate::view::color_support::ColorCapability::TrueColor,
8163 test_filesystem(),
8164 )
8165 .unwrap();
8166
8167 let cursor_id = editor.active_cursors().primary_id();
8169 editor.apply_event_to_active_buffer(&Event::Insert {
8170 position: 0,
8171 text: "hello world test".to_string(),
8172 cursor_id,
8173 });
8174
8175 editor.apply_event_to_active_buffer(&Event::AddCursor {
8177 cursor_id: CursorId(1),
8178 position: 5,
8179 anchor: None,
8180 });
8181 editor.apply_event_to_active_buffer(&Event::AddCursor {
8182 cursor_id: CursorId(2),
8183 position: 10,
8184 anchor: None,
8185 });
8186
8187 assert_eq!(editor.active_cursors().count(), 3);
8188
8189 let first_id = editor
8191 .active_cursors()
8192 .iter()
8193 .map(|(id, _)| id)
8194 .min_by_key(|id| id.0)
8195 .expect("Should have at least one cursor");
8196
8197 let events = editor.action_to_events(Action::RemoveSecondaryCursors);
8199 assert!(events.is_some());
8200
8201 let events = events.unwrap();
8202 let remove_cursor_events: Vec<_> = events
8205 .iter()
8206 .filter_map(|e| match e {
8207 Event::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
8208 _ => None,
8209 })
8210 .collect();
8211
8212 assert_eq!(remove_cursor_events.len(), 2);
8214
8215 for cursor_id in &remove_cursor_events {
8216 assert_ne!(*cursor_id, first_id);
8218 }
8219 }
8220
8221 #[test]
8222 fn test_action_to_events_scroll() {
8223 let config = Config::default();
8224 let (dir_context, _temp) = test_dir_context();
8225 let mut editor = Editor::new(
8226 config,
8227 80,
8228 24,
8229 dir_context,
8230 crate::view::color_support::ColorCapability::TrueColor,
8231 test_filesystem(),
8232 )
8233 .unwrap();
8234
8235 let events = editor.action_to_events(Action::ScrollUp);
8237 assert!(events.is_some());
8238 let events = events.unwrap();
8239 assert_eq!(events.len(), 1);
8240 match &events[0] {
8241 Event::Scroll { line_offset } => {
8242 assert_eq!(*line_offset, -1);
8243 }
8244 _ => panic!("Expected Scroll event"),
8245 }
8246
8247 let events = editor.action_to_events(Action::ScrollDown);
8249 assert!(events.is_some());
8250 let events = events.unwrap();
8251 assert_eq!(events.len(), 1);
8252 match &events[0] {
8253 Event::Scroll { line_offset } => {
8254 assert_eq!(*line_offset, 1);
8255 }
8256 _ => panic!("Expected Scroll event"),
8257 }
8258 }
8259
8260 #[test]
8261 fn test_action_to_events_none() {
8262 let config = Config::default();
8263 let (dir_context, _temp) = test_dir_context();
8264 let mut editor = Editor::new(
8265 config,
8266 80,
8267 24,
8268 dir_context,
8269 crate::view::color_support::ColorCapability::TrueColor,
8270 test_filesystem(),
8271 )
8272 .unwrap();
8273
8274 let events = editor.action_to_events(Action::None);
8276 assert!(events.is_none());
8277 }
8278
8279 #[test]
8280 fn test_lsp_incremental_insert_generates_correct_range() {
8281 use crate::model::buffer::Buffer;
8284
8285 let buffer = Buffer::from_str_test("hello\nworld");
8286
8287 let position = 0;
8290 let (line, character) = buffer.position_to_lsp_position(position);
8291
8292 assert_eq!(line, 0, "Insertion at start should be line 0");
8293 assert_eq!(character, 0, "Insertion at start should be char 0");
8294
8295 let lsp_pos = Position::new(line as u32, character as u32);
8297 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
8298
8299 assert_eq!(lsp_range.start.line, 0);
8300 assert_eq!(lsp_range.start.character, 0);
8301 assert_eq!(lsp_range.end.line, 0);
8302 assert_eq!(lsp_range.end.character, 0);
8303 assert_eq!(
8304 lsp_range.start, lsp_range.end,
8305 "Insert should have zero-width range"
8306 );
8307
8308 let position = 3;
8310 let (line, character) = buffer.position_to_lsp_position(position);
8311
8312 assert_eq!(line, 0);
8313 assert_eq!(character, 3);
8314
8315 let position = 6;
8317 let (line, character) = buffer.position_to_lsp_position(position);
8318
8319 assert_eq!(line, 1, "Position after newline should be line 1");
8320 assert_eq!(character, 0, "Position at start of line 2 should be char 0");
8321 }
8322
8323 #[test]
8324 fn test_lsp_incremental_delete_generates_correct_range() {
8325 use crate::model::buffer::Buffer;
8328
8329 let buffer = Buffer::from_str_test("hello\nworld");
8330
8331 let range_start = 1;
8333 let range_end = 5;
8334
8335 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
8336 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
8337
8338 assert_eq!(start_line, 0);
8339 assert_eq!(start_char, 1);
8340 assert_eq!(end_line, 0);
8341 assert_eq!(end_char, 5);
8342
8343 let lsp_range = LspRange::new(
8344 Position::new(start_line as u32, start_char as u32),
8345 Position::new(end_line as u32, end_char as u32),
8346 );
8347
8348 assert_eq!(lsp_range.start.line, 0);
8349 assert_eq!(lsp_range.start.character, 1);
8350 assert_eq!(lsp_range.end.line, 0);
8351 assert_eq!(lsp_range.end.character, 5);
8352 assert_ne!(
8353 lsp_range.start, lsp_range.end,
8354 "Delete should have non-zero range"
8355 );
8356
8357 let range_start = 4;
8359 let range_end = 8;
8360
8361 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
8362 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
8363
8364 assert_eq!(start_line, 0, "Delete start on line 0");
8365 assert_eq!(start_char, 4, "Delete start at char 4");
8366 assert_eq!(end_line, 1, "Delete end on line 1");
8367 assert_eq!(end_char, 2, "Delete end at char 2 of line 1");
8368 }
8369
8370 #[test]
8371 fn test_lsp_incremental_utf16_encoding() {
8372 use crate::model::buffer::Buffer;
8375
8376 let buffer = Buffer::from_str_test("😀hello");
8378
8379 let (line, character) = buffer.position_to_lsp_position(4);
8381
8382 assert_eq!(line, 0);
8383 assert_eq!(character, 2, "Emoji should count as 2 UTF-16 code units");
8384
8385 let (line, character) = buffer.position_to_lsp_position(9);
8387
8388 assert_eq!(line, 0);
8389 assert_eq!(
8390 character, 7,
8391 "Should be 2 (emoji) + 5 (text) = 7 UTF-16 code units"
8392 );
8393
8394 let buffer = Buffer::from_str_test("café");
8396
8397 let (line, character) = buffer.position_to_lsp_position(3);
8399
8400 assert_eq!(line, 0);
8401 assert_eq!(character, 3);
8402
8403 let (line, character) = buffer.position_to_lsp_position(5);
8405
8406 assert_eq!(line, 0);
8407 assert_eq!(character, 4, "é should count as 1 UTF-16 code unit");
8408 }
8409
8410 #[test]
8411 fn test_lsp_content_change_event_structure() {
8412 let insert_change = TextDocumentContentChangeEvent {
8416 range: Some(LspRange::new(Position::new(0, 5), Position::new(0, 5))),
8417 range_length: None,
8418 text: "NEW".to_string(),
8419 };
8420
8421 assert!(insert_change.range.is_some());
8422 assert_eq!(insert_change.text, "NEW");
8423 let range = insert_change.range.unwrap();
8424 assert_eq!(
8425 range.start, range.end,
8426 "Insert should have zero-width range"
8427 );
8428
8429 let delete_change = TextDocumentContentChangeEvent {
8431 range: Some(LspRange::new(Position::new(0, 2), Position::new(0, 7))),
8432 range_length: None,
8433 text: String::new(),
8434 };
8435
8436 assert!(delete_change.range.is_some());
8437 assert_eq!(delete_change.text, "");
8438 let range = delete_change.range.unwrap();
8439 assert_ne!(range.start, range.end, "Delete should have non-zero range");
8440 assert_eq!(range.start.line, 0);
8441 assert_eq!(range.start.character, 2);
8442 assert_eq!(range.end.line, 0);
8443 assert_eq!(range.end.character, 7);
8444 }
8445
8446 #[test]
8447 fn test_goto_matching_bracket_forward() {
8448 let config = Config::default();
8449 let (dir_context, _temp) = test_dir_context();
8450 let mut editor = Editor::new(
8451 config,
8452 80,
8453 24,
8454 dir_context,
8455 crate::view::color_support::ColorCapability::TrueColor,
8456 test_filesystem(),
8457 )
8458 .unwrap();
8459
8460 let cursor_id = editor.active_cursors().primary_id();
8462 editor.apply_event_to_active_buffer(&Event::Insert {
8463 position: 0,
8464 text: "fn main() { let x = (1 + 2); }".to_string(),
8465 cursor_id,
8466 });
8467
8468 editor.apply_event_to_active_buffer(&Event::MoveCursor {
8470 cursor_id,
8471 old_position: 31,
8472 new_position: 10,
8473 old_anchor: None,
8474 new_anchor: None,
8475 old_sticky_column: 0,
8476 new_sticky_column: 0,
8477 });
8478
8479 assert_eq!(editor.active_cursors().primary().position, 10);
8480
8481 editor.goto_matching_bracket();
8483
8484 assert_eq!(editor.active_cursors().primary().position, 29);
8489 }
8490
8491 #[test]
8492 fn test_goto_matching_bracket_backward() {
8493 let config = Config::default();
8494 let (dir_context, _temp) = test_dir_context();
8495 let mut editor = Editor::new(
8496 config,
8497 80,
8498 24,
8499 dir_context,
8500 crate::view::color_support::ColorCapability::TrueColor,
8501 test_filesystem(),
8502 )
8503 .unwrap();
8504
8505 let cursor_id = editor.active_cursors().primary_id();
8507 editor.apply_event_to_active_buffer(&Event::Insert {
8508 position: 0,
8509 text: "fn main() { let x = (1 + 2); }".to_string(),
8510 cursor_id,
8511 });
8512
8513 editor.apply_event_to_active_buffer(&Event::MoveCursor {
8515 cursor_id,
8516 old_position: 31,
8517 new_position: 26,
8518 old_anchor: None,
8519 new_anchor: None,
8520 old_sticky_column: 0,
8521 new_sticky_column: 0,
8522 });
8523
8524 editor.goto_matching_bracket();
8526
8527 assert_eq!(editor.active_cursors().primary().position, 20);
8529 }
8530
8531 #[test]
8532 fn test_goto_matching_bracket_nested() {
8533 let config = Config::default();
8534 let (dir_context, _temp) = test_dir_context();
8535 let mut editor = Editor::new(
8536 config,
8537 80,
8538 24,
8539 dir_context,
8540 crate::view::color_support::ColorCapability::TrueColor,
8541 test_filesystem(),
8542 )
8543 .unwrap();
8544
8545 let cursor_id = editor.active_cursors().primary_id();
8547 editor.apply_event_to_active_buffer(&Event::Insert {
8548 position: 0,
8549 text: "{a{b{c}d}e}".to_string(),
8550 cursor_id,
8551 });
8552
8553 editor.apply_event_to_active_buffer(&Event::MoveCursor {
8555 cursor_id,
8556 old_position: 11,
8557 new_position: 0,
8558 old_anchor: None,
8559 new_anchor: None,
8560 old_sticky_column: 0,
8561 new_sticky_column: 0,
8562 });
8563
8564 editor.goto_matching_bracket();
8566
8567 assert_eq!(editor.active_cursors().primary().position, 10);
8569 }
8570
8571 #[test]
8572 fn test_search_case_sensitive() {
8573 let config = Config::default();
8574 let (dir_context, _temp) = test_dir_context();
8575 let mut editor = Editor::new(
8576 config,
8577 80,
8578 24,
8579 dir_context,
8580 crate::view::color_support::ColorCapability::TrueColor,
8581 test_filesystem(),
8582 )
8583 .unwrap();
8584
8585 let cursor_id = editor.active_cursors().primary_id();
8587 editor.apply_event_to_active_buffer(&Event::Insert {
8588 position: 0,
8589 text: "Hello hello HELLO".to_string(),
8590 cursor_id,
8591 });
8592
8593 editor.search_case_sensitive = false;
8595 editor.perform_search("hello");
8596
8597 let search_state = editor.search_state.as_ref().unwrap();
8598 assert_eq!(
8599 search_state.matches.len(),
8600 3,
8601 "Should find all 3 matches case-insensitively"
8602 );
8603
8604 editor.search_case_sensitive = true;
8606 editor.perform_search("hello");
8607
8608 let search_state = editor.search_state.as_ref().unwrap();
8609 assert_eq!(
8610 search_state.matches.len(),
8611 1,
8612 "Should find only 1 exact match"
8613 );
8614 assert_eq!(
8615 search_state.matches[0], 6,
8616 "Should find 'hello' at position 6"
8617 );
8618 }
8619
8620 #[test]
8621 fn test_search_whole_word() {
8622 let config = Config::default();
8623 let (dir_context, _temp) = test_dir_context();
8624 let mut editor = Editor::new(
8625 config,
8626 80,
8627 24,
8628 dir_context,
8629 crate::view::color_support::ColorCapability::TrueColor,
8630 test_filesystem(),
8631 )
8632 .unwrap();
8633
8634 let cursor_id = editor.active_cursors().primary_id();
8636 editor.apply_event_to_active_buffer(&Event::Insert {
8637 position: 0,
8638 text: "test testing tested attest test".to_string(),
8639 cursor_id,
8640 });
8641
8642 editor.search_whole_word = false;
8644 editor.search_case_sensitive = true;
8645 editor.perform_search("test");
8646
8647 let search_state = editor.search_state.as_ref().unwrap();
8648 assert_eq!(
8649 search_state.matches.len(),
8650 5,
8651 "Should find 'test' in all occurrences"
8652 );
8653
8654 editor.search_whole_word = true;
8656 editor.perform_search("test");
8657
8658 let search_state = editor.search_state.as_ref().unwrap();
8659 assert_eq!(
8660 search_state.matches.len(),
8661 2,
8662 "Should find only whole word 'test'"
8663 );
8664 assert_eq!(search_state.matches[0], 0, "First match at position 0");
8665 assert_eq!(search_state.matches[1], 27, "Second match at position 27");
8666 }
8667
8668 #[test]
8669 fn test_search_scan_completes_when_capped() {
8670 let config = Config::default();
8676 let (dir_context, _temp) = test_dir_context();
8677 let mut editor = Editor::new(
8678 config,
8679 80,
8680 24,
8681 dir_context,
8682 crate::view::color_support::ColorCapability::TrueColor,
8683 test_filesystem(),
8684 )
8685 .unwrap();
8686
8687 let buffer_id = editor.active_buffer();
8690 let regex = regex::bytes::Regex::new("test").unwrap();
8691 let fake_chunks = vec![
8692 crate::model::buffer::LineScanChunk {
8693 leaf_index: 0,
8694 byte_len: 100,
8695 already_known: true,
8696 },
8697 crate::model::buffer::LineScanChunk {
8698 leaf_index: 1,
8699 byte_len: 100,
8700 already_known: true,
8701 },
8702 ];
8703
8704 editor.search_scan_state = Some(SearchScanState {
8705 buffer_id,
8706 leaves: Vec::new(),
8707 scan: crate::model::buffer::ChunkedSearchState {
8708 chunks: fake_chunks,
8709 next_chunk: 1, next_doc_offset: 100,
8711 total_bytes: 200,
8712 scanned_bytes: 100,
8713 regex,
8714 matches: vec![
8715 crate::model::buffer::SearchMatch {
8716 byte_offset: 10,
8717 length: 4,
8718 line: 1,
8719 column: 11,
8720 context: String::new(),
8721 },
8722 crate::model::buffer::SearchMatch {
8723 byte_offset: 50,
8724 length: 4,
8725 line: 1,
8726 column: 51,
8727 context: String::new(),
8728 },
8729 ],
8730 overlap_tail: Vec::new(),
8731 overlap_doc_offset: 0,
8732 max_matches: 10_000,
8733 capped: true, query_len: 4,
8735 running_line: 1,
8736 },
8737 query: "test".to_string(),
8738 search_range: None,
8739 case_sensitive: false,
8740 whole_word: false,
8741 use_regex: false,
8742 });
8743
8744 let result = editor.process_search_scan();
8746 assert!(
8747 result,
8748 "process_search_scan should return true (needs render)"
8749 );
8750
8751 assert!(
8753 editor.search_scan_state.is_none(),
8754 "search_scan_state should be None after capped scan completes"
8755 );
8756
8757 let search_state = editor
8759 .search_state
8760 .as_ref()
8761 .expect("search_state should be set after scan finishes");
8762 assert_eq!(search_state.matches.len(), 2, "Should have 2 matches");
8763 assert_eq!(search_state.query, "test");
8764 assert!(
8765 search_state.capped,
8766 "search_state should be marked as capped"
8767 );
8768 }
8769
8770 #[test]
8771 fn test_bookmarks() {
8772 let config = Config::default();
8773 let (dir_context, _temp) = test_dir_context();
8774 let mut editor = Editor::new(
8775 config,
8776 80,
8777 24,
8778 dir_context,
8779 crate::view::color_support::ColorCapability::TrueColor,
8780 test_filesystem(),
8781 )
8782 .unwrap();
8783
8784 let cursor_id = editor.active_cursors().primary_id();
8786 editor.apply_event_to_active_buffer(&Event::Insert {
8787 position: 0,
8788 text: "Line 1\nLine 2\nLine 3".to_string(),
8789 cursor_id,
8790 });
8791
8792 editor.apply_event_to_active_buffer(&Event::MoveCursor {
8794 cursor_id,
8795 old_position: 21,
8796 new_position: 7,
8797 old_anchor: None,
8798 new_anchor: None,
8799 old_sticky_column: 0,
8800 new_sticky_column: 0,
8801 });
8802
8803 editor.set_bookmark('1');
8805 assert!(editor.bookmarks.contains_key(&'1'));
8806 assert_eq!(editor.bookmarks.get(&'1').unwrap().position, 7);
8807
8808 editor.apply_event_to_active_buffer(&Event::MoveCursor {
8810 cursor_id,
8811 old_position: 7,
8812 new_position: 14,
8813 old_anchor: None,
8814 new_anchor: None,
8815 old_sticky_column: 0,
8816 new_sticky_column: 0,
8817 });
8818
8819 editor.jump_to_bookmark('1');
8821 assert_eq!(editor.active_cursors().primary().position, 7);
8822
8823 editor.clear_bookmark('1');
8825 assert!(!editor.bookmarks.contains_key(&'1'));
8826 }
8827
8828 #[test]
8829 fn test_action_enum_new_variants() {
8830 use serde_json::json;
8832
8833 let args = HashMap::new();
8834 assert_eq!(
8835 Action::from_str("smart_home", &args),
8836 Some(Action::SmartHome)
8837 );
8838 assert_eq!(
8839 Action::from_str("dedent_selection", &args),
8840 Some(Action::DedentSelection)
8841 );
8842 assert_eq!(
8843 Action::from_str("toggle_comment", &args),
8844 Some(Action::ToggleComment)
8845 );
8846 assert_eq!(
8847 Action::from_str("goto_matching_bracket", &args),
8848 Some(Action::GoToMatchingBracket)
8849 );
8850 assert_eq!(
8851 Action::from_str("list_bookmarks", &args),
8852 Some(Action::ListBookmarks)
8853 );
8854 assert_eq!(
8855 Action::from_str("toggle_search_case_sensitive", &args),
8856 Some(Action::ToggleSearchCaseSensitive)
8857 );
8858 assert_eq!(
8859 Action::from_str("toggle_search_whole_word", &args),
8860 Some(Action::ToggleSearchWholeWord)
8861 );
8862
8863 let mut args_with_char = HashMap::new();
8865 args_with_char.insert("char".to_string(), json!("5"));
8866 assert_eq!(
8867 Action::from_str("set_bookmark", &args_with_char),
8868 Some(Action::SetBookmark('5'))
8869 );
8870 assert_eq!(
8871 Action::from_str("jump_to_bookmark", &args_with_char),
8872 Some(Action::JumpToBookmark('5'))
8873 );
8874 assert_eq!(
8875 Action::from_str("clear_bookmark", &args_with_char),
8876 Some(Action::ClearBookmark('5'))
8877 );
8878 }
8879
8880 #[test]
8881 fn test_keybinding_new_defaults() {
8882 use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
8883
8884 let mut config = Config::default();
8888 config.active_keybinding_map = crate::config::KeybindingMapName("default".to_string());
8889 let resolver = KeybindingResolver::new(&config);
8890
8891 let event = KeyEvent {
8893 code: KeyCode::Char('/'),
8894 modifiers: KeyModifiers::CONTROL,
8895 kind: KeyEventKind::Press,
8896 state: KeyEventState::NONE,
8897 };
8898 let action = resolver.resolve(&event, KeyContext::Normal);
8899 assert_eq!(action, Action::ToggleComment);
8900
8901 let event = KeyEvent {
8903 code: KeyCode::Char(']'),
8904 modifiers: KeyModifiers::CONTROL,
8905 kind: KeyEventKind::Press,
8906 state: KeyEventState::NONE,
8907 };
8908 let action = resolver.resolve(&event, KeyContext::Normal);
8909 assert_eq!(action, Action::GoToMatchingBracket);
8910
8911 let event = KeyEvent {
8913 code: KeyCode::Tab,
8914 modifiers: KeyModifiers::SHIFT,
8915 kind: KeyEventKind::Press,
8916 state: KeyEventState::NONE,
8917 };
8918 let action = resolver.resolve(&event, KeyContext::Normal);
8919 assert_eq!(action, Action::DedentSelection);
8920
8921 let event = KeyEvent {
8923 code: KeyCode::Char('g'),
8924 modifiers: KeyModifiers::CONTROL,
8925 kind: KeyEventKind::Press,
8926 state: KeyEventState::NONE,
8927 };
8928 let action = resolver.resolve(&event, KeyContext::Normal);
8929 assert_eq!(action, Action::GotoLine);
8930
8931 let event = KeyEvent {
8933 code: KeyCode::Char('5'),
8934 modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
8935 kind: KeyEventKind::Press,
8936 state: KeyEventState::NONE,
8937 };
8938 let action = resolver.resolve(&event, KeyContext::Normal);
8939 assert_eq!(action, Action::SetBookmark('5'));
8940
8941 let event = KeyEvent {
8942 code: KeyCode::Char('5'),
8943 modifiers: KeyModifiers::ALT,
8944 kind: KeyEventKind::Press,
8945 state: KeyEventState::NONE,
8946 };
8947 let action = resolver.resolve(&event, KeyContext::Normal);
8948 assert_eq!(action, Action::JumpToBookmark('5'));
8949 }
8950
8951 #[test]
8963 fn test_lsp_rename_didchange_positions_bug() {
8964 use crate::model::buffer::Buffer;
8965
8966 let config = Config::default();
8967 let (dir_context, _temp) = test_dir_context();
8968 let mut editor = Editor::new(
8969 config,
8970 80,
8971 24,
8972 dir_context,
8973 crate::view::color_support::ColorCapability::TrueColor,
8974 test_filesystem(),
8975 )
8976 .unwrap();
8977
8978 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
8982 editor.active_state_mut().buffer =
8983 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
8984
8985 let cursor_id = editor.active_cursors().primary_id();
8990
8991 let batch = Event::Batch {
8992 events: vec![
8993 Event::Delete {
8995 range: 23..26, deleted_text: "val".to_string(),
8997 cursor_id,
8998 },
8999 Event::Insert {
9000 position: 23,
9001 text: "value".to_string(),
9002 cursor_id,
9003 },
9004 Event::Delete {
9006 range: 7..10, deleted_text: "val".to_string(),
9008 cursor_id,
9009 },
9010 Event::Insert {
9011 position: 7,
9012 text: "value".to_string(),
9013 cursor_id,
9014 },
9015 ],
9016 description: "LSP Rename".to_string(),
9017 };
9018
9019 let lsp_changes_before = editor.collect_lsp_changes(&batch);
9021
9022 editor.apply_event_to_active_buffer(&batch);
9024
9025 let lsp_changes_after = editor.collect_lsp_changes(&batch);
9028
9029 let final_content = editor.active_state().buffer.to_string().unwrap();
9031 assert_eq!(
9032 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
9033 "Buffer should have 'value' in both places"
9034 );
9035
9036 assert_eq!(lsp_changes_before.len(), 4, "Should have 4 changes");
9042
9043 let first_delete = &lsp_changes_before[0];
9044 let first_del_range = first_delete.range.unwrap();
9045 assert_eq!(
9046 first_del_range.start.line, 1,
9047 "First delete should be on line 1 (BEFORE)"
9048 );
9049 assert_eq!(
9050 first_del_range.start.character, 4,
9051 "First delete start should be at char 4 (BEFORE)"
9052 );
9053
9054 assert_eq!(lsp_changes_after.len(), 4, "Should have 4 changes");
9060
9061 let first_delete_after = &lsp_changes_after[0];
9062 let first_del_range_after = first_delete_after.range.unwrap();
9063
9064 eprintln!("BEFORE modification:");
9067 eprintln!(
9068 " Delete at line {}, char {}-{}",
9069 first_del_range.start.line,
9070 first_del_range.start.character,
9071 first_del_range.end.character
9072 );
9073 eprintln!("AFTER modification:");
9074 eprintln!(
9075 " Delete at line {}, char {}-{}",
9076 first_del_range_after.start.line,
9077 first_del_range_after.start.character,
9078 first_del_range_after.end.character
9079 );
9080
9081 assert_ne!(
9099 first_del_range_after.end.character, first_del_range.end.character,
9100 "BUG CONFIRMED: LSP positions are different when calculated after buffer modification!"
9101 );
9102
9103 eprintln!("\n=== BUG DEMONSTRATED ===");
9104 eprintln!("When collect_lsp_changes() is called AFTER buffer modification,");
9105 eprintln!("the positions are WRONG because they're calculated from the");
9106 eprintln!("modified buffer, not the original buffer.");
9107 eprintln!("This causes the second rename to fail with 'content modified' error.");
9108 eprintln!("========================\n");
9109 }
9110
9111 #[test]
9112 fn test_lsp_rename_preserves_cursor_position() {
9113 use crate::model::buffer::Buffer;
9114
9115 let config = Config::default();
9116 let (dir_context, _temp) = test_dir_context();
9117 let mut editor = Editor::new(
9118 config,
9119 80,
9120 24,
9121 dir_context,
9122 crate::view::color_support::ColorCapability::TrueColor,
9123 test_filesystem(),
9124 )
9125 .unwrap();
9126
9127 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
9131 editor.active_state_mut().buffer =
9132 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
9133
9134 let original_cursor_pos = 23;
9136 editor.active_cursors_mut().primary_mut().position = original_cursor_pos;
9137
9138 let buffer_text = editor.active_state().buffer.to_string().unwrap();
9140 let text_at_cursor = buffer_text[original_cursor_pos..original_cursor_pos + 3].to_string();
9141 assert_eq!(text_at_cursor, "val", "Cursor should be at 'val'");
9142
9143 let cursor_id = editor.active_cursors().primary_id();
9146 let buffer_id = editor.active_buffer();
9147
9148 let events = vec![
9149 Event::Delete {
9151 range: 23..26, deleted_text: "val".to_string(),
9153 cursor_id,
9154 },
9155 Event::Insert {
9156 position: 23,
9157 text: "value".to_string(),
9158 cursor_id,
9159 },
9160 Event::Delete {
9162 range: 7..10, deleted_text: "val".to_string(),
9164 cursor_id,
9165 },
9166 Event::Insert {
9167 position: 7,
9168 text: "value".to_string(),
9169 cursor_id,
9170 },
9171 ];
9172
9173 editor
9175 .apply_events_to_buffer_as_bulk_edit(buffer_id, events, "LSP Rename".to_string())
9176 .unwrap();
9177
9178 let final_content = editor.active_state().buffer.to_string().unwrap();
9180 assert_eq!(
9181 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
9182 "Buffer should have 'value' in both places"
9183 );
9184
9185 let final_cursor_pos = editor.active_cursors().primary().position;
9193 let expected_cursor_pos = 25; assert_eq!(
9196 final_cursor_pos, expected_cursor_pos,
9197 "Cursor should be at position {} (start of 'value' on line 1), but was at {}. \
9198 Original pos: {}, expected adjustment: +2 for first rename",
9199 expected_cursor_pos, final_cursor_pos, original_cursor_pos
9200 );
9201
9202 let text_at_new_cursor = &final_content[final_cursor_pos..final_cursor_pos + 5];
9204 assert_eq!(
9205 text_at_new_cursor, "value",
9206 "Cursor should be at the start of 'value' after rename"
9207 );
9208 }
9209
9210 #[test]
9211 fn test_lsp_rename_twice_consecutive() {
9212 use crate::model::buffer::Buffer;
9215
9216 let config = Config::default();
9217 let (dir_context, _temp) = test_dir_context();
9218 let mut editor = Editor::new(
9219 config,
9220 80,
9221 24,
9222 dir_context,
9223 crate::view::color_support::ColorCapability::TrueColor,
9224 test_filesystem(),
9225 )
9226 .unwrap();
9227
9228 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
9230 editor.active_state_mut().buffer =
9231 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
9232
9233 let cursor_id = editor.active_cursors().primary_id();
9234 let buffer_id = editor.active_buffer();
9235
9236 let events1 = vec![
9239 Event::Delete {
9241 range: 23..26,
9242 deleted_text: "val".to_string(),
9243 cursor_id,
9244 },
9245 Event::Insert {
9246 position: 23,
9247 text: "value".to_string(),
9248 cursor_id,
9249 },
9250 Event::Delete {
9252 range: 7..10,
9253 deleted_text: "val".to_string(),
9254 cursor_id,
9255 },
9256 Event::Insert {
9257 position: 7,
9258 text: "value".to_string(),
9259 cursor_id,
9260 },
9261 ];
9262
9263 let batch1 = Event::Batch {
9265 events: events1.clone(),
9266 description: "LSP Rename 1".to_string(),
9267 };
9268
9269 let lsp_changes1 = editor.collect_lsp_changes(&batch1);
9271
9272 assert_eq!(
9274 lsp_changes1.len(),
9275 4,
9276 "First rename should have 4 LSP changes"
9277 );
9278
9279 let first_del = &lsp_changes1[0];
9281 let first_del_range = first_del.range.unwrap();
9282 assert_eq!(first_del_range.start.line, 1, "First delete line");
9283 assert_eq!(
9284 first_del_range.start.character, 4,
9285 "First delete start char"
9286 );
9287 assert_eq!(first_del_range.end.character, 7, "First delete end char");
9288
9289 editor
9291 .apply_events_to_buffer_as_bulk_edit(buffer_id, events1, "LSP Rename 1".to_string())
9292 .unwrap();
9293
9294 let after_first = editor.active_state().buffer.to_string().unwrap();
9296 assert_eq!(
9297 after_first, "fn foo(value: i32) {\n value + 1\n}\n",
9298 "After first rename"
9299 );
9300
9301 let events2 = vec![
9311 Event::Delete {
9313 range: 25..30,
9314 deleted_text: "value".to_string(),
9315 cursor_id,
9316 },
9317 Event::Insert {
9318 position: 25,
9319 text: "x".to_string(),
9320 cursor_id,
9321 },
9322 Event::Delete {
9324 range: 7..12,
9325 deleted_text: "value".to_string(),
9326 cursor_id,
9327 },
9328 Event::Insert {
9329 position: 7,
9330 text: "x".to_string(),
9331 cursor_id,
9332 },
9333 ];
9334
9335 let batch2 = Event::Batch {
9337 events: events2.clone(),
9338 description: "LSP Rename 2".to_string(),
9339 };
9340
9341 let lsp_changes2 = editor.collect_lsp_changes(&batch2);
9343
9344 assert_eq!(
9348 lsp_changes2.len(),
9349 4,
9350 "Second rename should have 4 LSP changes"
9351 );
9352
9353 let second_first_del = &lsp_changes2[0];
9355 let second_first_del_range = second_first_del.range.unwrap();
9356 assert_eq!(
9357 second_first_del_range.start.line, 1,
9358 "Second rename first delete should be on line 1"
9359 );
9360 assert_eq!(
9361 second_first_del_range.start.character, 4,
9362 "Second rename first delete start should be at char 4"
9363 );
9364 assert_eq!(
9365 second_first_del_range.end.character, 9,
9366 "Second rename first delete end should be at char 9 (4 + 5 for 'value')"
9367 );
9368
9369 let second_third_del = &lsp_changes2[2];
9371 let second_third_del_range = second_third_del.range.unwrap();
9372 assert_eq!(
9373 second_third_del_range.start.line, 0,
9374 "Second rename third delete should be on line 0"
9375 );
9376 assert_eq!(
9377 second_third_del_range.start.character, 7,
9378 "Second rename third delete start should be at char 7"
9379 );
9380 assert_eq!(
9381 second_third_del_range.end.character, 12,
9382 "Second rename third delete end should be at char 12 (7 + 5 for 'value')"
9383 );
9384
9385 editor
9387 .apply_events_to_buffer_as_bulk_edit(buffer_id, events2, "LSP Rename 2".to_string())
9388 .unwrap();
9389
9390 let after_second = editor.active_state().buffer.to_string().unwrap();
9392 assert_eq!(
9393 after_second, "fn foo(x: i32) {\n x + 1\n}\n",
9394 "After second rename"
9395 );
9396 }
9397
9398 #[test]
9399 fn test_ensure_active_tab_visible_static_offset() {
9400 let config = Config::default();
9401 let (dir_context, _temp) = test_dir_context();
9402 let mut editor = Editor::new(
9403 config,
9404 80,
9405 24,
9406 dir_context,
9407 crate::view::color_support::ColorCapability::TrueColor,
9408 test_filesystem(),
9409 )
9410 .unwrap();
9411 let split_id = editor.split_manager.active_split();
9412
9413 let buf1 = editor.new_buffer();
9415 editor
9416 .buffers
9417 .get_mut(&buf1)
9418 .unwrap()
9419 .buffer
9420 .rename_file_path(std::path::PathBuf::from("aaa_long_name_01.txt"));
9421 let buf2 = editor.new_buffer();
9422 editor
9423 .buffers
9424 .get_mut(&buf2)
9425 .unwrap()
9426 .buffer
9427 .rename_file_path(std::path::PathBuf::from("bbb_long_name_02.txt"));
9428 let buf3 = editor.new_buffer();
9429 editor
9430 .buffers
9431 .get_mut(&buf3)
9432 .unwrap()
9433 .buffer
9434 .rename_file_path(std::path::PathBuf::from("ccc_long_name_03.txt"));
9435
9436 {
9437 use crate::view::split::TabTarget;
9438 let view_state = editor.split_view_states.get_mut(&split_id).unwrap();
9439 view_state.open_buffers = vec![
9440 TabTarget::Buffer(buf1),
9441 TabTarget::Buffer(buf2),
9442 TabTarget::Buffer(buf3),
9443 ];
9444 view_state.tab_scroll_offset = 50;
9445 }
9446
9447 editor.ensure_active_tab_visible(split_id, buf1, 25);
9451 assert_eq!(
9452 editor
9453 .split_view_states
9454 .get(&split_id)
9455 .unwrap()
9456 .tab_scroll_offset,
9457 0
9458 );
9459
9460 editor.ensure_active_tab_visible(split_id, buf3, 25);
9462 let view_state = editor.split_view_states.get(&split_id).unwrap();
9463 assert!(view_state.tab_scroll_offset > 0);
9464 let buffer_ids: Vec<_> = view_state.buffer_tab_ids_vec();
9465 let total_width: usize = buffer_ids
9466 .iter()
9467 .enumerate()
9468 .map(|(idx, id)| {
9469 let state = editor.buffers.get(id).unwrap();
9470 let name_len = state
9471 .buffer
9472 .file_path()
9473 .and_then(|p| p.file_name())
9474 .and_then(|n| n.to_str())
9475 .map(|s| s.chars().count())
9476 .unwrap_or(0);
9477 let tab_width = 2 + name_len;
9478 if idx < buffer_ids.len() - 1 {
9479 tab_width + 1 } else {
9481 tab_width
9482 }
9483 })
9484 .sum();
9485 assert!(view_state.tab_scroll_offset <= total_width);
9486 }
9487}