1mod async_messages;
2mod buffer_management;
3mod calibration_actions;
4pub mod calibration_wizard;
5mod clipboard;
6mod composite_buffer_actions;
7pub mod event_debug;
8mod event_debug_actions;
9mod file_explorer;
10pub mod file_open;
11mod file_open_input;
12mod file_operations;
13mod help;
14mod input;
15mod input_dispatch;
16pub mod keybinding_editor;
17mod keybinding_editor_actions;
18mod lsp_actions;
19mod lsp_requests;
20mod menu_actions;
21mod menu_context;
22mod mouse_input;
23mod on_save_actions;
24mod plugin_commands;
25mod popup_actions;
26mod prompt_actions;
27mod recovery_actions;
28mod regex_replace;
29mod render;
30mod settings_actions;
31mod shell_command;
32mod split_actions;
33mod tab_drag;
34mod terminal;
35mod terminal_input;
36mod terminal_mouse;
37mod theme_inspect;
38mod toggle_actions;
39pub mod types;
40mod undo_actions;
41mod view_actions;
42pub mod warning_domains;
43pub mod workspace;
44
45use anyhow::Result as AnyhowResult;
46use rust_i18n::t;
47use std::path::Component;
48
49pub fn editor_tick(
54 editor: &mut Editor,
55 mut clear_terminal: impl FnMut() -> AnyhowResult<()>,
56) -> AnyhowResult<bool> {
57 let mut needs_render = false;
58
59 if {
60 let _s = tracing::info_span!("process_async_messages").entered();
61 editor.process_async_messages()
62 } {
63 needs_render = true;
64 }
65 if {
66 let _s = tracing::info_span!("process_pending_file_opens").entered();
67 editor.process_pending_file_opens()
68 } {
69 needs_render = true;
70 }
71 if editor.process_line_scan() {
72 needs_render = true;
73 }
74 if {
75 let _s = tracing::info_span!("process_search_scan").entered();
76 editor.process_search_scan()
77 } {
78 needs_render = true;
79 }
80 if {
81 let _s = tracing::info_span!("check_search_overlay_refresh").entered();
82 editor.check_search_overlay_refresh()
83 } {
84 needs_render = true;
85 }
86 if editor.check_mouse_hover_timer() {
87 needs_render = true;
88 }
89 if editor.check_semantic_highlight_timer() {
90 needs_render = true;
91 }
92 if editor.check_completion_trigger_timer() {
93 needs_render = true;
94 }
95 editor.check_diagnostic_pull_timer();
96 if editor.check_warning_log() {
97 needs_render = true;
98 }
99 if editor.poll_stdin_streaming() {
100 needs_render = true;
101 }
102
103 if let Err(e) = editor.auto_recovery_save_dirty_buffers() {
104 tracing::debug!("Auto-recovery-save error: {}", e);
105 }
106 if let Err(e) = editor.auto_save_persistent_buffers() {
107 tracing::debug!("Auto-save (disk) error: {}", e);
108 }
109
110 if editor.take_full_redraw_request() {
111 clear_terminal()?;
112 needs_render = true;
113 }
114
115 Ok(needs_render)
116}
117
118pub(crate) fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
121 let mut components = Vec::new();
122
123 for component in path.components() {
124 match component {
125 Component::CurDir => {
126 }
128 Component::ParentDir => {
129 if let Some(Component::Normal(_)) = components.last() {
131 components.pop();
132 } else {
133 components.push(component);
135 }
136 }
137 _ => {
138 components.push(component);
139 }
140 }
141 }
142
143 if components.is_empty() {
144 std::path::PathBuf::from(".")
145 } else {
146 components.iter().collect()
147 }
148}
149
150use self::types::{
151 Bookmark, CachedLayout, EventLineInfo, InteractiveReplaceState, LspMessageEntry,
152 LspProgressInfo, MacroRecordingState, MouseState, SearchState, TabContextMenu,
153 DEFAULT_BACKGROUND_FILE,
154};
155use crate::config::Config;
156use crate::config_io::{ConfigLayer, ConfigResolver, DirectoryContext};
157use crate::input::actions::action_to_events as convert_action_to_events;
158use crate::input::buffer_mode::ModeRegistry;
159use crate::input::command_registry::CommandRegistry;
160use crate::input::commands::Suggestion;
161use crate::input::keybindings::{Action, KeyContext, KeybindingResolver};
162use crate::input::position_history::PositionHistory;
163use crate::input::quick_open::{
164 FileProvider, GotoLineProvider, QuickOpenContext, QuickOpenProvider, QuickOpenRegistry,
165};
166use crate::model::cursor::Cursors;
167use crate::model::event::{Event, EventLog, LeafId, SplitDirection, SplitId};
168use crate::model::filesystem::FileSystem;
169use crate::services::async_bridge::{AsyncBridge, AsyncMessage};
170use crate::services::fs::FsManager;
171use crate::services::lsp::manager::LspManager;
172use crate::services::plugins::PluginManager;
173use crate::services::recovery::{RecoveryConfig, RecoveryService};
174use crate::services::time_source::{RealTimeSource, SharedTimeSource};
175use crate::state::EditorState;
176use crate::types::LspServerConfig;
177use crate::view::file_tree::{FileTree, FileTreeView};
178use crate::view::prompt::{Prompt, PromptType};
179use crate::view::scroll_sync::ScrollSyncManager;
180use crate::view::split::{SplitManager, SplitViewState};
181use crate::view::ui::{
182 FileExplorerRenderer, SplitRenderer, StatusBarRenderer, SuggestionsRenderer,
183};
184use crossterm::event::{KeyCode, KeyModifiers};
185#[cfg(feature = "plugins")]
186use fresh_core::api::BufferSavedDiff;
187#[cfg(feature = "plugins")]
188use fresh_core::api::JsCallbackId;
189use fresh_core::api::PluginCommand;
190use lsp_types::{Position, Range as LspRange, TextDocumentContentChangeEvent};
191use ratatui::{
192 layout::{Constraint, Direction, Layout},
193 Frame,
194};
195use std::collections::{HashMap, HashSet};
196use std::ops::Range;
197use std::path::{Path, PathBuf};
198use std::sync::{Arc, RwLock};
199use std::time::Instant;
200
201pub use self::types::{BufferKind, BufferMetadata, HoverTarget};
203pub use self::warning_domains::{
204 GeneralWarningDomain, LspWarningDomain, WarningAction, WarningActionId, WarningDomain,
205 WarningDomainRegistry, WarningLevel, WarningPopupContent,
206};
207pub use crate::model::event::BufferId;
208
209fn uri_to_path(uri: &lsp_types::Uri) -> Result<PathBuf, String> {
211 url::Url::parse(uri.as_str())
213 .map_err(|e| format!("Failed to parse URI: {}", e))?
214 .to_file_path()
215 .map_err(|_| "URI is not a file path".to_string())
216}
217
218#[derive(Clone, Debug)]
220pub struct PendingGrammar {
221 pub language: String,
223 pub grammar_path: String,
225 pub extensions: Vec<String>,
227}
228
229#[derive(Clone, Debug)]
231struct SemanticTokenRangeRequest {
232 buffer_id: BufferId,
233 version: u64,
234 range: Range<usize>,
235 start_line: usize,
236 end_line: usize,
237}
238
239#[derive(Clone, Copy, Debug)]
240enum SemanticTokensFullRequestKind {
241 Full,
242 FullDelta,
243}
244
245#[derive(Clone, Debug)]
246struct SemanticTokenFullRequest {
247 buffer_id: BufferId,
248 version: u64,
249 kind: SemanticTokensFullRequestKind,
250}
251
252#[derive(Clone, Debug)]
253struct FoldingRangeRequest {
254 buffer_id: BufferId,
255 version: u64,
256}
257
258pub struct Editor {
260 buffers: HashMap<BufferId, EditorState>,
262
263 event_logs: HashMap<BufferId, EventLog>,
268
269 next_buffer_id: usize,
271
272 config: Config,
274
275 user_config_raw: serde_json::Value,
277
278 dir_context: DirectoryContext,
280
281 grammar_registry: std::sync::Arc<crate::primitives::grammar::GrammarRegistry>,
283
284 pending_grammars: Vec<PendingGrammar>,
286
287 grammar_reload_pending: bool,
291
292 grammar_build_in_progress: bool,
295
296 pending_grammar_callbacks: Vec<fresh_core::api::JsCallbackId>,
300
301 theme: crate::view::theme::Theme,
303
304 theme_registry: crate::view::theme::ThemeRegistry,
306
307 theme_cache: Arc<RwLock<HashMap<String, serde_json::Value>>>,
309
310 ansi_background: Option<crate::primitives::ansi_background::AnsiBackground>,
312
313 ansi_background_path: Option<PathBuf>,
315
316 background_fade: f32,
318
319 keybindings: KeybindingResolver,
321
322 clipboard: crate::services::clipboard::Clipboard,
324
325 should_quit: bool,
327
328 should_detach: bool,
330
331 session_mode: bool,
333
334 software_cursor_only: bool,
336
337 session_name: Option<String>,
339
340 pending_escape_sequences: Vec<u8>,
343
344 restart_with_dir: Option<PathBuf>,
347
348 status_message: Option<String>,
350
351 plugin_status_message: Option<String>,
353
354 plugin_errors: Vec<String>,
357
358 prompt: Option<Prompt>,
360
361 terminal_width: u16,
363 terminal_height: u16,
364
365 lsp: Option<LspManager>,
367
368 buffer_metadata: HashMap<BufferId, BufferMetadata>,
370
371 mode_registry: ModeRegistry,
373
374 tokio_runtime: Option<tokio::runtime::Runtime>,
376
377 async_bridge: Option<AsyncBridge>,
379
380 split_manager: SplitManager,
382
383 split_view_states: HashMap<LeafId, SplitViewState>,
387
388 previous_viewports: HashMap<LeafId, (usize, u16, u16)>,
392
393 scroll_sync_manager: ScrollSyncManager,
396
397 file_explorer: Option<FileTreeView>,
399
400 fs_manager: Arc<FsManager>,
402
403 filesystem: Arc<dyn FileSystem + Send + Sync>,
405
406 local_filesystem: Arc<dyn FileSystem + Send + Sync>,
409
410 process_spawner: Arc<dyn crate::services::remote::ProcessSpawner>,
412
413 file_explorer_visible: bool,
415
416 file_explorer_sync_in_progress: bool,
419
420 file_explorer_width_percent: f32,
423
424 pending_file_explorer_show_hidden: Option<bool>,
426
427 pending_file_explorer_show_gitignored: Option<bool>,
429
430 file_explorer_decorations: HashMap<String, Vec<crate::view::file_tree::FileExplorerDecoration>>,
432
433 file_explorer_decoration_cache: crate::view::file_tree::FileExplorerDecorationCache,
435
436 menu_bar_visible: bool,
438
439 menu_bar_auto_shown: bool,
442
443 tab_bar_visible: bool,
445
446 status_bar_visible: bool,
448
449 mouse_enabled: bool,
451
452 same_buffer_scroll_sync: bool,
454
455 mouse_cursor_position: Option<(u16, u16)>,
459
460 gpm_active: bool,
462
463 key_context: KeyContext,
465
466 menu_state: crate::view::ui::MenuState,
468
469 menus: crate::config::MenuConfig,
471
472 working_dir: PathBuf,
474
475 pub position_history: PositionHistory,
477
478 in_navigation: bool,
480
481 next_lsp_request_id: u64,
483
484 pending_completion_request: Option<u64>,
486
487 completion_items: Option<Vec<lsp_types::CompletionItem>>,
490
491 scheduled_completion_trigger: Option<Instant>,
494
495 pending_goto_definition_request: Option<u64>,
497
498 pending_hover_request: Option<u64>,
500
501 pending_references_request: Option<u64>,
503
504 pending_references_symbol: String,
506
507 pending_signature_help_request: Option<u64>,
509
510 pending_code_actions_request: Option<u64>,
512
513 pending_inlay_hints_request: Option<u64>,
515
516 pending_folding_range_requests: HashMap<u64, FoldingRangeRequest>,
518
519 folding_ranges_in_flight: HashMap<BufferId, (u64, u64)>,
521
522 folding_ranges_debounce: HashMap<BufferId, Instant>,
524
525 pending_semantic_token_requests: HashMap<u64, SemanticTokenFullRequest>,
527
528 semantic_tokens_in_flight: HashMap<BufferId, (u64, u64, SemanticTokensFullRequestKind)>,
530
531 pending_semantic_token_range_requests: HashMap<u64, SemanticTokenRangeRequest>,
533
534 semantic_tokens_range_in_flight: HashMap<BufferId, (u64, usize, usize, u64)>,
536
537 semantic_tokens_range_last_request: HashMap<BufferId, (usize, usize, u64, Instant)>,
539
540 semantic_tokens_range_applied: HashMap<BufferId, (usize, usize, u64)>,
542
543 semantic_tokens_full_debounce: HashMap<BufferId, Instant>,
545
546 hover_symbol_range: Option<(usize, usize)>,
549
550 hover_symbol_overlay: Option<crate::view::overlay::OverlayHandle>,
552
553 mouse_hover_screen_position: Option<(u16, u16)>,
556
557 search_state: Option<SearchState>,
559
560 search_namespace: crate::view::overlay::OverlayNamespace,
562
563 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace,
565
566 pending_search_range: Option<Range<usize>>,
568
569 interactive_replace_state: Option<InteractiveReplaceState>,
571
572 lsp_status: String,
574
575 mouse_state: MouseState,
577
578 tab_context_menu: Option<TabContextMenu>,
580
581 theme_info_popup: Option<types::ThemeInfoPopup>,
583
584 pub(crate) cached_layout: CachedLayout,
586
587 command_registry: Arc<RwLock<CommandRegistry>>,
589
590 #[allow(dead_code)]
593 quick_open_registry: QuickOpenRegistry,
594
595 file_provider: Arc<FileProvider>,
597
598 plugin_manager: PluginManager,
600
601 plugin_dev_workspaces:
605 HashMap<BufferId, crate::services::plugins::plugin_dev_workspace::PluginDevWorkspace>,
606
607 seen_byte_ranges: HashMap<BufferId, std::collections::HashSet<(usize, usize)>>,
611
612 panel_ids: HashMap<String, BufferId>,
615
616 background_process_handles: HashMap<u64, tokio::task::AbortHandle>,
619
620 prompt_histories: HashMap<String, crate::input::input_history::InputHistory>,
623
624 pending_async_prompt_callback: Option<fresh_core::api::JsCallbackId>,
628
629 lsp_progress: std::collections::HashMap<String, LspProgressInfo>,
631
632 lsp_server_statuses:
634 std::collections::HashMap<String, crate::services::async_bridge::LspServerStatus>,
635
636 lsp_window_messages: Vec<LspMessageEntry>,
638
639 lsp_log_messages: Vec<LspMessageEntry>,
641
642 diagnostic_result_ids: HashMap<String, String>,
645
646 scheduled_diagnostic_pull: Option<(BufferId, Instant)>,
649
650 stored_push_diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
652
653 stored_pull_diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
655
656 stored_diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
658
659 stored_folding_ranges: HashMap<String, Vec<lsp_types::FoldingRange>>,
662
663 event_broadcaster: crate::model::control_event::EventBroadcaster,
665
666 bookmarks: HashMap<char, Bookmark>,
668
669 search_case_sensitive: bool,
671 search_whole_word: bool,
672 search_use_regex: bool,
673 search_confirm_each: bool,
675
676 macros: HashMap<char, Vec<Action>>,
678
679 macro_recording: Option<MacroRecordingState>,
681
682 last_macro_register: Option<char>,
684
685 macro_playing: bool,
687
688 #[cfg(feature = "plugins")]
690 pending_plugin_actions: Vec<(
691 String,
692 crate::services::plugins::thread::oneshot::Receiver<anyhow::Result<()>>,
693 )>,
694
695 #[cfg(feature = "plugins")]
697 plugin_render_requested: bool,
698
699 chord_state: Vec<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)>,
702
703 pending_lsp_confirmation: Option<String>,
706
707 pending_close_buffer: Option<BufferId>,
710
711 auto_revert_enabled: bool,
713
714 last_auto_revert_poll: std::time::Instant,
716
717 last_file_tree_poll: std::time::Instant,
719
720 file_mod_times: HashMap<PathBuf, std::time::SystemTime>,
723
724 dir_mod_times: HashMap<PathBuf, std::time::SystemTime>,
727
728 file_rapid_change_counts: HashMap<PathBuf, (std::time::Instant, u32)>,
731
732 file_open_state: Option<file_open::FileOpenState>,
734
735 file_browser_layout: Option<crate::view::ui::FileBrowserLayout>,
737
738 recovery_service: RecoveryService,
740
741 full_redraw_requested: bool,
743
744 time_source: SharedTimeSource,
746
747 last_auto_recovery_save: std::time::Instant,
749
750 last_persistent_auto_save: std::time::Instant,
752
753 active_custom_contexts: HashSet<String>,
756
757 editor_mode: Option<String>,
760
761 warning_log: Option<(std::sync::mpsc::Receiver<()>, PathBuf)>,
763
764 status_log_path: Option<PathBuf>,
766
767 warning_domains: WarningDomainRegistry,
770
771 update_checker: Option<crate::services::release_checker::PeriodicUpdateChecker>,
773
774 terminal_manager: crate::services::terminal::TerminalManager,
776
777 terminal_buffers: HashMap<BufferId, crate::services::terminal::TerminalId>,
779
780 terminal_backing_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
782
783 terminal_log_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
785
786 terminal_mode: bool,
788
789 keyboard_capture: bool,
793
794 terminal_mode_resume: std::collections::HashSet<BufferId>,
798
799 previous_click_time: Option<std::time::Instant>,
801
802 previous_click_position: Option<(u16, u16)>,
805
806 click_count: u8,
808
809 pub(crate) settings_state: Option<crate::view::settings::SettingsState>,
811
812 pub(crate) calibration_wizard: Option<calibration_wizard::CalibrationWizard>,
814
815 pub(crate) event_debug: Option<event_debug::EventDebug>,
817
818 pub(crate) keybinding_editor: Option<keybinding_editor::KeybindingEditor>,
820
821 pub(crate) key_translator: crate::input::key_translator::KeyTranslator,
823
824 color_capability: crate::view::color_support::ColorCapability,
826
827 review_hunks: Vec<fresh_core::api::ReviewHunk>,
829
830 active_action_popup: Option<(String, Vec<(String, String)>)>,
833
834 composite_buffers: HashMap<BufferId, crate::model::composite_buffer::CompositeBuffer>,
837
838 composite_view_states:
841 HashMap<(LeafId, BufferId), crate::view::composite_view::CompositeViewState>,
842
843 pending_file_opens: Vec<PendingFileOpen>,
847
848 wait_tracking: HashMap<BufferId, (u64, bool)>,
850 completed_waits: Vec<u64>,
852
853 stdin_streaming: Option<StdinStreamingState>,
855
856 line_scan_state: Option<LineScanState>,
858
859 search_scan_state: Option<SearchScanState>,
861
862 search_overlay_top_byte: Option<usize>,
865}
866
867#[derive(Debug, Clone)]
869pub struct PendingFileOpen {
870 pub path: PathBuf,
872 pub line: Option<usize>,
874 pub column: Option<usize>,
876 pub end_line: Option<usize>,
878 pub end_column: Option<usize>,
880 pub message: Option<String>,
882 pub wait_id: Option<u64>,
884}
885
886#[allow(dead_code)] struct SearchScanState {
892 buffer_id: BufferId,
893 leaves: Vec<crate::model::piece_tree::LeafData>,
895 chunks: Vec<crate::model::buffer::LineScanChunk>,
897 next_chunk: usize,
898 next_doc_offset: usize,
900 total_bytes: usize,
901 scanned_bytes: usize,
902 regex: regex::Regex,
904 query: String,
906 match_ranges: Vec<(usize, usize)>,
908 overlap_tail: Vec<u8>,
910 overlap_doc_offset: usize,
912 search_range: Option<std::ops::Range<usize>>,
914 capped: bool,
916 case_sensitive: bool,
918 whole_word: bool,
919 use_regex: bool,
920}
921
922struct LineScanState {
924 buffer_id: BufferId,
925 leaves: Vec<crate::model::piece_tree::LeafData>,
927 chunks: Vec<crate::model::buffer::LineScanChunk>,
929 next_chunk: usize,
930 total_bytes: usize,
931 scanned_bytes: usize,
932 updates: Vec<(usize, usize)>,
934 open_goto_line_on_complete: bool,
937}
938
939pub struct StdinStreamingState {
941 pub temp_path: PathBuf,
943 pub buffer_id: BufferId,
945 pub last_known_size: usize,
947 pub complete: bool,
949 pub thread_handle: Option<std::thread::JoinHandle<anyhow::Result<()>>>,
951}
952
953impl Editor {
954 pub fn new(
957 config: Config,
958 width: u16,
959 height: u16,
960 dir_context: DirectoryContext,
961 color_capability: crate::view::color_support::ColorCapability,
962 filesystem: Arc<dyn FileSystem + Send + Sync>,
963 ) -> AnyhowResult<Self> {
964 Self::with_working_dir(
965 config,
966 width,
967 height,
968 None,
969 dir_context,
970 true,
971 color_capability,
972 filesystem,
973 )
974 }
975
976 #[allow(clippy::too_many_arguments)]
979 pub fn with_working_dir(
980 config: Config,
981 width: u16,
982 height: u16,
983 working_dir: Option<PathBuf>,
984 dir_context: DirectoryContext,
985 plugins_enabled: bool,
986 color_capability: crate::view::color_support::ColorCapability,
987 filesystem: Arc<dyn FileSystem + Send + Sync>,
988 ) -> AnyhowResult<Self> {
989 let grammar_registry = crate::primitives::grammar::GrammarRegistry::defaults_only();
990 let mut editor = Self::with_options(
991 config,
992 width,
993 height,
994 working_dir,
995 filesystem,
996 plugins_enabled,
997 dir_context,
998 None,
999 color_capability,
1000 grammar_registry,
1001 )?;
1002 editor.start_background_grammar_build();
1003 Ok(editor)
1004 }
1005
1006 #[allow(clippy::too_many_arguments)]
1011 pub fn for_test(
1012 config: Config,
1013 width: u16,
1014 height: u16,
1015 working_dir: Option<PathBuf>,
1016 dir_context: DirectoryContext,
1017 color_capability: crate::view::color_support::ColorCapability,
1018 filesystem: Arc<dyn FileSystem + Send + Sync>,
1019 time_source: Option<SharedTimeSource>,
1020 grammar_registry: Option<Arc<crate::primitives::grammar::GrammarRegistry>>,
1021 ) -> AnyhowResult<Self> {
1022 let grammar_registry =
1023 grammar_registry.unwrap_or_else(crate::primitives::grammar::GrammarRegistry::empty);
1024 Self::with_options(
1025 config,
1026 width,
1027 height,
1028 working_dir,
1029 filesystem,
1030 true,
1031 dir_context,
1032 time_source,
1033 color_capability,
1034 grammar_registry,
1035 )
1036 }
1037
1038 #[allow(clippy::too_many_arguments)]
1042 fn with_options(
1043 mut config: Config,
1044 width: u16,
1045 height: u16,
1046 working_dir: Option<PathBuf>,
1047 filesystem: Arc<dyn FileSystem + Send + Sync>,
1048 enable_plugins: bool,
1049 dir_context: DirectoryContext,
1050 time_source: Option<SharedTimeSource>,
1051 color_capability: crate::view::color_support::ColorCapability,
1052 grammar_registry: Arc<crate::primitives::grammar::GrammarRegistry>,
1053 ) -> AnyhowResult<Self> {
1054 let time_source = time_source.unwrap_or_else(RealTimeSource::shared);
1056 tracing::info!("Editor::new called with width={}, height={}", width, height);
1057
1058 let working_dir = working_dir
1060 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
1061
1062 let working_dir = working_dir.canonicalize().unwrap_or(working_dir);
1065
1066 let theme_loader = crate::view::theme::ThemeLoader::new(dir_context.themes_dir());
1068 let theme_registry = theme_loader.load_all();
1069
1070 let theme = theme_registry.get_cloned(&config.theme).unwrap_or_else(|| {
1072 tracing::warn!(
1073 "Theme '{}' not found, falling back to default theme",
1074 config.theme.0
1075 );
1076 theme_registry
1077 .get_cloned(&crate::config::ThemeName(
1078 crate::view::theme::THEME_HIGH_CONTRAST.to_string(),
1079 ))
1080 .expect("Default theme must exist")
1081 });
1082
1083 theme.set_terminal_cursor_color();
1085
1086 let keybindings = KeybindingResolver::new(&config);
1087
1088 let mut buffers = HashMap::new();
1090 let mut event_logs = HashMap::new();
1091
1092 let buffer_id = BufferId(1);
1097 let mut state = EditorState::new(
1098 width,
1099 height,
1100 config.editor.large_file_threshold_bytes as usize,
1101 Arc::clone(&filesystem),
1102 );
1103 state
1105 .margins
1106 .configure_for_line_numbers(config.editor.line_numbers);
1107 state.buffer_settings.tab_size = config.editor.tab_size;
1108 state.buffer_settings.auto_close = config.editor.auto_close;
1109 tracing::info!("EditorState created for buffer {:?}", buffer_id);
1111 buffers.insert(buffer_id, state);
1112 event_logs.insert(buffer_id, EventLog::new());
1113
1114 let mut buffer_metadata = HashMap::new();
1116 buffer_metadata.insert(buffer_id, BufferMetadata::new());
1117
1118 let root_uri = types::file_path_to_lsp_uri(&working_dir);
1120
1121 let tokio_runtime = tokio::runtime::Builder::new_multi_thread()
1123 .worker_threads(2) .thread_name("editor-async")
1125 .enable_all()
1126 .build()
1127 .ok();
1128
1129 let async_bridge = AsyncBridge::new();
1131
1132 if tokio_runtime.is_none() {
1133 tracing::warn!("Failed to create Tokio runtime - async features disabled");
1134 }
1135
1136 let mut lsp = LspManager::new(root_uri);
1138
1139 if let Some(ref runtime) = tokio_runtime {
1141 lsp.set_runtime(runtime.handle().clone(), async_bridge.clone());
1142 }
1143
1144 for (language, lsp_config) in &config.lsp {
1146 lsp.set_language_config(language.clone(), lsp_config.clone());
1147 }
1148
1149 let split_manager = SplitManager::new(buffer_id);
1151
1152 let mut split_view_states = HashMap::new();
1154 let initial_split_id = split_manager.active_split();
1155 let mut initial_view_state = SplitViewState::with_buffer(width, height, buffer_id);
1156 initial_view_state.apply_config_defaults(
1157 config.editor.line_numbers,
1158 config.editor.line_wrap,
1159 config.editor.wrap_indent,
1160 config.editor.rulers.clone(),
1161 );
1162 split_view_states.insert(initial_split_id, initial_view_state);
1163
1164 let fs_manager = Arc::new(FsManager::new(Arc::clone(&filesystem)));
1166
1167 let command_registry = Arc::new(RwLock::new(CommandRegistry::new()));
1169
1170 let file_provider = Arc::new(FileProvider::new());
1172
1173 let mut quick_open_registry = QuickOpenRegistry::new();
1175 quick_open_registry.register(Box::new(GotoLineProvider::new()));
1176 let theme_cache = Arc::new(RwLock::new(theme_registry.to_json_map()));
1181
1182 let plugin_manager = PluginManager::new(
1184 enable_plugins,
1185 Arc::clone(&command_registry),
1186 dir_context.clone(),
1187 Arc::clone(&theme_cache),
1188 );
1189
1190 #[cfg(feature = "plugins")]
1193 if let Some(snapshot_handle) = plugin_manager.state_snapshot_handle() {
1194 let mut snapshot = snapshot_handle.write().unwrap();
1195 snapshot.working_dir = working_dir.clone();
1196 }
1197
1198 if plugin_manager.is_active() {
1205 let mut plugin_dirs: Vec<std::path::PathBuf> = vec![];
1206
1207 if let Ok(exe_path) = std::env::current_exe() {
1209 if let Some(exe_dir) = exe_path.parent() {
1210 let exe_plugin_dir = exe_dir.join("plugins");
1211 if exe_plugin_dir.exists() {
1212 plugin_dirs.push(exe_plugin_dir);
1213 }
1214 }
1215 }
1216
1217 let working_plugin_dir = working_dir.join("plugins");
1219 if working_plugin_dir.exists() && !plugin_dirs.contains(&working_plugin_dir) {
1220 plugin_dirs.push(working_plugin_dir);
1221 }
1222
1223 #[cfg(feature = "embed-plugins")]
1225 if plugin_dirs.is_empty() {
1226 if let Some(embedded_dir) =
1227 crate::services::plugins::embedded::get_embedded_plugins_dir()
1228 {
1229 tracing::info!("Using embedded plugins from: {:?}", embedded_dir);
1230 plugin_dirs.push(embedded_dir.clone());
1231 }
1232 }
1233
1234 let user_plugins_dir = dir_context.config_dir.join("plugins");
1236 if user_plugins_dir.exists() && !plugin_dirs.contains(&user_plugins_dir) {
1237 tracing::info!("Found user plugins directory: {:?}", user_plugins_dir);
1238 plugin_dirs.push(user_plugins_dir.clone());
1239 }
1240
1241 let packages_dir = dir_context.config_dir.join("plugins").join("packages");
1243 if packages_dir.exists() {
1244 if let Ok(entries) = std::fs::read_dir(&packages_dir) {
1245 for entry in entries.flatten() {
1246 let path = entry.path();
1247 if path.is_dir() {
1249 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
1250 if !name.starts_with('.') {
1251 tracing::info!("Found package manager plugin: {:?}", path);
1252 plugin_dirs.push(path);
1253 }
1254 }
1255 }
1256 }
1257 }
1258 }
1259
1260 if plugin_dirs.is_empty() {
1261 tracing::debug!(
1262 "No plugins directory found next to executable or in working dir: {:?}",
1263 working_dir
1264 );
1265 }
1266
1267 for plugin_dir in plugin_dirs {
1269 tracing::info!("Loading TypeScript plugins from: {:?}", plugin_dir);
1270 let (errors, discovered_plugins) =
1271 plugin_manager.load_plugins_from_dir_with_config(&plugin_dir, &config.plugins);
1272
1273 for (name, plugin_config) in discovered_plugins {
1276 config.plugins.insert(name, plugin_config);
1277 }
1278
1279 if !errors.is_empty() {
1280 for err in &errors {
1281 tracing::error!("TypeScript plugin load error: {}", err);
1282 }
1283 #[cfg(debug_assertions)]
1285 panic!(
1286 "TypeScript plugin loading failed with {} error(s): {}",
1287 errors.len(),
1288 errors.join("; ")
1289 );
1290 }
1291 }
1292 }
1293
1294 let file_explorer_width = config.file_explorer.width;
1296 let recovery_enabled = config.editor.recovery_enabled;
1297 let check_for_updates = config.check_for_updates;
1298 let show_menu_bar = config.editor.show_menu_bar;
1299 let show_tab_bar = config.editor.show_tab_bar;
1300 let show_status_bar = config.editor.show_status_bar;
1301
1302 let update_checker = if check_for_updates {
1304 tracing::debug!("Update checking enabled, starting periodic checker");
1305 Some(
1306 crate::services::release_checker::start_periodic_update_check(
1307 crate::services::release_checker::DEFAULT_RELEASES_URL,
1308 time_source.clone(),
1309 dir_context.data_dir.clone(),
1310 ),
1311 )
1312 } else {
1313 tracing::debug!("Update checking disabled by config");
1314 None
1315 };
1316
1317 let user_config_raw = Config::read_user_config_raw(&working_dir);
1319
1320 let mut editor = Editor {
1321 buffers,
1322 event_logs,
1323 next_buffer_id: 2,
1324 config,
1325 user_config_raw,
1326 dir_context: dir_context.clone(),
1327 grammar_registry,
1328 pending_grammars: Vec::new(),
1329 grammar_reload_pending: false,
1330 grammar_build_in_progress: false,
1331 pending_grammar_callbacks: Vec::new(),
1332 theme,
1333 theme_registry,
1334 theme_cache,
1335 ansi_background: None,
1336 ansi_background_path: None,
1337 background_fade: crate::primitives::ansi_background::DEFAULT_BACKGROUND_FADE,
1338 keybindings,
1339 clipboard: crate::services::clipboard::Clipboard::new(),
1340 should_quit: false,
1341 should_detach: false,
1342 session_mode: false,
1343 software_cursor_only: false,
1344 session_name: None,
1345 pending_escape_sequences: Vec::new(),
1346 restart_with_dir: None,
1347 status_message: None,
1348 plugin_status_message: None,
1349 plugin_errors: Vec::new(),
1350 prompt: None,
1351 terminal_width: width,
1352 terminal_height: height,
1353 lsp: Some(lsp),
1354 buffer_metadata,
1355 mode_registry: ModeRegistry::new(),
1356 tokio_runtime,
1357 async_bridge: Some(async_bridge),
1358 split_manager,
1359 split_view_states,
1360 previous_viewports: HashMap::new(),
1361 scroll_sync_manager: ScrollSyncManager::new(),
1362 file_explorer: None,
1363 fs_manager,
1364 filesystem,
1365 local_filesystem: Arc::new(crate::model::filesystem::StdFileSystem),
1366 process_spawner: Arc::new(crate::services::remote::LocalProcessSpawner),
1367 file_explorer_visible: false,
1368 file_explorer_sync_in_progress: false,
1369 file_explorer_width_percent: file_explorer_width,
1370 pending_file_explorer_show_hidden: None,
1371 pending_file_explorer_show_gitignored: None,
1372 menu_bar_visible: show_menu_bar,
1373 file_explorer_decorations: HashMap::new(),
1374 file_explorer_decoration_cache:
1375 crate::view::file_tree::FileExplorerDecorationCache::default(),
1376 menu_bar_auto_shown: false,
1377 tab_bar_visible: show_tab_bar,
1378 status_bar_visible: show_status_bar,
1379 mouse_enabled: true,
1380 same_buffer_scroll_sync: false,
1381 mouse_cursor_position: None,
1382 gpm_active: false,
1383 key_context: KeyContext::Normal,
1384 menu_state: crate::view::ui::MenuState::new(dir_context.themes_dir()),
1385 menus: crate::config::MenuConfig::translated(),
1386 working_dir,
1387 position_history: PositionHistory::new(),
1388 in_navigation: false,
1389 next_lsp_request_id: 0,
1390 pending_completion_request: None,
1391 completion_items: None,
1392 scheduled_completion_trigger: None,
1393 pending_goto_definition_request: None,
1394 pending_hover_request: None,
1395 pending_references_request: None,
1396 pending_references_symbol: String::new(),
1397 pending_signature_help_request: None,
1398 pending_code_actions_request: None,
1399 pending_inlay_hints_request: None,
1400 pending_folding_range_requests: HashMap::new(),
1401 folding_ranges_in_flight: HashMap::new(),
1402 folding_ranges_debounce: HashMap::new(),
1403 pending_semantic_token_requests: HashMap::new(),
1404 semantic_tokens_in_flight: HashMap::new(),
1405 pending_semantic_token_range_requests: HashMap::new(),
1406 semantic_tokens_range_in_flight: HashMap::new(),
1407 semantic_tokens_range_last_request: HashMap::new(),
1408 semantic_tokens_range_applied: HashMap::new(),
1409 semantic_tokens_full_debounce: HashMap::new(),
1410 hover_symbol_range: None,
1411 hover_symbol_overlay: None,
1412 mouse_hover_screen_position: None,
1413 search_state: None,
1414 search_namespace: crate::view::overlay::OverlayNamespace::from_string(
1415 "search".to_string(),
1416 ),
1417 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace::from_string(
1418 "lsp-diagnostic".to_string(),
1419 ),
1420 pending_search_range: None,
1421 interactive_replace_state: None,
1422 lsp_status: String::new(),
1423 mouse_state: MouseState::default(),
1424 tab_context_menu: None,
1425 theme_info_popup: None,
1426 cached_layout: CachedLayout::default(),
1427 command_registry,
1428 quick_open_registry,
1429 file_provider,
1430 plugin_manager,
1431 plugin_dev_workspaces: HashMap::new(),
1432 seen_byte_ranges: HashMap::new(),
1433 panel_ids: HashMap::new(),
1434 background_process_handles: HashMap::new(),
1435 prompt_histories: {
1436 let mut histories = HashMap::new();
1438 for history_name in ["search", "replace", "goto_line"] {
1439 let path = dir_context.prompt_history_path(history_name);
1440 let history = crate::input::input_history::InputHistory::load_from_file(&path)
1441 .unwrap_or_else(|e| {
1442 tracing::warn!("Failed to load {} history: {}", history_name, e);
1443 crate::input::input_history::InputHistory::new()
1444 });
1445 histories.insert(history_name.to_string(), history);
1446 }
1447 histories
1448 },
1449 pending_async_prompt_callback: None,
1450 lsp_progress: std::collections::HashMap::new(),
1451 lsp_server_statuses: std::collections::HashMap::new(),
1452 lsp_window_messages: Vec::new(),
1453 lsp_log_messages: Vec::new(),
1454 diagnostic_result_ids: HashMap::new(),
1455 scheduled_diagnostic_pull: None,
1456 stored_push_diagnostics: HashMap::new(),
1457 stored_pull_diagnostics: HashMap::new(),
1458 stored_diagnostics: HashMap::new(),
1459 stored_folding_ranges: HashMap::new(),
1460 event_broadcaster: crate::model::control_event::EventBroadcaster::default(),
1461 bookmarks: HashMap::new(),
1462 search_case_sensitive: true,
1463 search_whole_word: false,
1464 search_use_regex: false,
1465 search_confirm_each: false,
1466 macros: HashMap::new(),
1467 macro_recording: None,
1468 last_macro_register: None,
1469 macro_playing: false,
1470 #[cfg(feature = "plugins")]
1471 pending_plugin_actions: Vec::new(),
1472 #[cfg(feature = "plugins")]
1473 plugin_render_requested: false,
1474 chord_state: Vec::new(),
1475 pending_lsp_confirmation: None,
1476 pending_close_buffer: None,
1477 auto_revert_enabled: true,
1478 last_auto_revert_poll: time_source.now(),
1479 last_file_tree_poll: time_source.now(),
1480 file_mod_times: HashMap::new(),
1481 dir_mod_times: HashMap::new(),
1482 file_rapid_change_counts: HashMap::new(),
1483 file_open_state: None,
1484 file_browser_layout: None,
1485 recovery_service: {
1486 let recovery_config = RecoveryConfig {
1487 enabled: recovery_enabled,
1488 ..RecoveryConfig::default()
1489 };
1490 RecoveryService::with_config_and_dir(recovery_config, dir_context.recovery_dir())
1491 },
1492 full_redraw_requested: false,
1493 time_source: time_source.clone(),
1494 last_auto_recovery_save: time_source.now(),
1495 last_persistent_auto_save: time_source.now(),
1496 active_custom_contexts: HashSet::new(),
1497 editor_mode: None,
1498 warning_log: None,
1499 status_log_path: None,
1500 warning_domains: WarningDomainRegistry::new(),
1501 update_checker,
1502 terminal_manager: crate::services::terminal::TerminalManager::new(),
1503 terminal_buffers: HashMap::new(),
1504 terminal_backing_files: HashMap::new(),
1505 terminal_log_files: HashMap::new(),
1506 terminal_mode: false,
1507 keyboard_capture: false,
1508 terminal_mode_resume: std::collections::HashSet::new(),
1509 previous_click_time: None,
1510 previous_click_position: None,
1511 click_count: 0,
1512 settings_state: None,
1513 calibration_wizard: None,
1514 event_debug: None,
1515 keybinding_editor: None,
1516 key_translator: crate::input::key_translator::KeyTranslator::load_from_config_dir(
1517 &dir_context.config_dir,
1518 )
1519 .unwrap_or_default(),
1520 color_capability,
1521 pending_file_opens: Vec::new(),
1522 wait_tracking: HashMap::new(),
1523 completed_waits: Vec::new(),
1524 stdin_streaming: None,
1525 line_scan_state: None,
1526 search_scan_state: None,
1527 search_overlay_top_byte: None,
1528 review_hunks: Vec::new(),
1529 active_action_popup: None,
1530 composite_buffers: HashMap::new(),
1531 composite_view_states: HashMap::new(),
1532 };
1533
1534 editor.clipboard.apply_config(&editor.config.clipboard);
1536
1537 #[cfg(feature = "plugins")]
1538 {
1539 editor.update_plugin_state_snapshot();
1540 if editor.plugin_manager.is_active() {
1541 editor.plugin_manager.run_hook(
1542 "editor_initialized",
1543 crate::services::plugins::hooks::HookArgs::EditorInitialized,
1544 );
1545 }
1546 }
1547
1548 Ok(editor)
1549 }
1550
1551 pub fn event_broadcaster(&self) -> &crate::model::control_event::EventBroadcaster {
1553 &self.event_broadcaster
1554 }
1555
1556 fn start_background_grammar_build(&mut self) {
1561 let Some(bridge) = &self.async_bridge else {
1562 return;
1563 };
1564 self.grammar_build_in_progress = true;
1565 let sender = bridge.sender();
1566 let config_dir = self.dir_context.config_dir.clone();
1567 std::thread::Builder::new()
1568 .name("grammar-build".to_string())
1569 .spawn(move || {
1570 let registry = crate::primitives::grammar::GrammarRegistry::for_editor(config_dir);
1571 drop(sender.send(
1572 crate::services::async_bridge::AsyncMessage::GrammarRegistryBuilt {
1573 registry,
1574 callback_ids: Vec::new(),
1575 },
1576 ));
1577 })
1578 .ok();
1579 }
1580
1581 pub fn async_bridge(&self) -> Option<&AsyncBridge> {
1583 self.async_bridge.as_ref()
1584 }
1585
1586 pub fn config(&self) -> &Config {
1588 &self.config
1589 }
1590
1591 pub fn key_translator(&self) -> &crate::input::key_translator::KeyTranslator {
1593 &self.key_translator
1594 }
1595
1596 pub fn time_source(&self) -> &SharedTimeSource {
1598 &self.time_source
1599 }
1600
1601 pub fn emit_event(&self, name: impl Into<String>, data: serde_json::Value) {
1603 self.event_broadcaster.emit_named(name, data);
1604 }
1605
1606 fn send_plugin_response(&self, response: fresh_core::api::PluginResponse) {
1608 self.plugin_manager.deliver_response(response);
1609 }
1610
1611 fn take_pending_semantic_token_request(
1613 &mut self,
1614 request_id: u64,
1615 ) -> Option<SemanticTokenFullRequest> {
1616 if let Some(request) = self.pending_semantic_token_requests.remove(&request_id) {
1617 self.semantic_tokens_in_flight.remove(&request.buffer_id);
1618 Some(request)
1619 } else {
1620 None
1621 }
1622 }
1623
1624 fn take_pending_semantic_token_range_request(
1626 &mut self,
1627 request_id: u64,
1628 ) -> Option<SemanticTokenRangeRequest> {
1629 if let Some(request) = self
1630 .pending_semantic_token_range_requests
1631 .remove(&request_id)
1632 {
1633 self.semantic_tokens_range_in_flight
1634 .remove(&request.buffer_id);
1635 Some(request)
1636 } else {
1637 None
1638 }
1639 }
1640
1641 pub fn get_all_keybindings(&self) -> Vec<(String, String)> {
1643 self.keybindings.get_all_bindings()
1644 }
1645
1646 pub fn get_keybinding_for_action(&self, action_name: &str) -> Option<String> {
1649 self.keybindings
1650 .find_keybinding_for_action(action_name, self.key_context)
1651 }
1652
1653 pub fn mode_registry_mut(&mut self) -> &mut ModeRegistry {
1655 &mut self.mode_registry
1656 }
1657
1658 pub fn mode_registry(&self) -> &ModeRegistry {
1660 &self.mode_registry
1661 }
1662
1663 #[inline]
1668 pub fn active_buffer(&self) -> BufferId {
1669 self.split_manager
1670 .active_buffer_id()
1671 .expect("Editor always has at least one buffer")
1672 }
1673
1674 pub fn active_buffer_mode(&self) -> Option<&str> {
1676 self.buffer_metadata
1677 .get(&self.active_buffer())
1678 .and_then(|meta| meta.virtual_mode())
1679 }
1680
1681 pub fn is_active_buffer_read_only(&self) -> bool {
1683 if let Some(metadata) = self.buffer_metadata.get(&self.active_buffer()) {
1684 if metadata.read_only {
1685 return true;
1686 }
1687 if let Some(mode_name) = metadata.virtual_mode() {
1689 return self.mode_registry.is_read_only(mode_name);
1690 }
1691 }
1692 false
1693 }
1694
1695 pub fn is_editing_disabled(&self) -> bool {
1698 self.active_state().editing_disabled
1699 }
1700
1701 pub fn mark_buffer_read_only(&mut self, buffer_id: BufferId, read_only: bool) {
1704 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
1705 metadata.read_only = read_only;
1706 }
1707 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1708 state.editing_disabled = read_only;
1709 }
1710 }
1711
1712 pub fn resolve_mode_keybinding(
1719 &self,
1720 code: KeyCode,
1721 modifiers: KeyModifiers,
1722 ) -> Option<String> {
1723 if let Some(ref global_mode) = self.editor_mode {
1725 if let Some(binding) =
1726 self.mode_registry
1727 .resolve_keybinding(global_mode, code, modifiers)
1728 {
1729 return Some(binding);
1730 }
1731 }
1732
1733 let mode_name = self.active_buffer_mode()?;
1735 self.mode_registry
1736 .resolve_keybinding(mode_name, code, modifiers)
1737 }
1738
1739 pub fn has_active_lsp_progress(&self) -> bool {
1741 !self.lsp_progress.is_empty()
1742 }
1743
1744 pub fn get_lsp_progress(&self) -> Vec<(String, String, Option<String>)> {
1746 self.lsp_progress
1747 .iter()
1748 .map(|(token, info)| (token.clone(), info.title.clone(), info.message.clone()))
1749 .collect()
1750 }
1751
1752 pub fn is_lsp_server_ready(&self, language: &str) -> bool {
1754 use crate::services::async_bridge::LspServerStatus;
1755 self.lsp_server_statuses
1756 .get(language)
1757 .map(|status| matches!(status, LspServerStatus::Running))
1758 .unwrap_or(false)
1759 }
1760
1761 pub fn get_lsp_status(&self) -> &str {
1763 &self.lsp_status
1764 }
1765
1766 pub fn get_stored_diagnostics(&self) -> &HashMap<String, Vec<lsp_types::Diagnostic>> {
1769 &self.stored_diagnostics
1770 }
1771
1772 pub fn is_update_available(&self) -> bool {
1774 self.update_checker
1775 .as_ref()
1776 .map(|c| c.is_update_available())
1777 .unwrap_or(false)
1778 }
1779
1780 pub fn latest_version(&self) -> Option<&str> {
1782 self.update_checker
1783 .as_ref()
1784 .and_then(|c| c.latest_version())
1785 }
1786
1787 pub fn get_update_result(
1789 &self,
1790 ) -> Option<&crate::services::release_checker::ReleaseCheckResult> {
1791 self.update_checker
1792 .as_ref()
1793 .and_then(|c| c.get_cached_result())
1794 }
1795
1796 #[doc(hidden)]
1801 pub fn set_update_checker(
1802 &mut self,
1803 checker: crate::services::release_checker::PeriodicUpdateChecker,
1804 ) {
1805 self.update_checker = Some(checker);
1806 }
1807
1808 pub fn set_lsp_config(&mut self, language: String, config: LspServerConfig) {
1810 if let Some(ref mut lsp) = self.lsp {
1811 lsp.set_language_config(language, config);
1812 }
1813 }
1814
1815 pub fn running_lsp_servers(&self) -> Vec<String> {
1817 self.lsp
1818 .as_ref()
1819 .map(|lsp| lsp.running_servers())
1820 .unwrap_or_default()
1821 }
1822
1823 pub fn shutdown_lsp_server(&mut self, language: &str) -> bool {
1827 if let Some(ref mut lsp) = self.lsp {
1828 lsp.shutdown_server(language)
1829 } else {
1830 false
1831 }
1832 }
1833
1834 pub fn enable_event_streaming<P: AsRef<Path>>(&mut self, path: P) -> AnyhowResult<()> {
1836 for event_log in self.event_logs.values_mut() {
1838 event_log.enable_streaming(&path)?;
1839 }
1840 Ok(())
1841 }
1842
1843 pub fn log_keystroke(&mut self, key_code: &str, modifiers: &str) {
1845 if let Some(event_log) = self.event_logs.get_mut(&self.active_buffer()) {
1846 event_log.log_keystroke(key_code, modifiers);
1847 }
1848 }
1849
1850 pub fn set_warning_log(&mut self, receiver: std::sync::mpsc::Receiver<()>, path: PathBuf) {
1855 self.warning_log = Some((receiver, path));
1856 }
1857
1858 pub fn set_status_log_path(&mut self, path: PathBuf) {
1860 self.status_log_path = Some(path);
1861 }
1862
1863 pub fn set_process_spawner(
1866 &mut self,
1867 spawner: Arc<dyn crate::services::remote::ProcessSpawner>,
1868 ) {
1869 self.process_spawner = spawner;
1870 }
1871
1872 pub fn remote_connection_info(&self) -> Option<&str> {
1876 self.filesystem.remote_connection_info()
1877 }
1878
1879 pub fn get_status_log_path(&self) -> Option<&PathBuf> {
1881 self.status_log_path.as_ref()
1882 }
1883
1884 pub fn open_status_log(&mut self) {
1886 if let Some(path) = self.status_log_path.clone() {
1887 match self.open_local_file(&path) {
1889 Ok(buffer_id) => {
1890 self.mark_buffer_read_only(buffer_id, true);
1891 }
1892 Err(e) => {
1893 tracing::error!("Failed to open status log: {}", e);
1894 }
1895 }
1896 } else {
1897 self.set_status_message("Status log not available".to_string());
1898 }
1899 }
1900
1901 pub fn check_warning_log(&mut self) -> bool {
1906 let Some((receiver, path)) = &self.warning_log else {
1907 return false;
1908 };
1909
1910 let mut new_warning_count = 0usize;
1912 while receiver.try_recv().is_ok() {
1913 new_warning_count += 1;
1914 }
1915
1916 if new_warning_count > 0 {
1917 self.warning_domains.general.add_warnings(new_warning_count);
1919 self.warning_domains.general.set_log_path(path.clone());
1920 }
1921
1922 new_warning_count > 0
1923 }
1924
1925 pub fn get_warning_domains(&self) -> &WarningDomainRegistry {
1927 &self.warning_domains
1928 }
1929
1930 pub fn get_warning_log_path(&self) -> Option<&PathBuf> {
1932 self.warning_domains.general.log_path.as_ref()
1933 }
1934
1935 pub fn open_warning_log(&mut self) {
1937 if let Some(path) = self.warning_domains.general.log_path.clone() {
1938 match self.open_local_file(&path) {
1940 Ok(buffer_id) => {
1941 self.mark_buffer_read_only(buffer_id, true);
1942 }
1943 Err(e) => {
1944 tracing::error!("Failed to open warning log: {}", e);
1945 }
1946 }
1947 }
1948 }
1949
1950 pub fn clear_warning_indicator(&mut self) {
1952 self.warning_domains.general.clear();
1953 }
1954
1955 pub fn clear_warnings(&mut self) {
1957 self.warning_domains.general.clear();
1958 self.warning_domains.lsp.clear();
1959 self.status_message = Some("Warnings cleared".to_string());
1960 }
1961
1962 pub fn has_lsp_error(&self) -> bool {
1964 self.warning_domains.lsp.level() == WarningLevel::Error
1965 }
1966
1967 pub fn get_effective_warning_level(&self) -> WarningLevel {
1970 self.warning_domains.lsp.level()
1971 }
1972
1973 pub fn get_general_warning_level(&self) -> WarningLevel {
1975 self.warning_domains.general.level()
1976 }
1977
1978 pub fn get_general_warning_count(&self) -> usize {
1980 self.warning_domains.general.count
1981 }
1982
1983 pub fn update_lsp_warning_domain(&mut self) {
1985 self.warning_domains
1986 .lsp
1987 .update_from_statuses(&self.lsp_server_statuses);
1988 }
1989
1990 pub fn check_mouse_hover_timer(&mut self) -> bool {
1996 if !self.config.editor.mouse_hover_enabled {
1998 return false;
1999 }
2000
2001 let hover_delay = std::time::Duration::from_millis(self.config.editor.mouse_hover_delay_ms);
2002
2003 let hover_info = match self.mouse_state.lsp_hover_state {
2005 Some((byte_pos, start_time, screen_x, screen_y)) => {
2006 if self.mouse_state.lsp_hover_request_sent {
2007 return false; }
2009 if start_time.elapsed() < hover_delay {
2010 return false; }
2012 Some((byte_pos, screen_x, screen_y))
2013 }
2014 None => return false,
2015 };
2016
2017 let Some((byte_pos, screen_x, screen_y)) = hover_info else {
2018 return false;
2019 };
2020
2021 self.mouse_state.lsp_hover_request_sent = true;
2023
2024 self.mouse_hover_screen_position = Some((screen_x, screen_y));
2026
2027 if let Err(e) = self.request_hover_at_position(byte_pos) {
2029 tracing::debug!("Failed to request hover: {}", e);
2030 return false;
2031 }
2032
2033 true
2034 }
2035
2036 pub fn check_semantic_highlight_timer(&self) -> bool {
2041 for state in self.buffers.values() {
2043 if let Some(remaining) = state.reference_highlight_overlay.needs_redraw() {
2044 if remaining.is_zero() {
2045 return true;
2046 }
2047 }
2048 }
2049 false
2050 }
2051
2052 pub fn check_diagnostic_pull_timer(&mut self) -> bool {
2057 let Some((buffer_id, trigger_time)) = self.scheduled_diagnostic_pull else {
2058 return false;
2059 };
2060
2061 if Instant::now() < trigger_time {
2062 return false;
2063 }
2064
2065 self.scheduled_diagnostic_pull = None;
2066
2067 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
2069 return false;
2070 };
2071 let Some(uri) = metadata.file_uri().cloned() else {
2072 return false;
2073 };
2074 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
2075 return false;
2076 };
2077
2078 let Some(lsp) = self.lsp.as_mut() else {
2079 return false;
2080 };
2081 let Some(client) = lsp.get_handle_mut(&language) else {
2082 return false;
2083 };
2084
2085 let request_id = self.next_lsp_request_id;
2086 self.next_lsp_request_id += 1;
2087 let previous_result_id = self.diagnostic_result_ids.get(uri.as_str()).cloned();
2088 if let Err(e) = client.document_diagnostic(request_id, uri.clone(), previous_result_id) {
2089 tracing::debug!(
2090 "Failed to pull diagnostics after edit for {}: {}",
2091 uri.as_str(),
2092 e
2093 );
2094 } else {
2095 tracing::debug!(
2096 "Pulling diagnostics after edit for {} (request_id={})",
2097 uri.as_str(),
2098 request_id
2099 );
2100 }
2101
2102 false }
2104
2105 pub fn check_completion_trigger_timer(&mut self) -> bool {
2111 let Some(trigger_time) = self.scheduled_completion_trigger else {
2113 return false;
2114 };
2115
2116 if Instant::now() < trigger_time {
2118 return false;
2119 }
2120
2121 self.scheduled_completion_trigger = None;
2123
2124 if self.active_state().popups.is_visible() {
2126 return false;
2127 }
2128
2129 self.request_completion();
2131
2132 true
2133 }
2134
2135 fn load_ansi_background(&mut self, input: &str) -> AnyhowResult<()> {
2137 let trimmed = input.trim();
2138
2139 if trimmed.is_empty() {
2140 self.ansi_background = None;
2141 self.ansi_background_path = None;
2142 self.set_status_message(t!("status.background_cleared").to_string());
2143 return Ok(());
2144 }
2145
2146 let input_path = Path::new(trimmed);
2147 let resolved = if input_path.is_absolute() {
2148 input_path.to_path_buf()
2149 } else {
2150 self.working_dir.join(input_path)
2151 };
2152
2153 let canonical = resolved.canonicalize().unwrap_or_else(|_| resolved.clone());
2154
2155 let parsed = crate::primitives::ansi_background::AnsiBackground::from_file(&canonical)?;
2156
2157 self.ansi_background = Some(parsed);
2158 self.ansi_background_path = Some(canonical.clone());
2159 self.set_status_message(
2160 t!(
2161 "view.background_set",
2162 path = canonical.display().to_string()
2163 )
2164 .to_string(),
2165 );
2166
2167 Ok(())
2168 }
2169
2170 fn effective_tabs_width(&self) -> u16 {
2175 if self.file_explorer_visible && self.file_explorer.is_some() {
2176 let editor_percent = 1.0 - self.file_explorer_width_percent;
2178 (self.terminal_width as f32 * editor_percent) as u16
2179 } else {
2180 self.terminal_width
2181 }
2182 }
2183
2184 fn set_active_buffer(&mut self, buffer_id: BufferId) {
2194 if self.active_buffer() == buffer_id {
2195 return; }
2197
2198 self.on_editor_focus_lost();
2200
2201 self.cancel_search_prompt_if_active();
2204
2205 let previous = self.active_buffer();
2207
2208 if self.terminal_mode && self.is_terminal_buffer(previous) {
2210 self.terminal_mode_resume.insert(previous);
2211 self.terminal_mode = false;
2212 self.key_context = crate::input::keybindings::KeyContext::Normal;
2213 }
2214
2215 self.split_manager.set_active_buffer_id(buffer_id);
2217
2218 let active_split = self.split_manager.active_split();
2220 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
2221 view_state.switch_buffer(buffer_id);
2222 view_state.add_buffer(buffer_id);
2223 view_state.push_focus(previous);
2225 }
2226
2227 if self.terminal_mode_resume.contains(&buffer_id) && self.is_terminal_buffer(buffer_id) {
2229 self.terminal_mode = true;
2230 self.key_context = crate::input::keybindings::KeyContext::Terminal;
2231 } else if self.is_terminal_buffer(buffer_id) {
2232 self.sync_terminal_to_buffer(buffer_id);
2235 }
2236
2237 self.ensure_active_tab_visible(active_split, buffer_id, self.effective_tabs_width());
2239
2240 #[cfg(feature = "plugins")]
2246 self.update_plugin_state_snapshot();
2247
2248 self.plugin_manager.run_hook(
2250 "buffer_activated",
2251 crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
2252 );
2253 }
2254
2255 pub(super) fn focus_split(&mut self, split_id: LeafId, buffer_id: BufferId) {
2266 let previous_split = self.split_manager.active_split();
2267 let previous_buffer = self.active_buffer(); let split_changed = previous_split != split_id;
2269
2270 if split_changed {
2271 if self.terminal_mode && self.is_terminal_buffer(previous_buffer) {
2273 self.terminal_mode = false;
2274 self.key_context = crate::input::keybindings::KeyContext::Normal;
2275 }
2276
2277 self.split_manager.set_active_split(split_id);
2279
2280 self.split_manager.set_active_buffer_id(buffer_id);
2282
2283 if self.is_terminal_buffer(buffer_id) {
2285 self.terminal_mode = true;
2286 self.key_context = crate::input::keybindings::KeyContext::Terminal;
2287 } else {
2288 self.key_context = crate::input::keybindings::KeyContext::Normal;
2291 }
2292
2293 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2296 view_state.switch_buffer(buffer_id);
2297 }
2298
2299 if previous_buffer != buffer_id {
2301 self.position_history.commit_pending_movement();
2302 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2303 view_state.add_buffer(buffer_id);
2304 view_state.push_focus(previous_buffer);
2305 }
2306 }
2309 } else {
2310 self.set_active_buffer(buffer_id);
2312 }
2313 }
2314
2315 pub fn active_state(&self) -> &EditorState {
2317 self.buffers.get(&self.active_buffer()).unwrap()
2318 }
2319
2320 pub fn active_state_mut(&mut self) -> &mut EditorState {
2322 self.buffers.get_mut(&self.active_buffer()).unwrap()
2323 }
2324
2325 pub fn active_cursors(&self) -> &Cursors {
2327 let split_id = self.split_manager.active_split();
2328 &self.split_view_states.get(&split_id).unwrap().cursors
2329 }
2330
2331 pub fn active_cursors_mut(&mut self) -> &mut Cursors {
2333 let split_id = self.split_manager.active_split();
2334 &mut self.split_view_states.get_mut(&split_id).unwrap().cursors
2335 }
2336
2337 pub fn set_completion_items(&mut self, items: Vec<lsp_types::CompletionItem>) {
2339 self.completion_items = Some(items);
2340 }
2341
2342 pub fn active_viewport(&self) -> &crate::view::viewport::Viewport {
2344 let active_split = self.split_manager.active_split();
2345 &self.split_view_states.get(&active_split).unwrap().viewport
2346 }
2347
2348 pub fn active_viewport_mut(&mut self) -> &mut crate::view::viewport::Viewport {
2350 let active_split = self.split_manager.active_split();
2351 &mut self
2352 .split_view_states
2353 .get_mut(&active_split)
2354 .unwrap()
2355 .viewport
2356 }
2357
2358 pub fn get_buffer_display_name(&self, buffer_id: BufferId) -> String {
2360 if let Some(composite) = self.composite_buffers.get(&buffer_id) {
2362 return composite.name.clone();
2363 }
2364
2365 self.buffer_metadata
2366 .get(&buffer_id)
2367 .map(|m| m.display_name.clone())
2368 .or_else(|| {
2369 self.buffers.get(&buffer_id).and_then(|state| {
2370 state
2371 .buffer
2372 .file_path()
2373 .and_then(|p| p.file_name())
2374 .and_then(|n| n.to_str())
2375 .map(|s| s.to_string())
2376 })
2377 })
2378 .unwrap_or_else(|| "[No Name]".to_string())
2379 }
2380
2381 pub fn apply_event_to_active_buffer(&mut self, event: &Event) {
2390 match event {
2393 Event::Scroll { line_offset } => {
2394 self.handle_scroll_event(*line_offset);
2395 return;
2396 }
2397 Event::SetViewport { top_line } => {
2398 self.handle_set_viewport_event(*top_line);
2399 return;
2400 }
2401 Event::Recenter => {
2402 self.handle_recenter_event();
2403 return;
2404 }
2405 _ => {}
2406 }
2407
2408 let lsp_changes = self.collect_lsp_changes(event);
2412
2413 let line_info = self.calculate_event_line_info(event);
2415
2416 {
2419 let split_id = self.split_manager.active_split();
2420 let active_buf = self.active_buffer();
2421 let cursors = &mut self
2422 .split_view_states
2423 .get_mut(&split_id)
2424 .unwrap()
2425 .keyed_states
2426 .get_mut(&active_buf)
2427 .unwrap()
2428 .cursors;
2429 let state = self.buffers.get_mut(&active_buf).unwrap();
2430 state.apply(cursors, event);
2431 }
2432
2433 match event {
2436 Event::Insert { .. } | Event::Delete { .. } | Event::BulkEdit { .. } => {
2437 self.invalidate_layouts_for_buffer(self.active_buffer());
2438 self.schedule_semantic_tokens_full_refresh(self.active_buffer());
2439 self.schedule_folding_ranges_refresh(self.active_buffer());
2440 }
2441 Event::Batch { events, .. } => {
2442 let has_edits = events
2443 .iter()
2444 .any(|e| matches!(e, Event::Insert { .. } | Event::Delete { .. }));
2445 if has_edits {
2446 self.invalidate_layouts_for_buffer(self.active_buffer());
2447 self.schedule_semantic_tokens_full_refresh(self.active_buffer());
2448 self.schedule_folding_ranges_refresh(self.active_buffer());
2449 }
2450 }
2451 _ => {}
2452 }
2453
2454 self.adjust_other_split_cursors_for_event(event);
2456
2457 let in_interactive_replace = self.interactive_replace_state.is_some();
2461
2462 let _ = in_interactive_replace; self.trigger_plugin_hooks_for_event(event, line_info);
2471
2472 self.send_lsp_changes_for_buffer(self.active_buffer(), lsp_changes);
2474 }
2475
2476 pub fn apply_events_as_bulk_edit(
2490 &mut self,
2491 events: Vec<Event>,
2492 description: String,
2493 ) -> Option<Event> {
2494 use crate::model::event::CursorId;
2495
2496 let has_buffer_mods = events
2498 .iter()
2499 .any(|e| matches!(e, Event::Insert { .. } | Event::Delete { .. }));
2500
2501 if !has_buffer_mods {
2502 return None;
2504 }
2505
2506 let active_buf = self.active_buffer();
2507 let split_id = self.split_manager.active_split();
2508
2509 let old_cursors: Vec<(CursorId, usize, Option<usize>)> = self
2511 .split_view_states
2512 .get(&split_id)
2513 .unwrap()
2514 .keyed_states
2515 .get(&active_buf)
2516 .unwrap()
2517 .cursors
2518 .iter()
2519 .map(|(id, c)| (id, c.position, c.anchor))
2520 .collect();
2521
2522 let state = self.buffers.get_mut(&active_buf).unwrap();
2523
2524 let old_snapshot = state.buffer.snapshot_buffer_state();
2526
2527 let mut edits: Vec<(usize, usize, String)> = Vec::new();
2531
2532 for event in &events {
2533 match event {
2534 Event::Insert { position, text, .. } => {
2535 edits.push((*position, 0, text.clone()));
2536 }
2537 Event::Delete { range, .. } => {
2538 edits.push((range.start, range.len(), String::new()));
2539 }
2540 _ => {}
2541 }
2542 }
2543
2544 edits.sort_by(|a, b| b.0.cmp(&a.0));
2546
2547 let edit_refs: Vec<(usize, usize, &str)> = edits
2549 .iter()
2550 .map(|(pos, del, text)| (*pos, *del, text.as_str()))
2551 .collect();
2552
2553 let _delta = state.buffer.apply_bulk_edits(&edit_refs);
2555
2556 let new_snapshot = state.buffer.snapshot_buffer_state();
2558
2559 let mut new_cursors: Vec<(CursorId, usize, Option<usize>)> = old_cursors.clone();
2562
2563 let mut position_deltas: Vec<(usize, isize)> = Vec::new();
2566 for (pos, del_len, text) in &edits {
2567 let delta = text.len() as isize - *del_len as isize;
2568 position_deltas.push((*pos, delta));
2569 }
2570 position_deltas.sort_by_key(|(pos, _)| *pos);
2571
2572 let calc_shift = |original_pos: usize| -> isize {
2574 let mut shift: isize = 0;
2575 for (edit_pos, delta) in &position_deltas {
2576 if *edit_pos < original_pos {
2577 shift += delta;
2578 }
2579 }
2580 shift
2581 };
2582
2583 for (cursor_id, ref mut pos, ref mut anchor) in &mut new_cursors {
2587 let mut found_move_cursor = false;
2588 let original_pos = *pos;
2590
2591 let insert_at_cursor_pos = events.iter().any(|e| {
2595 matches!(e, Event::Insert { position, cursor_id: c, .. }
2596 if *c == *cursor_id && *position == original_pos)
2597 });
2598
2599 for event in &events {
2601 if let Event::MoveCursor {
2602 cursor_id: event_cursor,
2603 new_position,
2604 new_anchor,
2605 ..
2606 } = event
2607 {
2608 if event_cursor == cursor_id {
2609 let shift = if insert_at_cursor_pos {
2613 calc_shift(original_pos)
2614 } else {
2615 0
2616 };
2617 *pos = (*new_position as isize + shift).max(0) as usize;
2618 *anchor = *new_anchor;
2619 found_move_cursor = true;
2620 }
2621 }
2622 }
2623
2624 if !found_move_cursor {
2626 let mut found_edit = false;
2627 for event in &events {
2628 match event {
2629 Event::Insert {
2630 position,
2631 text,
2632 cursor_id: event_cursor,
2633 } if event_cursor == cursor_id => {
2634 let shift = calc_shift(*position);
2637 let adjusted_pos = (*position as isize + shift).max(0) as usize;
2638 *pos = adjusted_pos.saturating_add(text.len());
2639 *anchor = None;
2640 found_edit = true;
2641 }
2642 Event::Delete {
2643 range,
2644 cursor_id: event_cursor,
2645 ..
2646 } if event_cursor == cursor_id => {
2647 let shift = calc_shift(range.start);
2650 *pos = (range.start as isize + shift).max(0) as usize;
2651 *anchor = None;
2652 found_edit = true;
2653 }
2654 _ => {}
2655 }
2656 }
2657
2658 if !found_edit {
2662 let shift = calc_shift(original_pos);
2663 *pos = (original_pos as isize + shift).max(0) as usize;
2664 }
2665 }
2666 }
2667
2668 {
2670 let cursors = &mut self
2671 .split_view_states
2672 .get_mut(&split_id)
2673 .unwrap()
2674 .keyed_states
2675 .get_mut(&active_buf)
2676 .unwrap()
2677 .cursors;
2678 for (cursor_id, position, anchor) in &new_cursors {
2679 if let Some(cursor) = cursors.get_mut(*cursor_id) {
2680 cursor.position = *position;
2681 cursor.anchor = *anchor;
2682 }
2683 }
2684 }
2685
2686 self.buffers
2688 .get_mut(&active_buf)
2689 .unwrap()
2690 .highlighter
2691 .invalidate_all();
2692
2693 let bulk_edit = Event::BulkEdit {
2695 old_snapshot: Some(old_snapshot),
2696 new_snapshot: Some(new_snapshot),
2697 old_cursors,
2698 new_cursors,
2699 description,
2700 };
2701
2702 self.invalidate_layouts_for_buffer(self.active_buffer());
2704 self.adjust_other_split_cursors_for_event(&bulk_edit);
2705 let buffer_id = self.active_buffer();
2712 let full_content_change = self
2713 .buffers
2714 .get(&buffer_id)
2715 .and_then(|s| s.buffer.to_string())
2716 .map(|text| {
2717 vec![TextDocumentContentChangeEvent {
2718 range: None,
2719 range_length: None,
2720 text,
2721 }]
2722 })
2723 .unwrap_or_default();
2724 if !full_content_change.is_empty() {
2725 self.send_lsp_changes_for_buffer(buffer_id, full_content_change);
2726 }
2727
2728 Some(bulk_edit)
2729 }
2730
2731 fn trigger_plugin_hooks_for_event(&mut self, event: &Event, line_info: EventLineInfo) {
2734 let buffer_id = self.active_buffer();
2735
2736 let mut cursor_changed_lines = false;
2738 let hook_args = match event {
2739 Event::Insert { position, text, .. } => {
2740 let insert_position = *position;
2741 let insert_len = text.len();
2742
2743 if let Some(seen) = self.seen_byte_ranges.get_mut(&buffer_id) {
2745 let adjusted: std::collections::HashSet<(usize, usize)> = seen
2750 .iter()
2751 .filter_map(|&(start, end)| {
2752 if end <= insert_position {
2753 Some((start, end))
2755 } else if start >= insert_position {
2756 Some((start + insert_len, end + insert_len))
2758 } else {
2759 None
2761 }
2762 })
2763 .collect();
2764 *seen = adjusted;
2765 }
2766
2767 Some((
2768 "after_insert",
2769 crate::services::plugins::hooks::HookArgs::AfterInsert {
2770 buffer_id,
2771 position: *position,
2772 text: text.clone(),
2773 affected_start: insert_position,
2775 affected_end: insert_position + insert_len,
2776 start_line: line_info.start_line,
2778 end_line: line_info.end_line,
2779 lines_added: line_info.line_delta.max(0) as usize,
2780 },
2781 ))
2782 }
2783 Event::Delete {
2784 range,
2785 deleted_text,
2786 ..
2787 } => {
2788 let delete_start = range.start;
2789
2790 let delete_end = range.end;
2792 let delete_len = delete_end - delete_start;
2793 if let Some(seen) = self.seen_byte_ranges.get_mut(&buffer_id) {
2794 let adjusted: std::collections::HashSet<(usize, usize)> = seen
2799 .iter()
2800 .filter_map(|&(start, end)| {
2801 if end <= delete_start {
2802 Some((start, end))
2804 } else if start >= delete_end {
2805 Some((start - delete_len, end - delete_len))
2807 } else {
2808 None
2810 }
2811 })
2812 .collect();
2813 *seen = adjusted;
2814 }
2815
2816 Some((
2817 "after_delete",
2818 crate::services::plugins::hooks::HookArgs::AfterDelete {
2819 buffer_id,
2820 range: range.clone(),
2821 deleted_text: deleted_text.clone(),
2822 affected_start: delete_start,
2824 deleted_len: deleted_text.len(),
2825 start_line: line_info.start_line,
2827 end_line: line_info.end_line,
2828 lines_removed: (-line_info.line_delta).max(0) as usize,
2829 },
2830 ))
2831 }
2832 Event::Batch { events, .. } => {
2833 for e in events {
2837 let sub_line_info = self.calculate_event_line_info(e);
2840 self.trigger_plugin_hooks_for_event(e, sub_line_info);
2841 }
2842 None
2843 }
2844 Event::MoveCursor {
2845 cursor_id,
2846 old_position,
2847 new_position,
2848 ..
2849 } => {
2850 let old_line = self.active_state().buffer.get_line_number(*old_position) + 1;
2852 let line = self.active_state().buffer.get_line_number(*new_position) + 1;
2853 cursor_changed_lines = old_line != line;
2854 let text_props = self
2855 .active_state()
2856 .text_properties
2857 .get_at(*new_position)
2858 .into_iter()
2859 .map(|tp| tp.properties.clone())
2860 .collect();
2861 Some((
2862 "cursor_moved",
2863 crate::services::plugins::hooks::HookArgs::CursorMoved {
2864 buffer_id,
2865 cursor_id: *cursor_id,
2866 old_position: *old_position,
2867 new_position: *new_position,
2868 line,
2869 text_properties: text_props,
2870 },
2871 ))
2872 }
2873 _ => None,
2874 };
2875
2876 if let Some((hook_name, ref args)) = hook_args {
2878 #[cfg(feature = "plugins")]
2882 self.update_plugin_state_snapshot();
2883
2884 self.plugin_manager.run_hook(hook_name, args.clone());
2885 }
2886
2887 if cursor_changed_lines {
2898 self.handle_refresh_lines(buffer_id);
2899 }
2900 }
2901
2902 fn handle_scroll_event(&mut self, line_offset: isize) {
2908 use crate::view::ui::view_pipeline::ViewLineIterator;
2909
2910 let active_split = self.split_manager.active_split();
2911
2912 if let Some(group) = self
2916 .scroll_sync_manager
2917 .find_group_for_split(active_split.into())
2918 {
2919 let left = group.left_split;
2920 let right = group.right_split;
2921 if let Some(vs) = self.split_view_states.get_mut(&LeafId(left)) {
2922 vs.viewport.set_skip_ensure_visible();
2923 }
2924 if let Some(vs) = self.split_view_states.get_mut(&LeafId(right)) {
2925 vs.viewport.set_skip_ensure_visible();
2926 }
2927 }
2929
2930 let sync_group = self
2932 .split_view_states
2933 .get(&active_split)
2934 .and_then(|vs| vs.sync_group);
2935 let splits_to_scroll = if let Some(group_id) = sync_group {
2936 self.split_manager
2937 .get_splits_in_group(group_id, &self.split_view_states)
2938 } else {
2939 vec![active_split]
2940 };
2941
2942 for split_id in splits_to_scroll {
2943 let buffer_id = if let Some(id) = self.split_manager.buffer_for_split(split_id) {
2944 id
2945 } else {
2946 continue;
2947 };
2948 let tab_size = self.config.editor.tab_size;
2949
2950 let view_transform_tokens = self
2952 .split_view_states
2953 .get(&split_id)
2954 .and_then(|vs| vs.view_transform.as_ref())
2955 .map(|vt| vt.tokens.clone());
2956
2957 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2959 let buffer = &mut state.buffer;
2960 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2961 if let Some(tokens) = view_transform_tokens {
2962 let view_lines: Vec<_> =
2964 ViewLineIterator::new(&tokens, false, false, tab_size, false).collect();
2965 view_state
2966 .viewport
2967 .scroll_view_lines(&view_lines, line_offset);
2968 } else {
2969 if line_offset > 0 {
2971 view_state
2972 .viewport
2973 .scroll_down(buffer, line_offset as usize);
2974 } else {
2975 view_state
2976 .viewport
2977 .scroll_up(buffer, line_offset.unsigned_abs());
2978 }
2979 }
2980 view_state.viewport.set_skip_ensure_visible();
2982 }
2983 }
2984 }
2985 }
2986
2987 fn handle_set_viewport_event(&mut self, top_line: usize) {
2989 let active_split = self.split_manager.active_split();
2990
2991 if self
2994 .scroll_sync_manager
2995 .is_split_synced(active_split.into())
2996 {
2997 if let Some(group) = self
2998 .scroll_sync_manager
2999 .find_group_for_split_mut(active_split.into())
3000 {
3001 let scroll_line = if group.is_left_split(active_split.into()) {
3003 top_line
3004 } else {
3005 group.right_to_left_line(top_line)
3006 };
3007 group.set_scroll_line(scroll_line);
3008 }
3009
3010 if let Some(group) = self
3012 .scroll_sync_manager
3013 .find_group_for_split(active_split.into())
3014 {
3015 let left = group.left_split;
3016 let right = group.right_split;
3017 if let Some(vs) = self.split_view_states.get_mut(&LeafId(left)) {
3018 vs.viewport.set_skip_ensure_visible();
3019 }
3020 if let Some(vs) = self.split_view_states.get_mut(&LeafId(right)) {
3021 vs.viewport.set_skip_ensure_visible();
3022 }
3023 }
3024 return;
3025 }
3026
3027 let sync_group = self
3029 .split_view_states
3030 .get(&active_split)
3031 .and_then(|vs| vs.sync_group);
3032 let splits_to_scroll = if let Some(group_id) = sync_group {
3033 self.split_manager
3034 .get_splits_in_group(group_id, &self.split_view_states)
3035 } else {
3036 vec![active_split]
3037 };
3038
3039 for split_id in splits_to_scroll {
3040 let buffer_id = if let Some(id) = self.split_manager.buffer_for_split(split_id) {
3041 id
3042 } else {
3043 continue;
3044 };
3045
3046 if let Some(state) = self.buffers.get_mut(&buffer_id) {
3047 let buffer = &mut state.buffer;
3048 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
3049 view_state.viewport.scroll_to(buffer, top_line);
3050 view_state.viewport.set_skip_ensure_visible();
3052 }
3053 }
3054 }
3055 }
3056
3057 fn handle_recenter_event(&mut self) {
3059 let active_split = self.split_manager.active_split();
3060
3061 let sync_group = self
3063 .split_view_states
3064 .get(&active_split)
3065 .and_then(|vs| vs.sync_group);
3066 let splits_to_recenter = if let Some(group_id) = sync_group {
3067 self.split_manager
3068 .get_splits_in_group(group_id, &self.split_view_states)
3069 } else {
3070 vec![active_split]
3071 };
3072
3073 for split_id in splits_to_recenter {
3074 let buffer_id = if let Some(id) = self.split_manager.buffer_for_split(split_id) {
3075 id
3076 } else {
3077 continue;
3078 };
3079
3080 if let Some(state) = self.buffers.get_mut(&buffer_id) {
3081 let buffer = &mut state.buffer;
3082 let view_state = self.split_view_states.get_mut(&split_id);
3083
3084 if let Some(view_state) = view_state {
3085 let cursor = *view_state.cursors.primary();
3087 let viewport_height = view_state.viewport.visible_line_count();
3088 let target_rows_from_top = viewport_height / 2;
3089
3090 let mut iter = buffer.line_iterator(cursor.position, 80);
3092 for _ in 0..target_rows_from_top {
3093 if iter.prev().is_none() {
3094 break;
3095 }
3096 }
3097 let new_top_byte = iter.current_position();
3098 view_state.viewport.top_byte = new_top_byte;
3099 view_state.viewport.set_skip_ensure_visible();
3101 }
3102 }
3103 }
3104 }
3105
3106 fn invalidate_layouts_for_buffer(&mut self, buffer_id: BufferId) {
3113 let splits_for_buffer = self.split_manager.splits_for_buffer(buffer_id);
3115
3116 for split_id in splits_for_buffer {
3118 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
3119 view_state.invalidate_layout();
3120 view_state.view_transform = None;
3124 view_state.view_transform_stale = true;
3127 }
3128 }
3129 }
3130
3131 pub fn active_event_log(&self) -> &EventLog {
3133 self.event_logs.get(&self.active_buffer()).unwrap()
3134 }
3135
3136 pub fn active_event_log_mut(&mut self) -> &mut EventLog {
3138 self.event_logs.get_mut(&self.active_buffer()).unwrap()
3139 }
3140
3141 pub(super) fn update_modified_from_event_log(&mut self) {
3145 let is_at_saved = self
3146 .event_logs
3147 .get(&self.active_buffer())
3148 .map(|log| log.is_at_saved_position())
3149 .unwrap_or(false);
3150
3151 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
3152 state.buffer.set_modified(!is_at_saved);
3153 }
3154 }
3155
3156 pub fn should_quit(&self) -> bool {
3158 self.should_quit
3159 }
3160
3161 pub fn should_detach(&self) -> bool {
3163 self.should_detach
3164 }
3165
3166 pub fn clear_detach(&mut self) {
3168 self.should_detach = false;
3169 }
3170
3171 pub fn set_session_mode(&mut self, session_mode: bool) {
3173 self.session_mode = session_mode;
3174 if session_mode {
3176 self.active_custom_contexts
3177 .insert(crate::types::context_keys::SESSION_MODE.to_string());
3178 } else {
3179 self.active_custom_contexts
3180 .remove(crate::types::context_keys::SESSION_MODE);
3181 }
3182 }
3183
3184 pub fn is_session_mode(&self) -> bool {
3186 self.session_mode
3187 }
3188
3189 pub fn set_software_cursor_only(&mut self, enabled: bool) {
3192 self.software_cursor_only = enabled;
3193 }
3194
3195 pub fn set_session_name(&mut self, name: Option<String>) {
3197 self.session_name = name;
3198 }
3199
3200 pub fn session_name(&self) -> Option<&str> {
3202 self.session_name.as_deref()
3203 }
3204
3205 pub fn queue_escape_sequences(&mut self, sequences: &[u8]) {
3207 self.pending_escape_sequences.extend_from_slice(sequences);
3208 }
3209
3210 pub fn take_pending_escape_sequences(&mut self) -> Vec<u8> {
3212 std::mem::take(&mut self.pending_escape_sequences)
3213 }
3214
3215 pub fn should_restart(&self) -> bool {
3217 self.restart_with_dir.is_some()
3218 }
3219
3220 pub fn take_restart_dir(&mut self) -> Option<PathBuf> {
3223 self.restart_with_dir.take()
3224 }
3225
3226 pub fn request_full_redraw(&mut self) {
3231 self.full_redraw_requested = true;
3232 }
3233
3234 pub fn take_full_redraw_request(&mut self) -> bool {
3236 let requested = self.full_redraw_requested;
3237 self.full_redraw_requested = false;
3238 requested
3239 }
3240
3241 pub fn request_restart(&mut self, new_working_dir: PathBuf) {
3242 tracing::info!(
3243 "Restart requested with new working directory: {}",
3244 new_working_dir.display()
3245 );
3246 self.restart_with_dir = Some(new_working_dir);
3247 self.should_quit = true;
3249 }
3250
3251 pub fn theme(&self) -> &crate::view::theme::Theme {
3253 &self.theme
3254 }
3255
3256 pub fn is_settings_open(&self) -> bool {
3258 self.settings_state.as_ref().is_some_and(|s| s.visible)
3259 }
3260
3261 pub fn quit(&mut self) {
3263 let modified_count = self.count_modified_buffers();
3265 if modified_count > 0 {
3266 let discard_key = t!("prompt.key.discard").to_string();
3268 let cancel_key = t!("prompt.key.cancel").to_string();
3269 let msg = if modified_count == 1 {
3270 t!(
3271 "prompt.quit_modified_one",
3272 discard_key = discard_key,
3273 cancel_key = cancel_key
3274 )
3275 .to_string()
3276 } else {
3277 t!(
3278 "prompt.quit_modified_many",
3279 count = modified_count,
3280 discard_key = discard_key,
3281 cancel_key = cancel_key
3282 )
3283 .to_string()
3284 };
3285 self.start_prompt(msg, PromptType::ConfirmQuitWithModified);
3286 } else {
3287 self.should_quit = true;
3288 }
3289 }
3290
3291 fn count_modified_buffers(&self) -> usize {
3293 self.buffers
3294 .values()
3295 .filter(|state| state.buffer.is_modified())
3296 .count()
3297 }
3298
3299 pub fn resize(&mut self, width: u16, height: u16) {
3301 self.terminal_width = width;
3303 self.terminal_height = height;
3304
3305 for view_state in self.split_view_states.values_mut() {
3307 view_state.viewport.resize(width, height);
3308 }
3309
3310 self.resize_visible_terminals();
3312
3313 self.plugin_manager.run_hook(
3315 "resize",
3316 fresh_core::hooks::HookArgs::Resize { width, height },
3317 );
3318 }
3319
3320 pub fn start_prompt(&mut self, message: String, prompt_type: PromptType) {
3324 self.start_prompt_with_suggestions(message, prompt_type, Vec::new());
3325 }
3326
3327 fn start_search_prompt(
3332 &mut self,
3333 message: String,
3334 prompt_type: PromptType,
3335 use_selection_range: bool,
3336 ) {
3337 self.pending_search_range = None;
3339
3340 let selection_range = self.active_cursors().primary().selection_range();
3341
3342 let selected_text = if let Some(range) = selection_range.clone() {
3343 let state = self.active_state_mut();
3344 let text = state.get_text_range(range.start, range.end);
3345 if !text.contains('\n') && !text.is_empty() {
3346 Some(text)
3347 } else {
3348 None
3349 }
3350 } else {
3351 None
3352 };
3353
3354 if use_selection_range {
3355 self.pending_search_range = selection_range;
3356 }
3357
3358 let from_history = selected_text.is_none();
3360 let default_text = selected_text.or_else(|| {
3361 self.get_prompt_history("search")
3362 .and_then(|h| h.last().map(|s| s.to_string()))
3363 });
3364
3365 self.start_prompt(message, prompt_type);
3367
3368 if let Some(text) = default_text {
3370 if let Some(ref mut prompt) = self.prompt {
3371 prompt.set_input(text.clone());
3372 prompt.selection_anchor = Some(0);
3373 prompt.cursor_pos = text.len();
3374 }
3375 if from_history {
3376 self.get_or_create_prompt_history("search").init_at_last();
3377 }
3378 self.update_search_highlights(&text);
3379 }
3380 }
3381
3382 pub fn start_prompt_with_suggestions(
3384 &mut self,
3385 message: String,
3386 prompt_type: PromptType,
3387 suggestions: Vec<Suggestion>,
3388 ) {
3389 self.on_editor_focus_lost();
3391
3392 match prompt_type {
3395 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
3396 self.clear_search_highlights();
3397 }
3398 _ => {}
3399 }
3400
3401 let needs_suggestions = matches!(
3403 prompt_type,
3404 PromptType::OpenFile
3405 | PromptType::SwitchProject
3406 | PromptType::SaveFileAs
3407 | PromptType::Command
3408 );
3409
3410 self.prompt = Some(Prompt::with_suggestions(message, prompt_type, suggestions));
3411
3412 if needs_suggestions {
3414 self.update_prompt_suggestions();
3415 }
3416 }
3417
3418 pub fn start_prompt_with_initial_text(
3420 &mut self,
3421 message: String,
3422 prompt_type: PromptType,
3423 initial_text: String,
3424 ) {
3425 self.on_editor_focus_lost();
3427
3428 self.prompt = Some(Prompt::with_initial_text(
3429 message,
3430 prompt_type,
3431 initial_text,
3432 ));
3433 }
3434
3435 pub fn start_quick_open(&mut self) {
3437 self.on_editor_focus_lost();
3439
3440 self.status_message = None;
3442
3443 let mut prompt = Prompt::with_suggestions(String::new(), PromptType::QuickOpen, vec![]);
3445 prompt.input = ">".to_string();
3446 prompt.cursor_pos = 1;
3447 self.prompt = Some(prompt);
3448
3449 self.update_quick_open_suggestions(">");
3451 }
3452
3453 fn update_quick_open_suggestions(&mut self, input: &str) {
3455 let suggestions = if let Some(query) = input.strip_prefix('>') {
3456 let active_buffer_mode = self
3458 .buffer_metadata
3459 .get(&self.active_buffer())
3460 .and_then(|m| m.virtual_mode());
3461 let has_lsp_config = {
3462 let language = self
3463 .buffers
3464 .get(&self.active_buffer())
3465 .map(|s| s.language.as_str());
3466 language
3467 .and_then(|lang| self.lsp.as_ref().and_then(|lsp| lsp.get_config(lang)))
3468 .is_some()
3469 };
3470 self.command_registry.read().unwrap().filter(
3471 query,
3472 self.key_context,
3473 &self.keybindings,
3474 self.has_active_selection(),
3475 &self.active_custom_contexts,
3476 active_buffer_mode,
3477 has_lsp_config,
3478 )
3479 } else if let Some(query) = input.strip_prefix('#') {
3480 self.get_buffer_suggestions(query)
3482 } else if let Some(line_str) = input.strip_prefix(':') {
3483 self.get_goto_line_suggestions(line_str)
3485 } else {
3486 let (path_part, _, _) = prompt_actions::parse_path_line_col(input);
3489 let query = if path_part.is_empty() {
3490 input
3491 } else {
3492 &path_part
3493 };
3494 self.get_file_suggestions(query)
3495 };
3496
3497 if let Some(prompt) = &mut self.prompt {
3498 prompt.suggestions = suggestions;
3499 prompt.selected_suggestion = if prompt.suggestions.is_empty() {
3500 None
3501 } else {
3502 Some(0)
3503 };
3504 }
3505 }
3506
3507 fn get_buffer_suggestions(&self, query: &str) -> Vec<Suggestion> {
3509 use crate::input::fuzzy::fuzzy_match;
3510
3511 let mut suggestions: Vec<(Suggestion, i32)> = self
3512 .buffers
3513 .iter()
3514 .filter_map(|(buffer_id, state)| {
3515 let path = state.buffer.file_path()?;
3516 let name = path
3517 .file_name()
3518 .map(|n| n.to_string_lossy().to_string())
3519 .unwrap_or_else(|| format!("Buffer {}", buffer_id.0));
3520
3521 let match_result = if query.is_empty() {
3522 crate::input::fuzzy::FuzzyMatch {
3523 matched: true,
3524 score: 0,
3525 match_positions: vec![],
3526 }
3527 } else {
3528 fuzzy_match(query, &name)
3529 };
3530
3531 if match_result.matched {
3532 let modified = state.buffer.is_modified();
3533 let display_name = if modified {
3534 format!("{} [+]", name)
3535 } else {
3536 name
3537 };
3538
3539 Some((
3540 Suggestion {
3541 text: display_name,
3542 description: Some(path.display().to_string()),
3543 value: Some(buffer_id.0.to_string()),
3544 disabled: false,
3545 keybinding: None,
3546 source: None,
3547 },
3548 match_result.score,
3549 ))
3550 } else {
3551 None
3552 }
3553 })
3554 .collect();
3555
3556 suggestions.sort_by(|a, b| b.1.cmp(&a.1));
3557 suggestions.into_iter().map(|(s, _)| s).collect()
3558 }
3559
3560 fn get_goto_line_suggestions(&self, line_str: &str) -> Vec<Suggestion> {
3562 if line_str.is_empty() {
3563 return vec![Suggestion {
3564 text: t!("quick_open.goto_line_hint").to_string(),
3565 description: Some(t!("quick_open.goto_line_desc").to_string()),
3566 value: None,
3567 disabled: true,
3568 keybinding: None,
3569 source: None,
3570 }];
3571 }
3572
3573 if let Ok(line_num) = line_str.parse::<usize>() {
3574 if line_num > 0 {
3575 return vec![Suggestion {
3576 text: t!("quick_open.goto_line", line = line_num.to_string()).to_string(),
3577 description: Some(t!("quick_open.press_enter").to_string()),
3578 value: Some(line_num.to_string()),
3579 disabled: false,
3580 keybinding: None,
3581 source: None,
3582 }];
3583 }
3584 }
3585
3586 vec![Suggestion {
3587 text: t!("quick_open.invalid_line").to_string(),
3588 description: Some(line_str.to_string()),
3589 value: None,
3590 disabled: true,
3591 keybinding: None,
3592 source: None,
3593 }]
3594 }
3595
3596 fn get_file_suggestions(&self, query: &str) -> Vec<Suggestion> {
3598 let cwd = self.working_dir.display().to_string();
3600 let context = QuickOpenContext {
3601 cwd: cwd.clone(),
3602 open_buffers: vec![], active_buffer_id: self.active_buffer().0,
3604 active_buffer_path: self
3605 .active_state()
3606 .buffer
3607 .file_path()
3608 .map(|p| p.display().to_string()),
3609 has_selection: self.has_active_selection(),
3610 key_context: self.key_context,
3611 custom_contexts: self.active_custom_contexts.clone(),
3612 buffer_mode: self
3613 .buffer_metadata
3614 .get(&self.active_buffer())
3615 .and_then(|m| m.virtual_mode())
3616 .map(|s| s.to_string()),
3617 has_lsp_config: false, };
3619
3620 self.file_provider.suggestions(query, &context)
3621 }
3622
3623 fn cancel_search_prompt_if_active(&mut self) {
3626 if let Some(ref prompt) = self.prompt {
3627 if matches!(
3628 prompt.prompt_type,
3629 PromptType::Search
3630 | PromptType::ReplaceSearch
3631 | PromptType::Replace { .. }
3632 | PromptType::QueryReplaceSearch
3633 | PromptType::QueryReplace { .. }
3634 | PromptType::QueryReplaceConfirm
3635 ) {
3636 self.prompt = None;
3637 self.interactive_replace_state = None;
3639 let ns = self.search_namespace.clone();
3641 let state = self.active_state_mut();
3642 state.overlays.clear_namespace(&ns, &mut state.marker_list);
3643 }
3644 }
3645 }
3646
3647 fn prefill_open_file_prompt(&mut self) {
3649 if let Some(prompt) = self.prompt.as_mut() {
3653 if prompt.prompt_type == PromptType::OpenFile {
3654 prompt.input.clear();
3655 prompt.cursor_pos = 0;
3656 prompt.selection_anchor = None;
3657 }
3658 }
3659 }
3660
3661 fn init_file_open_state(&mut self) {
3667 let buffer_id = self.active_buffer();
3669
3670 let initial_dir = if self.is_terminal_buffer(buffer_id) {
3673 self.get_terminal_id(buffer_id)
3674 .and_then(|tid| self.terminal_manager.get(tid))
3675 .and_then(|handle| handle.cwd())
3676 .unwrap_or_else(|| self.working_dir.clone())
3677 } else {
3678 self.active_state()
3679 .buffer
3680 .file_path()
3681 .and_then(|path| path.parent())
3682 .map(|p| p.to_path_buf())
3683 .unwrap_or_else(|| self.working_dir.clone())
3684 };
3685
3686 let show_hidden = self.config.file_browser.show_hidden;
3688 self.file_open_state = Some(file_open::FileOpenState::new(
3689 initial_dir.clone(),
3690 show_hidden,
3691 self.filesystem.clone(),
3692 ));
3693
3694 self.load_file_open_directory(initial_dir);
3696 self.load_file_open_shortcuts_async();
3697 }
3698
3699 fn init_folder_open_state(&mut self) {
3704 let initial_dir = self.working_dir.clone();
3706
3707 let show_hidden = self.config.file_browser.show_hidden;
3709 self.file_open_state = Some(file_open::FileOpenState::new(
3710 initial_dir.clone(),
3711 show_hidden,
3712 self.filesystem.clone(),
3713 ));
3714
3715 self.load_file_open_directory(initial_dir);
3717 self.load_file_open_shortcuts_async();
3718 }
3719
3720 pub fn change_working_dir(&mut self, new_path: PathBuf) {
3730 let new_path = new_path.canonicalize().unwrap_or(new_path);
3732
3733 self.request_restart(new_path);
3736 }
3737
3738 fn load_file_open_directory(&mut self, path: PathBuf) {
3740 if let Some(state) = &mut self.file_open_state {
3742 state.current_dir = path.clone();
3743 state.loading = true;
3744 state.error = None;
3745 state.update_shortcuts();
3746 }
3747
3748 if let Some(ref runtime) = self.tokio_runtime {
3750 let fs_manager = self.fs_manager.clone();
3751 let sender = self.async_bridge.as_ref().map(|b| b.sender());
3752
3753 runtime.spawn(async move {
3754 let result = fs_manager.list_dir_with_metadata(path).await;
3755 if let Some(sender) = sender {
3756 #[allow(clippy::let_underscore_must_use)]
3758 let _ = sender.send(AsyncMessage::FileOpenDirectoryLoaded(result));
3759 }
3760 });
3761 } else {
3762 if let Some(state) = &mut self.file_open_state {
3764 state.set_error("Async runtime not available".to_string());
3765 }
3766 }
3767 }
3768
3769 pub(super) fn handle_file_open_directory_loaded(
3771 &mut self,
3772 result: std::io::Result<Vec<crate::services::fs::DirEntry>>,
3773 ) {
3774 match result {
3775 Ok(entries) => {
3776 if let Some(state) = &mut self.file_open_state {
3777 state.set_entries(entries);
3778 }
3779 let filter = self
3781 .prompt
3782 .as_ref()
3783 .map(|p| p.input.clone())
3784 .unwrap_or_default();
3785 if !filter.is_empty() {
3786 if let Some(state) = &mut self.file_open_state {
3787 state.apply_filter(&filter);
3788 }
3789 }
3790 }
3791 Err(e) => {
3792 if let Some(state) = &mut self.file_open_state {
3793 state.set_error(e.to_string());
3794 }
3795 }
3796 }
3797 }
3798
3799 fn load_file_open_shortcuts_async(&mut self) {
3803 if let Some(ref runtime) = self.tokio_runtime {
3804 let filesystem = self.filesystem.clone();
3805 let sender = self.async_bridge.as_ref().map(|b| b.sender());
3806
3807 runtime.spawn(async move {
3808 let shortcuts = tokio::task::spawn_blocking(move || {
3810 file_open::FileOpenState::build_shortcuts_async(&*filesystem)
3811 })
3812 .await
3813 .unwrap_or_default();
3814
3815 if let Some(sender) = sender {
3816 #[allow(clippy::let_underscore_must_use)]
3818 let _ = sender.send(AsyncMessage::FileOpenShortcutsLoaded(shortcuts));
3819 }
3820 });
3821 }
3822 }
3823
3824 pub(super) fn handle_file_open_shortcuts_loaded(
3826 &mut self,
3827 shortcuts: Vec<file_open::NavigationShortcut>,
3828 ) {
3829 if let Some(state) = &mut self.file_open_state {
3830 state.merge_async_shortcuts(shortcuts);
3831 }
3832 }
3833
3834 pub fn cancel_prompt(&mut self) {
3836 let theme_to_restore = if let Some(ref prompt) = self.prompt {
3838 if let PromptType::SelectTheme { original_theme } = &prompt.prompt_type {
3839 Some(original_theme.clone())
3840 } else {
3841 None
3842 }
3843 } else {
3844 None
3845 };
3846
3847 if let Some(ref prompt) = self.prompt {
3849 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
3851 if let Some(history) = self.prompt_histories.get_mut(&key) {
3852 history.reset_navigation();
3853 }
3854 }
3855 match &prompt.prompt_type {
3856 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
3857 self.clear_search_highlights();
3858 }
3859 PromptType::Plugin { custom_type } => {
3860 use crate::services::plugins::hooks::HookArgs;
3862 self.plugin_manager.run_hook(
3863 "prompt_cancelled",
3864 HookArgs::PromptCancelled {
3865 prompt_type: custom_type.clone(),
3866 input: prompt.input.clone(),
3867 },
3868 );
3869 }
3870 PromptType::LspRename { overlay_handle, .. } => {
3871 let remove_overlay_event = crate::model::event::Event::RemoveOverlay {
3873 handle: overlay_handle.clone(),
3874 };
3875 self.apply_event_to_active_buffer(&remove_overlay_event);
3876 }
3877 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs => {
3878 self.file_open_state = None;
3880 self.file_browser_layout = None;
3881 }
3882 PromptType::AsyncPrompt => {
3883 if let Some(callback_id) = self.pending_async_prompt_callback.take() {
3885 self.plugin_manager
3886 .resolve_callback(callback_id, "null".to_string());
3887 }
3888 }
3889 _ => {}
3890 }
3891 }
3892
3893 self.prompt = None;
3894 self.pending_search_range = None;
3895 self.status_message = Some(t!("search.cancelled").to_string());
3896
3897 if let Some(original_theme) = theme_to_restore {
3899 self.preview_theme(&original_theme);
3900 }
3901 }
3902
3903 pub fn handle_prompt_scroll(&mut self, delta: i32) -> bool {
3906 if let Some(ref mut prompt) = self.prompt {
3907 if prompt.suggestions.is_empty() {
3908 return false;
3909 }
3910
3911 let current = prompt.selected_suggestion.unwrap_or(0);
3912 let len = prompt.suggestions.len();
3913
3914 let new_selected = if delta < 0 {
3917 current.saturating_sub((-delta) as usize)
3919 } else {
3920 (current + delta as usize).min(len.saturating_sub(1))
3922 };
3923
3924 prompt.selected_suggestion = Some(new_selected);
3925
3926 if !matches!(prompt.prompt_type, PromptType::Plugin { .. }) {
3928 if let Some(suggestion) = prompt.suggestions.get(new_selected) {
3929 prompt.input = suggestion.get_value().to_string();
3930 prompt.cursor_pos = prompt.input.len();
3931 }
3932 }
3933
3934 return true;
3935 }
3936 false
3937 }
3938
3939 pub fn confirm_prompt(&mut self) -> Option<(String, PromptType, Option<usize>)> {
3944 if let Some(prompt) = self.prompt.take() {
3945 let selected_index = prompt.selected_suggestion;
3946 let mut final_input = if prompt.sync_input_on_navigate {
3948 prompt.input.clone()
3951 } else if matches!(
3952 prompt.prompt_type,
3953 PromptType::Command
3954 | PromptType::OpenFile
3955 | PromptType::SwitchProject
3956 | PromptType::SaveFileAs
3957 | PromptType::StopLspServer
3958 | PromptType::SelectTheme { .. }
3959 | PromptType::SelectLocale
3960 | PromptType::SwitchToTab
3961 | PromptType::SetLanguage
3962 | PromptType::SetEncoding
3963 | PromptType::SetLineEnding
3964 | PromptType::Plugin { .. }
3965 ) {
3966 if let Some(selected_idx) = prompt.selected_suggestion {
3968 if let Some(suggestion) = prompt.suggestions.get(selected_idx) {
3969 if suggestion.disabled {
3971 if matches!(prompt.prompt_type, PromptType::Command) {
3973 self.command_registry
3974 .write()
3975 .unwrap()
3976 .record_usage(&suggestion.text);
3977 }
3978 self.set_status_message(
3979 t!(
3980 "error.command_not_available",
3981 command = suggestion.text.clone()
3982 )
3983 .to_string(),
3984 );
3985 return None;
3986 }
3987 suggestion.get_value().to_string()
3989 } else {
3990 prompt.input.clone()
3991 }
3992 } else {
3993 prompt.input.clone()
3994 }
3995 } else {
3996 prompt.input.clone()
3997 };
3998
3999 if matches!(prompt.prompt_type, PromptType::StopLspServer) {
4001 let is_valid = prompt
4002 .suggestions
4003 .iter()
4004 .any(|s| s.text == final_input || s.get_value() == final_input);
4005 if !is_valid {
4006 self.prompt = Some(prompt);
4008 self.set_status_message(
4009 t!("error.no_lsp_match", input = final_input.clone()).to_string(),
4010 );
4011 return None;
4012 }
4013 }
4014
4015 if matches!(prompt.prompt_type, PromptType::RemoveRuler) {
4019 if prompt.input.is_empty() {
4020 if let Some(selected_idx) = prompt.selected_suggestion {
4022 if let Some(suggestion) = prompt.suggestions.get(selected_idx) {
4023 final_input = suggestion.get_value().to_string();
4024 }
4025 } else {
4026 self.prompt = Some(prompt);
4027 return None;
4028 }
4029 } else {
4030 let typed = prompt.input.trim().to_string();
4032 let matched = prompt.suggestions.iter().find(|s| s.get_value() == typed);
4033 if let Some(suggestion) = matched {
4034 final_input = suggestion.get_value().to_string();
4035 } else {
4036 self.prompt = Some(prompt);
4038 return None;
4039 }
4040 }
4041 }
4042
4043 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
4045 let history = self.get_or_create_prompt_history(&key);
4046 history.push(final_input.clone());
4047 history.reset_navigation();
4048 }
4049
4050 Some((final_input, prompt.prompt_type, selected_index))
4051 } else {
4052 None
4053 }
4054 }
4055
4056 pub fn is_prompting(&self) -> bool {
4058 self.prompt.is_some()
4059 }
4060
4061 fn get_or_create_prompt_history(
4063 &mut self,
4064 key: &str,
4065 ) -> &mut crate::input::input_history::InputHistory {
4066 self.prompt_histories.entry(key.to_string()).or_default()
4067 }
4068
4069 fn get_prompt_history(&self, key: &str) -> Option<&crate::input::input_history::InputHistory> {
4071 self.prompt_histories.get(key)
4072 }
4073
4074 fn prompt_type_to_history_key(prompt_type: &crate::view::prompt::PromptType) -> Option<String> {
4076 use crate::view::prompt::PromptType;
4077 match prompt_type {
4078 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
4079 Some("search".to_string())
4080 }
4081 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
4082 Some("replace".to_string())
4083 }
4084 PromptType::GotoLine => Some("goto_line".to_string()),
4085 PromptType::Plugin { custom_type } => Some(format!("plugin:{}", custom_type)),
4086 _ => None,
4087 }
4088 }
4089
4090 pub fn editor_mode(&self) -> Option<String> {
4093 self.editor_mode.clone()
4094 }
4095
4096 pub fn command_registry(&self) -> &Arc<RwLock<CommandRegistry>> {
4098 &self.command_registry
4099 }
4100
4101 pub fn plugin_manager(&self) -> &PluginManager {
4103 &self.plugin_manager
4104 }
4105
4106 pub fn plugin_manager_mut(&mut self) -> &mut PluginManager {
4108 &mut self.plugin_manager
4109 }
4110
4111 pub fn file_explorer_is_focused(&self) -> bool {
4113 self.key_context == KeyContext::FileExplorer
4114 }
4115
4116 pub fn prompt_input(&self) -> Option<&str> {
4118 self.prompt.as_ref().map(|p| p.input.as_str())
4119 }
4120
4121 pub fn has_active_selection(&self) -> bool {
4123 self.active_cursors().primary().selection_range().is_some()
4124 }
4125
4126 pub fn prompt_mut(&mut self) -> Option<&mut Prompt> {
4128 self.prompt.as_mut()
4129 }
4130
4131 pub fn set_status_message(&mut self, message: String) {
4133 tracing::info!(target: "status", "{}", message);
4134 self.plugin_status_message = None;
4135 self.status_message = Some(message);
4136 }
4137
4138 pub fn get_status_message(&self) -> Option<&String> {
4140 self.plugin_status_message
4141 .as_ref()
4142 .or(self.status_message.as_ref())
4143 }
4144
4145 pub fn get_plugin_errors(&self) -> &[String] {
4148 &self.plugin_errors
4149 }
4150
4151 pub fn clear_plugin_errors(&mut self) {
4153 self.plugin_errors.clear();
4154 }
4155
4156 pub fn update_prompt_suggestions(&mut self) {
4158 let (prompt_type, input) = if let Some(prompt) = &self.prompt {
4160 (prompt.prompt_type.clone(), prompt.input.clone())
4161 } else {
4162 return;
4163 };
4164
4165 match prompt_type {
4166 PromptType::Command => {
4167 let selection_active = self.has_active_selection();
4168 let active_buffer_mode = self
4169 .buffer_metadata
4170 .get(&self.active_buffer())
4171 .and_then(|m| m.virtual_mode());
4172 let has_lsp_config = {
4173 let language = self
4174 .buffers
4175 .get(&self.active_buffer())
4176 .map(|s| s.language.as_str());
4177 language
4178 .and_then(|lang| self.lsp.as_ref().and_then(|lsp| lsp.get_config(lang)))
4179 .is_some()
4180 };
4181 if let Some(prompt) = &mut self.prompt {
4182 prompt.suggestions = self.command_registry.read().unwrap().filter(
4184 &input,
4185 self.key_context,
4186 &self.keybindings,
4187 selection_active,
4188 &self.active_custom_contexts,
4189 active_buffer_mode,
4190 has_lsp_config,
4191 );
4192 prompt.selected_suggestion = if prompt.suggestions.is_empty() {
4193 None
4194 } else {
4195 Some(0)
4196 };
4197 }
4198 }
4199 PromptType::QuickOpen => {
4200 self.update_quick_open_suggestions(&input);
4202 }
4203 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
4204 self.update_search_highlights(&input);
4206 if let Some(history) = self.prompt_histories.get_mut("search") {
4208 history.reset_navigation();
4209 }
4210 }
4211 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
4212 if let Some(history) = self.prompt_histories.get_mut("replace") {
4214 history.reset_navigation();
4215 }
4216 }
4217 PromptType::GotoLine => {
4218 if let Some(history) = self.prompt_histories.get_mut("goto_line") {
4220 history.reset_navigation();
4221 }
4222 }
4223 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs => {
4224 self.update_file_open_filter();
4226 }
4227 PromptType::Plugin { custom_type } => {
4228 let key = format!("plugin:{}", custom_type);
4230 if let Some(history) = self.prompt_histories.get_mut(&key) {
4231 history.reset_navigation();
4232 }
4233 use crate::services::plugins::hooks::HookArgs;
4235 self.plugin_manager.run_hook(
4236 "prompt_changed",
4237 HookArgs::PromptChanged {
4238 prompt_type: custom_type,
4239 input,
4240 },
4241 );
4242 if let Some(prompt) = &mut self.prompt {
4247 prompt.filter_suggestions(false);
4248 }
4249 }
4250 PromptType::SwitchToTab
4251 | PromptType::SelectTheme { .. }
4252 | PromptType::StopLspServer
4253 | PromptType::SetLanguage
4254 | PromptType::SetEncoding
4255 | PromptType::SetLineEnding => {
4256 if let Some(prompt) = &mut self.prompt {
4257 prompt.filter_suggestions(false);
4258 }
4259 }
4260 PromptType::SelectLocale => {
4261 if let Some(prompt) = &mut self.prompt {
4263 prompt.filter_suggestions(true);
4264 }
4265 }
4266 _ => {}
4267 }
4268 }
4269
4270 pub fn process_async_messages(&mut self) -> bool {
4278 self.plugin_manager.check_thread_health();
4281
4282 let Some(bridge) = &self.async_bridge else {
4283 return false;
4284 };
4285
4286 let messages = {
4287 let _s = tracing::info_span!("try_recv_all").entered();
4288 bridge.try_recv_all()
4289 };
4290 let needs_render = !messages.is_empty();
4291 tracing::trace!(
4292 async_message_count = messages.len(),
4293 "received async messages"
4294 );
4295
4296 for message in messages {
4297 match message {
4298 AsyncMessage::LspDiagnostics { uri, diagnostics } => {
4299 self.handle_lsp_diagnostics(uri, diagnostics);
4300 }
4301 AsyncMessage::LspInitialized {
4302 language,
4303 completion_trigger_characters,
4304 semantic_tokens_legend,
4305 semantic_tokens_full,
4306 semantic_tokens_full_delta,
4307 semantic_tokens_range,
4308 folding_ranges_supported,
4309 } => {
4310 tracing::info!("LSP server initialized for language: {}", language);
4311 tracing::debug!(
4312 "LSP completion trigger characters for {}: {:?}",
4313 language,
4314 completion_trigger_characters
4315 );
4316 self.status_message = Some(format!("LSP ({}) ready", language));
4317
4318 if let Some(lsp) = &mut self.lsp {
4320 lsp.set_completion_trigger_characters(
4321 &language,
4322 completion_trigger_characters,
4323 );
4324 lsp.set_semantic_tokens_capabilities(
4325 &language,
4326 semantic_tokens_legend,
4327 semantic_tokens_full,
4328 semantic_tokens_full_delta,
4329 semantic_tokens_range,
4330 );
4331 lsp.set_folding_ranges_supported(&language, folding_ranges_supported);
4332 }
4333
4334 self.resend_did_open_for_language(&language);
4336 self.request_semantic_tokens_for_language(&language);
4337 self.request_folding_ranges_for_language(&language);
4338 }
4339 AsyncMessage::LspError {
4340 language,
4341 error,
4342 stderr_log_path,
4343 } => {
4344 tracing::error!("LSP error for {}: {}", language, error);
4345 self.status_message = Some(format!("LSP error ({}): {}", language, error));
4346
4347 let server_command = self
4349 .config
4350 .lsp
4351 .get(&language)
4352 .map(|c| c.command.clone())
4353 .unwrap_or_else(|| "unknown".to_string());
4354
4355 let error_type = if error.contains("not found") || error.contains("NotFound") {
4357 "not_found"
4358 } else if error.contains("permission") || error.contains("PermissionDenied") {
4359 "spawn_failed"
4360 } else if error.contains("timeout") {
4361 "timeout"
4362 } else {
4363 "spawn_failed"
4364 }
4365 .to_string();
4366
4367 self.plugin_manager.run_hook(
4369 "lsp_server_error",
4370 crate::services::plugins::hooks::HookArgs::LspServerError {
4371 language: language.clone(),
4372 server_command,
4373 error_type,
4374 message: error.clone(),
4375 },
4376 );
4377
4378 if let Some(log_path) = stderr_log_path {
4381 let has_content = log_path.metadata().map(|m| m.len() > 0).unwrap_or(false);
4382 if has_content {
4383 tracing::info!("Opening LSP stderr log in background: {:?}", log_path);
4384 match self.open_file_no_focus(&log_path) {
4385 Ok(buffer_id) => {
4386 self.mark_buffer_read_only(buffer_id, true);
4387 self.status_message = Some(format!(
4388 "LSP error ({}): {} - See stderr log",
4389 language, error
4390 ));
4391 }
4392 Err(e) => {
4393 tracing::error!("Failed to open LSP stderr log: {}", e);
4394 }
4395 }
4396 }
4397 }
4398 }
4399 AsyncMessage::LspCompletion { request_id, items } => {
4400 if let Err(e) = self.handle_completion_response(request_id, items) {
4401 tracing::error!("Error handling completion response: {}", e);
4402 }
4403 }
4404 AsyncMessage::LspGotoDefinition {
4405 request_id,
4406 locations,
4407 } => {
4408 if let Err(e) = self.handle_goto_definition_response(request_id, locations) {
4409 tracing::error!("Error handling goto definition response: {}", e);
4410 }
4411 }
4412 AsyncMessage::LspRename { request_id, result } => {
4413 if let Err(e) = self.handle_rename_response(request_id, result) {
4414 tracing::error!("Error handling rename response: {}", e);
4415 }
4416 }
4417 AsyncMessage::LspHover {
4418 request_id,
4419 contents,
4420 is_markdown,
4421 range,
4422 } => {
4423 self.handle_hover_response(request_id, contents, is_markdown, range);
4424 }
4425 AsyncMessage::LspReferences {
4426 request_id,
4427 locations,
4428 } => {
4429 if let Err(e) = self.handle_references_response(request_id, locations) {
4430 tracing::error!("Error handling references response: {}", e);
4431 }
4432 }
4433 AsyncMessage::LspSignatureHelp {
4434 request_id,
4435 signature_help,
4436 } => {
4437 self.handle_signature_help_response(request_id, signature_help);
4438 }
4439 AsyncMessage::LspCodeActions {
4440 request_id,
4441 actions,
4442 } => {
4443 self.handle_code_actions_response(request_id, actions);
4444 }
4445 AsyncMessage::LspPulledDiagnostics {
4446 request_id: _,
4447 uri,
4448 result_id,
4449 diagnostics,
4450 unchanged,
4451 } => {
4452 self.handle_lsp_pulled_diagnostics(uri, result_id, diagnostics, unchanged);
4453 }
4454 AsyncMessage::LspInlayHints {
4455 request_id,
4456 uri,
4457 hints,
4458 } => {
4459 self.handle_lsp_inlay_hints(request_id, uri, hints);
4460 }
4461 AsyncMessage::LspFoldingRanges {
4462 request_id,
4463 uri,
4464 ranges,
4465 } => {
4466 self.handle_lsp_folding_ranges(request_id, uri, ranges);
4467 }
4468 AsyncMessage::LspSemanticTokens {
4469 request_id,
4470 uri,
4471 response,
4472 } => {
4473 self.handle_lsp_semantic_tokens(request_id, uri, response);
4474 }
4475 AsyncMessage::LspServerQuiescent { language } => {
4476 self.handle_lsp_server_quiescent(language);
4477 }
4478 AsyncMessage::LspDiagnosticRefresh { language } => {
4479 self.handle_lsp_diagnostic_refresh(language);
4480 }
4481 AsyncMessage::FileChanged { path } => {
4482 self.handle_async_file_changed(path);
4483 }
4484 AsyncMessage::GitStatusChanged { status } => {
4485 tracing::info!("Git status changed: {}", status);
4486 }
4488 AsyncMessage::FileExplorerInitialized(view) => {
4489 self.handle_file_explorer_initialized(view);
4490 }
4491 AsyncMessage::FileExplorerToggleNode(node_id) => {
4492 self.handle_file_explorer_toggle_node(node_id);
4493 }
4494 AsyncMessage::FileExplorerRefreshNode(node_id) => {
4495 self.handle_file_explorer_refresh_node(node_id);
4496 }
4497 AsyncMessage::FileExplorerExpandedToPath(view) => {
4498 self.handle_file_explorer_expanded_to_path(view);
4499 }
4500 AsyncMessage::Plugin(plugin_msg) => {
4501 use fresh_core::api::{JsCallbackId, PluginAsyncMessage};
4502 match plugin_msg {
4503 PluginAsyncMessage::ProcessOutput {
4504 process_id,
4505 stdout,
4506 stderr,
4507 exit_code,
4508 } => {
4509 self.handle_plugin_process_output(
4510 JsCallbackId::from(process_id),
4511 stdout,
4512 stderr,
4513 exit_code,
4514 );
4515 }
4516 PluginAsyncMessage::DelayComplete { callback_id } => {
4517 self.plugin_manager.resolve_callback(
4518 JsCallbackId::from(callback_id),
4519 "null".to_string(),
4520 );
4521 }
4522 PluginAsyncMessage::ProcessStdout { process_id, data } => {
4523 self.plugin_manager.run_hook(
4524 "onProcessStdout",
4525 crate::services::plugins::hooks::HookArgs::ProcessOutput {
4526 process_id,
4527 data,
4528 },
4529 );
4530 }
4531 PluginAsyncMessage::ProcessStderr { process_id, data } => {
4532 self.plugin_manager.run_hook(
4533 "onProcessStderr",
4534 crate::services::plugins::hooks::HookArgs::ProcessOutput {
4535 process_id,
4536 data,
4537 },
4538 );
4539 }
4540 PluginAsyncMessage::ProcessExit {
4541 process_id,
4542 callback_id,
4543 exit_code,
4544 } => {
4545 self.background_process_handles.remove(&process_id);
4546 let result = fresh_core::api::BackgroundProcessResult {
4547 process_id,
4548 exit_code,
4549 };
4550 self.plugin_manager.resolve_callback(
4551 JsCallbackId::from(callback_id),
4552 serde_json::to_string(&result).unwrap(),
4553 );
4554 }
4555 PluginAsyncMessage::LspResponse {
4556 language: _,
4557 request_id,
4558 result,
4559 } => {
4560 self.handle_plugin_lsp_response(request_id, result);
4561 }
4562 PluginAsyncMessage::PluginResponse(response) => {
4563 self.handle_plugin_response(response);
4564 }
4565 }
4566 }
4567 AsyncMessage::LspProgress {
4568 language,
4569 token,
4570 value,
4571 } => {
4572 self.handle_lsp_progress(language, token, value);
4573 }
4574 AsyncMessage::LspWindowMessage {
4575 language,
4576 message_type,
4577 message,
4578 } => {
4579 self.handle_lsp_window_message(language, message_type, message);
4580 }
4581 AsyncMessage::LspLogMessage {
4582 language,
4583 message_type,
4584 message,
4585 } => {
4586 self.handle_lsp_log_message(language, message_type, message);
4587 }
4588 AsyncMessage::LspStatusUpdate {
4589 language,
4590 status,
4591 message: _,
4592 } => {
4593 self.handle_lsp_status_update(language, status);
4594 }
4595 AsyncMessage::FileOpenDirectoryLoaded(result) => {
4596 self.handle_file_open_directory_loaded(result);
4597 }
4598 AsyncMessage::FileOpenShortcutsLoaded(shortcuts) => {
4599 self.handle_file_open_shortcuts_loaded(shortcuts);
4600 }
4601 AsyncMessage::TerminalOutput { terminal_id } => {
4602 tracing::trace!("Terminal output received for {:?}", terminal_id);
4604
4605 if self.config.terminal.jump_to_end_on_output && !self.terminal_mode {
4608 if let Some(&active_terminal_id) =
4610 self.terminal_buffers.get(&self.active_buffer())
4611 {
4612 if active_terminal_id == terminal_id {
4613 self.enter_terminal_mode();
4614 }
4615 }
4616 }
4617
4618 if self.terminal_mode {
4620 if let Some(handle) = self.terminal_manager.get(terminal_id) {
4621 if let Ok(mut state) = handle.state.lock() {
4622 state.scroll_to_bottom();
4623 }
4624 }
4625 }
4626 }
4627 AsyncMessage::TerminalExited { terminal_id } => {
4628 tracing::info!("Terminal {:?} exited", terminal_id);
4629 if let Some((&buffer_id, _)) = self
4631 .terminal_buffers
4632 .iter()
4633 .find(|(_, &tid)| tid == terminal_id)
4634 {
4635 if self.active_buffer() == buffer_id && self.terminal_mode {
4637 self.terminal_mode = false;
4638 self.key_context = crate::input::keybindings::KeyContext::Normal;
4639 }
4640
4641 self.sync_terminal_to_buffer(buffer_id);
4643
4644 let exit_msg = "\n[Terminal process exited]\n";
4646
4647 if let Some(backing_path) =
4648 self.terminal_backing_files.get(&terminal_id).cloned()
4649 {
4650 if let Ok(mut file) =
4651 self.filesystem.open_file_for_append(&backing_path)
4652 {
4653 use std::io::Write;
4654 if let Err(e) = file.write_all(exit_msg.as_bytes()) {
4655 tracing::warn!("Failed to write terminal exit message: {}", e);
4656 }
4657 }
4658
4659 if let Err(e) = self.revert_buffer_by_id(buffer_id, &backing_path) {
4661 tracing::warn!("Failed to revert terminal buffer: {}", e);
4662 }
4663 }
4664
4665 if let Some(state) = self.buffers.get_mut(&buffer_id) {
4667 state.editing_disabled = true;
4668 state.margins.configure_for_line_numbers(false);
4669 state.buffer.set_modified(false);
4670 }
4671
4672 self.terminal_buffers.remove(&buffer_id);
4674
4675 self.set_status_message(
4676 t!("terminal.exited", id = terminal_id.0).to_string(),
4677 );
4678 }
4679 self.terminal_manager.close(terminal_id);
4680 }
4681
4682 AsyncMessage::LspServerRequest {
4683 language,
4684 server_command,
4685 method,
4686 params,
4687 } => {
4688 self.handle_lsp_server_request(language, server_command, method, params);
4689 }
4690 AsyncMessage::PluginLspResponse {
4691 language: _,
4692 request_id,
4693 result,
4694 } => {
4695 self.handle_plugin_lsp_response(request_id, result);
4696 }
4697 AsyncMessage::PluginProcessOutput {
4698 process_id,
4699 stdout,
4700 stderr,
4701 exit_code,
4702 } => {
4703 self.handle_plugin_process_output(
4704 fresh_core::api::JsCallbackId::from(process_id),
4705 stdout,
4706 stderr,
4707 exit_code,
4708 );
4709 }
4710 AsyncMessage::GrammarRegistryBuilt {
4711 registry,
4712 callback_ids,
4713 } => {
4714 tracing::info!(
4715 "Background grammar build completed ({} syntaxes)",
4716 registry.available_syntaxes().len()
4717 );
4718 self.grammar_registry = registry;
4719 self.grammar_build_in_progress = false;
4720
4721 let buffers_to_update: Vec<_> = self
4723 .buffer_metadata
4724 .iter()
4725 .filter_map(|(id, meta)| meta.file_path().map(|p| (*id, p.to_path_buf())))
4726 .collect();
4727
4728 for (buf_id, path) in buffers_to_update {
4729 if let Some(state) = self.buffers.get_mut(&buf_id) {
4730 let detected =
4731 crate::primitives::detected_language::DetectedLanguage::from_path(
4732 &path,
4733 &self.grammar_registry,
4734 &self.config.languages,
4735 );
4736
4737 if detected.highlighter.has_highlighting()
4738 || !state.highlighter.has_highlighting()
4739 {
4740 state.apply_language(detected);
4741 }
4742 }
4743 }
4744
4745 #[cfg(feature = "plugins")]
4747 for cb_id in callback_ids {
4748 self.plugin_manager
4749 .resolve_callback(cb_id, "null".to_string());
4750 }
4751
4752 self.flush_pending_grammars();
4754 }
4755 }
4756 }
4757
4758 #[cfg(feature = "plugins")]
4761 {
4762 let _s = tracing::info_span!("update_plugin_state_snapshot").entered();
4763 self.update_plugin_state_snapshot();
4764 }
4765
4766 let processed_any_commands = {
4768 let _s = tracing::info_span!("process_plugin_commands").entered();
4769 self.process_plugin_commands()
4770 };
4771
4772 #[cfg(feature = "plugins")]
4776 if processed_any_commands {
4777 let _s = tracing::info_span!("update_plugin_state_snapshot_post").entered();
4778 self.update_plugin_state_snapshot();
4779 }
4780
4781 #[cfg(feature = "plugins")]
4783 {
4784 let _s = tracing::info_span!("process_pending_plugin_actions").entered();
4785 self.process_pending_plugin_actions();
4786 }
4787
4788 {
4790 let _s = tracing::info_span!("process_pending_lsp_restarts").entered();
4791 self.process_pending_lsp_restarts();
4792 }
4793
4794 #[cfg(feature = "plugins")]
4796 let plugin_render = {
4797 let render = self.plugin_render_requested;
4798 self.plugin_render_requested = false;
4799 render
4800 };
4801 #[cfg(not(feature = "plugins"))]
4802 let plugin_render = false;
4803
4804 if let Some(ref mut checker) = self.update_checker {
4806 let _ = checker.poll_result();
4808 }
4809
4810 let file_changes = {
4812 let _s = tracing::info_span!("poll_file_changes").entered();
4813 self.poll_file_changes()
4814 };
4815 let tree_changes = {
4816 let _s = tracing::info_span!("poll_file_tree_changes").entered();
4817 self.poll_file_tree_changes()
4818 };
4819
4820 needs_render || processed_any_commands || plugin_render || file_changes || tree_changes
4822 }
4823
4824 fn update_lsp_status_from_progress(&mut self) {
4826 if self.lsp_progress.is_empty() {
4827 self.update_lsp_status_from_server_statuses();
4829 return;
4830 }
4831
4832 if let Some((_, info)) = self.lsp_progress.iter().next() {
4834 let mut status = format!("LSP ({}): {}", info.language, info.title);
4835 if let Some(ref msg) = info.message {
4836 status.push_str(&format!(" - {}", msg));
4837 }
4838 if let Some(pct) = info.percentage {
4839 status.push_str(&format!(" ({}%)", pct));
4840 }
4841 self.lsp_status = status;
4842 }
4843 }
4844
4845 fn update_lsp_status_from_server_statuses(&mut self) {
4847 use crate::services::async_bridge::LspServerStatus;
4848
4849 let mut statuses: Vec<(String, LspServerStatus)> = self
4851 .lsp_server_statuses
4852 .iter()
4853 .map(|(lang, status)| (lang.clone(), *status))
4854 .collect();
4855
4856 if statuses.is_empty() {
4857 self.lsp_status = String::new();
4858 return;
4859 }
4860
4861 statuses.sort_by(|a, b| a.0.cmp(&b.0));
4863
4864 let status_parts: Vec<String> = statuses
4866 .iter()
4867 .map(|(lang, status)| {
4868 let status_str = match status {
4869 LspServerStatus::Starting => "starting",
4870 LspServerStatus::Initializing => "initializing",
4871 LspServerStatus::Running => "ready",
4872 LspServerStatus::Error => "error",
4873 LspServerStatus::Shutdown => "shutdown",
4874 };
4875 format!("{}: {}", lang, status_str)
4876 })
4877 .collect();
4878
4879 self.lsp_status = format!("LSP [{}]", status_parts.join(", "));
4880 }
4881
4882 #[cfg(feature = "plugins")]
4884 fn update_plugin_state_snapshot(&mut self) {
4885 if let Some(snapshot_handle) = self.plugin_manager.state_snapshot_handle() {
4887 use fresh_core::api::{BufferInfo, CursorInfo, ViewportInfo};
4888 let mut snapshot = snapshot_handle.write().unwrap();
4889
4890 snapshot.active_buffer_id = self.active_buffer();
4892
4893 snapshot.active_split_id = self.split_manager.active_split().0 .0;
4895
4896 snapshot.buffers.clear();
4898 snapshot.buffer_saved_diffs.clear();
4899 snapshot.buffer_cursor_positions.clear();
4900 snapshot.buffer_text_properties.clear();
4901
4902 for (buffer_id, state) in &self.buffers {
4903 let is_virtual = self
4904 .buffer_metadata
4905 .get(buffer_id)
4906 .map(|m| m.is_virtual())
4907 .unwrap_or(false);
4908 let active_split = self.split_manager.active_split();
4913 let active_vs = self.split_view_states.get(&active_split);
4914 let view_mode = active_vs
4915 .and_then(|vs| vs.buffer_state(*buffer_id))
4916 .map(|bs| match bs.view_mode {
4917 crate::state::ViewMode::Source => "source",
4918 crate::state::ViewMode::Compose => "compose",
4919 })
4920 .unwrap_or("source");
4921 let compose_width = active_vs
4922 .and_then(|vs| vs.buffer_state(*buffer_id))
4923 .and_then(|bs| bs.compose_width);
4924 let is_composing_in_any_split = self.split_view_states.values().any(|vs| {
4925 vs.buffer_state(*buffer_id)
4926 .map(|bs| matches!(bs.view_mode, crate::state::ViewMode::Compose))
4927 .unwrap_or(false)
4928 });
4929 let buffer_info = BufferInfo {
4930 id: *buffer_id,
4931 path: state.buffer.file_path().map(|p| p.to_path_buf()),
4932 modified: state.buffer.is_modified(),
4933 length: state.buffer.len(),
4934 is_virtual,
4935 view_mode: view_mode.to_string(),
4936 is_composing_in_any_split,
4937 compose_width,
4938 language: state.language.clone(),
4939 };
4940 snapshot.buffers.insert(*buffer_id, buffer_info);
4941
4942 let diff = {
4943 let diff = state.buffer.diff_since_saved();
4944 BufferSavedDiff {
4945 equal: diff.equal,
4946 byte_ranges: diff.byte_ranges.clone(),
4947 line_ranges: diff.line_ranges.clone(),
4948 }
4949 };
4950 snapshot.buffer_saved_diffs.insert(*buffer_id, diff);
4951
4952 let cursor_pos = self
4954 .split_view_states
4955 .values()
4956 .find_map(|vs| vs.buffer_state(*buffer_id))
4957 .map(|bs| bs.cursors.primary().position)
4958 .unwrap_or(0);
4959 snapshot
4960 .buffer_cursor_positions
4961 .insert(*buffer_id, cursor_pos);
4962
4963 if !state.text_properties.is_empty() {
4965 snapshot
4966 .buffer_text_properties
4967 .insert(*buffer_id, state.text_properties.all().to_vec());
4968 }
4969 }
4970
4971 if let Some(active_vs) = self
4973 .split_view_states
4974 .get(&self.split_manager.active_split())
4975 {
4976 let active_cursors = &active_vs.cursors;
4978 let primary = active_cursors.primary();
4979 let primary_position = primary.position;
4980 let primary_selection = primary.selection_range();
4981
4982 snapshot.primary_cursor = Some(CursorInfo {
4983 position: primary_position,
4984 selection: primary_selection.clone(),
4985 });
4986
4987 snapshot.all_cursors = active_cursors
4989 .iter()
4990 .map(|(_, cursor)| CursorInfo {
4991 position: cursor.position,
4992 selection: cursor.selection_range(),
4993 })
4994 .collect();
4995
4996 if let Some(range) = primary_selection {
4998 if let Some(active_state) = self.buffers.get_mut(&self.active_buffer()) {
4999 snapshot.selected_text =
5000 Some(active_state.get_text_range(range.start, range.end));
5001 }
5002 }
5003
5004 let top_line = self.buffers.get(&self.active_buffer()).and_then(|state| {
5006 if state.buffer.line_count().is_some() {
5007 Some(state.buffer.get_line_number(active_vs.viewport.top_byte))
5008 } else {
5009 None
5010 }
5011 });
5012 snapshot.viewport = Some(ViewportInfo {
5013 top_byte: active_vs.viewport.top_byte,
5014 top_line,
5015 left_column: active_vs.viewport.left_column,
5016 width: active_vs.viewport.width,
5017 height: active_vs.viewport.height,
5018 });
5019 } else {
5020 snapshot.primary_cursor = None;
5021 snapshot.all_cursors.clear();
5022 snapshot.viewport = None;
5023 snapshot.selected_text = None;
5024 }
5025
5026 snapshot.clipboard = self.clipboard.get_internal().to_string();
5028
5029 snapshot.working_dir = self.working_dir.clone();
5031
5032 snapshot.diagnostics = self.stored_diagnostics.clone();
5034
5035 snapshot.folding_ranges = self.stored_folding_ranges.clone();
5037
5038 snapshot.config = serde_json::to_value(&self.config).unwrap_or(serde_json::Value::Null);
5040
5041 snapshot.user_config = self.user_config_raw.clone();
5044
5045 snapshot.editor_mode = self.editor_mode.clone();
5047
5048 let active_split_id = self.split_manager.active_split().0 .0;
5053 let split_changed = snapshot.plugin_view_states_split != active_split_id;
5054 if split_changed {
5055 snapshot.plugin_view_states.clear();
5056 snapshot.plugin_view_states_split = active_split_id;
5057 }
5058
5059 {
5061 let open_bids: Vec<_> = snapshot.buffers.keys().copied().collect();
5062 snapshot
5063 .plugin_view_states
5064 .retain(|bid, _| open_bids.contains(bid));
5065 }
5066
5067 if let Some(active_vs) = self
5069 .split_view_states
5070 .get(&self.split_manager.active_split())
5071 {
5072 for (buffer_id, buf_state) in &active_vs.keyed_states {
5073 if !buf_state.plugin_state.is_empty() {
5074 let entry = snapshot.plugin_view_states.entry(*buffer_id).or_default();
5075 for (key, value) in &buf_state.plugin_state {
5076 entry.entry(key.clone()).or_insert_with(|| value.clone());
5078 }
5079 }
5080 }
5081 }
5082 }
5083 }
5084
5085 pub fn handle_plugin_command(&mut self, command: PluginCommand) -> AnyhowResult<()> {
5087 match command {
5088 PluginCommand::InsertText {
5090 buffer_id,
5091 position,
5092 text,
5093 } => {
5094 self.handle_insert_text(buffer_id, position, text);
5095 }
5096 PluginCommand::DeleteRange { buffer_id, range } => {
5097 self.handle_delete_range(buffer_id, range);
5098 }
5099 PluginCommand::InsertAtCursor { text } => {
5100 self.handle_insert_at_cursor(text);
5101 }
5102 PluginCommand::DeleteSelection => {
5103 self.handle_delete_selection();
5104 }
5105
5106 PluginCommand::AddOverlay {
5108 buffer_id,
5109 namespace,
5110 range,
5111 options,
5112 } => {
5113 self.handle_add_overlay(buffer_id, namespace, range, options);
5114 }
5115 PluginCommand::RemoveOverlay { buffer_id, handle } => {
5116 self.handle_remove_overlay(buffer_id, handle);
5117 }
5118 PluginCommand::ClearAllOverlays { buffer_id } => {
5119 self.handle_clear_all_overlays(buffer_id);
5120 }
5121 PluginCommand::ClearNamespace {
5122 buffer_id,
5123 namespace,
5124 } => {
5125 self.handle_clear_namespace(buffer_id, namespace);
5126 }
5127 PluginCommand::ClearOverlaysInRange {
5128 buffer_id,
5129 start,
5130 end,
5131 } => {
5132 self.handle_clear_overlays_in_range(buffer_id, start, end);
5133 }
5134
5135 PluginCommand::AddVirtualText {
5137 buffer_id,
5138 virtual_text_id,
5139 position,
5140 text,
5141 color,
5142 use_bg,
5143 before,
5144 } => {
5145 self.handle_add_virtual_text(
5146 buffer_id,
5147 virtual_text_id,
5148 position,
5149 text,
5150 color,
5151 use_bg,
5152 before,
5153 );
5154 }
5155 PluginCommand::RemoveVirtualText {
5156 buffer_id,
5157 virtual_text_id,
5158 } => {
5159 self.handle_remove_virtual_text(buffer_id, virtual_text_id);
5160 }
5161 PluginCommand::RemoveVirtualTextsByPrefix { buffer_id, prefix } => {
5162 self.handle_remove_virtual_texts_by_prefix(buffer_id, prefix);
5163 }
5164 PluginCommand::ClearVirtualTexts { buffer_id } => {
5165 self.handle_clear_virtual_texts(buffer_id);
5166 }
5167 PluginCommand::AddVirtualLine {
5168 buffer_id,
5169 position,
5170 text,
5171 fg_color,
5172 bg_color,
5173 above,
5174 namespace,
5175 priority,
5176 } => {
5177 self.handle_add_virtual_line(
5178 buffer_id, position, text, fg_color, bg_color, above, namespace, priority,
5179 );
5180 }
5181 PluginCommand::ClearVirtualTextNamespace {
5182 buffer_id,
5183 namespace,
5184 } => {
5185 self.handle_clear_virtual_text_namespace(buffer_id, namespace);
5186 }
5187
5188 PluginCommand::AddConceal {
5190 buffer_id,
5191 namespace,
5192 start,
5193 end,
5194 replacement,
5195 } => {
5196 self.handle_add_conceal(buffer_id, namespace, start, end, replacement);
5197 }
5198 PluginCommand::ClearConcealNamespace {
5199 buffer_id,
5200 namespace,
5201 } => {
5202 self.handle_clear_conceal_namespace(buffer_id, namespace);
5203 }
5204 PluginCommand::ClearConcealsInRange {
5205 buffer_id,
5206 start,
5207 end,
5208 } => {
5209 self.handle_clear_conceals_in_range(buffer_id, start, end);
5210 }
5211
5212 PluginCommand::AddSoftBreak {
5214 buffer_id,
5215 namespace,
5216 position,
5217 indent,
5218 } => {
5219 self.handle_add_soft_break(buffer_id, namespace, position, indent);
5220 }
5221 PluginCommand::ClearSoftBreakNamespace {
5222 buffer_id,
5223 namespace,
5224 } => {
5225 self.handle_clear_soft_break_namespace(buffer_id, namespace);
5226 }
5227 PluginCommand::ClearSoftBreaksInRange {
5228 buffer_id,
5229 start,
5230 end,
5231 } => {
5232 self.handle_clear_soft_breaks_in_range(buffer_id, start, end);
5233 }
5234
5235 PluginCommand::AddMenuItem {
5237 menu_label,
5238 item,
5239 position,
5240 } => {
5241 self.handle_add_menu_item(menu_label, item, position);
5242 }
5243 PluginCommand::AddMenu { menu, position } => {
5244 self.handle_add_menu(menu, position);
5245 }
5246 PluginCommand::RemoveMenuItem {
5247 menu_label,
5248 item_label,
5249 } => {
5250 self.handle_remove_menu_item(menu_label, item_label);
5251 }
5252 PluginCommand::RemoveMenu { menu_label } => {
5253 self.handle_remove_menu(menu_label);
5254 }
5255
5256 PluginCommand::FocusSplit { split_id } => {
5258 self.handle_focus_split(split_id);
5259 }
5260 PluginCommand::SetSplitBuffer {
5261 split_id,
5262 buffer_id,
5263 } => {
5264 self.handle_set_split_buffer(split_id, buffer_id);
5265 }
5266 PluginCommand::SetSplitScroll { split_id, top_byte } => {
5267 self.handle_set_split_scroll(split_id, top_byte);
5268 }
5269 PluginCommand::RequestHighlights {
5270 buffer_id,
5271 range,
5272 request_id,
5273 } => {
5274 self.handle_request_highlights(buffer_id, range, request_id);
5275 }
5276 PluginCommand::CloseSplit { split_id } => {
5277 self.handle_close_split(split_id);
5278 }
5279 PluginCommand::SetSplitRatio { split_id, ratio } => {
5280 self.handle_set_split_ratio(split_id, ratio);
5281 }
5282 PluginCommand::SetSplitLabel { split_id, label } => {
5283 self.split_manager.set_label(LeafId(split_id), label);
5284 }
5285 PluginCommand::ClearSplitLabel { split_id } => {
5286 self.split_manager.clear_label(split_id);
5287 }
5288 PluginCommand::GetSplitByLabel { label, request_id } => {
5289 let split_id = self.split_manager.find_split_by_label(&label);
5290 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
5291 let json = serde_json::to_string(&split_id.map(|s| s.0 .0))
5292 .unwrap_or_else(|_| "null".to_string());
5293 self.plugin_manager.resolve_callback(callback_id, json);
5294 }
5295 PluginCommand::DistributeSplitsEvenly { split_ids: _ } => {
5296 self.handle_distribute_splits_evenly();
5297 }
5298 PluginCommand::SetBufferCursor {
5299 buffer_id,
5300 position,
5301 } => {
5302 self.handle_set_buffer_cursor(buffer_id, position);
5303 }
5304
5305 PluginCommand::SetLayoutHints {
5307 buffer_id,
5308 split_id,
5309 range: _,
5310 hints,
5311 } => {
5312 self.handle_set_layout_hints(buffer_id, split_id, hints);
5313 }
5314 PluginCommand::SetLineNumbers { buffer_id, enabled } => {
5315 self.handle_set_line_numbers(buffer_id, enabled);
5316 }
5317 PluginCommand::SetViewMode { buffer_id, mode } => {
5318 self.handle_set_view_mode(buffer_id, &mode);
5319 }
5320 PluginCommand::SetLineWrap {
5321 buffer_id,
5322 split_id,
5323 enabled,
5324 } => {
5325 self.handle_set_line_wrap(buffer_id, split_id, enabled);
5326 }
5327 PluginCommand::SubmitViewTransform {
5328 buffer_id,
5329 split_id,
5330 payload,
5331 } => {
5332 self.handle_submit_view_transform(buffer_id, split_id, payload);
5333 }
5334 PluginCommand::ClearViewTransform {
5335 buffer_id: _,
5336 split_id,
5337 } => {
5338 self.handle_clear_view_transform(split_id);
5339 }
5340 PluginCommand::SetViewState {
5341 buffer_id,
5342 key,
5343 value,
5344 } => {
5345 self.handle_set_view_state(buffer_id, key, value);
5346 }
5347 PluginCommand::RefreshLines { buffer_id } => {
5348 self.handle_refresh_lines(buffer_id);
5349 }
5350 PluginCommand::RefreshAllLines => {
5351 self.handle_refresh_all_lines();
5352 }
5353 PluginCommand::HookCompleted { .. } => {
5354 }
5356 PluginCommand::SetLineIndicator {
5357 buffer_id,
5358 line,
5359 namespace,
5360 symbol,
5361 color,
5362 priority,
5363 } => {
5364 self.handle_set_line_indicator(buffer_id, line, namespace, symbol, color, priority);
5365 }
5366 PluginCommand::SetLineIndicators {
5367 buffer_id,
5368 lines,
5369 namespace,
5370 symbol,
5371 color,
5372 priority,
5373 } => {
5374 self.handle_set_line_indicators(
5375 buffer_id, lines, namespace, symbol, color, priority,
5376 );
5377 }
5378 PluginCommand::ClearLineIndicators {
5379 buffer_id,
5380 namespace,
5381 } => {
5382 self.handle_clear_line_indicators(buffer_id, namespace);
5383 }
5384 PluginCommand::SetFileExplorerDecorations {
5385 namespace,
5386 decorations,
5387 } => {
5388 self.handle_set_file_explorer_decorations(namespace, decorations);
5389 }
5390 PluginCommand::ClearFileExplorerDecorations { namespace } => {
5391 self.handle_clear_file_explorer_decorations(&namespace);
5392 }
5393
5394 PluginCommand::SetStatus { message } => {
5396 self.handle_set_status(message);
5397 }
5398 PluginCommand::ApplyTheme { theme_name } => {
5399 self.apply_theme(&theme_name);
5400 }
5401 PluginCommand::ReloadConfig => {
5402 self.reload_config();
5403 }
5404 PluginCommand::ReloadThemes { apply_theme } => {
5405 self.reload_themes();
5406 if let Some(theme_name) = apply_theme {
5407 self.apply_theme(&theme_name);
5408 }
5409 }
5410 PluginCommand::RegisterGrammar {
5411 language,
5412 grammar_path,
5413 extensions,
5414 } => {
5415 self.handle_register_grammar(language, grammar_path, extensions);
5416 }
5417 PluginCommand::RegisterLanguageConfig { language, config } => {
5418 self.handle_register_language_config(language, config);
5419 }
5420 PluginCommand::RegisterLspServer { language, config } => {
5421 self.handle_register_lsp_server(language, config);
5422 }
5423 PluginCommand::ReloadGrammars { callback_id } => {
5424 self.handle_reload_grammars(callback_id);
5425 }
5426 PluginCommand::StartPrompt { label, prompt_type } => {
5427 self.handle_start_prompt(label, prompt_type);
5428 }
5429 PluginCommand::StartPromptWithInitial {
5430 label,
5431 prompt_type,
5432 initial_value,
5433 } => {
5434 self.handle_start_prompt_with_initial(label, prompt_type, initial_value);
5435 }
5436 PluginCommand::StartPromptAsync {
5437 label,
5438 initial_value,
5439 callback_id,
5440 } => {
5441 self.handle_start_prompt_async(label, initial_value, callback_id);
5442 }
5443 PluginCommand::SetPromptSuggestions { suggestions } => {
5444 self.handle_set_prompt_suggestions(suggestions);
5445 }
5446 PluginCommand::SetPromptInputSync { sync } => {
5447 if let Some(prompt) = &mut self.prompt {
5448 prompt.sync_input_on_navigate = sync;
5449 }
5450 }
5451
5452 PluginCommand::RegisterCommand { command } => {
5454 self.handle_register_command(command);
5455 }
5456 PluginCommand::UnregisterCommand { name } => {
5457 self.handle_unregister_command(name);
5458 }
5459 PluginCommand::DefineMode {
5460 name,
5461 parent,
5462 bindings,
5463 read_only,
5464 } => {
5465 self.handle_define_mode(name, parent, bindings, read_only);
5466 }
5467
5468 PluginCommand::OpenFileInBackground { path } => {
5470 self.handle_open_file_in_background(path);
5471 }
5472 PluginCommand::OpenFileAtLocation { path, line, column } => {
5473 return self.handle_open_file_at_location(path, line, column);
5474 }
5475 PluginCommand::OpenFileInSplit {
5476 split_id,
5477 path,
5478 line,
5479 column,
5480 } => {
5481 return self.handle_open_file_in_split(split_id, path, line, column);
5482 }
5483 PluginCommand::ShowBuffer { buffer_id } => {
5484 self.handle_show_buffer(buffer_id);
5485 }
5486 PluginCommand::CloseBuffer { buffer_id } => {
5487 self.handle_close_buffer(buffer_id);
5488 }
5489
5490 PluginCommand::SendLspRequest {
5492 language,
5493 method,
5494 params,
5495 request_id,
5496 } => {
5497 self.handle_send_lsp_request(language, method, params, request_id);
5498 }
5499
5500 PluginCommand::SetClipboard { text } => {
5502 self.handle_set_clipboard(text);
5503 }
5504
5505 PluginCommand::SpawnProcess {
5507 command,
5508 args,
5509 cwd,
5510 callback_id,
5511 } => {
5512 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
5515 let effective_cwd = cwd.or_else(|| {
5516 std::env::current_dir()
5517 .map(|p| p.to_string_lossy().to_string())
5518 .ok()
5519 });
5520 let sender = bridge.sender();
5521 let spawner = self.process_spawner.clone();
5522
5523 runtime.spawn(async move {
5524 #[allow(clippy::let_underscore_must_use)]
5526 match spawner.spawn(command, args, effective_cwd).await {
5527 Ok(result) => {
5528 let _ = sender.send(AsyncMessage::PluginProcessOutput {
5529 process_id: callback_id.as_u64(),
5530 stdout: result.stdout,
5531 stderr: result.stderr,
5532 exit_code: result.exit_code,
5533 });
5534 }
5535 Err(e) => {
5536 let _ = sender.send(AsyncMessage::PluginProcessOutput {
5537 process_id: callback_id.as_u64(),
5538 stdout: String::new(),
5539 stderr: e.to_string(),
5540 exit_code: -1,
5541 });
5542 }
5543 }
5544 });
5545 } else {
5546 self.plugin_manager
5548 .reject_callback(callback_id, "Async runtime not available".to_string());
5549 }
5550 }
5551
5552 PluginCommand::SpawnProcessWait {
5553 process_id,
5554 callback_id,
5555 } => {
5556 tracing::warn!(
5559 "SpawnProcessWait not fully implemented - process_id={}",
5560 process_id
5561 );
5562 self.plugin_manager.reject_callback(
5563 callback_id,
5564 format!(
5565 "SpawnProcessWait not yet fully implemented for process_id={}",
5566 process_id
5567 ),
5568 );
5569 }
5570
5571 PluginCommand::Delay {
5572 callback_id,
5573 duration_ms,
5574 } => {
5575 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
5577 let sender = bridge.sender();
5578 let callback_id_u64 = callback_id.as_u64();
5579 runtime.spawn(async move {
5580 tokio::time::sleep(tokio::time::Duration::from_millis(duration_ms)).await;
5581 #[allow(clippy::let_underscore_must_use)]
5583 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
5584 fresh_core::api::PluginAsyncMessage::DelayComplete {
5585 callback_id: callback_id_u64,
5586 },
5587 ));
5588 });
5589 } else {
5590 std::thread::sleep(std::time::Duration::from_millis(duration_ms));
5592 self.plugin_manager
5593 .resolve_callback(callback_id, "null".to_string());
5594 }
5595 }
5596
5597 PluginCommand::SpawnBackgroundProcess {
5598 process_id,
5599 command,
5600 args,
5601 cwd,
5602 callback_id,
5603 } => {
5604 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
5606 use tokio::io::{AsyncBufReadExt, BufReader};
5607 use tokio::process::Command as TokioCommand;
5608
5609 let effective_cwd = cwd.unwrap_or_else(|| {
5610 std::env::current_dir()
5611 .map(|p| p.to_string_lossy().to_string())
5612 .unwrap_or_else(|_| ".".to_string())
5613 });
5614
5615 let sender = bridge.sender();
5616 let sender_stdout = sender.clone();
5617 let sender_stderr = sender.clone();
5618 let callback_id_u64 = callback_id.as_u64();
5619
5620 #[allow(clippy::let_underscore_must_use)]
5622 let handle = runtime.spawn(async move {
5623 let mut child = match TokioCommand::new(&command)
5624 .args(&args)
5625 .current_dir(&effective_cwd)
5626 .stdout(std::process::Stdio::piped())
5627 .stderr(std::process::Stdio::piped())
5628 .spawn()
5629 {
5630 Ok(child) => child,
5631 Err(e) => {
5632 let _ = sender.send(
5633 crate::services::async_bridge::AsyncMessage::Plugin(
5634 fresh_core::api::PluginAsyncMessage::ProcessExit {
5635 process_id,
5636 callback_id: callback_id_u64,
5637 exit_code: -1,
5638 },
5639 ),
5640 );
5641 tracing::error!("Failed to spawn background process: {}", e);
5642 return;
5643 }
5644 };
5645
5646 let stdout = child.stdout.take();
5648 let stderr = child.stderr.take();
5649 let pid = process_id;
5650
5651 if let Some(stdout) = stdout {
5653 let sender = sender_stdout;
5654 tokio::spawn(async move {
5655 let reader = BufReader::new(stdout);
5656 let mut lines = reader.lines();
5657 while let Ok(Some(line)) = lines.next_line().await {
5658 let _ = sender.send(
5659 crate::services::async_bridge::AsyncMessage::Plugin(
5660 fresh_core::api::PluginAsyncMessage::ProcessStdout {
5661 process_id: pid,
5662 data: line + "\n",
5663 },
5664 ),
5665 );
5666 }
5667 });
5668 }
5669
5670 if let Some(stderr) = stderr {
5672 let sender = sender_stderr;
5673 tokio::spawn(async move {
5674 let reader = BufReader::new(stderr);
5675 let mut lines = reader.lines();
5676 while let Ok(Some(line)) = lines.next_line().await {
5677 let _ = sender.send(
5678 crate::services::async_bridge::AsyncMessage::Plugin(
5679 fresh_core::api::PluginAsyncMessage::ProcessStderr {
5680 process_id: pid,
5681 data: line + "\n",
5682 },
5683 ),
5684 );
5685 }
5686 });
5687 }
5688
5689 let exit_code = match child.wait().await {
5691 Ok(status) => status.code().unwrap_or(-1),
5692 Err(_) => -1,
5693 };
5694
5695 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
5696 fresh_core::api::PluginAsyncMessage::ProcessExit {
5697 process_id,
5698 callback_id: callback_id_u64,
5699 exit_code,
5700 },
5701 ));
5702 });
5703
5704 self.background_process_handles
5706 .insert(process_id, handle.abort_handle());
5707 } else {
5708 self.plugin_manager
5710 .reject_callback(callback_id, "Async runtime not available".to_string());
5711 }
5712 }
5713
5714 PluginCommand::KillBackgroundProcess { process_id } => {
5715 if let Some(handle) = self.background_process_handles.remove(&process_id) {
5716 handle.abort();
5717 tracing::debug!("Killed background process {}", process_id);
5718 }
5719 }
5720
5721 PluginCommand::CreateVirtualBuffer {
5723 name,
5724 mode,
5725 read_only,
5726 } => {
5727 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
5728 tracing::info!(
5729 "Created virtual buffer '{}' with mode '{}' (id={:?})",
5730 name,
5731 mode,
5732 buffer_id
5733 );
5734 }
5736 PluginCommand::CreateVirtualBufferWithContent {
5737 name,
5738 mode,
5739 read_only,
5740 entries,
5741 show_line_numbers,
5742 show_cursors,
5743 editing_disabled,
5744 hidden_from_tabs,
5745 request_id,
5746 } => {
5747 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
5748 tracing::info!(
5749 "Created virtual buffer '{}' with mode '{}' (id={:?})",
5750 name,
5751 mode,
5752 buffer_id
5753 );
5754
5755 if let Some(state) = self.buffers.get_mut(&buffer_id) {
5762 state.margins.configure_for_line_numbers(show_line_numbers);
5763 state.show_cursors = show_cursors;
5764 state.editing_disabled = editing_disabled;
5765 tracing::debug!(
5766 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
5767 buffer_id,
5768 show_line_numbers,
5769 show_cursors,
5770 editing_disabled
5771 );
5772 }
5773 let active_split = self.split_manager.active_split();
5774 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
5775 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
5776 }
5777
5778 if hidden_from_tabs {
5780 if let Some(meta) = self.buffer_metadata.get_mut(&buffer_id) {
5781 meta.hidden_from_tabs = true;
5782 }
5783 }
5784
5785 match self.set_virtual_buffer_content(buffer_id, entries) {
5787 Ok(()) => {
5788 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
5789 self.set_active_buffer(buffer_id);
5791 tracing::debug!("Switched to virtual buffer {:?}", buffer_id);
5792
5793 if let Some(req_id) = request_id {
5795 tracing::info!(
5796 "CreateVirtualBufferWithContent: resolving callback for request_id={}, buffer_id={:?}",
5797 req_id,
5798 buffer_id
5799 );
5800 let result = fresh_core::api::VirtualBufferResult {
5802 buffer_id: buffer_id.0 as u64,
5803 split_id: None,
5804 };
5805 self.plugin_manager.resolve_callback(
5806 fresh_core::api::JsCallbackId::from(req_id),
5807 serde_json::to_string(&result).unwrap_or_default(),
5808 );
5809 tracing::info!("CreateVirtualBufferWithContent: resolve_callback sent for request_id={}", req_id);
5810 }
5811 }
5812 Err(e) => {
5813 tracing::error!("Failed to set virtual buffer content: {}", e);
5814 }
5815 }
5816 }
5817 PluginCommand::CreateVirtualBufferInSplit {
5818 name,
5819 mode,
5820 read_only,
5821 entries,
5822 ratio,
5823 direction,
5824 panel_id,
5825 show_line_numbers,
5826 show_cursors,
5827 editing_disabled,
5828 line_wrap,
5829 before,
5830 request_id,
5831 } => {
5832 if let Some(pid) = &panel_id {
5834 if let Some(&existing_buffer_id) = self.panel_ids.get(pid) {
5835 if self.buffers.contains_key(&existing_buffer_id) {
5837 if let Err(e) =
5839 self.set_virtual_buffer_content(existing_buffer_id, entries)
5840 {
5841 tracing::error!("Failed to update panel content: {}", e);
5842 } else {
5843 tracing::info!("Updated existing panel '{}' content", pid);
5844 }
5845
5846 let splits = self.split_manager.splits_for_buffer(existing_buffer_id);
5848 if let Some(&split_id) = splits.first() {
5849 self.split_manager.set_active_split(split_id);
5850 self.split_manager.set_active_buffer_id(existing_buffer_id);
5853 tracing::debug!(
5854 "Focused split {:?} containing panel buffer",
5855 split_id
5856 );
5857 }
5858
5859 if let Some(req_id) = request_id {
5861 let result = fresh_core::api::VirtualBufferResult {
5862 buffer_id: existing_buffer_id.0 as u64,
5863 split_id: splits.first().map(|s| s.0 .0 as u64),
5864 };
5865 self.plugin_manager.resolve_callback(
5866 fresh_core::api::JsCallbackId::from(req_id),
5867 serde_json::to_string(&result).unwrap_or_default(),
5868 );
5869 }
5870 return Ok(());
5871 } else {
5872 tracing::warn!(
5874 "Removing stale panel_id '{}' pointing to non-existent buffer {:?}",
5875 pid,
5876 existing_buffer_id
5877 );
5878 self.panel_ids.remove(pid);
5879 }
5881 }
5882 }
5883
5884 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
5886 tracing::info!(
5887 "Created virtual buffer '{}' with mode '{}' in split (id={:?})",
5888 name,
5889 mode,
5890 buffer_id
5891 );
5892
5893 if let Some(state) = self.buffers.get_mut(&buffer_id) {
5895 state.margins.configure_for_line_numbers(show_line_numbers);
5896 state.show_cursors = show_cursors;
5897 state.editing_disabled = editing_disabled;
5898 tracing::debug!(
5899 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
5900 buffer_id,
5901 show_line_numbers,
5902 show_cursors,
5903 editing_disabled
5904 );
5905 }
5906
5907 if let Some(pid) = panel_id {
5909 self.panel_ids.insert(pid, buffer_id);
5910 }
5911
5912 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
5914 tracing::error!("Failed to set virtual buffer content: {}", e);
5915 return Ok(());
5916 }
5917
5918 let split_dir = match direction.as_deref() {
5920 Some("vertical") => crate::model::event::SplitDirection::Vertical,
5921 _ => crate::model::event::SplitDirection::Horizontal,
5922 };
5923
5924 let created_split_id = match self
5926 .split_manager
5927 .split_active_positioned(split_dir, buffer_id, ratio, before)
5928 {
5929 Ok(new_split_id) => {
5930 let mut view_state = SplitViewState::with_buffer(
5932 self.terminal_width,
5933 self.terminal_height,
5934 buffer_id,
5935 );
5936 view_state.apply_config_defaults(
5937 self.config.editor.line_numbers,
5938 line_wrap.unwrap_or(self.config.editor.line_wrap),
5939 self.config.editor.wrap_indent,
5940 self.config.editor.rulers.clone(),
5941 );
5942 view_state.ensure_buffer_state(buffer_id).show_line_numbers =
5944 show_line_numbers;
5945 self.split_view_states.insert(new_split_id, view_state);
5946
5947 self.split_manager.set_active_split(new_split_id);
5949 tracing::info!(
5952 "Created {:?} split with virtual buffer {:?}",
5953 split_dir,
5954 buffer_id
5955 );
5956 Some(new_split_id)
5957 }
5958 Err(e) => {
5959 tracing::error!("Failed to create split: {}", e);
5960 self.set_active_buffer(buffer_id);
5962 None
5963 }
5964 };
5965
5966 if let Some(req_id) = request_id {
5969 tracing::trace!("CreateVirtualBufferInSplit: resolving callback for request_id={}, buffer_id={:?}, split_id={:?}", req_id, buffer_id, created_split_id);
5970 let result = fresh_core::api::VirtualBufferResult {
5971 buffer_id: buffer_id.0 as u64,
5972 split_id: created_split_id.map(|s| s.0 .0 as u64),
5973 };
5974 self.plugin_manager.resolve_callback(
5975 fresh_core::api::JsCallbackId::from(req_id),
5976 serde_json::to_string(&result).unwrap_or_default(),
5977 );
5978 }
5979 }
5980 PluginCommand::SetVirtualBufferContent { buffer_id, entries } => {
5981 match self.set_virtual_buffer_content(buffer_id, entries) {
5982 Ok(()) => {
5983 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
5984 }
5985 Err(e) => {
5986 tracing::error!("Failed to set virtual buffer content: {}", e);
5987 }
5988 }
5989 }
5990 PluginCommand::GetTextPropertiesAtCursor { buffer_id } => {
5991 if let Some(state) = self.buffers.get(&buffer_id) {
5993 let cursor_pos = self
5994 .split_view_states
5995 .values()
5996 .find_map(|vs| vs.buffer_state(buffer_id))
5997 .map(|bs| bs.cursors.primary().position)
5998 .unwrap_or(0);
5999 let properties = state.text_properties.get_at(cursor_pos);
6000 tracing::debug!(
6001 "Text properties at cursor in {:?}: {} properties found",
6002 buffer_id,
6003 properties.len()
6004 );
6005 }
6007 }
6008 PluginCommand::CreateVirtualBufferInExistingSplit {
6009 name,
6010 mode,
6011 read_only,
6012 entries,
6013 split_id,
6014 show_line_numbers,
6015 show_cursors,
6016 editing_disabled,
6017 line_wrap,
6018 request_id,
6019 } => {
6020 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
6022 tracing::info!(
6023 "Created virtual buffer '{}' with mode '{}' for existing split {:?} (id={:?})",
6024 name,
6025 mode,
6026 split_id,
6027 buffer_id
6028 );
6029
6030 if let Some(state) = self.buffers.get_mut(&buffer_id) {
6032 state.margins.configure_for_line_numbers(show_line_numbers);
6033 state.show_cursors = show_cursors;
6034 state.editing_disabled = editing_disabled;
6035 }
6036
6037 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
6039 tracing::error!("Failed to set virtual buffer content: {}", e);
6040 return Ok(());
6041 }
6042
6043 let leaf_id = LeafId(split_id);
6045 self.split_manager.set_split_buffer(leaf_id, buffer_id);
6046
6047 self.split_manager.set_active_split(leaf_id);
6049 self.split_manager.set_active_buffer_id(buffer_id);
6050
6051 if let Some(view_state) = self.split_view_states.get_mut(&leaf_id) {
6053 view_state.switch_buffer(buffer_id);
6054 view_state.add_buffer(buffer_id);
6055 view_state.ensure_buffer_state(buffer_id).show_line_numbers = show_line_numbers;
6056
6057 if let Some(wrap) = line_wrap {
6059 view_state.active_state_mut().viewport.line_wrap_enabled = wrap;
6060 }
6061 }
6062
6063 tracing::info!(
6064 "Displayed virtual buffer {:?} in split {:?}",
6065 buffer_id,
6066 split_id
6067 );
6068
6069 if let Some(req_id) = request_id {
6071 let result = fresh_core::api::VirtualBufferResult {
6072 buffer_id: buffer_id.0 as u64,
6073 split_id: Some(split_id.0 as u64),
6074 };
6075 self.plugin_manager.resolve_callback(
6076 fresh_core::api::JsCallbackId::from(req_id),
6077 serde_json::to_string(&result).unwrap_or_default(),
6078 );
6079 }
6080 }
6081
6082 PluginCommand::SetContext { name, active } => {
6084 if active {
6085 self.active_custom_contexts.insert(name.clone());
6086 tracing::debug!("Set custom context: {}", name);
6087 } else {
6088 self.active_custom_contexts.remove(&name);
6089 tracing::debug!("Unset custom context: {}", name);
6090 }
6091 }
6092
6093 PluginCommand::SetReviewDiffHunks { hunks } => {
6095 self.review_hunks = hunks;
6096 tracing::debug!("Set {} review hunks", self.review_hunks.len());
6097 }
6098
6099 PluginCommand::ExecuteAction { action_name } => {
6101 self.handle_execute_action(action_name);
6102 }
6103 PluginCommand::ExecuteActions { actions } => {
6104 self.handle_execute_actions(actions);
6105 }
6106 PluginCommand::GetBufferText {
6107 buffer_id,
6108 start,
6109 end,
6110 request_id,
6111 } => {
6112 self.handle_get_buffer_text(buffer_id, start, end, request_id);
6113 }
6114 PluginCommand::GetLineStartPosition {
6115 buffer_id,
6116 line,
6117 request_id,
6118 } => {
6119 self.handle_get_line_start_position(buffer_id, line, request_id);
6120 }
6121 PluginCommand::GetLineEndPosition {
6122 buffer_id,
6123 line,
6124 request_id,
6125 } => {
6126 self.handle_get_line_end_position(buffer_id, line, request_id);
6127 }
6128 PluginCommand::GetBufferLineCount {
6129 buffer_id,
6130 request_id,
6131 } => {
6132 self.handle_get_buffer_line_count(buffer_id, request_id);
6133 }
6134 PluginCommand::ScrollToLineCenter {
6135 split_id,
6136 buffer_id,
6137 line,
6138 } => {
6139 self.handle_scroll_to_line_center(split_id, buffer_id, line);
6140 }
6141 PluginCommand::SetEditorMode { mode } => {
6142 self.handle_set_editor_mode(mode);
6143 }
6144
6145 PluginCommand::ShowActionPopup {
6147 popup_id,
6148 title,
6149 message,
6150 actions,
6151 } => {
6152 tracing::info!(
6153 "Action popup requested: id={}, title={}, actions={}",
6154 popup_id,
6155 title,
6156 actions.len()
6157 );
6158
6159 let items: Vec<crate::model::event::PopupListItemData> = actions
6161 .iter()
6162 .map(|action| crate::model::event::PopupListItemData {
6163 text: action.label.clone(),
6164 detail: None,
6165 icon: None,
6166 data: Some(action.id.clone()),
6167 })
6168 .collect();
6169
6170 let action_ids: Vec<(String, String)> =
6172 actions.into_iter().map(|a| (a.id, a.label)).collect();
6173 self.active_action_popup = Some((popup_id.clone(), action_ids));
6174
6175 let popup = crate::model::event::PopupData {
6177 kind: crate::model::event::PopupKindHint::List,
6178 title: Some(title),
6179 description: Some(message),
6180 transient: false,
6181 content: crate::model::event::PopupContentData::List { items, selected: 0 },
6182 position: crate::model::event::PopupPositionData::BottomRight,
6183 width: 60,
6184 max_height: 15,
6185 bordered: true,
6186 };
6187
6188 self.show_popup(popup);
6189 tracing::info!(
6190 "Action popup shown: id={}, active_action_popup={:?}",
6191 popup_id,
6192 self.active_action_popup.as_ref().map(|(id, _)| id)
6193 );
6194 }
6195
6196 PluginCommand::DisableLspForLanguage { language } => {
6197 tracing::info!("Disabling LSP for language: {}", language);
6198
6199 if let Some(ref mut lsp) = self.lsp {
6201 lsp.shutdown_server(&language);
6202 tracing::info!("Stopped LSP server for {}", language);
6203 }
6204
6205 if let Some(lsp_config) = self.config.lsp.get_mut(&language) {
6207 lsp_config.enabled = false;
6208 lsp_config.auto_start = false;
6209 tracing::info!("Disabled LSP config for {}", language);
6210 }
6211
6212 if let Err(e) = self.save_config() {
6214 tracing::error!("Failed to save config: {}", e);
6215 self.status_message = Some(format!(
6216 "LSP disabled for {} (config save failed)",
6217 language
6218 ));
6219 } else {
6220 self.status_message = Some(format!("LSP disabled for {}", language));
6221 }
6222
6223 self.warning_domains.lsp.clear();
6225 }
6226
6227 PluginCommand::RestartLspForLanguage { language } => {
6228 tracing::info!("Plugin restarting LSP for language: {}", language);
6229
6230 let success = if let Some(ref mut lsp) = self.lsp {
6231 let (ok, msg) = lsp.manual_restart(&language);
6232 self.status_message = Some(msg);
6233 ok
6234 } else {
6235 self.status_message = Some("No LSP manager available".to_string());
6236 false
6237 };
6238
6239 if success {
6240 self.reopen_buffers_for_language(&language);
6241 }
6242 }
6243
6244 PluginCommand::SetLspRootUri { language, uri } => {
6245 tracing::info!("Plugin setting LSP root URI for {}: {}", language, uri);
6246
6247 match uri.parse::<lsp_types::Uri>() {
6249 Ok(parsed_uri) => {
6250 if let Some(ref mut lsp) = self.lsp {
6251 let restarted = lsp.set_language_root_uri(&language, parsed_uri);
6252 if restarted {
6253 self.status_message = Some(format!(
6254 "LSP root updated for {} (restarting server)",
6255 language
6256 ));
6257 } else {
6258 self.status_message =
6259 Some(format!("LSP root set for {}", language));
6260 }
6261 }
6262 }
6263 Err(e) => {
6264 tracing::error!("Invalid LSP root URI '{}': {}", uri, e);
6265 self.status_message = Some(format!("Invalid LSP root URI: {}", e));
6266 }
6267 }
6268 }
6269
6270 PluginCommand::CreateScrollSyncGroup {
6272 group_id,
6273 left_split,
6274 right_split,
6275 } => {
6276 let success = self.scroll_sync_manager.create_group_with_id(
6277 group_id,
6278 left_split,
6279 right_split,
6280 );
6281 if success {
6282 tracing::debug!(
6283 "Created scroll sync group {} for splits {:?} and {:?}",
6284 group_id,
6285 left_split,
6286 right_split
6287 );
6288 } else {
6289 tracing::warn!(
6290 "Failed to create scroll sync group {} (ID already exists)",
6291 group_id
6292 );
6293 }
6294 }
6295 PluginCommand::SetScrollSyncAnchors { group_id, anchors } => {
6296 use crate::view::scroll_sync::SyncAnchor;
6297 let anchor_count = anchors.len();
6298 let sync_anchors: Vec<SyncAnchor> = anchors
6299 .into_iter()
6300 .map(|(left_line, right_line)| SyncAnchor {
6301 left_line,
6302 right_line,
6303 })
6304 .collect();
6305 self.scroll_sync_manager.set_anchors(group_id, sync_anchors);
6306 tracing::debug!(
6307 "Set {} anchors for scroll sync group {}",
6308 anchor_count,
6309 group_id
6310 );
6311 }
6312 PluginCommand::RemoveScrollSyncGroup { group_id } => {
6313 if self.scroll_sync_manager.remove_group(group_id) {
6314 tracing::debug!("Removed scroll sync group {}", group_id);
6315 } else {
6316 tracing::warn!("Scroll sync group {} not found", group_id);
6317 }
6318 }
6319
6320 PluginCommand::CreateCompositeBuffer {
6322 name,
6323 mode,
6324 layout,
6325 sources,
6326 hunks,
6327 request_id,
6328 } => {
6329 self.handle_create_composite_buffer(name, mode, layout, sources, hunks, request_id);
6330 }
6331 PluginCommand::UpdateCompositeAlignment { buffer_id, hunks } => {
6332 self.handle_update_composite_alignment(buffer_id, hunks);
6333 }
6334 PluginCommand::CloseCompositeBuffer { buffer_id } => {
6335 self.close_composite_buffer(buffer_id);
6336 }
6337
6338 PluginCommand::SaveBufferToPath { buffer_id, path } => {
6340 self.handle_save_buffer_to_path(buffer_id, path);
6341 }
6342
6343 #[cfg(feature = "plugins")]
6345 PluginCommand::LoadPlugin { path, callback_id } => {
6346 self.handle_load_plugin(path, callback_id);
6347 }
6348 #[cfg(feature = "plugins")]
6349 PluginCommand::UnloadPlugin { name, callback_id } => {
6350 self.handle_unload_plugin(name, callback_id);
6351 }
6352 #[cfg(feature = "plugins")]
6353 PluginCommand::ReloadPlugin { name, callback_id } => {
6354 self.handle_reload_plugin(name, callback_id);
6355 }
6356 #[cfg(feature = "plugins")]
6357 PluginCommand::ListPlugins { callback_id } => {
6358 self.handle_list_plugins(callback_id);
6359 }
6360 #[cfg(not(feature = "plugins"))]
6362 PluginCommand::LoadPlugin { .. }
6363 | PluginCommand::UnloadPlugin { .. }
6364 | PluginCommand::ReloadPlugin { .. }
6365 | PluginCommand::ListPlugins { .. } => {
6366 tracing::warn!("Plugin management commands require the 'plugins' feature");
6367 }
6368
6369 PluginCommand::CreateTerminal {
6371 cwd,
6372 direction,
6373 ratio,
6374 focus,
6375 request_id,
6376 } => {
6377 let (cols, rows) = self.get_terminal_dimensions();
6378
6379 if let Some(ref bridge) = self.async_bridge {
6381 self.terminal_manager.set_async_bridge(bridge.clone());
6382 }
6383
6384 let working_dir = cwd
6386 .map(std::path::PathBuf::from)
6387 .unwrap_or_else(|| self.working_dir.clone());
6388
6389 let terminal_root = self.dir_context.terminal_dir_for(&working_dir);
6391 if let Err(e) = self.filesystem.create_dir_all(&terminal_root) {
6392 tracing::warn!("Failed to create terminal directory: {}", e);
6393 }
6394 let predicted_terminal_id = self.terminal_manager.next_terminal_id();
6395 let log_path =
6396 terminal_root.join(format!("fresh-terminal-{}.log", predicted_terminal_id.0));
6397 let backing_path =
6398 terminal_root.join(format!("fresh-terminal-{}.txt", predicted_terminal_id.0));
6399 self.terminal_backing_files
6400 .insert(predicted_terminal_id, backing_path);
6401 let backing_path_for_spawn = self
6402 .terminal_backing_files
6403 .get(&predicted_terminal_id)
6404 .cloned();
6405
6406 match self.terminal_manager.spawn(
6407 cols,
6408 rows,
6409 Some(working_dir),
6410 Some(log_path.clone()),
6411 backing_path_for_spawn,
6412 ) {
6413 Ok(terminal_id) => {
6414 self.terminal_log_files
6416 .insert(terminal_id, log_path.clone());
6417 if terminal_id != predicted_terminal_id {
6419 self.terminal_backing_files.remove(&predicted_terminal_id);
6420 let backing_path =
6421 terminal_root.join(format!("fresh-terminal-{}.txt", terminal_id.0));
6422 self.terminal_backing_files
6423 .insert(terminal_id, backing_path);
6424 }
6425
6426 let active_split = self.split_manager.active_split();
6428 let buffer_id =
6429 self.create_terminal_buffer_attached(terminal_id, active_split);
6430
6431 let created_split_id = if let Some(dir_str) = direction.as_deref() {
6435 let split_dir = match dir_str {
6436 "horizontal" => crate::model::event::SplitDirection::Horizontal,
6437 _ => crate::model::event::SplitDirection::Vertical,
6438 };
6439
6440 let split_ratio = ratio.unwrap_or(0.5);
6441 match self
6442 .split_manager
6443 .split_active(split_dir, buffer_id, split_ratio)
6444 {
6445 Ok(new_split_id) => {
6446 let mut view_state = SplitViewState::with_buffer(
6447 self.terminal_width,
6448 self.terminal_height,
6449 buffer_id,
6450 );
6451 view_state.apply_config_defaults(
6452 self.config.editor.line_numbers,
6453 false,
6454 false,
6455 self.config.editor.rulers.clone(),
6456 );
6457 self.split_view_states.insert(new_split_id, view_state);
6458
6459 if focus.unwrap_or(true) {
6460 self.split_manager.set_active_split(new_split_id);
6461 }
6462
6463 tracing::info!(
6464 "Created {:?} split for terminal {:?} with buffer {:?}",
6465 split_dir,
6466 terminal_id,
6467 buffer_id
6468 );
6469 Some(new_split_id)
6470 }
6471 Err(e) => {
6472 tracing::error!("Failed to create split for terminal: {}", e);
6473 self.set_active_buffer(buffer_id);
6474 None
6475 }
6476 }
6477 } else {
6478 self.set_active_buffer(buffer_id);
6480 None
6481 };
6482
6483 self.resize_visible_terminals();
6485
6486 let result = fresh_core::api::TerminalResult {
6488 buffer_id: buffer_id.0 as u64,
6489 terminal_id: terminal_id.0 as u64,
6490 split_id: created_split_id.map(|s| s.0 .0 as u64),
6491 };
6492 self.plugin_manager.resolve_callback(
6493 fresh_core::api::JsCallbackId::from(request_id),
6494 serde_json::to_string(&result).unwrap_or_default(),
6495 );
6496
6497 tracing::info!(
6498 "Plugin created terminal {:?} with buffer {:?}",
6499 terminal_id,
6500 buffer_id
6501 );
6502 }
6503 Err(e) => {
6504 tracing::error!("Failed to create terminal for plugin: {}", e);
6505 self.plugin_manager.reject_callback(
6506 fresh_core::api::JsCallbackId::from(request_id),
6507 format!("Failed to create terminal: {}", e),
6508 );
6509 }
6510 }
6511 }
6512
6513 PluginCommand::SendTerminalInput { terminal_id, data } => {
6514 if let Some(handle) = self.terminal_manager.get(terminal_id) {
6515 handle.write(data.as_bytes());
6516 tracing::trace!(
6517 "Plugin sent {} bytes to terminal {:?}",
6518 data.len(),
6519 terminal_id
6520 );
6521 } else {
6522 tracing::warn!(
6523 "Plugin tried to send input to non-existent terminal {:?}",
6524 terminal_id
6525 );
6526 }
6527 }
6528
6529 PluginCommand::CloseTerminal { terminal_id } => {
6530 let buffer_to_close = self
6532 .terminal_buffers
6533 .iter()
6534 .find(|(_, &tid)| tid == terminal_id)
6535 .map(|(&bid, _)| bid);
6536
6537 if let Some(buffer_id) = buffer_to_close {
6538 if let Err(e) = self.close_buffer(buffer_id) {
6539 tracing::warn!("Failed to close terminal buffer: {}", e);
6540 }
6541 tracing::info!("Plugin closed terminal {:?}", terminal_id);
6542 } else {
6543 self.terminal_manager.close(terminal_id);
6545 tracing::info!("Plugin closed terminal {:?} (no buffer found)", terminal_id);
6546 }
6547 }
6548 }
6549 Ok(())
6550 }
6551
6552 fn handle_save_buffer_to_path(&mut self, buffer_id: BufferId, path: std::path::PathBuf) {
6554 if let Some(state) = self.buffers.get_mut(&buffer_id) {
6555 match state.buffer.save_to_file(&path) {
6557 Ok(()) => {
6558 if let Err(e) = self.finalize_save(Some(path)) {
6561 tracing::warn!("Failed to finalize save: {}", e);
6562 }
6563 tracing::debug!("Saved buffer {:?} to path", buffer_id);
6564 }
6565 Err(e) => {
6566 self.handle_set_status(format!("Error saving: {}", e));
6567 tracing::error!("Failed to save buffer to path: {}", e);
6568 }
6569 }
6570 } else {
6571 self.handle_set_status(format!("Buffer {:?} not found", buffer_id));
6572 tracing::warn!("SaveBufferToPath: buffer {:?} not found", buffer_id);
6573 }
6574 }
6575
6576 #[cfg(feature = "plugins")]
6578 fn handle_load_plugin(&mut self, path: std::path::PathBuf, callback_id: JsCallbackId) {
6579 match self.plugin_manager.load_plugin(&path) {
6580 Ok(()) => {
6581 tracing::info!("Loaded plugin from {:?}", path);
6582 self.plugin_manager
6583 .resolve_callback(callback_id, "true".to_string());
6584 }
6585 Err(e) => {
6586 tracing::error!("Failed to load plugin from {:?}: {}", path, e);
6587 self.plugin_manager
6588 .reject_callback(callback_id, format!("{}", e));
6589 }
6590 }
6591 }
6592
6593 #[cfg(feature = "plugins")]
6595 fn handle_unload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
6596 match self.plugin_manager.unload_plugin(&name) {
6597 Ok(()) => {
6598 tracing::info!("Unloaded plugin: {}", name);
6599 self.plugin_manager
6600 .resolve_callback(callback_id, "true".to_string());
6601 }
6602 Err(e) => {
6603 tracing::error!("Failed to unload plugin '{}': {}", name, e);
6604 self.plugin_manager
6605 .reject_callback(callback_id, format!("{}", e));
6606 }
6607 }
6608 }
6609
6610 #[cfg(feature = "plugins")]
6612 fn handle_reload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
6613 match self.plugin_manager.reload_plugin(&name) {
6614 Ok(()) => {
6615 tracing::info!("Reloaded plugin: {}", name);
6616 self.plugin_manager
6617 .resolve_callback(callback_id, "true".to_string());
6618 }
6619 Err(e) => {
6620 tracing::error!("Failed to reload plugin '{}': {}", name, e);
6621 self.plugin_manager
6622 .reject_callback(callback_id, format!("{}", e));
6623 }
6624 }
6625 }
6626
6627 #[cfg(feature = "plugins")]
6629 fn handle_list_plugins(&mut self, callback_id: JsCallbackId) {
6630 let plugins = self.plugin_manager.list_plugins();
6631 let json_array: Vec<serde_json::Value> = plugins
6633 .iter()
6634 .map(|p| {
6635 serde_json::json!({
6636 "name": p.name,
6637 "path": p.path.to_string_lossy(),
6638 "enabled": p.enabled
6639 })
6640 })
6641 .collect();
6642 let json_str = serde_json::to_string(&json_array).unwrap_or_else(|_| "[]".to_string());
6643 self.plugin_manager.resolve_callback(callback_id, json_str);
6644 }
6645
6646 fn handle_execute_action(&mut self, action_name: String) {
6648 use crate::input::keybindings::Action;
6649 use std::collections::HashMap;
6650
6651 if let Some(action) = Action::from_str(&action_name, &HashMap::new()) {
6653 if let Err(e) = self.handle_action(action) {
6655 tracing::warn!("Failed to execute action '{}': {}", action_name, e);
6656 } else {
6657 tracing::debug!("Executed action: {}", action_name);
6658 }
6659 } else {
6660 tracing::warn!("Unknown action: {}", action_name);
6661 }
6662 }
6663
6664 fn handle_execute_actions(&mut self, actions: Vec<fresh_core::api::ActionSpec>) {
6667 use crate::input::keybindings::Action;
6668 use std::collections::HashMap;
6669
6670 for action_spec in actions {
6671 if let Some(action) = Action::from_str(&action_spec.action, &HashMap::new()) {
6672 for _ in 0..action_spec.count {
6674 if let Err(e) = self.handle_action(action.clone()) {
6675 tracing::warn!("Failed to execute action '{}': {}", action_spec.action, e);
6676 return; }
6678 }
6679 tracing::debug!(
6680 "Executed action '{}' {} time(s)",
6681 action_spec.action,
6682 action_spec.count
6683 );
6684 } else {
6685 tracing::warn!("Unknown action: {}", action_spec.action);
6686 return; }
6688 }
6689 }
6690
6691 fn handle_get_buffer_text(
6693 &mut self,
6694 buffer_id: BufferId,
6695 start: usize,
6696 end: usize,
6697 request_id: u64,
6698 ) {
6699 let result = if let Some(state) = self.buffers.get_mut(&buffer_id) {
6700 let len = state.buffer.len();
6702 if start <= end && end <= len {
6703 Ok(state.get_text_range(start, end))
6704 } else {
6705 Err(format!(
6706 "Invalid range {}..{} for buffer of length {}",
6707 start, end, len
6708 ))
6709 }
6710 } else {
6711 Err(format!("Buffer {:?} not found", buffer_id))
6712 };
6713
6714 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
6716 match result {
6717 Ok(text) => {
6718 let json = serde_json::to_string(&text).unwrap_or_else(|_| "null".to_string());
6720 self.plugin_manager.resolve_callback(callback_id, json);
6721 }
6722 Err(error) => {
6723 self.plugin_manager.reject_callback(callback_id, error);
6724 }
6725 }
6726 }
6727
6728 fn handle_set_editor_mode(&mut self, mode: Option<String>) {
6730 self.editor_mode = mode.clone();
6731 tracing::debug!("Set editor mode: {:?}", mode);
6732 }
6733
6734 fn handle_get_line_start_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
6736 let actual_buffer_id = if buffer_id.0 == 0 {
6738 self.active_buffer_id()
6739 } else {
6740 buffer_id
6741 };
6742
6743 let result = if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
6744 let line_number = line as usize;
6746 let buffer_len = state.buffer.len();
6747
6748 if line_number == 0 {
6749 Some(0)
6751 } else {
6752 let mut current_line = 0;
6754 let mut line_start = None;
6755
6756 let content = state.get_text_range(0, buffer_len);
6758 for (byte_idx, c) in content.char_indices() {
6759 if c == '\n' {
6760 current_line += 1;
6761 if current_line == line_number {
6762 line_start = Some(byte_idx + 1);
6764 break;
6765 }
6766 }
6767 }
6768 line_start
6769 }
6770 } else {
6771 None
6772 };
6773
6774 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
6776 let json = serde_json::to_string(&result).unwrap_or_else(|_| "null".to_string());
6778 self.plugin_manager.resolve_callback(callback_id, json);
6779 }
6780
6781 fn handle_get_line_end_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
6784 let actual_buffer_id = if buffer_id.0 == 0 {
6786 self.active_buffer_id()
6787 } else {
6788 buffer_id
6789 };
6790
6791 let result = if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
6792 let line_number = line as usize;
6793 let buffer_len = state.buffer.len();
6794
6795 let content = state.get_text_range(0, buffer_len);
6797 let mut current_line = 0;
6798 let mut line_end = None;
6799
6800 for (byte_idx, c) in content.char_indices() {
6801 if c == '\n' {
6802 if current_line == line_number {
6803 line_end = Some(byte_idx);
6805 break;
6806 }
6807 current_line += 1;
6808 }
6809 }
6810
6811 if line_end.is_none() && current_line == line_number {
6813 line_end = Some(buffer_len);
6814 }
6815
6816 line_end
6817 } else {
6818 None
6819 };
6820
6821 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
6822 let json = serde_json::to_string(&result).unwrap_or_else(|_| "null".to_string());
6823 self.plugin_manager.resolve_callback(callback_id, json);
6824 }
6825
6826 fn handle_get_buffer_line_count(&mut self, buffer_id: BufferId, request_id: u64) {
6828 let actual_buffer_id = if buffer_id.0 == 0 {
6830 self.active_buffer_id()
6831 } else {
6832 buffer_id
6833 };
6834
6835 let result = if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
6836 let buffer_len = state.buffer.len();
6837 let content = state.get_text_range(0, buffer_len);
6838
6839 if content.is_empty() {
6841 Some(1) } else {
6843 let newline_count = content.chars().filter(|&c| c == '\n').count();
6844 let ends_with_newline = content.ends_with('\n');
6846 if ends_with_newline {
6847 Some(newline_count)
6848 } else {
6849 Some(newline_count + 1)
6850 }
6851 }
6852 } else {
6853 None
6854 };
6855
6856 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
6857 let json = serde_json::to_string(&result).unwrap_or_else(|_| "null".to_string());
6858 self.plugin_manager.resolve_callback(callback_id, json);
6859 }
6860
6861 fn handle_scroll_to_line_center(
6863 &mut self,
6864 split_id: SplitId,
6865 buffer_id: BufferId,
6866 line: usize,
6867 ) {
6868 let actual_split_id = if split_id.0 == 0 {
6870 self.split_manager.active_split()
6871 } else {
6872 LeafId(split_id)
6873 };
6874
6875 let actual_buffer_id = if buffer_id.0 == 0 {
6877 self.active_buffer()
6878 } else {
6879 buffer_id
6880 };
6881
6882 let viewport_height = if let Some(view_state) = self.split_view_states.get(&actual_split_id)
6884 {
6885 view_state.viewport.height as usize
6886 } else {
6887 return;
6888 };
6889
6890 let lines_above = viewport_height / 2;
6892 let target_line = line.saturating_sub(lines_above);
6893
6894 if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
6896 let buffer = &mut state.buffer;
6897 if let Some(view_state) = self.split_view_states.get_mut(&actual_split_id) {
6898 view_state.viewport.scroll_to(buffer, target_line);
6899 view_state.viewport.set_skip_ensure_visible();
6901 }
6902 }
6903 }
6904}
6905
6906fn parse_key_string(key_str: &str) -> Option<(KeyCode, KeyModifiers)> {
6915 use crossterm::event::{KeyCode, KeyModifiers};
6916
6917 let mut modifiers = KeyModifiers::NONE;
6918 let mut remaining = key_str;
6919
6920 loop {
6922 if remaining.starts_with("C-") {
6923 modifiers |= KeyModifiers::CONTROL;
6924 remaining = &remaining[2..];
6925 } else if remaining.starts_with("M-") {
6926 modifiers |= KeyModifiers::ALT;
6927 remaining = &remaining[2..];
6928 } else if remaining.starts_with("S-") {
6929 modifiers |= KeyModifiers::SHIFT;
6930 remaining = &remaining[2..];
6931 } else {
6932 break;
6933 }
6934 }
6935
6936 let upper = remaining.to_uppercase();
6939 let code = match upper.as_str() {
6940 "RET" | "RETURN" | "ENTER" => KeyCode::Enter,
6941 "TAB" => KeyCode::Tab,
6942 "BACKTAB" => KeyCode::BackTab,
6943 "ESC" | "ESCAPE" => KeyCode::Esc,
6944 "SPC" | "SPACE" => KeyCode::Char(' '),
6945 "DEL" | "DELETE" => KeyCode::Delete,
6946 "BS" | "BACKSPACE" => KeyCode::Backspace,
6947 "UP" => KeyCode::Up,
6948 "DOWN" => KeyCode::Down,
6949 "LEFT" => KeyCode::Left,
6950 "RIGHT" => KeyCode::Right,
6951 "HOME" => KeyCode::Home,
6952 "END" => KeyCode::End,
6953 "PAGEUP" | "PGUP" => KeyCode::PageUp,
6954 "PAGEDOWN" | "PGDN" => KeyCode::PageDown,
6955 s if s.starts_with('F') && s.len() > 1 => {
6956 if let Ok(n) = s[1..].parse::<u8>() {
6958 KeyCode::F(n)
6959 } else {
6960 return None;
6961 }
6962 }
6963 _ if remaining.len() == 1 => {
6964 let c = remaining.chars().next()?;
6967 if c.is_ascii_uppercase() {
6968 modifiers |= KeyModifiers::SHIFT;
6969 }
6970 KeyCode::Char(c.to_ascii_lowercase())
6971 }
6972 _ => return None,
6973 };
6974
6975 Some((code, modifiers))
6976}
6977
6978#[cfg(test)]
6979mod tests {
6980 use super::*;
6981 use tempfile::TempDir;
6982
6983 fn test_dir_context() -> (DirectoryContext, TempDir) {
6985 let temp_dir = TempDir::new().unwrap();
6986 let dir_context = DirectoryContext::for_testing(temp_dir.path());
6987 (dir_context, temp_dir)
6988 }
6989
6990 fn test_filesystem() -> Arc<dyn FileSystem + Send + Sync> {
6992 Arc::new(crate::model::filesystem::StdFileSystem)
6993 }
6994
6995 #[test]
6996 fn test_editor_new() {
6997 let config = Config::default();
6998 let (dir_context, _temp) = test_dir_context();
6999 let editor = Editor::new(
7000 config,
7001 80,
7002 24,
7003 dir_context,
7004 crate::view::color_support::ColorCapability::TrueColor,
7005 test_filesystem(),
7006 )
7007 .unwrap();
7008
7009 assert_eq!(editor.buffers.len(), 1);
7010 assert!(!editor.should_quit());
7011 }
7012
7013 #[test]
7014 fn test_new_buffer() {
7015 let config = Config::default();
7016 let (dir_context, _temp) = test_dir_context();
7017 let mut editor = Editor::new(
7018 config,
7019 80,
7020 24,
7021 dir_context,
7022 crate::view::color_support::ColorCapability::TrueColor,
7023 test_filesystem(),
7024 )
7025 .unwrap();
7026
7027 let id = editor.new_buffer();
7028 assert_eq!(editor.buffers.len(), 2);
7029 assert_eq!(editor.active_buffer(), id);
7030 }
7031
7032 #[test]
7033 #[ignore]
7034 fn test_clipboard() {
7035 let config = Config::default();
7036 let (dir_context, _temp) = test_dir_context();
7037 let mut editor = Editor::new(
7038 config,
7039 80,
7040 24,
7041 dir_context,
7042 crate::view::color_support::ColorCapability::TrueColor,
7043 test_filesystem(),
7044 )
7045 .unwrap();
7046
7047 editor.clipboard.set_internal("test".to_string());
7049
7050 editor.paste();
7052
7053 let content = editor.active_state().buffer.to_string().unwrap();
7054 assert_eq!(content, "test");
7055 }
7056
7057 #[test]
7058 fn test_action_to_events_insert_char() {
7059 let config = Config::default();
7060 let (dir_context, _temp) = test_dir_context();
7061 let mut editor = Editor::new(
7062 config,
7063 80,
7064 24,
7065 dir_context,
7066 crate::view::color_support::ColorCapability::TrueColor,
7067 test_filesystem(),
7068 )
7069 .unwrap();
7070
7071 let events = editor.action_to_events(Action::InsertChar('a'));
7072 assert!(events.is_some());
7073
7074 let events = events.unwrap();
7075 assert_eq!(events.len(), 1);
7076
7077 match &events[0] {
7078 Event::Insert { position, text, .. } => {
7079 assert_eq!(*position, 0);
7080 assert_eq!(text, "a");
7081 }
7082 _ => panic!("Expected Insert event"),
7083 }
7084 }
7085
7086 #[test]
7087 fn test_action_to_events_move_right() {
7088 let config = Config::default();
7089 let (dir_context, _temp) = test_dir_context();
7090 let mut editor = Editor::new(
7091 config,
7092 80,
7093 24,
7094 dir_context,
7095 crate::view::color_support::ColorCapability::TrueColor,
7096 test_filesystem(),
7097 )
7098 .unwrap();
7099
7100 let cursor_id = editor.active_cursors().primary_id();
7102 editor.apply_event_to_active_buffer(&Event::Insert {
7103 position: 0,
7104 text: "hello".to_string(),
7105 cursor_id,
7106 });
7107
7108 let events = editor.action_to_events(Action::MoveRight);
7109 assert!(events.is_some());
7110
7111 let events = events.unwrap();
7112 assert_eq!(events.len(), 1);
7113
7114 match &events[0] {
7115 Event::MoveCursor {
7116 new_position,
7117 new_anchor,
7118 ..
7119 } => {
7120 assert_eq!(*new_position, 5);
7122 assert_eq!(*new_anchor, None); }
7124 _ => panic!("Expected MoveCursor event"),
7125 }
7126 }
7127
7128 #[test]
7129 fn test_action_to_events_move_up_down() {
7130 let config = Config::default();
7131 let (dir_context, _temp) = test_dir_context();
7132 let mut editor = Editor::new(
7133 config,
7134 80,
7135 24,
7136 dir_context,
7137 crate::view::color_support::ColorCapability::TrueColor,
7138 test_filesystem(),
7139 )
7140 .unwrap();
7141
7142 let cursor_id = editor.active_cursors().primary_id();
7144 editor.apply_event_to_active_buffer(&Event::Insert {
7145 position: 0,
7146 text: "line1\nline2\nline3".to_string(),
7147 cursor_id,
7148 });
7149
7150 editor.apply_event_to_active_buffer(&Event::MoveCursor {
7152 cursor_id,
7153 old_position: 0, new_position: 6,
7155 old_anchor: None, new_anchor: None,
7157 old_sticky_column: 0,
7158 new_sticky_column: 0,
7159 });
7160
7161 let events = editor.action_to_events(Action::MoveUp);
7163 assert!(events.is_some());
7164 let events = events.unwrap();
7165 assert_eq!(events.len(), 1);
7166
7167 match &events[0] {
7168 Event::MoveCursor { new_position, .. } => {
7169 assert_eq!(*new_position, 0); }
7171 _ => panic!("Expected MoveCursor event"),
7172 }
7173 }
7174
7175 #[test]
7176 fn test_action_to_events_insert_newline() {
7177 let config = Config::default();
7178 let (dir_context, _temp) = test_dir_context();
7179 let mut editor = Editor::new(
7180 config,
7181 80,
7182 24,
7183 dir_context,
7184 crate::view::color_support::ColorCapability::TrueColor,
7185 test_filesystem(),
7186 )
7187 .unwrap();
7188
7189 let events = editor.action_to_events(Action::InsertNewline);
7190 assert!(events.is_some());
7191
7192 let events = events.unwrap();
7193 assert_eq!(events.len(), 1);
7194
7195 match &events[0] {
7196 Event::Insert { text, .. } => {
7197 assert_eq!(text, "\n");
7198 }
7199 _ => panic!("Expected Insert event"),
7200 }
7201 }
7202
7203 #[test]
7204 fn test_action_to_events_unimplemented() {
7205 let config = Config::default();
7206 let (dir_context, _temp) = test_dir_context();
7207 let mut editor = Editor::new(
7208 config,
7209 80,
7210 24,
7211 dir_context,
7212 crate::view::color_support::ColorCapability::TrueColor,
7213 test_filesystem(),
7214 )
7215 .unwrap();
7216
7217 assert!(editor.action_to_events(Action::Save).is_none());
7219 assert!(editor.action_to_events(Action::Quit).is_none());
7220 assert!(editor.action_to_events(Action::Undo).is_none());
7221 }
7222
7223 #[test]
7224 fn test_action_to_events_delete_backward() {
7225 let config = Config::default();
7226 let (dir_context, _temp) = test_dir_context();
7227 let mut editor = Editor::new(
7228 config,
7229 80,
7230 24,
7231 dir_context,
7232 crate::view::color_support::ColorCapability::TrueColor,
7233 test_filesystem(),
7234 )
7235 .unwrap();
7236
7237 let cursor_id = editor.active_cursors().primary_id();
7239 editor.apply_event_to_active_buffer(&Event::Insert {
7240 position: 0,
7241 text: "hello".to_string(),
7242 cursor_id,
7243 });
7244
7245 let events = editor.action_to_events(Action::DeleteBackward);
7246 assert!(events.is_some());
7247
7248 let events = events.unwrap();
7249 assert_eq!(events.len(), 1);
7250
7251 match &events[0] {
7252 Event::Delete {
7253 range,
7254 deleted_text,
7255 ..
7256 } => {
7257 assert_eq!(range.clone(), 4..5); assert_eq!(deleted_text, "o");
7259 }
7260 _ => panic!("Expected Delete event"),
7261 }
7262 }
7263
7264 #[test]
7265 fn test_action_to_events_delete_forward() {
7266 let config = Config::default();
7267 let (dir_context, _temp) = test_dir_context();
7268 let mut editor = Editor::new(
7269 config,
7270 80,
7271 24,
7272 dir_context,
7273 crate::view::color_support::ColorCapability::TrueColor,
7274 test_filesystem(),
7275 )
7276 .unwrap();
7277
7278 let cursor_id = editor.active_cursors().primary_id();
7280 editor.apply_event_to_active_buffer(&Event::Insert {
7281 position: 0,
7282 text: "hello".to_string(),
7283 cursor_id,
7284 });
7285
7286 editor.apply_event_to_active_buffer(&Event::MoveCursor {
7288 cursor_id,
7289 old_position: 0, new_position: 0,
7291 old_anchor: None, new_anchor: None,
7293 old_sticky_column: 0,
7294 new_sticky_column: 0,
7295 });
7296
7297 let events = editor.action_to_events(Action::DeleteForward);
7298 assert!(events.is_some());
7299
7300 let events = events.unwrap();
7301 assert_eq!(events.len(), 1);
7302
7303 match &events[0] {
7304 Event::Delete {
7305 range,
7306 deleted_text,
7307 ..
7308 } => {
7309 assert_eq!(range.clone(), 0..1); assert_eq!(deleted_text, "h");
7311 }
7312 _ => panic!("Expected Delete event"),
7313 }
7314 }
7315
7316 #[test]
7317 fn test_action_to_events_select_right() {
7318 let config = Config::default();
7319 let (dir_context, _temp) = test_dir_context();
7320 let mut editor = Editor::new(
7321 config,
7322 80,
7323 24,
7324 dir_context,
7325 crate::view::color_support::ColorCapability::TrueColor,
7326 test_filesystem(),
7327 )
7328 .unwrap();
7329
7330 let cursor_id = editor.active_cursors().primary_id();
7332 editor.apply_event_to_active_buffer(&Event::Insert {
7333 position: 0,
7334 text: "hello".to_string(),
7335 cursor_id,
7336 });
7337
7338 editor.apply_event_to_active_buffer(&Event::MoveCursor {
7340 cursor_id,
7341 old_position: 0, new_position: 0,
7343 old_anchor: None, new_anchor: None,
7345 old_sticky_column: 0,
7346 new_sticky_column: 0,
7347 });
7348
7349 let events = editor.action_to_events(Action::SelectRight);
7350 assert!(events.is_some());
7351
7352 let events = events.unwrap();
7353 assert_eq!(events.len(), 1);
7354
7355 match &events[0] {
7356 Event::MoveCursor {
7357 new_position,
7358 new_anchor,
7359 ..
7360 } => {
7361 assert_eq!(*new_position, 1); assert_eq!(*new_anchor, Some(0)); }
7364 _ => panic!("Expected MoveCursor event"),
7365 }
7366 }
7367
7368 #[test]
7369 fn test_action_to_events_select_all() {
7370 let config = Config::default();
7371 let (dir_context, _temp) = test_dir_context();
7372 let mut editor = Editor::new(
7373 config,
7374 80,
7375 24,
7376 dir_context,
7377 crate::view::color_support::ColorCapability::TrueColor,
7378 test_filesystem(),
7379 )
7380 .unwrap();
7381
7382 let cursor_id = editor.active_cursors().primary_id();
7384 editor.apply_event_to_active_buffer(&Event::Insert {
7385 position: 0,
7386 text: "hello world".to_string(),
7387 cursor_id,
7388 });
7389
7390 let events = editor.action_to_events(Action::SelectAll);
7391 assert!(events.is_some());
7392
7393 let events = events.unwrap();
7394 assert_eq!(events.len(), 1);
7395
7396 match &events[0] {
7397 Event::MoveCursor {
7398 new_position,
7399 new_anchor,
7400 ..
7401 } => {
7402 assert_eq!(*new_position, 11); assert_eq!(*new_anchor, Some(0)); }
7405 _ => panic!("Expected MoveCursor event"),
7406 }
7407 }
7408
7409 #[test]
7410 fn test_action_to_events_document_nav() {
7411 let config = Config::default();
7412 let (dir_context, _temp) = test_dir_context();
7413 let mut editor = Editor::new(
7414 config,
7415 80,
7416 24,
7417 dir_context,
7418 crate::view::color_support::ColorCapability::TrueColor,
7419 test_filesystem(),
7420 )
7421 .unwrap();
7422
7423 let cursor_id = editor.active_cursors().primary_id();
7425 editor.apply_event_to_active_buffer(&Event::Insert {
7426 position: 0,
7427 text: "line1\nline2\nline3".to_string(),
7428 cursor_id,
7429 });
7430
7431 let events = editor.action_to_events(Action::MoveDocumentStart);
7433 assert!(events.is_some());
7434 let events = events.unwrap();
7435 match &events[0] {
7436 Event::MoveCursor { new_position, .. } => {
7437 assert_eq!(*new_position, 0);
7438 }
7439 _ => panic!("Expected MoveCursor event"),
7440 }
7441
7442 let events = editor.action_to_events(Action::MoveDocumentEnd);
7444 assert!(events.is_some());
7445 let events = events.unwrap();
7446 match &events[0] {
7447 Event::MoveCursor { new_position, .. } => {
7448 assert_eq!(*new_position, 17); }
7450 _ => panic!("Expected MoveCursor event"),
7451 }
7452 }
7453
7454 #[test]
7455 fn test_action_to_events_remove_secondary_cursors() {
7456 use crate::model::event::CursorId;
7457
7458 let config = Config::default();
7459 let (dir_context, _temp) = test_dir_context();
7460 let mut editor = Editor::new(
7461 config,
7462 80,
7463 24,
7464 dir_context,
7465 crate::view::color_support::ColorCapability::TrueColor,
7466 test_filesystem(),
7467 )
7468 .unwrap();
7469
7470 let cursor_id = editor.active_cursors().primary_id();
7472 editor.apply_event_to_active_buffer(&Event::Insert {
7473 position: 0,
7474 text: "hello world test".to_string(),
7475 cursor_id,
7476 });
7477
7478 editor.apply_event_to_active_buffer(&Event::AddCursor {
7480 cursor_id: CursorId(1),
7481 position: 5,
7482 anchor: None,
7483 });
7484 editor.apply_event_to_active_buffer(&Event::AddCursor {
7485 cursor_id: CursorId(2),
7486 position: 10,
7487 anchor: None,
7488 });
7489
7490 assert_eq!(editor.active_cursors().count(), 3);
7491
7492 let first_id = editor
7494 .active_cursors()
7495 .iter()
7496 .map(|(id, _)| id)
7497 .min_by_key(|id| id.0)
7498 .expect("Should have at least one cursor");
7499
7500 let events = editor.action_to_events(Action::RemoveSecondaryCursors);
7502 assert!(events.is_some());
7503
7504 let events = events.unwrap();
7505 let remove_cursor_events: Vec<_> = events
7508 .iter()
7509 .filter_map(|e| match e {
7510 Event::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
7511 _ => None,
7512 })
7513 .collect();
7514
7515 assert_eq!(remove_cursor_events.len(), 2);
7517
7518 for cursor_id in &remove_cursor_events {
7519 assert_ne!(*cursor_id, first_id);
7521 }
7522 }
7523
7524 #[test]
7525 fn test_action_to_events_scroll() {
7526 let config = Config::default();
7527 let (dir_context, _temp) = test_dir_context();
7528 let mut editor = Editor::new(
7529 config,
7530 80,
7531 24,
7532 dir_context,
7533 crate::view::color_support::ColorCapability::TrueColor,
7534 test_filesystem(),
7535 )
7536 .unwrap();
7537
7538 let events = editor.action_to_events(Action::ScrollUp);
7540 assert!(events.is_some());
7541 let events = events.unwrap();
7542 assert_eq!(events.len(), 1);
7543 match &events[0] {
7544 Event::Scroll { line_offset } => {
7545 assert_eq!(*line_offset, -1);
7546 }
7547 _ => panic!("Expected Scroll event"),
7548 }
7549
7550 let events = editor.action_to_events(Action::ScrollDown);
7552 assert!(events.is_some());
7553 let events = events.unwrap();
7554 assert_eq!(events.len(), 1);
7555 match &events[0] {
7556 Event::Scroll { line_offset } => {
7557 assert_eq!(*line_offset, 1);
7558 }
7559 _ => panic!("Expected Scroll event"),
7560 }
7561 }
7562
7563 #[test]
7564 fn test_action_to_events_none() {
7565 let config = Config::default();
7566 let (dir_context, _temp) = test_dir_context();
7567 let mut editor = Editor::new(
7568 config,
7569 80,
7570 24,
7571 dir_context,
7572 crate::view::color_support::ColorCapability::TrueColor,
7573 test_filesystem(),
7574 )
7575 .unwrap();
7576
7577 let events = editor.action_to_events(Action::None);
7579 assert!(events.is_none());
7580 }
7581
7582 #[test]
7583 fn test_lsp_incremental_insert_generates_correct_range() {
7584 use crate::model::buffer::Buffer;
7587
7588 let buffer = Buffer::from_str_test("hello\nworld");
7589
7590 let position = 0;
7593 let (line, character) = buffer.position_to_lsp_position(position);
7594
7595 assert_eq!(line, 0, "Insertion at start should be line 0");
7596 assert_eq!(character, 0, "Insertion at start should be char 0");
7597
7598 let lsp_pos = Position::new(line as u32, character as u32);
7600 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
7601
7602 assert_eq!(lsp_range.start.line, 0);
7603 assert_eq!(lsp_range.start.character, 0);
7604 assert_eq!(lsp_range.end.line, 0);
7605 assert_eq!(lsp_range.end.character, 0);
7606 assert_eq!(
7607 lsp_range.start, lsp_range.end,
7608 "Insert should have zero-width range"
7609 );
7610
7611 let position = 3;
7613 let (line, character) = buffer.position_to_lsp_position(position);
7614
7615 assert_eq!(line, 0);
7616 assert_eq!(character, 3);
7617
7618 let position = 6;
7620 let (line, character) = buffer.position_to_lsp_position(position);
7621
7622 assert_eq!(line, 1, "Position after newline should be line 1");
7623 assert_eq!(character, 0, "Position at start of line 2 should be char 0");
7624 }
7625
7626 #[test]
7627 fn test_lsp_incremental_delete_generates_correct_range() {
7628 use crate::model::buffer::Buffer;
7631
7632 let buffer = Buffer::from_str_test("hello\nworld");
7633
7634 let range_start = 1;
7636 let range_end = 5;
7637
7638 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
7639 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
7640
7641 assert_eq!(start_line, 0);
7642 assert_eq!(start_char, 1);
7643 assert_eq!(end_line, 0);
7644 assert_eq!(end_char, 5);
7645
7646 let lsp_range = LspRange::new(
7647 Position::new(start_line as u32, start_char as u32),
7648 Position::new(end_line as u32, end_char as u32),
7649 );
7650
7651 assert_eq!(lsp_range.start.line, 0);
7652 assert_eq!(lsp_range.start.character, 1);
7653 assert_eq!(lsp_range.end.line, 0);
7654 assert_eq!(lsp_range.end.character, 5);
7655 assert_ne!(
7656 lsp_range.start, lsp_range.end,
7657 "Delete should have non-zero range"
7658 );
7659
7660 let range_start = 4;
7662 let range_end = 8;
7663
7664 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
7665 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
7666
7667 assert_eq!(start_line, 0, "Delete start on line 0");
7668 assert_eq!(start_char, 4, "Delete start at char 4");
7669 assert_eq!(end_line, 1, "Delete end on line 1");
7670 assert_eq!(end_char, 2, "Delete end at char 2 of line 1");
7671 }
7672
7673 #[test]
7674 fn test_lsp_incremental_utf16_encoding() {
7675 use crate::model::buffer::Buffer;
7678
7679 let buffer = Buffer::from_str_test("😀hello");
7681
7682 let (line, character) = buffer.position_to_lsp_position(4);
7684
7685 assert_eq!(line, 0);
7686 assert_eq!(character, 2, "Emoji should count as 2 UTF-16 code units");
7687
7688 let (line, character) = buffer.position_to_lsp_position(9);
7690
7691 assert_eq!(line, 0);
7692 assert_eq!(
7693 character, 7,
7694 "Should be 2 (emoji) + 5 (text) = 7 UTF-16 code units"
7695 );
7696
7697 let buffer = Buffer::from_str_test("café");
7699
7700 let (line, character) = buffer.position_to_lsp_position(3);
7702
7703 assert_eq!(line, 0);
7704 assert_eq!(character, 3);
7705
7706 let (line, character) = buffer.position_to_lsp_position(5);
7708
7709 assert_eq!(line, 0);
7710 assert_eq!(character, 4, "é should count as 1 UTF-16 code unit");
7711 }
7712
7713 #[test]
7714 fn test_lsp_content_change_event_structure() {
7715 let insert_change = TextDocumentContentChangeEvent {
7719 range: Some(LspRange::new(Position::new(0, 5), Position::new(0, 5))),
7720 range_length: None,
7721 text: "NEW".to_string(),
7722 };
7723
7724 assert!(insert_change.range.is_some());
7725 assert_eq!(insert_change.text, "NEW");
7726 let range = insert_change.range.unwrap();
7727 assert_eq!(
7728 range.start, range.end,
7729 "Insert should have zero-width range"
7730 );
7731
7732 let delete_change = TextDocumentContentChangeEvent {
7734 range: Some(LspRange::new(Position::new(0, 2), Position::new(0, 7))),
7735 range_length: None,
7736 text: String::new(),
7737 };
7738
7739 assert!(delete_change.range.is_some());
7740 assert_eq!(delete_change.text, "");
7741 let range = delete_change.range.unwrap();
7742 assert_ne!(range.start, range.end, "Delete should have non-zero range");
7743 assert_eq!(range.start.line, 0);
7744 assert_eq!(range.start.character, 2);
7745 assert_eq!(range.end.line, 0);
7746 assert_eq!(range.end.character, 7);
7747 }
7748
7749 #[test]
7750 fn test_goto_matching_bracket_forward() {
7751 let config = Config::default();
7752 let (dir_context, _temp) = test_dir_context();
7753 let mut editor = Editor::new(
7754 config,
7755 80,
7756 24,
7757 dir_context,
7758 crate::view::color_support::ColorCapability::TrueColor,
7759 test_filesystem(),
7760 )
7761 .unwrap();
7762
7763 let cursor_id = editor.active_cursors().primary_id();
7765 editor.apply_event_to_active_buffer(&Event::Insert {
7766 position: 0,
7767 text: "fn main() { let x = (1 + 2); }".to_string(),
7768 cursor_id,
7769 });
7770
7771 editor.apply_event_to_active_buffer(&Event::MoveCursor {
7773 cursor_id,
7774 old_position: 31,
7775 new_position: 10,
7776 old_anchor: None,
7777 new_anchor: None,
7778 old_sticky_column: 0,
7779 new_sticky_column: 0,
7780 });
7781
7782 assert_eq!(editor.active_cursors().primary().position, 10);
7783
7784 editor.goto_matching_bracket();
7786
7787 assert_eq!(editor.active_cursors().primary().position, 29);
7792 }
7793
7794 #[test]
7795 fn test_goto_matching_bracket_backward() {
7796 let config = Config::default();
7797 let (dir_context, _temp) = test_dir_context();
7798 let mut editor = Editor::new(
7799 config,
7800 80,
7801 24,
7802 dir_context,
7803 crate::view::color_support::ColorCapability::TrueColor,
7804 test_filesystem(),
7805 )
7806 .unwrap();
7807
7808 let cursor_id = editor.active_cursors().primary_id();
7810 editor.apply_event_to_active_buffer(&Event::Insert {
7811 position: 0,
7812 text: "fn main() { let x = (1 + 2); }".to_string(),
7813 cursor_id,
7814 });
7815
7816 editor.apply_event_to_active_buffer(&Event::MoveCursor {
7818 cursor_id,
7819 old_position: 31,
7820 new_position: 26,
7821 old_anchor: None,
7822 new_anchor: None,
7823 old_sticky_column: 0,
7824 new_sticky_column: 0,
7825 });
7826
7827 editor.goto_matching_bracket();
7829
7830 assert_eq!(editor.active_cursors().primary().position, 20);
7832 }
7833
7834 #[test]
7835 fn test_goto_matching_bracket_nested() {
7836 let config = Config::default();
7837 let (dir_context, _temp) = test_dir_context();
7838 let mut editor = Editor::new(
7839 config,
7840 80,
7841 24,
7842 dir_context,
7843 crate::view::color_support::ColorCapability::TrueColor,
7844 test_filesystem(),
7845 )
7846 .unwrap();
7847
7848 let cursor_id = editor.active_cursors().primary_id();
7850 editor.apply_event_to_active_buffer(&Event::Insert {
7851 position: 0,
7852 text: "{a{b{c}d}e}".to_string(),
7853 cursor_id,
7854 });
7855
7856 editor.apply_event_to_active_buffer(&Event::MoveCursor {
7858 cursor_id,
7859 old_position: 11,
7860 new_position: 0,
7861 old_anchor: None,
7862 new_anchor: None,
7863 old_sticky_column: 0,
7864 new_sticky_column: 0,
7865 });
7866
7867 editor.goto_matching_bracket();
7869
7870 assert_eq!(editor.active_cursors().primary().position, 10);
7872 }
7873
7874 #[test]
7875 fn test_search_case_sensitive() {
7876 let config = Config::default();
7877 let (dir_context, _temp) = test_dir_context();
7878 let mut editor = Editor::new(
7879 config,
7880 80,
7881 24,
7882 dir_context,
7883 crate::view::color_support::ColorCapability::TrueColor,
7884 test_filesystem(),
7885 )
7886 .unwrap();
7887
7888 let cursor_id = editor.active_cursors().primary_id();
7890 editor.apply_event_to_active_buffer(&Event::Insert {
7891 position: 0,
7892 text: "Hello hello HELLO".to_string(),
7893 cursor_id,
7894 });
7895
7896 editor.search_case_sensitive = false;
7898 editor.perform_search("hello");
7899
7900 let search_state = editor.search_state.as_ref().unwrap();
7901 assert_eq!(
7902 search_state.matches.len(),
7903 3,
7904 "Should find all 3 matches case-insensitively"
7905 );
7906
7907 editor.search_case_sensitive = true;
7909 editor.perform_search("hello");
7910
7911 let search_state = editor.search_state.as_ref().unwrap();
7912 assert_eq!(
7913 search_state.matches.len(),
7914 1,
7915 "Should find only 1 exact match"
7916 );
7917 assert_eq!(
7918 search_state.matches[0], 6,
7919 "Should find 'hello' at position 6"
7920 );
7921 }
7922
7923 #[test]
7924 fn test_search_whole_word() {
7925 let config = Config::default();
7926 let (dir_context, _temp) = test_dir_context();
7927 let mut editor = Editor::new(
7928 config,
7929 80,
7930 24,
7931 dir_context,
7932 crate::view::color_support::ColorCapability::TrueColor,
7933 test_filesystem(),
7934 )
7935 .unwrap();
7936
7937 let cursor_id = editor.active_cursors().primary_id();
7939 editor.apply_event_to_active_buffer(&Event::Insert {
7940 position: 0,
7941 text: "test testing tested attest test".to_string(),
7942 cursor_id,
7943 });
7944
7945 editor.search_whole_word = false;
7947 editor.search_case_sensitive = true;
7948 editor.perform_search("test");
7949
7950 let search_state = editor.search_state.as_ref().unwrap();
7951 assert_eq!(
7952 search_state.matches.len(),
7953 5,
7954 "Should find 'test' in all occurrences"
7955 );
7956
7957 editor.search_whole_word = true;
7959 editor.perform_search("test");
7960
7961 let search_state = editor.search_state.as_ref().unwrap();
7962 assert_eq!(
7963 search_state.matches.len(),
7964 2,
7965 "Should find only whole word 'test'"
7966 );
7967 assert_eq!(search_state.matches[0], 0, "First match at position 0");
7968 assert_eq!(search_state.matches[1], 27, "Second match at position 27");
7969 }
7970
7971 #[test]
7972 fn test_search_scan_completes_when_capped() {
7973 let config = Config::default();
7979 let (dir_context, _temp) = test_dir_context();
7980 let mut editor = Editor::new(
7981 config,
7982 80,
7983 24,
7984 dir_context,
7985 crate::view::color_support::ColorCapability::TrueColor,
7986 test_filesystem(),
7987 )
7988 .unwrap();
7989
7990 let buffer_id = editor.active_buffer();
7993 let regex = regex::Regex::new("test").unwrap();
7994 let fake_chunks = vec![
7995 crate::model::buffer::LineScanChunk {
7996 leaf_index: 0,
7997 byte_len: 100,
7998 already_known: true,
7999 },
8000 crate::model::buffer::LineScanChunk {
8001 leaf_index: 1,
8002 byte_len: 100,
8003 already_known: true,
8004 },
8005 ];
8006
8007 editor.search_scan_state = Some(SearchScanState {
8008 buffer_id,
8009 leaves: Vec::new(),
8010 chunks: fake_chunks,
8011 next_chunk: 1, next_doc_offset: 100,
8013 total_bytes: 200,
8014 scanned_bytes: 100,
8015 regex,
8016 query: "test".to_string(),
8017 match_ranges: vec![(10, 4), (50, 4)],
8018 overlap_tail: Vec::new(),
8019 overlap_doc_offset: 0,
8020 search_range: None,
8021 capped: true, case_sensitive: false,
8023 whole_word: false,
8024 use_regex: false,
8025 });
8026
8027 let result = editor.process_search_scan();
8029 assert!(
8030 result,
8031 "process_search_scan should return true (needs render)"
8032 );
8033
8034 assert!(
8036 editor.search_scan_state.is_none(),
8037 "search_scan_state should be None after capped scan completes"
8038 );
8039
8040 let search_state = editor
8042 .search_state
8043 .as_ref()
8044 .expect("search_state should be set after scan finishes");
8045 assert_eq!(search_state.matches.len(), 2, "Should have 2 matches");
8046 assert_eq!(search_state.query, "test");
8047 assert!(
8048 search_state.capped,
8049 "search_state should be marked as capped"
8050 );
8051 }
8052
8053 #[test]
8054 fn test_bookmarks() {
8055 let config = Config::default();
8056 let (dir_context, _temp) = test_dir_context();
8057 let mut editor = Editor::new(
8058 config,
8059 80,
8060 24,
8061 dir_context,
8062 crate::view::color_support::ColorCapability::TrueColor,
8063 test_filesystem(),
8064 )
8065 .unwrap();
8066
8067 let cursor_id = editor.active_cursors().primary_id();
8069 editor.apply_event_to_active_buffer(&Event::Insert {
8070 position: 0,
8071 text: "Line 1\nLine 2\nLine 3".to_string(),
8072 cursor_id,
8073 });
8074
8075 editor.apply_event_to_active_buffer(&Event::MoveCursor {
8077 cursor_id,
8078 old_position: 21,
8079 new_position: 7,
8080 old_anchor: None,
8081 new_anchor: None,
8082 old_sticky_column: 0,
8083 new_sticky_column: 0,
8084 });
8085
8086 editor.set_bookmark('1');
8088 assert!(editor.bookmarks.contains_key(&'1'));
8089 assert_eq!(editor.bookmarks.get(&'1').unwrap().position, 7);
8090
8091 editor.apply_event_to_active_buffer(&Event::MoveCursor {
8093 cursor_id,
8094 old_position: 7,
8095 new_position: 14,
8096 old_anchor: None,
8097 new_anchor: None,
8098 old_sticky_column: 0,
8099 new_sticky_column: 0,
8100 });
8101
8102 editor.jump_to_bookmark('1');
8104 assert_eq!(editor.active_cursors().primary().position, 7);
8105
8106 editor.clear_bookmark('1');
8108 assert!(!editor.bookmarks.contains_key(&'1'));
8109 }
8110
8111 #[test]
8112 fn test_action_enum_new_variants() {
8113 use serde_json::json;
8115
8116 let args = HashMap::new();
8117 assert_eq!(
8118 Action::from_str("smart_home", &args),
8119 Some(Action::SmartHome)
8120 );
8121 assert_eq!(
8122 Action::from_str("dedent_selection", &args),
8123 Some(Action::DedentSelection)
8124 );
8125 assert_eq!(
8126 Action::from_str("toggle_comment", &args),
8127 Some(Action::ToggleComment)
8128 );
8129 assert_eq!(
8130 Action::from_str("goto_matching_bracket", &args),
8131 Some(Action::GoToMatchingBracket)
8132 );
8133 assert_eq!(
8134 Action::from_str("list_bookmarks", &args),
8135 Some(Action::ListBookmarks)
8136 );
8137 assert_eq!(
8138 Action::from_str("toggle_search_case_sensitive", &args),
8139 Some(Action::ToggleSearchCaseSensitive)
8140 );
8141 assert_eq!(
8142 Action::from_str("toggle_search_whole_word", &args),
8143 Some(Action::ToggleSearchWholeWord)
8144 );
8145
8146 let mut args_with_char = HashMap::new();
8148 args_with_char.insert("char".to_string(), json!("5"));
8149 assert_eq!(
8150 Action::from_str("set_bookmark", &args_with_char),
8151 Some(Action::SetBookmark('5'))
8152 );
8153 assert_eq!(
8154 Action::from_str("jump_to_bookmark", &args_with_char),
8155 Some(Action::JumpToBookmark('5'))
8156 );
8157 assert_eq!(
8158 Action::from_str("clear_bookmark", &args_with_char),
8159 Some(Action::ClearBookmark('5'))
8160 );
8161 }
8162
8163 #[test]
8164 fn test_keybinding_new_defaults() {
8165 use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
8166
8167 let mut config = Config::default();
8171 config.active_keybinding_map = crate::config::KeybindingMapName("default".to_string());
8172 let resolver = KeybindingResolver::new(&config);
8173
8174 let event = KeyEvent {
8176 code: KeyCode::Char('/'),
8177 modifiers: KeyModifiers::CONTROL,
8178 kind: KeyEventKind::Press,
8179 state: KeyEventState::NONE,
8180 };
8181 let action = resolver.resolve(&event, KeyContext::Normal);
8182 assert_eq!(action, Action::ToggleComment);
8183
8184 let event = KeyEvent {
8186 code: KeyCode::Char(']'),
8187 modifiers: KeyModifiers::CONTROL,
8188 kind: KeyEventKind::Press,
8189 state: KeyEventState::NONE,
8190 };
8191 let action = resolver.resolve(&event, KeyContext::Normal);
8192 assert_eq!(action, Action::GoToMatchingBracket);
8193
8194 let event = KeyEvent {
8196 code: KeyCode::Tab,
8197 modifiers: KeyModifiers::SHIFT,
8198 kind: KeyEventKind::Press,
8199 state: KeyEventState::NONE,
8200 };
8201 let action = resolver.resolve(&event, KeyContext::Normal);
8202 assert_eq!(action, Action::DedentSelection);
8203
8204 let event = KeyEvent {
8206 code: KeyCode::Char('g'),
8207 modifiers: KeyModifiers::CONTROL,
8208 kind: KeyEventKind::Press,
8209 state: KeyEventState::NONE,
8210 };
8211 let action = resolver.resolve(&event, KeyContext::Normal);
8212 assert_eq!(action, Action::GotoLine);
8213
8214 let event = KeyEvent {
8216 code: KeyCode::Char('5'),
8217 modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
8218 kind: KeyEventKind::Press,
8219 state: KeyEventState::NONE,
8220 };
8221 let action = resolver.resolve(&event, KeyContext::Normal);
8222 assert_eq!(action, Action::SetBookmark('5'));
8223
8224 let event = KeyEvent {
8225 code: KeyCode::Char('5'),
8226 modifiers: KeyModifiers::ALT,
8227 kind: KeyEventKind::Press,
8228 state: KeyEventState::NONE,
8229 };
8230 let action = resolver.resolve(&event, KeyContext::Normal);
8231 assert_eq!(action, Action::JumpToBookmark('5'));
8232 }
8233
8234 #[test]
8246 fn test_lsp_rename_didchange_positions_bug() {
8247 use crate::model::buffer::Buffer;
8248
8249 let config = Config::default();
8250 let (dir_context, _temp) = test_dir_context();
8251 let mut editor = Editor::new(
8252 config,
8253 80,
8254 24,
8255 dir_context,
8256 crate::view::color_support::ColorCapability::TrueColor,
8257 test_filesystem(),
8258 )
8259 .unwrap();
8260
8261 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
8265 editor.active_state_mut().buffer =
8266 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
8267
8268 let cursor_id = editor.active_cursors().primary_id();
8273
8274 let batch = Event::Batch {
8275 events: vec![
8276 Event::Delete {
8278 range: 23..26, deleted_text: "val".to_string(),
8280 cursor_id,
8281 },
8282 Event::Insert {
8283 position: 23,
8284 text: "value".to_string(),
8285 cursor_id,
8286 },
8287 Event::Delete {
8289 range: 7..10, deleted_text: "val".to_string(),
8291 cursor_id,
8292 },
8293 Event::Insert {
8294 position: 7,
8295 text: "value".to_string(),
8296 cursor_id,
8297 },
8298 ],
8299 description: "LSP Rename".to_string(),
8300 };
8301
8302 let lsp_changes_before = editor.collect_lsp_changes(&batch);
8304
8305 editor.apply_event_to_active_buffer(&batch);
8307
8308 let lsp_changes_after = editor.collect_lsp_changes(&batch);
8311
8312 let final_content = editor.active_state().buffer.to_string().unwrap();
8314 assert_eq!(
8315 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
8316 "Buffer should have 'value' in both places"
8317 );
8318
8319 assert_eq!(lsp_changes_before.len(), 4, "Should have 4 changes");
8325
8326 let first_delete = &lsp_changes_before[0];
8327 let first_del_range = first_delete.range.unwrap();
8328 assert_eq!(
8329 first_del_range.start.line, 1,
8330 "First delete should be on line 1 (BEFORE)"
8331 );
8332 assert_eq!(
8333 first_del_range.start.character, 4,
8334 "First delete start should be at char 4 (BEFORE)"
8335 );
8336
8337 assert_eq!(lsp_changes_after.len(), 4, "Should have 4 changes");
8343
8344 let first_delete_after = &lsp_changes_after[0];
8345 let first_del_range_after = first_delete_after.range.unwrap();
8346
8347 eprintln!("BEFORE modification:");
8350 eprintln!(
8351 " Delete at line {}, char {}-{}",
8352 first_del_range.start.line,
8353 first_del_range.start.character,
8354 first_del_range.end.character
8355 );
8356 eprintln!("AFTER modification:");
8357 eprintln!(
8358 " Delete at line {}, char {}-{}",
8359 first_del_range_after.start.line,
8360 first_del_range_after.start.character,
8361 first_del_range_after.end.character
8362 );
8363
8364 assert_ne!(
8382 first_del_range_after.end.character, first_del_range.end.character,
8383 "BUG CONFIRMED: LSP positions are different when calculated after buffer modification!"
8384 );
8385
8386 eprintln!("\n=== BUG DEMONSTRATED ===");
8387 eprintln!("When collect_lsp_changes() is called AFTER buffer modification,");
8388 eprintln!("the positions are WRONG because they're calculated from the");
8389 eprintln!("modified buffer, not the original buffer.");
8390 eprintln!("This causes the second rename to fail with 'content modified' error.");
8391 eprintln!("========================\n");
8392 }
8393
8394 #[test]
8395 fn test_lsp_rename_preserves_cursor_position() {
8396 use crate::model::buffer::Buffer;
8397
8398 let config = Config::default();
8399 let (dir_context, _temp) = test_dir_context();
8400 let mut editor = Editor::new(
8401 config,
8402 80,
8403 24,
8404 dir_context,
8405 crate::view::color_support::ColorCapability::TrueColor,
8406 test_filesystem(),
8407 )
8408 .unwrap();
8409
8410 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
8414 editor.active_state_mut().buffer =
8415 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
8416
8417 let original_cursor_pos = 23;
8419 editor.active_cursors_mut().primary_mut().position = original_cursor_pos;
8420
8421 let buffer_text = editor.active_state().buffer.to_string().unwrap();
8423 let text_at_cursor = buffer_text[original_cursor_pos..original_cursor_pos + 3].to_string();
8424 assert_eq!(text_at_cursor, "val", "Cursor should be at 'val'");
8425
8426 let cursor_id = editor.active_cursors().primary_id();
8429 let buffer_id = editor.active_buffer();
8430
8431 let events = vec![
8432 Event::Delete {
8434 range: 23..26, deleted_text: "val".to_string(),
8436 cursor_id,
8437 },
8438 Event::Insert {
8439 position: 23,
8440 text: "value".to_string(),
8441 cursor_id,
8442 },
8443 Event::Delete {
8445 range: 7..10, deleted_text: "val".to_string(),
8447 cursor_id,
8448 },
8449 Event::Insert {
8450 position: 7,
8451 text: "value".to_string(),
8452 cursor_id,
8453 },
8454 ];
8455
8456 editor
8458 .apply_events_to_buffer_as_bulk_edit(buffer_id, events, "LSP Rename".to_string())
8459 .unwrap();
8460
8461 let final_content = editor.active_state().buffer.to_string().unwrap();
8463 assert_eq!(
8464 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
8465 "Buffer should have 'value' in both places"
8466 );
8467
8468 let final_cursor_pos = editor.active_cursors().primary().position;
8476 let expected_cursor_pos = 25; assert_eq!(
8479 final_cursor_pos, expected_cursor_pos,
8480 "Cursor should be at position {} (start of 'value' on line 1), but was at {}. \
8481 Original pos: {}, expected adjustment: +2 for first rename",
8482 expected_cursor_pos, final_cursor_pos, original_cursor_pos
8483 );
8484
8485 let text_at_new_cursor = &final_content[final_cursor_pos..final_cursor_pos + 5];
8487 assert_eq!(
8488 text_at_new_cursor, "value",
8489 "Cursor should be at the start of 'value' after rename"
8490 );
8491 }
8492
8493 #[test]
8494 fn test_lsp_rename_twice_consecutive() {
8495 use crate::model::buffer::Buffer;
8498
8499 let config = Config::default();
8500 let (dir_context, _temp) = test_dir_context();
8501 let mut editor = Editor::new(
8502 config,
8503 80,
8504 24,
8505 dir_context,
8506 crate::view::color_support::ColorCapability::TrueColor,
8507 test_filesystem(),
8508 )
8509 .unwrap();
8510
8511 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
8513 editor.active_state_mut().buffer =
8514 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
8515
8516 let cursor_id = editor.active_cursors().primary_id();
8517 let buffer_id = editor.active_buffer();
8518
8519 let events1 = vec![
8522 Event::Delete {
8524 range: 23..26,
8525 deleted_text: "val".to_string(),
8526 cursor_id,
8527 },
8528 Event::Insert {
8529 position: 23,
8530 text: "value".to_string(),
8531 cursor_id,
8532 },
8533 Event::Delete {
8535 range: 7..10,
8536 deleted_text: "val".to_string(),
8537 cursor_id,
8538 },
8539 Event::Insert {
8540 position: 7,
8541 text: "value".to_string(),
8542 cursor_id,
8543 },
8544 ];
8545
8546 let batch1 = Event::Batch {
8548 events: events1.clone(),
8549 description: "LSP Rename 1".to_string(),
8550 };
8551
8552 let lsp_changes1 = editor.collect_lsp_changes(&batch1);
8554
8555 assert_eq!(
8557 lsp_changes1.len(),
8558 4,
8559 "First rename should have 4 LSP changes"
8560 );
8561
8562 let first_del = &lsp_changes1[0];
8564 let first_del_range = first_del.range.unwrap();
8565 assert_eq!(first_del_range.start.line, 1, "First delete line");
8566 assert_eq!(
8567 first_del_range.start.character, 4,
8568 "First delete start char"
8569 );
8570 assert_eq!(first_del_range.end.character, 7, "First delete end char");
8571
8572 editor
8574 .apply_events_to_buffer_as_bulk_edit(buffer_id, events1, "LSP Rename 1".to_string())
8575 .unwrap();
8576
8577 let after_first = editor.active_state().buffer.to_string().unwrap();
8579 assert_eq!(
8580 after_first, "fn foo(value: i32) {\n value + 1\n}\n",
8581 "After first rename"
8582 );
8583
8584 let events2 = vec![
8594 Event::Delete {
8596 range: 25..30,
8597 deleted_text: "value".to_string(),
8598 cursor_id,
8599 },
8600 Event::Insert {
8601 position: 25,
8602 text: "x".to_string(),
8603 cursor_id,
8604 },
8605 Event::Delete {
8607 range: 7..12,
8608 deleted_text: "value".to_string(),
8609 cursor_id,
8610 },
8611 Event::Insert {
8612 position: 7,
8613 text: "x".to_string(),
8614 cursor_id,
8615 },
8616 ];
8617
8618 let batch2 = Event::Batch {
8620 events: events2.clone(),
8621 description: "LSP Rename 2".to_string(),
8622 };
8623
8624 let lsp_changes2 = editor.collect_lsp_changes(&batch2);
8626
8627 assert_eq!(
8631 lsp_changes2.len(),
8632 4,
8633 "Second rename should have 4 LSP changes"
8634 );
8635
8636 let second_first_del = &lsp_changes2[0];
8638 let second_first_del_range = second_first_del.range.unwrap();
8639 assert_eq!(
8640 second_first_del_range.start.line, 1,
8641 "Second rename first delete should be on line 1"
8642 );
8643 assert_eq!(
8644 second_first_del_range.start.character, 4,
8645 "Second rename first delete start should be at char 4"
8646 );
8647 assert_eq!(
8648 second_first_del_range.end.character, 9,
8649 "Second rename first delete end should be at char 9 (4 + 5 for 'value')"
8650 );
8651
8652 let second_third_del = &lsp_changes2[2];
8654 let second_third_del_range = second_third_del.range.unwrap();
8655 assert_eq!(
8656 second_third_del_range.start.line, 0,
8657 "Second rename third delete should be on line 0"
8658 );
8659 assert_eq!(
8660 second_third_del_range.start.character, 7,
8661 "Second rename third delete start should be at char 7"
8662 );
8663 assert_eq!(
8664 second_third_del_range.end.character, 12,
8665 "Second rename third delete end should be at char 12 (7 + 5 for 'value')"
8666 );
8667
8668 editor
8670 .apply_events_to_buffer_as_bulk_edit(buffer_id, events2, "LSP Rename 2".to_string())
8671 .unwrap();
8672
8673 let after_second = editor.active_state().buffer.to_string().unwrap();
8675 assert_eq!(
8676 after_second, "fn foo(x: i32) {\n x + 1\n}\n",
8677 "After second rename"
8678 );
8679 }
8680
8681 #[test]
8682 fn test_ensure_active_tab_visible_static_offset() {
8683 let config = Config::default();
8684 let (dir_context, _temp) = test_dir_context();
8685 let mut editor = Editor::new(
8686 config,
8687 80,
8688 24,
8689 dir_context,
8690 crate::view::color_support::ColorCapability::TrueColor,
8691 test_filesystem(),
8692 )
8693 .unwrap();
8694 let split_id = editor.split_manager.active_split();
8695
8696 let buf1 = editor.new_buffer();
8698 editor
8699 .buffers
8700 .get_mut(&buf1)
8701 .unwrap()
8702 .buffer
8703 .rename_file_path(std::path::PathBuf::from("aaa_long_name_01.txt"));
8704 let buf2 = editor.new_buffer();
8705 editor
8706 .buffers
8707 .get_mut(&buf2)
8708 .unwrap()
8709 .buffer
8710 .rename_file_path(std::path::PathBuf::from("bbb_long_name_02.txt"));
8711 let buf3 = editor.new_buffer();
8712 editor
8713 .buffers
8714 .get_mut(&buf3)
8715 .unwrap()
8716 .buffer
8717 .rename_file_path(std::path::PathBuf::from("ccc_long_name_03.txt"));
8718
8719 {
8720 let view_state = editor.split_view_states.get_mut(&split_id).unwrap();
8721 view_state.open_buffers = vec![buf1, buf2, buf3];
8722 view_state.tab_scroll_offset = 50;
8723 }
8724
8725 editor.ensure_active_tab_visible(split_id, buf1, 25);
8729 assert_eq!(
8730 editor
8731 .split_view_states
8732 .get(&split_id)
8733 .unwrap()
8734 .tab_scroll_offset,
8735 0
8736 );
8737
8738 editor.ensure_active_tab_visible(split_id, buf3, 25);
8740 let view_state = editor.split_view_states.get(&split_id).unwrap();
8741 assert!(view_state.tab_scroll_offset > 0);
8742 let total_width: usize = view_state
8743 .open_buffers
8744 .iter()
8745 .enumerate()
8746 .map(|(idx, id)| {
8747 let state = editor.buffers.get(id).unwrap();
8748 let name_len = state
8749 .buffer
8750 .file_path()
8751 .and_then(|p| p.file_name())
8752 .and_then(|n| n.to_str())
8753 .map(|s| s.chars().count())
8754 .unwrap_or(0);
8755 let tab_width = 2 + name_len;
8756 if idx < view_state.open_buffers.len() - 1 {
8757 tab_width + 1 } else {
8759 tab_width
8760 }
8761 })
8762 .sum();
8763 assert!(view_state.tab_scroll_offset <= total_width);
8764 }
8765}