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;
16mod lsp_actions;
17mod lsp_requests;
18mod menu_actions;
19mod menu_context;
20mod mouse_input;
21mod on_save_actions;
22mod plugin_commands;
23mod popup_actions;
24mod prompt_actions;
25mod recovery_actions;
26mod render;
27pub mod session;
28mod settings_actions;
29mod shell_command;
30mod split_actions;
31mod tab_drag;
32mod terminal;
33mod terminal_input;
34mod terminal_mouse;
35mod toggle_actions;
36pub mod types;
37mod undo_actions;
38mod view_actions;
39pub mod warning_domains;
40
41use anyhow::Result as AnyhowResult;
42use rust_i18n::t;
43use std::path::Component;
44
45pub(crate) fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
48 let mut components = Vec::new();
49
50 for component in path.components() {
51 match component {
52 Component::CurDir => {
53 }
55 Component::ParentDir => {
56 if let Some(Component::Normal(_)) = components.last() {
58 components.pop();
59 } else {
60 components.push(component);
62 }
63 }
64 _ => {
65 components.push(component);
66 }
67 }
68 }
69
70 if components.is_empty() {
71 std::path::PathBuf::from(".")
72 } else {
73 components.iter().collect()
74 }
75}
76
77use self::types::{
78 Bookmark, CachedLayout, EventLineInfo, InteractiveReplaceState, LspMessageEntry,
79 LspProgressInfo, MacroRecordingState, MouseState, SearchState, TabContextMenu,
80 DEFAULT_BACKGROUND_FILE,
81};
82use crate::config::Config;
83use crate::config_io::{ConfigLayer, ConfigResolver, DirectoryContext};
84use crate::input::actions::action_to_events as convert_action_to_events;
85use crate::input::buffer_mode::ModeRegistry;
86use crate::input::command_registry::CommandRegistry;
87use crate::input::commands::Suggestion;
88use crate::input::keybindings::{Action, KeyContext, KeybindingResolver};
89use crate::input::position_history::PositionHistory;
90use crate::input::quick_open::{
91 FileProvider, GotoLineProvider, QuickOpenContext, QuickOpenProvider, QuickOpenRegistry,
92};
93use crate::model::event::{Event, EventLog, SplitDirection, SplitId};
94use crate::model::filesystem::FileSystem;
95use crate::services::async_bridge::{AsyncBridge, AsyncMessage};
96use crate::services::fs::FsManager;
97use crate::services::lsp::manager::{detect_language, LspManager};
98use crate::services::plugins::PluginManager;
99use crate::services::recovery::{RecoveryConfig, RecoveryService};
100use crate::services::time_source::{RealTimeSource, SharedTimeSource};
101use crate::state::EditorState;
102use crate::types::LspServerConfig;
103use crate::view::file_tree::{FileTree, FileTreeView};
104use crate::view::prompt::{Prompt, PromptType};
105use crate::view::scroll_sync::ScrollSyncManager;
106use crate::view::split::{SplitManager, SplitViewState};
107use crate::view::ui::{
108 FileExplorerRenderer, SplitRenderer, StatusBarRenderer, SuggestionsRenderer,
109};
110use crossterm::event::{KeyCode, KeyModifiers};
111#[cfg(feature = "plugins")]
112use fresh_core::api::BufferSavedDiff;
113#[cfg(feature = "plugins")]
114use fresh_core::api::JsCallbackId;
115use fresh_core::api::PluginCommand;
116use lsp_types::{Position, Range as LspRange, TextDocumentContentChangeEvent};
117use ratatui::{
118 layout::{Constraint, Direction, Layout},
119 Frame,
120};
121use std::collections::{HashMap, HashSet};
122use std::ops::Range;
123use std::path::{Path, PathBuf};
124use std::sync::{Arc, RwLock};
125use std::time::Instant;
126
127pub use self::types::{BufferKind, BufferMetadata, HoverTarget};
129pub use self::warning_domains::{
130 GeneralWarningDomain, LspWarningDomain, WarningAction, WarningActionId, WarningDomain,
131 WarningDomainRegistry, WarningLevel, WarningPopupContent,
132};
133pub use crate::model::event::BufferId;
134
135fn uri_to_path(uri: &lsp_types::Uri) -> Result<PathBuf, String> {
137 url::Url::parse(uri.as_str())
139 .map_err(|e| format!("Failed to parse URI: {}", e))?
140 .to_file_path()
141 .map_err(|_| "URI is not a file path".to_string())
142}
143
144#[derive(Clone, Debug)]
146pub struct PendingGrammar {
147 pub language: String,
149 pub grammar_path: String,
151 pub extensions: Vec<String>,
153}
154
155#[derive(Clone, Debug)]
157struct SemanticTokenRangeRequest {
158 buffer_id: BufferId,
159 version: u64,
160 range: Range<usize>,
161 start_line: usize,
162 end_line: usize,
163}
164
165#[derive(Clone, Copy, Debug)]
166enum SemanticTokensFullRequestKind {
167 Full,
168 FullDelta,
169}
170
171#[derive(Clone, Debug)]
172struct SemanticTokenFullRequest {
173 buffer_id: BufferId,
174 version: u64,
175 kind: SemanticTokensFullRequestKind,
176}
177
178pub struct Editor {
180 buffers: HashMap<BufferId, EditorState>,
182
183 event_logs: HashMap<BufferId, EventLog>,
188
189 next_buffer_id: usize,
191
192 config: Config,
194
195 dir_context: DirectoryContext,
197
198 grammar_registry: std::sync::Arc<crate::primitives::grammar::GrammarRegistry>,
200
201 pending_grammars: Vec<PendingGrammar>,
203
204 theme: crate::view::theme::Theme,
206
207 theme_registry: crate::view::theme::ThemeRegistry,
209
210 ansi_background: Option<crate::primitives::ansi_background::AnsiBackground>,
212
213 ansi_background_path: Option<PathBuf>,
215
216 background_fade: f32,
218
219 keybindings: KeybindingResolver,
221
222 clipboard: crate::services::clipboard::Clipboard,
224
225 should_quit: bool,
227
228 restart_with_dir: Option<PathBuf>,
231
232 status_message: Option<String>,
234
235 plugin_status_message: Option<String>,
237
238 plugin_errors: Vec<String>,
241
242 prompt: Option<Prompt>,
244
245 terminal_width: u16,
247 terminal_height: u16,
248
249 lsp: Option<LspManager>,
251
252 buffer_metadata: HashMap<BufferId, BufferMetadata>,
254
255 mode_registry: ModeRegistry,
257
258 tokio_runtime: Option<tokio::runtime::Runtime>,
260
261 async_bridge: Option<AsyncBridge>,
263
264 split_manager: SplitManager,
266
267 split_view_states: HashMap<SplitId, SplitViewState>,
271
272 previous_viewports: HashMap<SplitId, (usize, u16, u16)>,
276
277 scroll_sync_manager: ScrollSyncManager,
280
281 file_explorer: Option<FileTreeView>,
283
284 fs_manager: Arc<FsManager>,
286
287 filesystem: Arc<dyn FileSystem + Send + Sync>,
289
290 local_filesystem: Arc<dyn FileSystem + Send + Sync>,
293
294 process_spawner: Arc<dyn crate::services::remote::ProcessSpawner>,
296
297 file_explorer_visible: bool,
299
300 file_explorer_sync_in_progress: bool,
303
304 file_explorer_width_percent: f32,
307
308 pending_file_explorer_show_hidden: Option<bool>,
310
311 pending_file_explorer_show_gitignored: Option<bool>,
313
314 file_explorer_decorations: HashMap<String, Vec<crate::view::file_tree::FileExplorerDecoration>>,
316
317 file_explorer_decoration_cache: crate::view::file_tree::FileExplorerDecorationCache,
319
320 menu_bar_visible: bool,
322
323 menu_bar_auto_shown: bool,
326
327 tab_bar_visible: bool,
329
330 mouse_enabled: bool,
332
333 mouse_cursor_position: Option<(u16, u16)>,
337
338 gpm_active: bool,
340
341 key_context: KeyContext,
343
344 menu_state: crate::view::ui::MenuState,
346
347 menus: crate::config::MenuConfig,
349
350 working_dir: PathBuf,
352
353 pub position_history: PositionHistory,
355
356 in_navigation: bool,
358
359 next_lsp_request_id: u64,
361
362 pending_completion_request: Option<u64>,
364
365 completion_items: Option<Vec<lsp_types::CompletionItem>>,
368
369 scheduled_completion_trigger: Option<Instant>,
372
373 pending_goto_definition_request: Option<u64>,
375
376 pending_hover_request: Option<u64>,
378
379 pending_references_request: Option<u64>,
381
382 pending_references_symbol: String,
384
385 pending_signature_help_request: Option<u64>,
387
388 pending_code_actions_request: Option<u64>,
390
391 pending_inlay_hints_request: Option<u64>,
393
394 pending_semantic_token_requests: HashMap<u64, SemanticTokenFullRequest>,
396
397 semantic_tokens_in_flight: HashMap<BufferId, (u64, u64, SemanticTokensFullRequestKind)>,
399
400 pending_semantic_token_range_requests: HashMap<u64, SemanticTokenRangeRequest>,
402
403 semantic_tokens_range_in_flight: HashMap<BufferId, (u64, usize, usize, u64)>,
405
406 semantic_tokens_range_last_request: HashMap<BufferId, (usize, usize, u64, Instant)>,
408
409 semantic_tokens_range_applied: HashMap<BufferId, (usize, usize, u64)>,
411
412 semantic_tokens_full_debounce: HashMap<BufferId, Instant>,
414
415 hover_symbol_range: Option<(usize, usize)>,
418
419 hover_symbol_overlay: Option<crate::view::overlay::OverlayHandle>,
421
422 mouse_hover_screen_position: Option<(u16, u16)>,
425
426 search_state: Option<SearchState>,
428
429 search_namespace: crate::view::overlay::OverlayNamespace,
431
432 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace,
434
435 pending_search_range: Option<Range<usize>>,
437
438 interactive_replace_state: Option<InteractiveReplaceState>,
440
441 lsp_status: String,
443
444 mouse_state: MouseState,
446
447 tab_context_menu: Option<TabContextMenu>,
449
450 pub(crate) cached_layout: CachedLayout,
452
453 command_registry: Arc<RwLock<CommandRegistry>>,
455
456 #[allow(dead_code)]
459 quick_open_registry: QuickOpenRegistry,
460
461 file_provider: Arc<FileProvider>,
463
464 plugin_manager: PluginManager,
466
467 seen_byte_ranges: HashMap<BufferId, std::collections::HashSet<(usize, usize)>>,
471
472 panel_ids: HashMap<String, BufferId>,
475
476 background_process_handles: HashMap<u64, tokio::task::AbortHandle>,
479
480 prompt_histories: HashMap<String, crate::input::input_history::InputHistory>,
483
484 pending_async_prompt_callback: Option<fresh_core::api::JsCallbackId>,
488
489 lsp_progress: std::collections::HashMap<String, LspProgressInfo>,
491
492 lsp_server_statuses:
494 std::collections::HashMap<String, crate::services::async_bridge::LspServerStatus>,
495
496 lsp_window_messages: Vec<LspMessageEntry>,
498
499 lsp_log_messages: Vec<LspMessageEntry>,
501
502 diagnostic_result_ids: HashMap<String, String>,
505
506 stored_diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
509
510 event_broadcaster: crate::model::control_event::EventBroadcaster,
512
513 bookmarks: HashMap<char, Bookmark>,
515
516 search_case_sensitive: bool,
518 search_whole_word: bool,
519 search_use_regex: bool,
520 search_confirm_each: bool,
522
523 macros: HashMap<char, Vec<Action>>,
525
526 macro_recording: Option<MacroRecordingState>,
528
529 last_macro_register: Option<char>,
531
532 macro_playing: bool,
534
535 #[cfg(feature = "plugins")]
537 pending_plugin_actions: Vec<(
538 String,
539 crate::services::plugins::thread::oneshot::Receiver<anyhow::Result<()>>,
540 )>,
541
542 #[cfg(feature = "plugins")]
544 plugin_render_requested: bool,
545
546 chord_state: Vec<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)>,
549
550 pending_lsp_confirmation: Option<String>,
553
554 pending_close_buffer: Option<BufferId>,
557
558 auto_revert_enabled: bool,
560
561 last_auto_revert_poll: std::time::Instant,
563
564 last_file_tree_poll: std::time::Instant,
566
567 file_mod_times: HashMap<PathBuf, std::time::SystemTime>,
570
571 dir_mod_times: HashMap<PathBuf, std::time::SystemTime>,
574
575 file_rapid_change_counts: HashMap<PathBuf, (std::time::Instant, u32)>,
578
579 file_open_state: Option<file_open::FileOpenState>,
581
582 file_browser_layout: Option<crate::view::ui::FileBrowserLayout>,
584
585 recovery_service: RecoveryService,
587
588 full_redraw_requested: bool,
590
591 time_source: SharedTimeSource,
593
594 last_auto_save: std::time::Instant,
596
597 active_custom_contexts: HashSet<String>,
600
601 editor_mode: Option<String>,
604
605 warning_log: Option<(std::sync::mpsc::Receiver<()>, PathBuf)>,
607
608 status_log_path: Option<PathBuf>,
610
611 warning_domains: WarningDomainRegistry,
614
615 update_checker: Option<crate::services::release_checker::PeriodicUpdateChecker>,
617
618 terminal_manager: crate::services::terminal::TerminalManager,
620
621 terminal_buffers: HashMap<BufferId, crate::services::terminal::TerminalId>,
623
624 terminal_backing_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
626
627 terminal_log_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
629
630 terminal_mode: bool,
632
633 keyboard_capture: bool,
637
638 terminal_mode_resume: std::collections::HashSet<BufferId>,
642
643 previous_click_time: Option<std::time::Instant>,
645
646 previous_click_position: Option<(u16, u16)>,
649
650 pub(crate) settings_state: Option<crate::view::settings::SettingsState>,
652
653 pub(crate) calibration_wizard: Option<calibration_wizard::CalibrationWizard>,
655
656 pub(crate) event_debug: Option<event_debug::EventDebug>,
658
659 pub(crate) key_translator: crate::input::key_translator::KeyTranslator,
661
662 color_capability: crate::view::color_support::ColorCapability,
664
665 review_hunks: Vec<fresh_core::api::ReviewHunk>,
667
668 active_action_popup: Option<(String, Vec<(String, String)>)>,
671
672 composite_buffers: HashMap<BufferId, crate::model::composite_buffer::CompositeBuffer>,
675
676 composite_view_states:
679 HashMap<(SplitId, BufferId), crate::view::composite_view::CompositeViewState>,
680
681 stdin_streaming: Option<StdinStreamingState>,
683}
684
685pub struct StdinStreamingState {
687 pub temp_path: PathBuf,
689 pub buffer_id: BufferId,
691 pub last_known_size: usize,
693 pub complete: bool,
695 pub thread_handle: Option<std::thread::JoinHandle<anyhow::Result<()>>>,
697}
698
699impl Editor {
700 pub fn new(
703 config: Config,
704 width: u16,
705 height: u16,
706 dir_context: DirectoryContext,
707 color_capability: crate::view::color_support::ColorCapability,
708 filesystem: Arc<dyn FileSystem + Send + Sync>,
709 ) -> AnyhowResult<Self> {
710 Self::with_working_dir(
711 config,
712 width,
713 height,
714 None,
715 dir_context,
716 true,
717 color_capability,
718 filesystem,
719 )
720 }
721
722 #[allow(clippy::too_many_arguments)]
725 pub fn with_working_dir(
726 config: Config,
727 width: u16,
728 height: u16,
729 working_dir: Option<PathBuf>,
730 dir_context: DirectoryContext,
731 plugins_enabled: bool,
732 color_capability: crate::view::color_support::ColorCapability,
733 filesystem: Arc<dyn FileSystem + Send + Sync>,
734 ) -> AnyhowResult<Self> {
735 Self::with_options(
736 config,
737 width,
738 height,
739 working_dir,
740 filesystem,
741 plugins_enabled,
742 dir_context,
743 None,
744 color_capability,
745 crate::primitives::grammar::GrammarRegistry::for_editor(),
746 )
747 }
748
749 #[allow(clippy::too_many_arguments)]
752 pub fn for_test(
753 config: Config,
754 width: u16,
755 height: u16,
756 working_dir: Option<PathBuf>,
757 dir_context: DirectoryContext,
758 color_capability: crate::view::color_support::ColorCapability,
759 filesystem: Arc<dyn FileSystem + Send + Sync>,
760 time_source: Option<SharedTimeSource>,
761 ) -> AnyhowResult<Self> {
762 Self::with_options(
763 config,
764 width,
765 height,
766 working_dir,
767 filesystem,
768 true,
769 dir_context,
770 time_source,
771 color_capability,
772 crate::primitives::grammar::GrammarRegistry::empty(),
773 )
774 }
775
776 #[allow(clippy::too_many_arguments)]
780 fn with_options(
781 mut config: Config,
782 width: u16,
783 height: u16,
784 working_dir: Option<PathBuf>,
785 filesystem: Arc<dyn FileSystem + Send + Sync>,
786 enable_plugins: bool,
787 dir_context: DirectoryContext,
788 time_source: Option<SharedTimeSource>,
789 color_capability: crate::view::color_support::ColorCapability,
790 grammar_registry: Arc<crate::primitives::grammar::GrammarRegistry>,
791 ) -> AnyhowResult<Self> {
792 let time_source = time_source.unwrap_or_else(RealTimeSource::shared);
794 tracing::info!("Editor::new called with width={}, height={}", width, height);
795
796 let working_dir = working_dir
798 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
799
800 let working_dir = working_dir.canonicalize().unwrap_or(working_dir);
803
804 let theme_loader = crate::view::theme::ThemeLoader::new();
806 let theme_registry = theme_loader.load_all();
807
808 let theme = theme_registry.get_cloned(&config.theme).unwrap_or_else(|| {
810 tracing::warn!(
811 "Theme '{}' not found, falling back to default theme",
812 config.theme.0
813 );
814 theme_registry
815 .get_cloned(&crate::config::ThemeName(
816 crate::view::theme::THEME_HIGH_CONTRAST.to_string(),
817 ))
818 .expect("Default theme must exist")
819 });
820
821 theme.set_terminal_cursor_color();
823
824 tracing::info!(
825 "Grammar registry has {} syntaxes",
826 grammar_registry.available_syntaxes().len()
827 );
828
829 let keybindings = KeybindingResolver::new(&config);
830
831 let mut buffers = HashMap::new();
833 let mut event_logs = HashMap::new();
834
835 let buffer_id = BufferId(0);
836 let mut state = EditorState::new(
837 width,
838 height,
839 config.editor.large_file_threshold_bytes as usize,
840 Arc::clone(&filesystem),
841 );
842 state.margins.set_line_numbers(config.editor.line_numbers);
844 tracing::info!("EditorState created for buffer {:?}", buffer_id);
846 buffers.insert(buffer_id, state);
847 event_logs.insert(buffer_id, EventLog::new());
848
849 let mut buffer_metadata = HashMap::new();
851 buffer_metadata.insert(buffer_id, BufferMetadata::new());
852
853 let root_uri = url::Url::from_file_path(&working_dir)
855 .ok()
856 .and_then(|u| u.as_str().parse::<lsp_types::Uri>().ok());
857
858 let tokio_runtime = tokio::runtime::Builder::new_multi_thread()
860 .worker_threads(2) .thread_name("editor-async")
862 .enable_all()
863 .build()
864 .ok();
865
866 let async_bridge = AsyncBridge::new();
868
869 if tokio_runtime.is_none() {
870 tracing::warn!("Failed to create Tokio runtime - async features disabled");
871 }
872
873 let mut lsp = LspManager::new(root_uri);
875
876 if let Some(ref runtime) = tokio_runtime {
878 lsp.set_runtime(runtime.handle().clone(), async_bridge.clone());
879 }
880
881 for (language, lsp_config) in &config.lsp {
883 lsp.set_language_config(language.clone(), lsp_config.clone());
884 }
885
886 let split_manager = SplitManager::new(buffer_id);
888
889 let mut split_view_states = HashMap::new();
891 let initial_split_id = split_manager.active_split();
892 let mut initial_view_state = SplitViewState::with_buffer(width, height, buffer_id);
893 initial_view_state.viewport.line_wrap_enabled = config.editor.line_wrap;
894 split_view_states.insert(initial_split_id, initial_view_state);
895
896 let fs_manager = Arc::new(FsManager::new(Arc::clone(&filesystem)));
898
899 let command_registry = Arc::new(RwLock::new(CommandRegistry::new()));
901
902 let file_provider = Arc::new(FileProvider::new());
904
905 let mut quick_open_registry = QuickOpenRegistry::new();
907 quick_open_registry.register(Box::new(GotoLineProvider::new()));
908 let plugin_manager = PluginManager::new(
913 enable_plugins,
914 Arc::clone(&command_registry),
915 dir_context.clone(),
916 );
917
918 #[cfg(feature = "plugins")]
921 if let Some(snapshot_handle) = plugin_manager.state_snapshot_handle() {
922 let mut snapshot = snapshot_handle.write().unwrap();
923 snapshot.working_dir = working_dir.clone();
924 }
925
926 if plugin_manager.is_active() {
933 let mut plugin_dirs: Vec<std::path::PathBuf> = vec![];
934
935 if let Ok(exe_path) = std::env::current_exe() {
937 if let Some(exe_dir) = exe_path.parent() {
938 let exe_plugin_dir = exe_dir.join("plugins");
939 if exe_plugin_dir.exists() {
940 plugin_dirs.push(exe_plugin_dir);
941 }
942 }
943 }
944
945 let working_plugin_dir = working_dir.join("plugins");
947 if working_plugin_dir.exists() && !plugin_dirs.contains(&working_plugin_dir) {
948 plugin_dirs.push(working_plugin_dir);
949 }
950
951 #[cfg(feature = "embed-plugins")]
953 if plugin_dirs.is_empty() {
954 if let Some(embedded_dir) =
955 crate::services::plugins::embedded::get_embedded_plugins_dir()
956 {
957 tracing::info!("Using embedded plugins from: {:?}", embedded_dir);
958 plugin_dirs.push(embedded_dir.clone());
959 }
960 }
961
962 let user_plugins_dir = dir_context.config_dir.join("plugins");
964 if user_plugins_dir.exists() && !plugin_dirs.contains(&user_plugins_dir) {
965 tracing::info!("Found user plugins directory: {:?}", user_plugins_dir);
966 plugin_dirs.push(user_plugins_dir.clone());
967 }
968
969 let packages_dir = dir_context.config_dir.join("plugins").join("packages");
971 if packages_dir.exists() {
972 if let Ok(entries) = std::fs::read_dir(&packages_dir) {
973 for entry in entries.flatten() {
974 let path = entry.path();
975 if path.is_dir() {
977 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
978 if !name.starts_with('.') {
979 tracing::info!("Found package manager plugin: {:?}", path);
980 plugin_dirs.push(path);
981 }
982 }
983 }
984 }
985 }
986 }
987
988 if plugin_dirs.is_empty() {
989 tracing::debug!(
990 "No plugins directory found next to executable or in working dir: {:?}",
991 working_dir
992 );
993 }
994
995 for plugin_dir in plugin_dirs {
997 tracing::info!("Loading TypeScript plugins from: {:?}", plugin_dir);
998 let (errors, discovered_plugins) =
999 plugin_manager.load_plugins_from_dir_with_config(&plugin_dir, &config.plugins);
1000
1001 for (name, plugin_config) in discovered_plugins {
1004 config.plugins.insert(name, plugin_config);
1005 }
1006
1007 if !errors.is_empty() {
1008 for err in &errors {
1009 tracing::error!("TypeScript plugin load error: {}", err);
1010 }
1011 #[cfg(debug_assertions)]
1013 panic!(
1014 "TypeScript plugin loading failed with {} error(s): {}",
1015 errors.len(),
1016 errors.join("; ")
1017 );
1018 }
1019 }
1020 }
1021
1022 let file_explorer_width = config.file_explorer.width;
1024 let recovery_enabled = config.editor.recovery_enabled;
1025 let auto_save_interval_secs = config.editor.auto_save_interval_secs;
1026 let check_for_updates = config.check_for_updates;
1027 let show_menu_bar = config.editor.show_menu_bar;
1028 let show_tab_bar = config.editor.show_tab_bar;
1029
1030 let update_checker = if check_for_updates {
1032 tracing::debug!("Update checking enabled, starting periodic checker");
1033 Some(
1034 crate::services::release_checker::start_periodic_update_check(
1035 crate::services::release_checker::DEFAULT_RELEASES_URL,
1036 time_source.clone(),
1037 dir_context.data_dir.clone(),
1038 ),
1039 )
1040 } else {
1041 tracing::debug!("Update checking disabled by config");
1042 None
1043 };
1044
1045 let mut editor = Editor {
1046 buffers,
1047 event_logs,
1048 next_buffer_id: 1,
1049 config,
1050 dir_context: dir_context.clone(),
1051 grammar_registry,
1052 pending_grammars: Vec::new(),
1053 theme,
1054 theme_registry,
1055 ansi_background: None,
1056 ansi_background_path: None,
1057 background_fade: crate::primitives::ansi_background::DEFAULT_BACKGROUND_FADE,
1058 keybindings,
1059 clipboard: crate::services::clipboard::Clipboard::new(),
1060 should_quit: false,
1061 restart_with_dir: None,
1062 status_message: None,
1063 plugin_status_message: None,
1064 plugin_errors: Vec::new(),
1065 prompt: None,
1066 terminal_width: width,
1067 terminal_height: height,
1068 lsp: Some(lsp),
1069 buffer_metadata,
1070 mode_registry: ModeRegistry::new(),
1071 tokio_runtime,
1072 async_bridge: Some(async_bridge),
1073 split_manager,
1074 split_view_states,
1075 previous_viewports: HashMap::new(),
1076 scroll_sync_manager: ScrollSyncManager::new(),
1077 file_explorer: None,
1078 fs_manager,
1079 filesystem,
1080 local_filesystem: Arc::new(crate::model::filesystem::StdFileSystem),
1081 process_spawner: Arc::new(crate::services::remote::LocalProcessSpawner),
1082 file_explorer_visible: false,
1083 file_explorer_sync_in_progress: false,
1084 file_explorer_width_percent: file_explorer_width,
1085 pending_file_explorer_show_hidden: None,
1086 pending_file_explorer_show_gitignored: None,
1087 menu_bar_visible: show_menu_bar,
1088 file_explorer_decorations: HashMap::new(),
1089 file_explorer_decoration_cache:
1090 crate::view::file_tree::FileExplorerDecorationCache::default(),
1091 menu_bar_auto_shown: false,
1092 tab_bar_visible: show_tab_bar,
1093 mouse_enabled: true,
1094 mouse_cursor_position: None,
1095 gpm_active: false,
1096 key_context: KeyContext::Normal,
1097 menu_state: crate::view::ui::MenuState::new(),
1098 menus: crate::config::MenuConfig::translated(),
1099 working_dir,
1100 position_history: PositionHistory::new(),
1101 in_navigation: false,
1102 next_lsp_request_id: 0,
1103 pending_completion_request: None,
1104 completion_items: None,
1105 scheduled_completion_trigger: None,
1106 pending_goto_definition_request: None,
1107 pending_hover_request: None,
1108 pending_references_request: None,
1109 pending_references_symbol: String::new(),
1110 pending_signature_help_request: None,
1111 pending_code_actions_request: None,
1112 pending_inlay_hints_request: None,
1113 pending_semantic_token_requests: HashMap::new(),
1114 semantic_tokens_in_flight: HashMap::new(),
1115 pending_semantic_token_range_requests: HashMap::new(),
1116 semantic_tokens_range_in_flight: HashMap::new(),
1117 semantic_tokens_range_last_request: HashMap::new(),
1118 semantic_tokens_range_applied: HashMap::new(),
1119 semantic_tokens_full_debounce: HashMap::new(),
1120 hover_symbol_range: None,
1121 hover_symbol_overlay: None,
1122 mouse_hover_screen_position: None,
1123 search_state: None,
1124 search_namespace: crate::view::overlay::OverlayNamespace::from_string(
1125 "search".to_string(),
1126 ),
1127 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace::from_string(
1128 "lsp-diagnostic".to_string(),
1129 ),
1130 pending_search_range: None,
1131 interactive_replace_state: None,
1132 lsp_status: String::new(),
1133 mouse_state: MouseState::default(),
1134 tab_context_menu: None,
1135 cached_layout: CachedLayout::default(),
1136 command_registry,
1137 quick_open_registry,
1138 file_provider,
1139 plugin_manager,
1140 seen_byte_ranges: HashMap::new(),
1141 panel_ids: HashMap::new(),
1142 background_process_handles: HashMap::new(),
1143 prompt_histories: {
1144 let mut histories = HashMap::new();
1146 for history_name in ["search", "replace", "goto_line"] {
1147 let path = dir_context.prompt_history_path(history_name);
1148 let history = crate::input::input_history::InputHistory::load_from_file(&path)
1149 .unwrap_or_else(|e| {
1150 tracing::warn!("Failed to load {} history: {}", history_name, e);
1151 crate::input::input_history::InputHistory::new()
1152 });
1153 histories.insert(history_name.to_string(), history);
1154 }
1155 histories
1156 },
1157 pending_async_prompt_callback: None,
1158 lsp_progress: std::collections::HashMap::new(),
1159 lsp_server_statuses: std::collections::HashMap::new(),
1160 lsp_window_messages: Vec::new(),
1161 lsp_log_messages: Vec::new(),
1162 diagnostic_result_ids: HashMap::new(),
1163 stored_diagnostics: HashMap::new(),
1164 event_broadcaster: crate::model::control_event::EventBroadcaster::default(),
1165 bookmarks: HashMap::new(),
1166 search_case_sensitive: true,
1167 search_whole_word: false,
1168 search_use_regex: false,
1169 search_confirm_each: false,
1170 macros: HashMap::new(),
1171 macro_recording: None,
1172 last_macro_register: None,
1173 macro_playing: false,
1174 #[cfg(feature = "plugins")]
1175 pending_plugin_actions: Vec::new(),
1176 #[cfg(feature = "plugins")]
1177 plugin_render_requested: false,
1178 chord_state: Vec::new(),
1179 pending_lsp_confirmation: None,
1180 pending_close_buffer: None,
1181 auto_revert_enabled: true,
1182 last_auto_revert_poll: time_source.now(),
1183 last_file_tree_poll: time_source.now(),
1184 file_mod_times: HashMap::new(),
1185 dir_mod_times: HashMap::new(),
1186 file_rapid_change_counts: HashMap::new(),
1187 file_open_state: None,
1188 file_browser_layout: None,
1189 recovery_service: {
1190 let recovery_config = RecoveryConfig {
1191 enabled: recovery_enabled,
1192 auto_save_interval_secs,
1193 ..RecoveryConfig::default()
1194 };
1195 RecoveryService::with_config_and_dir(recovery_config, dir_context.recovery_dir())
1196 },
1197 full_redraw_requested: false,
1198 time_source: time_source.clone(),
1199 last_auto_save: time_source.now(),
1200 active_custom_contexts: HashSet::new(),
1201 editor_mode: None,
1202 warning_log: None,
1203 status_log_path: None,
1204 warning_domains: WarningDomainRegistry::new(),
1205 update_checker,
1206 terminal_manager: crate::services::terminal::TerminalManager::new(),
1207 terminal_buffers: HashMap::new(),
1208 terminal_backing_files: HashMap::new(),
1209 terminal_log_files: HashMap::new(),
1210 terminal_mode: false,
1211 keyboard_capture: false,
1212 terminal_mode_resume: std::collections::HashSet::new(),
1213 previous_click_time: None,
1214 previous_click_position: None,
1215 settings_state: None,
1216 calibration_wizard: None,
1217 event_debug: None,
1218 key_translator: crate::input::key_translator::KeyTranslator::load_default()
1219 .unwrap_or_default(),
1220 color_capability,
1221 stdin_streaming: None,
1222 review_hunks: Vec::new(),
1223 active_action_popup: None,
1224 composite_buffers: HashMap::new(),
1225 composite_view_states: HashMap::new(),
1226 };
1227
1228 #[cfg(feature = "plugins")]
1229 {
1230 editor.update_plugin_state_snapshot();
1231 if editor.plugin_manager.is_active() {
1232 editor.plugin_manager.run_hook(
1233 "editor_initialized",
1234 crate::services::plugins::hooks::HookArgs::EditorInitialized,
1235 );
1236 }
1237 }
1238
1239 Ok(editor)
1240 }
1241
1242 pub fn event_broadcaster(&self) -> &crate::model::control_event::EventBroadcaster {
1244 &self.event_broadcaster
1245 }
1246
1247 pub fn async_bridge(&self) -> Option<&AsyncBridge> {
1249 self.async_bridge.as_ref()
1250 }
1251
1252 pub fn config(&self) -> &Config {
1254 &self.config
1255 }
1256
1257 pub fn key_translator(&self) -> &crate::input::key_translator::KeyTranslator {
1259 &self.key_translator
1260 }
1261
1262 pub fn time_source(&self) -> &SharedTimeSource {
1264 &self.time_source
1265 }
1266
1267 pub fn emit_event(&self, name: impl Into<String>, data: serde_json::Value) {
1269 self.event_broadcaster.emit_named(name, data);
1270 }
1271
1272 fn send_plugin_response(&self, response: fresh_core::api::PluginResponse) {
1274 self.plugin_manager.deliver_response(response);
1275 }
1276
1277 fn take_pending_semantic_token_request(
1279 &mut self,
1280 request_id: u64,
1281 ) -> Option<SemanticTokenFullRequest> {
1282 if let Some(request) = self.pending_semantic_token_requests.remove(&request_id) {
1283 self.semantic_tokens_in_flight.remove(&request.buffer_id);
1284 Some(request)
1285 } else {
1286 None
1287 }
1288 }
1289
1290 fn take_pending_semantic_token_range_request(
1292 &mut self,
1293 request_id: u64,
1294 ) -> Option<SemanticTokenRangeRequest> {
1295 if let Some(request) = self
1296 .pending_semantic_token_range_requests
1297 .remove(&request_id)
1298 {
1299 self.semantic_tokens_range_in_flight
1300 .remove(&request.buffer_id);
1301 Some(request)
1302 } else {
1303 None
1304 }
1305 }
1306
1307 pub fn get_all_keybindings(&self) -> Vec<(String, String)> {
1309 self.keybindings.get_all_bindings()
1310 }
1311
1312 pub fn get_keybinding_for_action(&self, action_name: &str) -> Option<String> {
1315 self.keybindings
1316 .find_keybinding_for_action(action_name, self.key_context)
1317 }
1318
1319 pub fn mode_registry_mut(&mut self) -> &mut ModeRegistry {
1321 &mut self.mode_registry
1322 }
1323
1324 pub fn mode_registry(&self) -> &ModeRegistry {
1326 &self.mode_registry
1327 }
1328
1329 #[inline]
1334 pub fn active_buffer(&self) -> BufferId {
1335 self.split_manager
1336 .active_buffer_id()
1337 .expect("Editor always has at least one buffer")
1338 }
1339
1340 pub fn active_buffer_mode(&self) -> Option<&str> {
1342 self.buffer_metadata
1343 .get(&self.active_buffer())
1344 .and_then(|meta| meta.virtual_mode())
1345 }
1346
1347 pub fn is_active_buffer_read_only(&self) -> bool {
1349 if let Some(metadata) = self.buffer_metadata.get(&self.active_buffer()) {
1350 if metadata.read_only {
1351 return true;
1352 }
1353 if let Some(mode_name) = metadata.virtual_mode() {
1355 return self.mode_registry.is_read_only(mode_name);
1356 }
1357 }
1358 false
1359 }
1360
1361 pub fn is_editing_disabled(&self) -> bool {
1364 self.active_state().editing_disabled
1365 }
1366
1367 pub fn resolve_mode_keybinding(
1374 &self,
1375 code: KeyCode,
1376 modifiers: KeyModifiers,
1377 ) -> Option<String> {
1378 if let Some(ref global_mode) = self.editor_mode {
1380 if let Some(binding) =
1381 self.mode_registry
1382 .resolve_keybinding(global_mode, code, modifiers)
1383 {
1384 return Some(binding);
1385 }
1386 }
1387
1388 let mode_name = self.active_buffer_mode()?;
1390 self.mode_registry
1391 .resolve_keybinding(mode_name, code, modifiers)
1392 }
1393
1394 pub fn has_active_lsp_progress(&self) -> bool {
1396 !self.lsp_progress.is_empty()
1397 }
1398
1399 pub fn get_lsp_progress(&self) -> Vec<(String, String, Option<String>)> {
1401 self.lsp_progress
1402 .iter()
1403 .map(|(token, info)| (token.clone(), info.title.clone(), info.message.clone()))
1404 .collect()
1405 }
1406
1407 pub fn is_lsp_server_ready(&self, language: &str) -> bool {
1409 use crate::services::async_bridge::LspServerStatus;
1410 self.lsp_server_statuses
1411 .get(language)
1412 .map(|status| matches!(status, LspServerStatus::Running))
1413 .unwrap_or(false)
1414 }
1415
1416 pub fn get_lsp_status(&self) -> &str {
1418 &self.lsp_status
1419 }
1420
1421 pub fn get_stored_diagnostics(&self) -> &HashMap<String, Vec<lsp_types::Diagnostic>> {
1424 &self.stored_diagnostics
1425 }
1426
1427 pub fn is_update_available(&self) -> bool {
1429 self.update_checker
1430 .as_ref()
1431 .map(|c| c.is_update_available())
1432 .unwrap_or(false)
1433 }
1434
1435 pub fn latest_version(&self) -> Option<&str> {
1437 self.update_checker
1438 .as_ref()
1439 .and_then(|c| c.latest_version())
1440 }
1441
1442 pub fn get_update_result(
1444 &self,
1445 ) -> Option<&crate::services::release_checker::ReleaseCheckResult> {
1446 self.update_checker
1447 .as_ref()
1448 .and_then(|c| c.get_cached_result())
1449 }
1450
1451 #[doc(hidden)]
1456 pub fn set_update_checker(
1457 &mut self,
1458 checker: crate::services::release_checker::PeriodicUpdateChecker,
1459 ) {
1460 self.update_checker = Some(checker);
1461 }
1462
1463 pub fn set_lsp_config(&mut self, language: String, config: LspServerConfig) {
1465 if let Some(ref mut lsp) = self.lsp {
1466 lsp.set_language_config(language, config);
1467 }
1468 }
1469
1470 pub fn running_lsp_servers(&self) -> Vec<String> {
1472 self.lsp
1473 .as_ref()
1474 .map(|lsp| lsp.running_servers())
1475 .unwrap_or_default()
1476 }
1477
1478 pub fn shutdown_lsp_server(&mut self, language: &str) -> bool {
1482 if let Some(ref mut lsp) = self.lsp {
1483 lsp.shutdown_server(language)
1484 } else {
1485 false
1486 }
1487 }
1488
1489 pub fn enable_event_streaming<P: AsRef<Path>>(&mut self, path: P) -> AnyhowResult<()> {
1491 for event_log in self.event_logs.values_mut() {
1493 event_log.enable_streaming(&path)?;
1494 }
1495 Ok(())
1496 }
1497
1498 pub fn log_keystroke(&mut self, key_code: &str, modifiers: &str) {
1500 if let Some(event_log) = self.event_logs.get_mut(&self.active_buffer()) {
1501 event_log.log_keystroke(key_code, modifiers);
1502 }
1503 }
1504
1505 pub fn set_warning_log(&mut self, receiver: std::sync::mpsc::Receiver<()>, path: PathBuf) {
1510 self.warning_log = Some((receiver, path));
1511 }
1512
1513 pub fn set_status_log_path(&mut self, path: PathBuf) {
1515 self.status_log_path = Some(path);
1516 }
1517
1518 pub fn set_process_spawner(
1521 &mut self,
1522 spawner: Arc<dyn crate::services::remote::ProcessSpawner>,
1523 ) {
1524 self.process_spawner = spawner;
1525 }
1526
1527 pub fn remote_connection_info(&self) -> Option<&str> {
1531 self.filesystem.remote_connection_info()
1532 }
1533
1534 pub fn get_status_log_path(&self) -> Option<&PathBuf> {
1536 self.status_log_path.as_ref()
1537 }
1538
1539 pub fn open_status_log(&mut self) {
1541 if let Some(path) = self.status_log_path.clone() {
1542 if let Err(e) = self.open_local_file(&path) {
1544 tracing::error!("Failed to open status log: {}", e);
1545 }
1546 } else {
1547 self.set_status_message("Status log not available".to_string());
1548 }
1549 }
1550
1551 pub fn check_warning_log(&mut self) -> bool {
1556 let Some((receiver, path)) = &self.warning_log else {
1557 return false;
1558 };
1559
1560 let mut new_warning_count = 0usize;
1562 while receiver.try_recv().is_ok() {
1563 new_warning_count += 1;
1564 }
1565
1566 if new_warning_count > 0 {
1567 self.warning_domains.general.add_warnings(new_warning_count);
1569 self.warning_domains.general.set_log_path(path.clone());
1570 }
1571
1572 new_warning_count > 0
1573 }
1574
1575 pub fn get_warning_domains(&self) -> &WarningDomainRegistry {
1577 &self.warning_domains
1578 }
1579
1580 pub fn get_warning_log_path(&self) -> Option<&PathBuf> {
1582 self.warning_domains.general.log_path.as_ref()
1583 }
1584
1585 pub fn open_warning_log(&mut self) {
1587 if let Some(path) = self.warning_domains.general.log_path.clone() {
1588 if let Err(e) = self.open_local_file(&path) {
1590 tracing::error!("Failed to open warning log: {}", e);
1591 }
1592 }
1593 }
1594
1595 pub fn clear_warning_indicator(&mut self) {
1597 self.warning_domains.general.clear();
1598 }
1599
1600 pub fn clear_warnings(&mut self) {
1602 self.warning_domains.general.clear();
1603 self.warning_domains.lsp.clear();
1604 self.status_message = Some("Warnings cleared".to_string());
1605 }
1606
1607 pub fn has_lsp_error(&self) -> bool {
1609 self.warning_domains.lsp.level() == WarningLevel::Error
1610 }
1611
1612 pub fn get_effective_warning_level(&self) -> WarningLevel {
1615 self.warning_domains.lsp.level()
1616 }
1617
1618 pub fn get_general_warning_level(&self) -> WarningLevel {
1620 self.warning_domains.general.level()
1621 }
1622
1623 pub fn get_general_warning_count(&self) -> usize {
1625 self.warning_domains.general.count
1626 }
1627
1628 pub fn update_lsp_warning_domain(&mut self) {
1630 self.warning_domains
1631 .lsp
1632 .update_from_statuses(&self.lsp_server_statuses);
1633 }
1634
1635 pub fn check_mouse_hover_timer(&mut self) -> bool {
1641 if !self.config.editor.mouse_hover_enabled {
1643 return false;
1644 }
1645
1646 let hover_delay = std::time::Duration::from_millis(self.config.editor.mouse_hover_delay_ms);
1647
1648 let hover_info = match self.mouse_state.lsp_hover_state {
1650 Some((byte_pos, start_time, screen_x, screen_y)) => {
1651 if self.mouse_state.lsp_hover_request_sent {
1652 return false; }
1654 if start_time.elapsed() < hover_delay {
1655 return false; }
1657 Some((byte_pos, screen_x, screen_y))
1658 }
1659 None => return false,
1660 };
1661
1662 let Some((byte_pos, screen_x, screen_y)) = hover_info else {
1663 return false;
1664 };
1665
1666 self.mouse_state.lsp_hover_request_sent = true;
1668
1669 self.mouse_hover_screen_position = Some((screen_x, screen_y));
1671
1672 if let Err(e) = self.request_hover_at_position(byte_pos) {
1674 tracing::debug!("Failed to request hover: {}", e);
1675 return false;
1676 }
1677
1678 true
1679 }
1680
1681 pub fn check_semantic_highlight_timer(&self) -> bool {
1686 for state in self.buffers.values() {
1688 if let Some(remaining) = state.reference_highlight_overlay.needs_redraw() {
1689 if remaining.is_zero() {
1690 return true;
1691 }
1692 }
1693 }
1694 false
1695 }
1696
1697 pub fn check_completion_trigger_timer(&mut self) -> bool {
1703 let Some(trigger_time) = self.scheduled_completion_trigger else {
1705 return false;
1706 };
1707
1708 if Instant::now() < trigger_time {
1710 return false;
1711 }
1712
1713 self.scheduled_completion_trigger = None;
1715
1716 if self.active_state().popups.is_visible() {
1718 return false;
1719 }
1720
1721 if let Err(e) = self.request_completion() {
1723 tracing::debug!("Failed to trigger debounced completion: {}", e);
1724 return false;
1725 }
1726
1727 true
1728 }
1729
1730 fn load_ansi_background(&mut self, input: &str) -> AnyhowResult<()> {
1732 let trimmed = input.trim();
1733
1734 if trimmed.is_empty() {
1735 self.ansi_background = None;
1736 self.ansi_background_path = None;
1737 self.set_status_message(t!("status.background_cleared").to_string());
1738 return Ok(());
1739 }
1740
1741 let input_path = Path::new(trimmed);
1742 let resolved = if input_path.is_absolute() {
1743 input_path.to_path_buf()
1744 } else {
1745 self.working_dir.join(input_path)
1746 };
1747
1748 let canonical = resolved.canonicalize().unwrap_or_else(|_| resolved.clone());
1749
1750 let parsed = crate::primitives::ansi_background::AnsiBackground::from_file(&canonical)?;
1751
1752 self.ansi_background = Some(parsed);
1753 self.ansi_background_path = Some(canonical.clone());
1754 self.set_status_message(
1755 t!(
1756 "view.background_set",
1757 path = canonical.display().to_string()
1758 )
1759 .to_string(),
1760 );
1761
1762 Ok(())
1763 }
1764
1765 fn effective_tabs_width(&self) -> u16 {
1770 if self.file_explorer_visible && self.file_explorer.is_some() {
1771 let editor_percent = 1.0 - self.file_explorer_width_percent;
1773 (self.terminal_width as f32 * editor_percent) as u16
1774 } else {
1775 self.terminal_width
1776 }
1777 }
1778
1779 fn set_active_buffer(&mut self, buffer_id: BufferId) {
1789 if self.active_buffer() == buffer_id {
1790 return; }
1792
1793 self.on_editor_focus_lost();
1795
1796 self.cancel_search_prompt_if_active();
1799
1800 let previous = self.active_buffer();
1802
1803 if self.terminal_mode && self.is_terminal_buffer(previous) {
1805 self.terminal_mode_resume.insert(previous);
1806 self.terminal_mode = false;
1807 self.key_context = crate::input::keybindings::KeyContext::Normal;
1808 }
1809
1810 self.split_manager.set_active_buffer_id(buffer_id);
1812
1813 if self.terminal_mode_resume.contains(&buffer_id) && self.is_terminal_buffer(buffer_id) {
1815 self.terminal_mode = true;
1816 self.key_context = crate::input::keybindings::KeyContext::Terminal;
1817 } else if self.is_terminal_buffer(buffer_id) {
1818 self.sync_terminal_to_buffer(buffer_id);
1821 }
1822
1823 let active_split = self.split_manager.active_split();
1825 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
1826 view_state.add_buffer(buffer_id);
1827 view_state.push_focus(previous);
1829 }
1830
1831 self.ensure_active_tab_visible(active_split, buffer_id, self.effective_tabs_width());
1833
1834 self.plugin_manager.run_hook(
1839 "buffer_activated",
1840 crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
1841 );
1842 }
1843
1844 pub(super) fn focus_split(
1855 &mut self,
1856 split_id: crate::model::event::SplitId,
1857 buffer_id: BufferId,
1858 ) {
1859 let previous_split = self.split_manager.active_split();
1860 let previous_buffer = self.active_buffer(); let split_changed = previous_split != split_id;
1862
1863 if split_changed {
1864 if self.terminal_mode && self.is_terminal_buffer(previous_buffer) {
1866 self.terminal_mode = false;
1867 self.key_context = crate::input::keybindings::KeyContext::Normal;
1868 }
1869
1870 self.split_manager.set_active_split(split_id);
1872
1873 self.split_manager.set_active_buffer_id(buffer_id);
1875
1876 if self.is_terminal_buffer(buffer_id) {
1878 self.terminal_mode = true;
1879 self.key_context = crate::input::keybindings::KeyContext::Terminal;
1880 } else {
1881 self.key_context = crate::input::keybindings::KeyContext::Normal;
1884 }
1885
1886 if previous_buffer != buffer_id {
1888 self.position_history.commit_pending_movement();
1889 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
1890 view_state.add_buffer(buffer_id);
1891 view_state.push_focus(previous_buffer);
1892 }
1893 }
1896 } else {
1897 self.set_active_buffer(buffer_id);
1899 }
1900 }
1901
1902 pub fn active_state(&self) -> &EditorState {
1904 self.buffers.get(&self.active_buffer()).unwrap()
1905 }
1906
1907 pub fn active_state_mut(&mut self) -> &mut EditorState {
1909 self.buffers.get_mut(&self.active_buffer()).unwrap()
1910 }
1911
1912 pub fn set_completion_items(&mut self, items: Vec<lsp_types::CompletionItem>) {
1914 self.completion_items = Some(items);
1915 }
1916
1917 pub fn active_viewport(&self) -> &crate::view::viewport::Viewport {
1919 let active_split = self.split_manager.active_split();
1920 &self.split_view_states.get(&active_split).unwrap().viewport
1921 }
1922
1923 pub fn active_viewport_mut(&mut self) -> &mut crate::view::viewport::Viewport {
1925 let active_split = self.split_manager.active_split();
1926 &mut self
1927 .split_view_states
1928 .get_mut(&active_split)
1929 .unwrap()
1930 .viewport
1931 }
1932
1933 pub fn get_buffer_display_name(&self, buffer_id: BufferId) -> String {
1935 if let Some(composite) = self.composite_buffers.get(&buffer_id) {
1937 return composite.name.clone();
1938 }
1939
1940 self.buffer_metadata
1941 .get(&buffer_id)
1942 .map(|m| m.display_name.clone())
1943 .or_else(|| {
1944 self.buffers.get(&buffer_id).and_then(|state| {
1945 state
1946 .buffer
1947 .file_path()
1948 .and_then(|p| p.file_name())
1949 .and_then(|n| n.to_str())
1950 .map(|s| s.to_string())
1951 })
1952 })
1953 .unwrap_or_else(|| "[No Name]".to_string())
1954 }
1955
1956 pub fn apply_event_to_active_buffer(&mut self, event: &Event) {
1965 match event {
1968 Event::Scroll { line_offset } => {
1969 self.handle_scroll_event(*line_offset);
1970 return;
1971 }
1972 Event::SetViewport { top_line } => {
1973 self.handle_set_viewport_event(*top_line);
1974 return;
1975 }
1976 Event::Recenter => {
1977 self.handle_recenter_event();
1978 return;
1979 }
1980 _ => {}
1981 }
1982
1983 let lsp_changes = self.collect_lsp_changes(event);
1987
1988 let line_info = self.calculate_event_line_info(event);
1990
1991 self.active_state_mut().apply(event);
1993
1994 self.sync_editor_state_to_split_view_state();
1997
1998 match event {
2001 Event::Insert { .. } | Event::Delete { .. } | Event::BulkEdit { .. } => {
2002 self.invalidate_layouts_for_buffer(self.active_buffer());
2003 self.schedule_semantic_tokens_full_refresh(self.active_buffer());
2004 }
2005 Event::Batch { events, .. } => {
2006 let has_edits = events
2007 .iter()
2008 .any(|e| matches!(e, Event::Insert { .. } | Event::Delete { .. }));
2009 if has_edits {
2010 self.invalidate_layouts_for_buffer(self.active_buffer());
2011 self.schedule_semantic_tokens_full_refresh(self.active_buffer());
2012 }
2013 }
2014 _ => {}
2015 }
2016
2017 self.adjust_other_split_cursors_for_event(event);
2019
2020 let in_interactive_replace = self.interactive_replace_state.is_some();
2024
2025 let _ = in_interactive_replace; self.trigger_plugin_hooks_for_event(event, line_info);
2034
2035 self.send_lsp_changes_for_buffer(self.active_buffer(), lsp_changes);
2037 }
2038
2039 pub fn apply_events_as_bulk_edit(
2053 &mut self,
2054 events: Vec<Event>,
2055 description: String,
2056 ) -> Option<Event> {
2057 use crate::model::event::CursorId;
2058
2059 let has_buffer_mods = events
2061 .iter()
2062 .any(|e| matches!(e, Event::Insert { .. } | Event::Delete { .. }));
2063
2064 if !has_buffer_mods {
2065 return None;
2067 }
2068
2069 let state = self.active_state_mut();
2070
2071 let old_cursors: Vec<(CursorId, usize, Option<usize>)> = state
2073 .cursors
2074 .iter()
2075 .map(|(id, c)| (id, c.position, c.anchor))
2076 .collect();
2077
2078 let old_tree = state.buffer.snapshot_piece_tree();
2080
2081 let mut edits: Vec<(usize, usize, String)> = Vec::new();
2085
2086 for event in &events {
2087 match event {
2088 Event::Insert { position, text, .. } => {
2089 edits.push((*position, 0, text.clone()));
2090 }
2091 Event::Delete { range, .. } => {
2092 edits.push((range.start, range.len(), String::new()));
2093 }
2094 _ => {}
2095 }
2096 }
2097
2098 edits.sort_by(|a, b| b.0.cmp(&a.0));
2100
2101 let edit_refs: Vec<(usize, usize, &str)> = edits
2103 .iter()
2104 .map(|(pos, del, text)| (*pos, *del, text.as_str()))
2105 .collect();
2106
2107 let _delta = state.buffer.apply_bulk_edits(&edit_refs);
2109
2110 let new_tree = state.buffer.snapshot_piece_tree();
2112
2113 let mut new_cursors: Vec<(CursorId, usize, Option<usize>)> = old_cursors.clone();
2116
2117 let mut position_deltas: Vec<(usize, isize)> = Vec::new();
2120 for (pos, del_len, text) in &edits {
2121 let delta = text.len() as isize - *del_len as isize;
2122 position_deltas.push((*pos, delta));
2123 }
2124 position_deltas.sort_by_key(|(pos, _)| *pos);
2125
2126 let calc_shift = |original_pos: usize| -> isize {
2128 let mut shift: isize = 0;
2129 for (edit_pos, delta) in &position_deltas {
2130 if *edit_pos < original_pos {
2131 shift += delta;
2132 }
2133 }
2134 shift
2135 };
2136
2137 for (cursor_id, ref mut pos, ref mut anchor) in &mut new_cursors {
2141 let mut found_move_cursor = false;
2142 let original_pos = *pos;
2144
2145 let insert_at_cursor_pos = events.iter().any(|e| {
2149 matches!(e, Event::Insert { position, cursor_id: c, .. }
2150 if *c == *cursor_id && *position == original_pos)
2151 });
2152
2153 for event in &events {
2155 if let Event::MoveCursor {
2156 cursor_id: event_cursor,
2157 new_position,
2158 new_anchor,
2159 ..
2160 } = event
2161 {
2162 if event_cursor == cursor_id {
2163 let shift = if insert_at_cursor_pos {
2167 calc_shift(original_pos)
2168 } else {
2169 0
2170 };
2171 *pos = (*new_position as isize + shift) as usize;
2172 *anchor = *new_anchor;
2173 found_move_cursor = true;
2174 }
2175 }
2176 }
2177
2178 if !found_move_cursor {
2180 for event in &events {
2181 match event {
2182 Event::Insert {
2183 position,
2184 text,
2185 cursor_id: event_cursor,
2186 } if event_cursor == cursor_id => {
2187 let shift = calc_shift(*position);
2190 let adjusted_pos = (*position as isize + shift) as usize;
2191 *pos = adjusted_pos + text.len();
2192 *anchor = None;
2193 }
2194 Event::Delete {
2195 range,
2196 cursor_id: event_cursor,
2197 ..
2198 } if event_cursor == cursor_id => {
2199 let shift = calc_shift(range.start);
2202 *pos = (range.start as isize + shift) as usize;
2203 *anchor = None;
2204 }
2205 _ => {}
2206 }
2207 }
2208 }
2209 }
2210
2211 for (cursor_id, position, anchor) in &new_cursors {
2213 if let Some(cursor) = state.cursors.get_mut(*cursor_id) {
2214 cursor.position = *position;
2215 cursor.anchor = *anchor;
2216 }
2217 }
2218
2219 state.highlighter.invalidate_all();
2221
2222 let bulk_edit = Event::BulkEdit {
2224 old_tree: Some(old_tree),
2225 new_tree: Some(new_tree),
2226 old_cursors,
2227 new_cursors,
2228 description,
2229 };
2230
2231 self.sync_editor_state_to_split_view_state();
2233 self.invalidate_layouts_for_buffer(self.active_buffer());
2234 self.adjust_other_split_cursors_for_event(&bulk_edit);
2235 Some(bulk_edit)
2238 }
2239
2240 fn trigger_plugin_hooks_for_event(&mut self, event: &Event, line_info: EventLineInfo) {
2243 let buffer_id = self.active_buffer();
2244
2245 let hook_args = match event {
2247 Event::Insert { position, text, .. } => {
2248 let insert_position = *position;
2249 let insert_len = text.len();
2250
2251 if let Some(seen) = self.seen_byte_ranges.get_mut(&buffer_id) {
2253 let adjusted: std::collections::HashSet<(usize, usize)> = seen
2258 .iter()
2259 .filter_map(|&(start, end)| {
2260 if end <= insert_position {
2261 Some((start, end))
2263 } else if start >= insert_position {
2264 Some((start + insert_len, end + insert_len))
2266 } else {
2267 None
2269 }
2270 })
2271 .collect();
2272 *seen = adjusted;
2273 }
2274
2275 Some((
2276 "after_insert",
2277 crate::services::plugins::hooks::HookArgs::AfterInsert {
2278 buffer_id,
2279 position: *position,
2280 text: text.clone(),
2281 affected_start: insert_position,
2283 affected_end: insert_position + insert_len,
2284 start_line: line_info.start_line,
2286 end_line: line_info.end_line,
2287 lines_added: line_info.line_delta.max(0) as usize,
2288 },
2289 ))
2290 }
2291 Event::Delete {
2292 range,
2293 deleted_text,
2294 ..
2295 } => {
2296 let delete_start = range.start;
2297
2298 let delete_end = range.end;
2300 let delete_len = delete_end - delete_start;
2301 if let Some(seen) = self.seen_byte_ranges.get_mut(&buffer_id) {
2302 let adjusted: std::collections::HashSet<(usize, usize)> = seen
2307 .iter()
2308 .filter_map(|&(start, end)| {
2309 if end <= delete_start {
2310 Some((start, end))
2312 } else if start >= delete_end {
2313 Some((start - delete_len, end - delete_len))
2315 } else {
2316 None
2318 }
2319 })
2320 .collect();
2321 *seen = adjusted;
2322 }
2323
2324 Some((
2325 "after_delete",
2326 crate::services::plugins::hooks::HookArgs::AfterDelete {
2327 buffer_id,
2328 range: range.clone(),
2329 deleted_text: deleted_text.clone(),
2330 affected_start: delete_start,
2332 deleted_len: deleted_text.len(),
2333 start_line: line_info.start_line,
2335 end_line: line_info.end_line,
2336 lines_removed: (-line_info.line_delta).max(0) as usize,
2337 },
2338 ))
2339 }
2340 Event::Batch { events, .. } => {
2341 for e in events {
2345 let sub_line_info = self.calculate_event_line_info(e);
2348 self.trigger_plugin_hooks_for_event(e, sub_line_info);
2349 }
2350 None
2351 }
2352 Event::MoveCursor {
2353 cursor_id,
2354 old_position,
2355 new_position,
2356 ..
2357 } => {
2358 let line = self.active_state().buffer.get_line_number(*new_position) + 1;
2360 Some((
2361 "cursor_moved",
2362 crate::services::plugins::hooks::HookArgs::CursorMoved {
2363 buffer_id,
2364 cursor_id: *cursor_id,
2365 old_position: *old_position,
2366 new_position: *new_position,
2367 line,
2368 },
2369 ))
2370 }
2371 _ => None,
2372 };
2373
2374 if let Some((hook_name, args)) = hook_args {
2376 #[cfg(feature = "plugins")]
2380 self.update_plugin_state_snapshot();
2381
2382 self.plugin_manager.run_hook(hook_name, args);
2383 }
2384 }
2385
2386 fn handle_scroll_event(&mut self, line_offset: isize) {
2392 use crate::view::ui::view_pipeline::ViewLineIterator;
2393
2394 let active_split = self.split_manager.active_split();
2395
2396 if let Some(group) = self.scroll_sync_manager.find_group_for_split(active_split) {
2400 let left = group.left_split;
2401 let right = group.right_split;
2402 if let Some(vs) = self.split_view_states.get_mut(&left) {
2403 vs.viewport.set_skip_ensure_visible();
2404 }
2405 if let Some(vs) = self.split_view_states.get_mut(&right) {
2406 vs.viewport.set_skip_ensure_visible();
2407 }
2408 }
2410
2411 let sync_group = self
2413 .split_view_states
2414 .get(&active_split)
2415 .and_then(|vs| vs.sync_group);
2416 let splits_to_scroll = if let Some(group_id) = sync_group {
2417 self.split_manager
2418 .get_splits_in_group(group_id, &self.split_view_states)
2419 } else {
2420 vec![active_split]
2421 };
2422
2423 for split_id in splits_to_scroll {
2424 let buffer_id = if let Some(id) = self.split_manager.buffer_for_split(split_id) {
2425 id
2426 } else {
2427 continue;
2428 };
2429 let tab_size = self.config.editor.tab_size;
2430
2431 let view_transform_tokens = self
2433 .split_view_states
2434 .get(&split_id)
2435 .and_then(|vs| vs.view_transform.as_ref())
2436 .map(|vt| vt.tokens.clone());
2437
2438 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2440 let buffer = &mut state.buffer;
2441 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2442 if let Some(tokens) = view_transform_tokens {
2443 let view_lines: Vec<_> =
2445 ViewLineIterator::new(&tokens, false, false, tab_size).collect();
2446 view_state
2447 .viewport
2448 .scroll_view_lines(&view_lines, line_offset);
2449 } else {
2450 if line_offset > 0 {
2452 view_state
2453 .viewport
2454 .scroll_down(buffer, line_offset as usize);
2455 } else {
2456 view_state
2457 .viewport
2458 .scroll_up(buffer, line_offset.unsigned_abs());
2459 }
2460 }
2461 view_state.viewport.set_skip_ensure_visible();
2463 }
2464 }
2465 }
2466 }
2467
2468 fn handle_set_viewport_event(&mut self, top_line: usize) {
2470 let active_split = self.split_manager.active_split();
2471
2472 if self.scroll_sync_manager.is_split_synced(active_split) {
2475 if let Some(group) = self
2476 .scroll_sync_manager
2477 .find_group_for_split_mut(active_split)
2478 {
2479 let scroll_line = if group.is_left_split(active_split) {
2481 top_line
2482 } else {
2483 group.right_to_left_line(top_line)
2484 };
2485 group.set_scroll_line(scroll_line);
2486 }
2487
2488 if let Some(group) = self.scroll_sync_manager.find_group_for_split(active_split) {
2490 let left = group.left_split;
2491 let right = group.right_split;
2492 if let Some(vs) = self.split_view_states.get_mut(&left) {
2493 vs.viewport.set_skip_ensure_visible();
2494 }
2495 if let Some(vs) = self.split_view_states.get_mut(&right) {
2496 vs.viewport.set_skip_ensure_visible();
2497 }
2498 }
2499 return;
2500 }
2501
2502 let sync_group = self
2504 .split_view_states
2505 .get(&active_split)
2506 .and_then(|vs| vs.sync_group);
2507 let splits_to_scroll = if let Some(group_id) = sync_group {
2508 self.split_manager
2509 .get_splits_in_group(group_id, &self.split_view_states)
2510 } else {
2511 vec![active_split]
2512 };
2513
2514 for split_id in splits_to_scroll {
2515 let buffer_id = if let Some(id) = self.split_manager.buffer_for_split(split_id) {
2516 id
2517 } else {
2518 continue;
2519 };
2520
2521 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2522 let buffer = &mut state.buffer;
2523 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2524 view_state.viewport.scroll_to(buffer, top_line);
2525 view_state.viewport.set_skip_ensure_visible();
2527 }
2528 }
2529 }
2530 }
2531
2532 fn handle_recenter_event(&mut self) {
2534 let active_split = self.split_manager.active_split();
2535
2536 let sync_group = self
2538 .split_view_states
2539 .get(&active_split)
2540 .and_then(|vs| vs.sync_group);
2541 let splits_to_recenter = if let Some(group_id) = sync_group {
2542 self.split_manager
2543 .get_splits_in_group(group_id, &self.split_view_states)
2544 } else {
2545 vec![active_split]
2546 };
2547
2548 for split_id in splits_to_recenter {
2549 let buffer_id = if let Some(id) = self.split_manager.buffer_for_split(split_id) {
2550 id
2551 } else {
2552 continue;
2553 };
2554
2555 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2556 let buffer = &mut state.buffer;
2557 let view_state = self.split_view_states.get_mut(&split_id);
2558
2559 if let Some(view_state) = view_state {
2560 let cursor = *view_state.cursors.primary();
2562 let viewport_height = view_state.viewport.visible_line_count();
2563 let target_rows_from_top = viewport_height / 2;
2564
2565 let mut iter = buffer.line_iterator(cursor.position, 80);
2567 for _ in 0..target_rows_from_top {
2568 if iter.prev().is_none() {
2569 break;
2570 }
2571 }
2572 let new_top_byte = iter.current_position();
2573 view_state.viewport.top_byte = new_top_byte;
2574 view_state.viewport.set_skip_ensure_visible();
2576 }
2577 }
2578 }
2579 }
2580
2581 fn invalidate_layouts_for_buffer(&mut self, buffer_id: BufferId) {
2586 let splits_for_buffer = self.split_manager.splits_for_buffer(buffer_id);
2588
2589 for split_id in splits_for_buffer {
2591 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2592 view_state.invalidate_layout();
2593 }
2594 }
2595 }
2596
2597 pub fn active_event_log(&self) -> &EventLog {
2599 self.event_logs.get(&self.active_buffer()).unwrap()
2600 }
2601
2602 pub fn active_event_log_mut(&mut self) -> &mut EventLog {
2604 self.event_logs.get_mut(&self.active_buffer()).unwrap()
2605 }
2606
2607 pub(super) fn update_modified_from_event_log(&mut self) {
2611 let is_at_saved = self
2612 .event_logs
2613 .get(&self.active_buffer())
2614 .map(|log| log.is_at_saved_position())
2615 .unwrap_or(false);
2616
2617 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
2618 state.buffer.set_modified(!is_at_saved);
2619 }
2620 }
2621
2622 pub fn should_quit(&self) -> bool {
2624 self.should_quit
2625 }
2626
2627 pub fn should_restart(&self) -> bool {
2629 self.restart_with_dir.is_some()
2630 }
2631
2632 pub fn take_restart_dir(&mut self) -> Option<PathBuf> {
2635 self.restart_with_dir.take()
2636 }
2637
2638 pub fn request_full_redraw(&mut self) {
2643 self.full_redraw_requested = true;
2644 }
2645
2646 pub fn take_full_redraw_request(&mut self) -> bool {
2648 let requested = self.full_redraw_requested;
2649 self.full_redraw_requested = false;
2650 requested
2651 }
2652
2653 pub fn request_restart(&mut self, new_working_dir: PathBuf) {
2654 tracing::info!(
2655 "Restart requested with new working directory: {}",
2656 new_working_dir.display()
2657 );
2658 self.restart_with_dir = Some(new_working_dir);
2659 self.should_quit = true;
2661 }
2662
2663 pub fn theme(&self) -> &crate::view::theme::Theme {
2665 &self.theme
2666 }
2667
2668 pub fn is_settings_open(&self) -> bool {
2670 self.settings_state.as_ref().is_some_and(|s| s.visible)
2671 }
2672
2673 pub fn quit(&mut self) {
2675 let modified_count = self.count_modified_buffers();
2677 if modified_count > 0 {
2678 let discard_key = t!("prompt.key.discard").to_string();
2680 let cancel_key = t!("prompt.key.cancel").to_string();
2681 let msg = if modified_count == 1 {
2682 t!(
2683 "prompt.quit_modified_one",
2684 discard_key = discard_key,
2685 cancel_key = cancel_key
2686 )
2687 .to_string()
2688 } else {
2689 t!(
2690 "prompt.quit_modified_many",
2691 count = modified_count,
2692 discard_key = discard_key,
2693 cancel_key = cancel_key
2694 )
2695 .to_string()
2696 };
2697 self.start_prompt(msg, PromptType::ConfirmQuitWithModified);
2698 } else {
2699 self.should_quit = true;
2700 }
2701 }
2702
2703 fn count_modified_buffers(&self) -> usize {
2705 self.buffers
2706 .values()
2707 .filter(|state| state.buffer.is_modified())
2708 .count()
2709 }
2710
2711 pub fn resize(&mut self, width: u16, height: u16) {
2713 self.terminal_width = width;
2715 self.terminal_height = height;
2716
2717 for view_state in self.split_view_states.values_mut() {
2719 view_state.viewport.resize(width, height);
2720 }
2721
2722 self.resize_visible_terminals();
2724 }
2725
2726 pub fn start_prompt(&mut self, message: String, prompt_type: PromptType) {
2730 self.start_prompt_with_suggestions(message, prompt_type, Vec::new());
2731 }
2732
2733 fn start_search_prompt(
2738 &mut self,
2739 message: String,
2740 prompt_type: PromptType,
2741 use_selection_range: bool,
2742 ) {
2743 self.pending_search_range = None;
2745
2746 let selection_range = {
2747 let state = self.active_state();
2748 state.cursors.primary().selection_range()
2749 };
2750
2751 let selected_text = if let Some(range) = selection_range.clone() {
2752 let state = self.active_state_mut();
2753 let text = state.get_text_range(range.start, range.end);
2754 if !text.contains('\n') && !text.is_empty() {
2755 Some(text)
2756 } else {
2757 None
2758 }
2759 } else {
2760 None
2761 };
2762
2763 if use_selection_range {
2764 self.pending_search_range = selection_range;
2765 }
2766
2767 let from_history = selected_text.is_none();
2769 let default_text = selected_text.or_else(|| {
2770 self.get_prompt_history("search")
2771 .and_then(|h| h.last().map(|s| s.to_string()))
2772 });
2773
2774 self.start_prompt(message, prompt_type);
2776
2777 if let Some(text) = default_text {
2779 if let Some(ref mut prompt) = self.prompt {
2780 prompt.set_input(text.clone());
2781 prompt.selection_anchor = Some(0);
2782 prompt.cursor_pos = text.len();
2783 }
2784 if from_history {
2785 self.get_or_create_prompt_history("search").init_at_last();
2786 }
2787 self.update_search_highlights(&text);
2788 }
2789 }
2790
2791 pub fn start_prompt_with_suggestions(
2793 &mut self,
2794 message: String,
2795 prompt_type: PromptType,
2796 suggestions: Vec<Suggestion>,
2797 ) {
2798 self.on_editor_focus_lost();
2800
2801 match prompt_type {
2804 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
2805 self.clear_search_highlights();
2806 }
2807 _ => {}
2808 }
2809
2810 let needs_suggestions = matches!(
2812 prompt_type,
2813 PromptType::OpenFile
2814 | PromptType::SwitchProject
2815 | PromptType::SaveFileAs
2816 | PromptType::Command
2817 );
2818
2819 self.prompt = Some(Prompt::with_suggestions(message, prompt_type, suggestions));
2820
2821 if needs_suggestions {
2823 self.update_prompt_suggestions();
2824 }
2825 }
2826
2827 pub fn start_prompt_with_initial_text(
2829 &mut self,
2830 message: String,
2831 prompt_type: PromptType,
2832 initial_text: String,
2833 ) {
2834 self.on_editor_focus_lost();
2836
2837 self.prompt = Some(Prompt::with_initial_text(
2838 message,
2839 prompt_type,
2840 initial_text,
2841 ));
2842 }
2843
2844 pub fn start_quick_open(&mut self) {
2846 self.on_editor_focus_lost();
2848
2849 self.status_message = None;
2851
2852 let mut prompt = Prompt::with_suggestions(String::new(), PromptType::QuickOpen, vec![]);
2854 prompt.input = ">".to_string();
2855 prompt.cursor_pos = 1;
2856 self.prompt = Some(prompt);
2857
2858 self.update_quick_open_suggestions(">");
2860 }
2861
2862 fn update_quick_open_suggestions(&mut self, input: &str) {
2864 let suggestions = if input.starts_with('>') {
2865 let query = &input[1..];
2867 let active_buffer_mode = self
2868 .buffer_metadata
2869 .get(&self.active_buffer())
2870 .and_then(|m| m.virtual_mode());
2871 self.command_registry.read().unwrap().filter(
2872 query,
2873 self.key_context,
2874 &self.keybindings,
2875 self.has_active_selection(),
2876 &self.active_custom_contexts,
2877 active_buffer_mode,
2878 )
2879 } else if input.starts_with('#') {
2880 let query = &input[1..];
2882 self.get_buffer_suggestions(query)
2883 } else if input.starts_with(':') {
2884 let line_str = &input[1..];
2886 self.get_goto_line_suggestions(line_str)
2887 } else {
2888 self.get_file_suggestions(input)
2890 };
2891
2892 if let Some(prompt) = &mut self.prompt {
2893 prompt.suggestions = suggestions;
2894 prompt.selected_suggestion = if prompt.suggestions.is_empty() {
2895 None
2896 } else {
2897 Some(0)
2898 };
2899 }
2900 }
2901
2902 fn get_buffer_suggestions(&self, query: &str) -> Vec<Suggestion> {
2904 use crate::input::fuzzy::fuzzy_match;
2905
2906 let mut suggestions: Vec<(Suggestion, i32)> = self
2907 .buffers
2908 .iter()
2909 .filter_map(|(buffer_id, state)| {
2910 let path = state.buffer.file_path()?;
2911 let name = path
2912 .file_name()
2913 .map(|n| n.to_string_lossy().to_string())
2914 .unwrap_or_else(|| format!("Buffer {}", buffer_id.0));
2915
2916 let match_result = if query.is_empty() {
2917 crate::input::fuzzy::FuzzyMatch {
2918 matched: true,
2919 score: 0,
2920 match_positions: vec![],
2921 }
2922 } else {
2923 fuzzy_match(query, &name)
2924 };
2925
2926 if match_result.matched {
2927 let modified = state.buffer.is_modified();
2928 let display_name = if modified {
2929 format!("{} [+]", name)
2930 } else {
2931 name
2932 };
2933
2934 Some((
2935 Suggestion {
2936 text: display_name,
2937 description: Some(path.display().to_string()),
2938 value: Some(buffer_id.0.to_string()),
2939 disabled: false,
2940 keybinding: None,
2941 source: None,
2942 },
2943 match_result.score,
2944 ))
2945 } else {
2946 None
2947 }
2948 })
2949 .collect();
2950
2951 suggestions.sort_by(|a, b| b.1.cmp(&a.1));
2952 suggestions.into_iter().map(|(s, _)| s).collect()
2953 }
2954
2955 fn get_goto_line_suggestions(&self, line_str: &str) -> Vec<Suggestion> {
2957 if line_str.is_empty() {
2958 return vec![Suggestion {
2959 text: t!("quick_open.goto_line_hint").to_string(),
2960 description: Some(t!("quick_open.goto_line_desc").to_string()),
2961 value: None,
2962 disabled: true,
2963 keybinding: None,
2964 source: None,
2965 }];
2966 }
2967
2968 if let Ok(line_num) = line_str.parse::<usize>() {
2969 if line_num > 0 {
2970 return vec![Suggestion {
2971 text: t!("quick_open.goto_line", line = line_num.to_string()).to_string(),
2972 description: Some(t!("quick_open.press_enter").to_string()),
2973 value: Some(line_num.to_string()),
2974 disabled: false,
2975 keybinding: None,
2976 source: None,
2977 }];
2978 }
2979 }
2980
2981 vec![Suggestion {
2982 text: t!("quick_open.invalid_line").to_string(),
2983 description: Some(line_str.to_string()),
2984 value: None,
2985 disabled: true,
2986 keybinding: None,
2987 source: None,
2988 }]
2989 }
2990
2991 fn get_file_suggestions(&self, query: &str) -> Vec<Suggestion> {
2993 let cwd = self.working_dir.display().to_string();
2995 let context = QuickOpenContext {
2996 cwd: cwd.clone(),
2997 open_buffers: vec![], active_buffer_id: self.active_buffer().0,
2999 active_buffer_path: self
3000 .active_state()
3001 .buffer
3002 .file_path()
3003 .map(|p| p.display().to_string()),
3004 has_selection: self.has_active_selection(),
3005 key_context: self.key_context,
3006 custom_contexts: self.active_custom_contexts.clone(),
3007 buffer_mode: self
3008 .buffer_metadata
3009 .get(&self.active_buffer())
3010 .and_then(|m| m.virtual_mode())
3011 .map(|s| s.to_string()),
3012 };
3013
3014 self.file_provider.suggestions(query, &context)
3015 }
3016
3017 fn cancel_search_prompt_if_active(&mut self) {
3020 if let Some(ref prompt) = self.prompt {
3021 if matches!(
3022 prompt.prompt_type,
3023 PromptType::Search
3024 | PromptType::ReplaceSearch
3025 | PromptType::Replace { .. }
3026 | PromptType::QueryReplaceSearch
3027 | PromptType::QueryReplace { .. }
3028 | PromptType::QueryReplaceConfirm
3029 ) {
3030 self.prompt = None;
3031 self.interactive_replace_state = None;
3033 let ns = self.search_namespace.clone();
3035 let state = self.active_state_mut();
3036 state.overlays.clear_namespace(&ns, &mut state.marker_list);
3037 }
3038 }
3039 }
3040
3041 fn prefill_open_file_prompt(&mut self) {
3043 if let Some(prompt) = self.prompt.as_mut() {
3047 if prompt.prompt_type == PromptType::OpenFile {
3048 prompt.input.clear();
3049 prompt.cursor_pos = 0;
3050 prompt.selection_anchor = None;
3051 }
3052 }
3053 }
3054
3055 fn init_file_open_state(&mut self) {
3061 let buffer_id = self.active_buffer();
3063
3064 let initial_dir = if self.is_terminal_buffer(buffer_id) {
3067 self.get_terminal_id(buffer_id)
3068 .and_then(|tid| self.terminal_manager.get(tid))
3069 .and_then(|handle| handle.cwd())
3070 .unwrap_or_else(|| self.working_dir.clone())
3071 } else {
3072 self.active_state()
3073 .buffer
3074 .file_path()
3075 .and_then(|path| path.parent())
3076 .map(|p| p.to_path_buf())
3077 .unwrap_or_else(|| self.working_dir.clone())
3078 };
3079
3080 let show_hidden = self.config.file_browser.show_hidden;
3082 self.file_open_state = Some(file_open::FileOpenState::new(
3083 initial_dir.clone(),
3084 show_hidden,
3085 ));
3086
3087 self.load_file_open_directory(initial_dir);
3089 }
3090
3091 fn init_folder_open_state(&mut self) {
3096 let initial_dir = self.working_dir.clone();
3098
3099 let show_hidden = self.config.file_browser.show_hidden;
3101 self.file_open_state = Some(file_open::FileOpenState::new(
3102 initial_dir.clone(),
3103 show_hidden,
3104 ));
3105
3106 self.load_file_open_directory(initial_dir);
3108 }
3109
3110 pub fn change_working_dir(&mut self, new_path: PathBuf) {
3120 let new_path = new_path.canonicalize().unwrap_or(new_path);
3122
3123 self.request_restart(new_path);
3126 }
3127
3128 fn load_file_open_directory(&mut self, path: PathBuf) {
3130 if let Some(state) = &mut self.file_open_state {
3132 state.current_dir = path.clone();
3133 state.loading = true;
3134 state.error = None;
3135 state.update_shortcuts();
3136 }
3137
3138 if let Some(ref runtime) = self.tokio_runtime {
3140 let fs_manager = self.fs_manager.clone();
3141 let sender = self.async_bridge.as_ref().map(|b| b.sender());
3142
3143 runtime.spawn(async move {
3144 let result = fs_manager.list_dir_with_metadata(path).await;
3145 if let Some(sender) = sender {
3146 let _ = sender.send(AsyncMessage::FileOpenDirectoryLoaded(result));
3147 }
3148 });
3149 } else {
3150 if let Some(state) = &mut self.file_open_state {
3152 state.set_error("Async runtime not available".to_string());
3153 }
3154 }
3155 }
3156
3157 pub(super) fn handle_file_open_directory_loaded(
3159 &mut self,
3160 result: std::io::Result<Vec<crate::services::fs::DirEntry>>,
3161 ) {
3162 match result {
3163 Ok(entries) => {
3164 if let Some(state) = &mut self.file_open_state {
3165 state.set_entries(entries);
3166 }
3167 let filter = self
3169 .prompt
3170 .as_ref()
3171 .map(|p| p.input.clone())
3172 .unwrap_or_default();
3173 if !filter.is_empty() {
3174 if let Some(state) = &mut self.file_open_state {
3175 state.apply_filter(&filter);
3176 }
3177 }
3178 }
3179 Err(e) => {
3180 if let Some(state) = &mut self.file_open_state {
3181 state.set_error(e.to_string());
3182 }
3183 }
3184 }
3185 }
3186
3187 pub fn cancel_prompt(&mut self) {
3189 let theme_to_restore = if let Some(ref prompt) = self.prompt {
3191 if let PromptType::SelectTheme { original_theme } = &prompt.prompt_type {
3192 Some(original_theme.clone())
3193 } else {
3194 None
3195 }
3196 } else {
3197 None
3198 };
3199
3200 if let Some(ref prompt) = self.prompt {
3202 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
3204 if let Some(history) = self.prompt_histories.get_mut(&key) {
3205 history.reset_navigation();
3206 }
3207 }
3208 match &prompt.prompt_type {
3209 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
3210 self.clear_search_highlights();
3211 }
3212 PromptType::Plugin { custom_type } => {
3213 use crate::services::plugins::hooks::HookArgs;
3215 self.plugin_manager.run_hook(
3216 "prompt_cancelled",
3217 HookArgs::PromptCancelled {
3218 prompt_type: custom_type.clone(),
3219 input: prompt.input.clone(),
3220 },
3221 );
3222 }
3223 PromptType::LspRename { overlay_handle, .. } => {
3224 let remove_overlay_event = crate::model::event::Event::RemoveOverlay {
3226 handle: overlay_handle.clone(),
3227 };
3228 self.apply_event_to_active_buffer(&remove_overlay_event);
3229 }
3230 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs => {
3231 self.file_open_state = None;
3233 self.file_browser_layout = None;
3234 }
3235 PromptType::AsyncPrompt => {
3236 if let Some(callback_id) = self.pending_async_prompt_callback.take() {
3238 self.plugin_manager
3239 .resolve_callback(callback_id, "null".to_string());
3240 }
3241 }
3242 _ => {}
3243 }
3244 }
3245
3246 self.prompt = None;
3247 self.pending_search_range = None;
3248 self.status_message = Some(t!("search.cancelled").to_string());
3249
3250 if let Some(original_theme) = theme_to_restore {
3252 self.preview_theme(&original_theme);
3253 }
3254 }
3255
3256 pub fn handle_prompt_scroll(&mut self, delta: i32) -> bool {
3259 if let Some(ref mut prompt) = self.prompt {
3260 if prompt.suggestions.is_empty() {
3261 return false;
3262 }
3263
3264 let current = prompt.selected_suggestion.unwrap_or(0);
3265 let len = prompt.suggestions.len();
3266
3267 let new_selected = if delta < 0 {
3270 current.saturating_sub((-delta) as usize)
3272 } else {
3273 (current + delta as usize).min(len.saturating_sub(1))
3275 };
3276
3277 prompt.selected_suggestion = Some(new_selected);
3278
3279 if !matches!(prompt.prompt_type, PromptType::Plugin { .. }) {
3281 if let Some(suggestion) = prompt.suggestions.get(new_selected) {
3282 prompt.input = suggestion.get_value().to_string();
3283 prompt.cursor_pos = prompt.input.len();
3284 }
3285 }
3286
3287 return true;
3288 }
3289 false
3290 }
3291
3292 pub fn confirm_prompt(&mut self) -> Option<(String, PromptType, Option<usize>)> {
3297 if let Some(prompt) = self.prompt.take() {
3298 let selected_index = prompt.selected_suggestion;
3299 let final_input = if matches!(
3301 prompt.prompt_type,
3302 PromptType::Command
3303 | PromptType::OpenFile
3304 | PromptType::SwitchProject
3305 | PromptType::SaveFileAs
3306 | PromptType::StopLspServer
3307 | PromptType::SelectTheme { .. }
3308 | PromptType::SelectLocale
3309 | PromptType::SwitchToTab
3310 | PromptType::SetLanguage
3311 | PromptType::Plugin { .. }
3312 ) {
3313 if let Some(selected_idx) = prompt.selected_suggestion {
3315 if let Some(suggestion) = prompt.suggestions.get(selected_idx) {
3316 if suggestion.disabled {
3318 if matches!(prompt.prompt_type, PromptType::Command) {
3320 self.command_registry
3321 .write()
3322 .unwrap()
3323 .record_usage(&suggestion.text);
3324 }
3325 self.set_status_message(
3326 t!(
3327 "error.command_not_available",
3328 command = suggestion.text.clone()
3329 )
3330 .to_string(),
3331 );
3332 return None;
3333 }
3334 suggestion.get_value().to_string()
3336 } else {
3337 prompt.input.clone()
3338 }
3339 } else {
3340 prompt.input.clone()
3341 }
3342 } else {
3343 prompt.input.clone()
3344 };
3345
3346 if matches!(prompt.prompt_type, PromptType::StopLspServer) {
3348 let is_valid = prompt
3349 .suggestions
3350 .iter()
3351 .any(|s| s.text == final_input || s.get_value() == final_input);
3352 if !is_valid {
3353 self.prompt = Some(prompt);
3355 self.set_status_message(
3356 t!("error.no_lsp_match", input = final_input.clone()).to_string(),
3357 );
3358 return None;
3359 }
3360 }
3361
3362 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
3364 let history = self.get_or_create_prompt_history(&key);
3365 history.push(final_input.clone());
3366 history.reset_navigation();
3367 }
3368
3369 Some((final_input, prompt.prompt_type, selected_index))
3370 } else {
3371 None
3372 }
3373 }
3374
3375 pub fn is_prompting(&self) -> bool {
3377 self.prompt.is_some()
3378 }
3379
3380 fn get_or_create_prompt_history(
3382 &mut self,
3383 key: &str,
3384 ) -> &mut crate::input::input_history::InputHistory {
3385 self.prompt_histories.entry(key.to_string()).or_default()
3386 }
3387
3388 fn get_prompt_history(&self, key: &str) -> Option<&crate::input::input_history::InputHistory> {
3390 self.prompt_histories.get(key)
3391 }
3392
3393 fn prompt_type_to_history_key(prompt_type: &crate::view::prompt::PromptType) -> Option<String> {
3395 use crate::view::prompt::PromptType;
3396 match prompt_type {
3397 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
3398 Some("search".to_string())
3399 }
3400 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
3401 Some("replace".to_string())
3402 }
3403 PromptType::GotoLine => Some("goto_line".to_string()),
3404 PromptType::Plugin { custom_type } => Some(format!("plugin:{}", custom_type)),
3405 _ => None,
3406 }
3407 }
3408
3409 pub fn editor_mode(&self) -> Option<String> {
3412 self.editor_mode.clone()
3413 }
3414
3415 pub fn command_registry(&self) -> &Arc<RwLock<CommandRegistry>> {
3417 &self.command_registry
3418 }
3419
3420 pub fn plugin_manager(&self) -> &PluginManager {
3422 &self.plugin_manager
3423 }
3424
3425 pub fn plugin_manager_mut(&mut self) -> &mut PluginManager {
3427 &mut self.plugin_manager
3428 }
3429
3430 pub fn file_explorer_is_focused(&self) -> bool {
3432 self.key_context == KeyContext::FileExplorer
3433 }
3434
3435 pub fn prompt_input(&self) -> Option<&str> {
3437 self.prompt.as_ref().map(|p| p.input.as_str())
3438 }
3439
3440 pub fn has_active_selection(&self) -> bool {
3442 self.active_state()
3443 .cursors
3444 .primary()
3445 .selection_range()
3446 .is_some()
3447 }
3448
3449 pub fn prompt_mut(&mut self) -> Option<&mut Prompt> {
3451 self.prompt.as_mut()
3452 }
3453
3454 pub fn set_status_message(&mut self, message: String) {
3456 tracing::info!(target: "status", "{}", message);
3457 self.plugin_status_message = None;
3458 self.status_message = Some(message);
3459 }
3460
3461 pub fn get_status_message(&self) -> Option<&String> {
3463 self.plugin_status_message
3464 .as_ref()
3465 .or(self.status_message.as_ref())
3466 }
3467
3468 pub fn get_plugin_errors(&self) -> &[String] {
3471 &self.plugin_errors
3472 }
3473
3474 pub fn clear_plugin_errors(&mut self) {
3476 self.plugin_errors.clear();
3477 }
3478
3479 pub fn update_prompt_suggestions(&mut self) {
3481 let (prompt_type, input) = if let Some(prompt) = &self.prompt {
3483 (prompt.prompt_type.clone(), prompt.input.clone())
3484 } else {
3485 return;
3486 };
3487
3488 match prompt_type {
3489 PromptType::Command => {
3490 let selection_active = self.has_active_selection();
3491 let active_buffer_mode = self
3492 .buffer_metadata
3493 .get(&self.active_buffer())
3494 .and_then(|m| m.virtual_mode());
3495 if let Some(prompt) = &mut self.prompt {
3496 prompt.suggestions = self.command_registry.read().unwrap().filter(
3498 &input,
3499 self.key_context,
3500 &self.keybindings,
3501 selection_active,
3502 &self.active_custom_contexts,
3503 active_buffer_mode,
3504 );
3505 prompt.selected_suggestion = if prompt.suggestions.is_empty() {
3506 None
3507 } else {
3508 Some(0)
3509 };
3510 }
3511 }
3512 PromptType::QuickOpen => {
3513 self.update_quick_open_suggestions(&input);
3515 }
3516 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
3517 self.update_search_highlights(&input);
3519 if let Some(history) = self.prompt_histories.get_mut("search") {
3521 history.reset_navigation();
3522 }
3523 }
3524 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
3525 if let Some(history) = self.prompt_histories.get_mut("replace") {
3527 history.reset_navigation();
3528 }
3529 }
3530 PromptType::GotoLine => {
3531 if let Some(history) = self.prompt_histories.get_mut("goto_line") {
3533 history.reset_navigation();
3534 }
3535 }
3536 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs => {
3537 self.update_file_open_filter();
3539 }
3540 PromptType::Plugin { custom_type } => {
3541 let key = format!("plugin:{}", custom_type);
3543 if let Some(history) = self.prompt_histories.get_mut(&key) {
3544 history.reset_navigation();
3545 }
3546 use crate::services::plugins::hooks::HookArgs;
3548 self.plugin_manager.run_hook(
3549 "prompt_changed",
3550 HookArgs::PromptChanged {
3551 prompt_type: custom_type,
3552 input,
3553 },
3554 );
3555 if let Some(prompt) = &mut self.prompt {
3560 prompt.filter_suggestions(false);
3561 }
3562 }
3563 PromptType::SwitchToTab
3564 | PromptType::SelectTheme { .. }
3565 | PromptType::StopLspServer
3566 | PromptType::SetLanguage => {
3567 if let Some(prompt) = &mut self.prompt {
3568 prompt.filter_suggestions(false);
3569 }
3570 }
3571 PromptType::SelectLocale => {
3572 if let Some(prompt) = &mut self.prompt {
3574 prompt.filter_suggestions(true);
3575 }
3576 }
3577 _ => {}
3578 }
3579 }
3580
3581 pub fn process_async_messages(&mut self) -> bool {
3589 self.plugin_manager.check_thread_health();
3592
3593 let Some(bridge) = &self.async_bridge else {
3594 return false;
3595 };
3596
3597 let messages = bridge.try_recv_all();
3598 let needs_render = !messages.is_empty();
3599
3600 for message in messages {
3601 match message {
3602 AsyncMessage::LspDiagnostics { uri, diagnostics } => {
3603 self.handle_lsp_diagnostics(uri, diagnostics);
3604 }
3605 AsyncMessage::LspInitialized {
3606 language,
3607 completion_trigger_characters,
3608 semantic_tokens_legend,
3609 semantic_tokens_full,
3610 semantic_tokens_full_delta,
3611 semantic_tokens_range,
3612 } => {
3613 tracing::info!("LSP server initialized for language: {}", language);
3614 tracing::debug!(
3615 "LSP completion trigger characters for {}: {:?}",
3616 language,
3617 completion_trigger_characters
3618 );
3619 self.status_message = Some(format!("LSP ({}) ready", language));
3620
3621 if let Some(lsp) = &mut self.lsp {
3623 lsp.set_completion_trigger_characters(
3624 &language,
3625 completion_trigger_characters,
3626 );
3627 lsp.set_semantic_tokens_capabilities(
3628 &language,
3629 semantic_tokens_legend,
3630 semantic_tokens_full,
3631 semantic_tokens_full_delta,
3632 semantic_tokens_range,
3633 );
3634 }
3635
3636 self.resend_did_open_for_language(&language);
3638 self.request_semantic_tokens_for_language(&language);
3639 }
3640 AsyncMessage::LspError {
3641 language,
3642 error,
3643 stderr_log_path,
3644 } => {
3645 tracing::error!("LSP error for {}: {}", language, error);
3646 self.status_message = Some(format!("LSP error ({}): {}", language, error));
3647
3648 let server_command = self
3650 .config
3651 .lsp
3652 .get(&language)
3653 .map(|c| c.command.clone())
3654 .unwrap_or_else(|| "unknown".to_string());
3655
3656 let error_type = if error.contains("not found") || error.contains("NotFound") {
3658 "not_found"
3659 } else if error.contains("permission") || error.contains("PermissionDenied") {
3660 "spawn_failed"
3661 } else if error.contains("timeout") {
3662 "timeout"
3663 } else {
3664 "spawn_failed"
3665 }
3666 .to_string();
3667
3668 self.plugin_manager.run_hook(
3670 "lsp_server_error",
3671 crate::services::plugins::hooks::HookArgs::LspServerError {
3672 language: language.clone(),
3673 server_command,
3674 error_type,
3675 message: error.clone(),
3676 },
3677 );
3678
3679 if let Some(log_path) = stderr_log_path {
3682 let has_content = log_path.metadata().map(|m| m.len() > 0).unwrap_or(false);
3683 if has_content {
3684 tracing::info!("Opening LSP stderr log in background: {:?}", log_path);
3685 match self.open_file_no_focus(&log_path) {
3686 Ok(buffer_id) => {
3687 if let Some(state) = self.buffers.get_mut(&buffer_id) {
3689 state.editing_disabled = true;
3690 }
3691 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id)
3692 {
3693 metadata.read_only = true;
3694 }
3695 self.status_message = Some(format!(
3696 "LSP error ({}): {} - See stderr log",
3697 language, error
3698 ));
3699 }
3700 Err(e) => {
3701 tracing::error!("Failed to open LSP stderr log: {}", e);
3702 }
3703 }
3704 }
3705 }
3706 }
3707 AsyncMessage::LspCompletion { request_id, items } => {
3708 if let Err(e) = self.handle_completion_response(request_id, items) {
3709 tracing::error!("Error handling completion response: {}", e);
3710 }
3711 }
3712 AsyncMessage::LspGotoDefinition {
3713 request_id,
3714 locations,
3715 } => {
3716 if let Err(e) = self.handle_goto_definition_response(request_id, locations) {
3717 tracing::error!("Error handling goto definition response: {}", e);
3718 }
3719 }
3720 AsyncMessage::LspRename { request_id, result } => {
3721 if let Err(e) = self.handle_rename_response(request_id, result) {
3722 tracing::error!("Error handling rename response: {}", e);
3723 }
3724 }
3725 AsyncMessage::LspHover {
3726 request_id,
3727 contents,
3728 is_markdown,
3729 range,
3730 } => {
3731 self.handle_hover_response(request_id, contents, is_markdown, range);
3732 }
3733 AsyncMessage::LspReferences {
3734 request_id,
3735 locations,
3736 } => {
3737 if let Err(e) = self.handle_references_response(request_id, locations) {
3738 tracing::error!("Error handling references response: {}", e);
3739 }
3740 }
3741 AsyncMessage::LspSignatureHelp {
3742 request_id,
3743 signature_help,
3744 } => {
3745 self.handle_signature_help_response(request_id, signature_help);
3746 }
3747 AsyncMessage::LspCodeActions {
3748 request_id,
3749 actions,
3750 } => {
3751 self.handle_code_actions_response(request_id, actions);
3752 }
3753 AsyncMessage::LspPulledDiagnostics {
3754 request_id: _,
3755 uri,
3756 result_id,
3757 diagnostics,
3758 unchanged,
3759 } => {
3760 self.handle_lsp_pulled_diagnostics(uri, result_id, diagnostics, unchanged);
3761 }
3762 AsyncMessage::LspInlayHints {
3763 request_id,
3764 uri,
3765 hints,
3766 } => {
3767 self.handle_lsp_inlay_hints(request_id, uri, hints);
3768 }
3769 AsyncMessage::LspSemanticTokens {
3770 request_id,
3771 uri,
3772 response,
3773 } => {
3774 self.handle_lsp_semantic_tokens(request_id, uri, response);
3775 }
3776 AsyncMessage::LspServerQuiescent { language } => {
3777 self.handle_lsp_server_quiescent(language);
3778 }
3779 AsyncMessage::FileChanged { path } => {
3780 self.handle_async_file_changed(path);
3781 }
3782 AsyncMessage::GitStatusChanged { status } => {
3783 tracing::info!("Git status changed: {}", status);
3784 }
3786 AsyncMessage::FileExplorerInitialized(view) => {
3787 self.handle_file_explorer_initialized(view);
3788 }
3789 AsyncMessage::FileExplorerToggleNode(node_id) => {
3790 self.handle_file_explorer_toggle_node(node_id);
3791 }
3792 AsyncMessage::FileExplorerRefreshNode(node_id) => {
3793 self.handle_file_explorer_refresh_node(node_id);
3794 }
3795 AsyncMessage::FileExplorerExpandedToPath(view) => {
3796 self.handle_file_explorer_expanded_to_path(view);
3797 }
3798 AsyncMessage::Plugin(plugin_msg) => {
3799 use fresh_core::api::{JsCallbackId, PluginAsyncMessage};
3800 match plugin_msg {
3801 PluginAsyncMessage::ProcessOutput {
3802 process_id,
3803 stdout,
3804 stderr,
3805 exit_code,
3806 } => {
3807 self.handle_plugin_process_output(
3808 JsCallbackId::from(process_id),
3809 stdout,
3810 stderr,
3811 exit_code,
3812 );
3813 }
3814 PluginAsyncMessage::DelayComplete { callback_id } => {
3815 self.plugin_manager.resolve_callback(
3816 JsCallbackId::from(callback_id),
3817 "null".to_string(),
3818 );
3819 }
3820 PluginAsyncMessage::ProcessStdout { process_id, data } => {
3821 self.plugin_manager.run_hook(
3822 "onProcessStdout",
3823 crate::services::plugins::hooks::HookArgs::ProcessOutput {
3824 process_id,
3825 data,
3826 },
3827 );
3828 }
3829 PluginAsyncMessage::ProcessStderr { process_id, data } => {
3830 self.plugin_manager.run_hook(
3831 "onProcessStderr",
3832 crate::services::plugins::hooks::HookArgs::ProcessOutput {
3833 process_id,
3834 data,
3835 },
3836 );
3837 }
3838 PluginAsyncMessage::ProcessExit {
3839 process_id,
3840 callback_id,
3841 exit_code,
3842 } => {
3843 self.background_process_handles.remove(&process_id);
3844 let result = fresh_core::api::BackgroundProcessResult {
3845 process_id,
3846 exit_code,
3847 };
3848 self.plugin_manager.resolve_callback(
3849 JsCallbackId::from(callback_id),
3850 serde_json::to_string(&result).unwrap(),
3851 );
3852 }
3853 PluginAsyncMessage::LspResponse {
3854 language: _,
3855 request_id,
3856 result,
3857 } => {
3858 self.handle_plugin_lsp_response(request_id, result);
3859 }
3860 PluginAsyncMessage::PluginResponse(response) => {
3861 self.handle_plugin_response(response);
3862 }
3863 }
3864 }
3865 AsyncMessage::LspProgress {
3866 language,
3867 token,
3868 value,
3869 } => {
3870 self.handle_lsp_progress(language, token, value);
3871 }
3872 AsyncMessage::LspWindowMessage {
3873 language,
3874 message_type,
3875 message,
3876 } => {
3877 self.handle_lsp_window_message(language, message_type, message);
3878 }
3879 AsyncMessage::LspLogMessage {
3880 language,
3881 message_type,
3882 message,
3883 } => {
3884 self.handle_lsp_log_message(language, message_type, message);
3885 }
3886 AsyncMessage::LspStatusUpdate {
3887 language,
3888 status,
3889 message: _,
3890 } => {
3891 self.handle_lsp_status_update(language, status);
3892 }
3893 AsyncMessage::FileOpenDirectoryLoaded(result) => {
3894 self.handle_file_open_directory_loaded(result);
3895 }
3896 AsyncMessage::TerminalOutput { terminal_id } => {
3897 tracing::trace!("Terminal output received for {:?}", terminal_id);
3899
3900 if self.config.terminal.jump_to_end_on_output && !self.terminal_mode {
3903 if let Some(&active_terminal_id) =
3905 self.terminal_buffers.get(&self.active_buffer())
3906 {
3907 if active_terminal_id == terminal_id {
3908 self.enter_terminal_mode();
3909 }
3910 }
3911 }
3912
3913 if self.terminal_mode {
3915 if let Some(handle) = self.terminal_manager.get(terminal_id) {
3916 if let Ok(mut state) = handle.state.lock() {
3917 state.scroll_to_bottom();
3918 }
3919 }
3920 }
3921 }
3922 AsyncMessage::TerminalExited { terminal_id } => {
3923 tracing::info!("Terminal {:?} exited", terminal_id);
3924 if let Some((&buffer_id, _)) = self
3926 .terminal_buffers
3927 .iter()
3928 .find(|(_, &tid)| tid == terminal_id)
3929 {
3930 if self.active_buffer() == buffer_id && self.terminal_mode {
3932 self.terminal_mode = false;
3933 self.key_context = crate::input::keybindings::KeyContext::Normal;
3934 }
3935
3936 self.sync_terminal_to_buffer(buffer_id);
3938
3939 let exit_msg = "\n[Terminal process exited]\n";
3941
3942 if let Some(backing_path) =
3943 self.terminal_backing_files.get(&terminal_id).cloned()
3944 {
3945 if let Ok(mut file) =
3946 self.filesystem.open_file_for_append(&backing_path)
3947 {
3948 use std::io::Write;
3949 let _ = file.write_all(exit_msg.as_bytes());
3950 }
3951
3952 let _ = self.revert_buffer_by_id(buffer_id, &backing_path);
3954 }
3955
3956 if let Some(state) = self.buffers.get_mut(&buffer_id) {
3958 state.editing_disabled = true;
3959 state.margins.set_line_numbers(false);
3960 state.buffer.set_modified(false);
3961 }
3962
3963 self.terminal_buffers.remove(&buffer_id);
3965
3966 self.set_status_message(
3967 t!("terminal.exited", id = terminal_id.0).to_string(),
3968 );
3969 }
3970 self.terminal_manager.close(terminal_id);
3971 }
3972
3973 AsyncMessage::LspServerRequest {
3974 language,
3975 server_command,
3976 method,
3977 params,
3978 } => {
3979 self.handle_lsp_server_request(language, server_command, method, params);
3980 }
3981 AsyncMessage::PluginLspResponse {
3982 language: _,
3983 request_id,
3984 result,
3985 } => {
3986 self.handle_plugin_lsp_response(request_id, result);
3987 }
3988 AsyncMessage::PluginProcessOutput {
3989 process_id,
3990 stdout,
3991 stderr,
3992 exit_code,
3993 } => {
3994 self.handle_plugin_process_output(
3995 fresh_core::api::JsCallbackId::from(process_id),
3996 stdout,
3997 stderr,
3998 exit_code,
3999 );
4000 }
4001 }
4002 }
4003
4004 #[cfg(feature = "plugins")]
4007 self.update_plugin_state_snapshot();
4008
4009 let processed_any_commands = self.process_plugin_commands();
4011
4012 #[cfg(feature = "plugins")]
4014 self.process_pending_plugin_actions();
4015
4016 self.process_pending_lsp_restarts();
4018
4019 #[cfg(feature = "plugins")]
4021 let plugin_render = {
4022 let render = self.plugin_render_requested;
4023 self.plugin_render_requested = false;
4024 render
4025 };
4026 #[cfg(not(feature = "plugins"))]
4027 let plugin_render = false;
4028
4029 if let Some(ref mut checker) = self.update_checker {
4031 let _ = checker.poll_result();
4033 }
4034
4035 let file_changes = self.poll_file_changes();
4037 let tree_changes = self.poll_file_tree_changes();
4038
4039 needs_render || processed_any_commands || plugin_render || file_changes || tree_changes
4041 }
4042
4043 fn update_lsp_status_from_progress(&mut self) {
4045 if self.lsp_progress.is_empty() {
4046 self.update_lsp_status_from_server_statuses();
4048 return;
4049 }
4050
4051 if let Some((_, info)) = self.lsp_progress.iter().next() {
4053 let mut status = format!("LSP ({}): {}", info.language, info.title);
4054 if let Some(ref msg) = info.message {
4055 status.push_str(&format!(" - {}", msg));
4056 }
4057 if let Some(pct) = info.percentage {
4058 status.push_str(&format!(" ({}%)", pct));
4059 }
4060 self.lsp_status = status;
4061 }
4062 }
4063
4064 fn update_lsp_status_from_server_statuses(&mut self) {
4066 use crate::services::async_bridge::LspServerStatus;
4067
4068 let mut statuses: Vec<(String, LspServerStatus)> = self
4070 .lsp_server_statuses
4071 .iter()
4072 .map(|(lang, status)| (lang.clone(), *status))
4073 .collect();
4074
4075 if statuses.is_empty() {
4076 self.lsp_status = String::new();
4077 return;
4078 }
4079
4080 statuses.sort_by(|a, b| a.0.cmp(&b.0));
4082
4083 let status_parts: Vec<String> = statuses
4085 .iter()
4086 .map(|(lang, status)| {
4087 let status_str = match status {
4088 LspServerStatus::Starting => "starting",
4089 LspServerStatus::Initializing => "initializing",
4090 LspServerStatus::Running => "ready",
4091 LspServerStatus::Error => "error",
4092 LspServerStatus::Shutdown => "shutdown",
4093 };
4094 format!("{}: {}", lang, status_str)
4095 })
4096 .collect();
4097
4098 self.lsp_status = format!("LSP [{}]", status_parts.join(", "));
4099 }
4100
4101 #[cfg(feature = "plugins")]
4103 fn update_plugin_state_snapshot(&mut self) {
4104 if let Some(snapshot_handle) = self.plugin_manager.state_snapshot_handle() {
4106 use fresh_core::api::{BufferInfo, CursorInfo, ViewportInfo};
4107 let mut snapshot = snapshot_handle.write().unwrap();
4108
4109 snapshot.active_buffer_id = self.active_buffer();
4111
4112 snapshot.active_split_id = self.split_manager.active_split().0;
4114
4115 snapshot.buffers.clear();
4117 snapshot.buffer_saved_diffs.clear();
4118 snapshot.buffer_cursor_positions.clear();
4119 snapshot.buffer_text_properties.clear();
4120
4121 for (buffer_id, state) in &self.buffers {
4122 let buffer_info = BufferInfo {
4123 id: *buffer_id,
4124 path: state.buffer.file_path().map(|p| p.to_path_buf()),
4125 modified: state.buffer.is_modified(),
4126 length: state.buffer.len(),
4127 };
4128 snapshot.buffers.insert(*buffer_id, buffer_info);
4129
4130 let is_large_file = state.buffer.line_count().is_none();
4133 let diff = if is_large_file {
4134 BufferSavedDiff {
4135 equal: !state.buffer.is_modified(),
4136 byte_ranges: vec![],
4137 line_ranges: None,
4138 }
4139 } else {
4140 let diff = state.buffer.diff_since_saved();
4141 BufferSavedDiff {
4142 equal: diff.equal,
4143 byte_ranges: diff.byte_ranges.clone(),
4144 line_ranges: diff.line_ranges.clone(),
4145 }
4146 };
4147 snapshot.buffer_saved_diffs.insert(*buffer_id, diff);
4148
4149 let cursor_pos = state.cursors.primary().position;
4151 snapshot
4152 .buffer_cursor_positions
4153 .insert(*buffer_id, cursor_pos);
4154
4155 if !state.text_properties.is_empty() {
4157 snapshot
4158 .buffer_text_properties
4159 .insert(*buffer_id, state.text_properties.all().to_vec());
4160 }
4161 }
4162
4163 if let Some(active_state) = self.buffers.get_mut(&self.active_buffer()) {
4165 let primary = active_state.cursors.primary();
4167 let primary_position = primary.position;
4168 let primary_selection = primary.selection_range();
4169
4170 snapshot.primary_cursor = Some(CursorInfo {
4171 position: primary_position,
4172 selection: primary_selection.clone(),
4173 });
4174
4175 snapshot.selected_text = primary_selection
4177 .map(|range| active_state.get_text_range(range.start, range.end));
4178
4179 snapshot.all_cursors = active_state
4181 .cursors
4182 .iter()
4183 .map(|(_, cursor)| CursorInfo {
4184 position: cursor.position,
4185 selection: cursor.selection_range(),
4186 })
4187 .collect();
4188
4189 let active_split = self.split_manager.active_split();
4191 if let Some(view_state) = self.split_view_states.get(&active_split) {
4192 snapshot.viewport = Some(ViewportInfo {
4193 top_byte: view_state.viewport.top_byte,
4194 left_column: view_state.viewport.left_column,
4195 width: view_state.viewport.width,
4196 height: view_state.viewport.height,
4197 });
4198 } else {
4199 snapshot.viewport = None;
4200 }
4201 } else {
4202 snapshot.primary_cursor = None;
4203 snapshot.all_cursors.clear();
4204 snapshot.viewport = None;
4205 snapshot.selected_text = None;
4206 }
4207
4208 snapshot.clipboard = self.clipboard.get_internal().to_string();
4210
4211 snapshot.working_dir = self.working_dir.clone();
4213
4214 snapshot.diagnostics = self.stored_diagnostics.clone();
4216
4217 snapshot.config = serde_json::to_value(&self.config).unwrap_or(serde_json::Value::Null);
4219
4220 snapshot.user_config = Config::read_user_config_raw(&self.working_dir);
4223
4224 snapshot.editor_mode = self.editor_mode.clone();
4226 }
4227 }
4228
4229 pub fn handle_plugin_command(&mut self, command: PluginCommand) -> AnyhowResult<()> {
4231 match command {
4232 PluginCommand::InsertText {
4234 buffer_id,
4235 position,
4236 text,
4237 } => {
4238 self.handle_insert_text(buffer_id, position, text);
4239 }
4240 PluginCommand::DeleteRange { buffer_id, range } => {
4241 self.handle_delete_range(buffer_id, range);
4242 }
4243 PluginCommand::InsertAtCursor { text } => {
4244 self.handle_insert_at_cursor(text);
4245 }
4246 PluginCommand::DeleteSelection => {
4247 self.handle_delete_selection();
4248 }
4249
4250 PluginCommand::AddOverlay {
4252 buffer_id,
4253 namespace,
4254 range,
4255 options,
4256 } => {
4257 self.handle_add_overlay(buffer_id, namespace, range, options);
4258 }
4259 PluginCommand::RemoveOverlay { buffer_id, handle } => {
4260 self.handle_remove_overlay(buffer_id, handle);
4261 }
4262 PluginCommand::ClearAllOverlays { buffer_id } => {
4263 self.handle_clear_all_overlays(buffer_id);
4264 }
4265 PluginCommand::ClearNamespace {
4266 buffer_id,
4267 namespace,
4268 } => {
4269 self.handle_clear_namespace(buffer_id, namespace);
4270 }
4271 PluginCommand::ClearOverlaysInRange {
4272 buffer_id,
4273 start,
4274 end,
4275 } => {
4276 self.handle_clear_overlays_in_range(buffer_id, start, end);
4277 }
4278
4279 PluginCommand::AddVirtualText {
4281 buffer_id,
4282 virtual_text_id,
4283 position,
4284 text,
4285 color,
4286 use_bg,
4287 before,
4288 } => {
4289 self.handle_add_virtual_text(
4290 buffer_id,
4291 virtual_text_id,
4292 position,
4293 text,
4294 color,
4295 use_bg,
4296 before,
4297 );
4298 }
4299 PluginCommand::RemoveVirtualText {
4300 buffer_id,
4301 virtual_text_id,
4302 } => {
4303 self.handle_remove_virtual_text(buffer_id, virtual_text_id);
4304 }
4305 PluginCommand::RemoveVirtualTextsByPrefix { buffer_id, prefix } => {
4306 self.handle_remove_virtual_texts_by_prefix(buffer_id, prefix);
4307 }
4308 PluginCommand::ClearVirtualTexts { buffer_id } => {
4309 self.handle_clear_virtual_texts(buffer_id);
4310 }
4311 PluginCommand::AddVirtualLine {
4312 buffer_id,
4313 position,
4314 text,
4315 fg_color,
4316 bg_color,
4317 above,
4318 namespace,
4319 priority,
4320 } => {
4321 self.handle_add_virtual_line(
4322 buffer_id, position, text, fg_color, bg_color, above, namespace, priority,
4323 );
4324 }
4325 PluginCommand::ClearVirtualTextNamespace {
4326 buffer_id,
4327 namespace,
4328 } => {
4329 self.handle_clear_virtual_text_namespace(buffer_id, namespace);
4330 }
4331
4332 PluginCommand::AddMenuItem {
4334 menu_label,
4335 item,
4336 position,
4337 } => {
4338 self.handle_add_menu_item(menu_label, item, position);
4339 }
4340 PluginCommand::AddMenu { menu, position } => {
4341 self.handle_add_menu(menu, position);
4342 }
4343 PluginCommand::RemoveMenuItem {
4344 menu_label,
4345 item_label,
4346 } => {
4347 self.handle_remove_menu_item(menu_label, item_label);
4348 }
4349 PluginCommand::RemoveMenu { menu_label } => {
4350 self.handle_remove_menu(menu_label);
4351 }
4352
4353 PluginCommand::FocusSplit { split_id } => {
4355 self.handle_focus_split(split_id);
4356 }
4357 PluginCommand::SetSplitBuffer {
4358 split_id,
4359 buffer_id,
4360 } => {
4361 self.handle_set_split_buffer(split_id, buffer_id);
4362 }
4363 PluginCommand::SetSplitScroll { split_id, top_byte } => {
4364 self.handle_set_split_scroll(split_id, top_byte);
4365 }
4366 PluginCommand::RequestHighlights {
4367 buffer_id,
4368 range,
4369 request_id,
4370 } => {
4371 self.handle_request_highlights(buffer_id, range, request_id);
4372 }
4373 PluginCommand::CloseSplit { split_id } => {
4374 self.handle_close_split(split_id);
4375 }
4376 PluginCommand::SetSplitRatio { split_id, ratio } => {
4377 self.handle_set_split_ratio(split_id, ratio);
4378 }
4379 PluginCommand::DistributeSplitsEvenly { split_ids: _ } => {
4380 self.handle_distribute_splits_evenly();
4381 }
4382 PluginCommand::SetBufferCursor {
4383 buffer_id,
4384 position,
4385 } => {
4386 self.handle_set_buffer_cursor(buffer_id, position);
4387 }
4388
4389 PluginCommand::SetLayoutHints {
4391 buffer_id,
4392 split_id,
4393 range: _,
4394 hints,
4395 } => {
4396 self.handle_set_layout_hints(buffer_id, split_id, hints);
4397 }
4398 PluginCommand::SetLineNumbers { buffer_id, enabled } => {
4399 self.handle_set_line_numbers(buffer_id, enabled);
4400 }
4401 PluginCommand::SubmitViewTransform {
4402 buffer_id,
4403 split_id,
4404 payload,
4405 } => {
4406 self.handle_submit_view_transform(buffer_id, split_id, payload);
4407 }
4408 PluginCommand::ClearViewTransform {
4409 buffer_id: _,
4410 split_id,
4411 } => {
4412 self.handle_clear_view_transform(split_id);
4413 }
4414 PluginCommand::RefreshLines { buffer_id } => {
4415 self.handle_refresh_lines(buffer_id);
4416 }
4417 PluginCommand::SetLineIndicator {
4418 buffer_id,
4419 line,
4420 namespace,
4421 symbol,
4422 color,
4423 priority,
4424 } => {
4425 self.handle_set_line_indicator(buffer_id, line, namespace, symbol, color, priority);
4426 }
4427 PluginCommand::ClearLineIndicators {
4428 buffer_id,
4429 namespace,
4430 } => {
4431 self.handle_clear_line_indicators(buffer_id, namespace);
4432 }
4433 PluginCommand::SetFileExplorerDecorations {
4434 namespace,
4435 decorations,
4436 } => {
4437 self.handle_set_file_explorer_decorations(namespace, decorations);
4438 }
4439 PluginCommand::ClearFileExplorerDecorations { namespace } => {
4440 self.handle_clear_file_explorer_decorations(&namespace);
4441 }
4442
4443 PluginCommand::SetStatus { message } => {
4445 self.handle_set_status(message);
4446 }
4447 PluginCommand::ApplyTheme { theme_name } => {
4448 self.apply_theme(&theme_name);
4449 }
4450 PluginCommand::ReloadConfig => {
4451 self.reload_config();
4452 }
4453 PluginCommand::ReloadThemes => {
4454 self.reload_themes();
4455 }
4456 PluginCommand::RegisterGrammar {
4457 language,
4458 grammar_path,
4459 extensions,
4460 } => {
4461 self.handle_register_grammar(language, grammar_path, extensions);
4462 }
4463 PluginCommand::RegisterLanguageConfig { language, config } => {
4464 self.handle_register_language_config(language, config);
4465 }
4466 PluginCommand::RegisterLspServer { language, config } => {
4467 self.handle_register_lsp_server(language, config);
4468 }
4469 PluginCommand::ReloadGrammars => {
4470 self.handle_reload_grammars();
4471 }
4472 PluginCommand::StartPrompt { label, prompt_type } => {
4473 self.handle_start_prompt(label, prompt_type);
4474 }
4475 PluginCommand::StartPromptWithInitial {
4476 label,
4477 prompt_type,
4478 initial_value,
4479 } => {
4480 self.handle_start_prompt_with_initial(label, prompt_type, initial_value);
4481 }
4482 PluginCommand::StartPromptAsync {
4483 label,
4484 initial_value,
4485 callback_id,
4486 } => {
4487 self.handle_start_prompt_async(label, initial_value, callback_id);
4488 }
4489 PluginCommand::SetPromptSuggestions { suggestions } => {
4490 self.handle_set_prompt_suggestions(suggestions);
4491 }
4492
4493 PluginCommand::RegisterCommand { command } => {
4495 self.handle_register_command(command);
4496 }
4497 PluginCommand::UnregisterCommand { name } => {
4498 self.handle_unregister_command(name);
4499 }
4500 PluginCommand::DefineMode {
4501 name,
4502 parent,
4503 bindings,
4504 read_only,
4505 } => {
4506 self.handle_define_mode(name, parent, bindings, read_only);
4507 }
4508
4509 PluginCommand::OpenFileInBackground { path } => {
4511 self.handle_open_file_in_background(path);
4512 }
4513 PluginCommand::OpenFileAtLocation { path, line, column } => {
4514 return self.handle_open_file_at_location(path, line, column);
4515 }
4516 PluginCommand::OpenFileInSplit {
4517 split_id,
4518 path,
4519 line,
4520 column,
4521 } => {
4522 return self.handle_open_file_in_split(split_id, path, line, column);
4523 }
4524 PluginCommand::ShowBuffer { buffer_id } => {
4525 self.handle_show_buffer(buffer_id);
4526 }
4527 PluginCommand::CloseBuffer { buffer_id } => {
4528 self.handle_close_buffer(buffer_id);
4529 }
4530
4531 PluginCommand::SendLspRequest {
4533 language,
4534 method,
4535 params,
4536 request_id,
4537 } => {
4538 self.handle_send_lsp_request(language, method, params, request_id);
4539 }
4540
4541 PluginCommand::SetClipboard { text } => {
4543 self.handle_set_clipboard(text);
4544 }
4545
4546 PluginCommand::SpawnProcess {
4548 command,
4549 args,
4550 cwd,
4551 callback_id,
4552 } => {
4553 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
4556 let effective_cwd = cwd.or_else(|| {
4557 std::env::current_dir()
4558 .map(|p| p.to_string_lossy().to_string())
4559 .ok()
4560 });
4561 let sender = bridge.sender();
4562 let spawner = self.process_spawner.clone();
4563
4564 runtime.spawn(async move {
4565 match spawner.spawn(command, args, effective_cwd).await {
4566 Ok(result) => {
4567 let _ = sender.send(AsyncMessage::PluginProcessOutput {
4568 process_id: callback_id.as_u64(),
4569 stdout: result.stdout,
4570 stderr: result.stderr,
4571 exit_code: result.exit_code,
4572 });
4573 }
4574 Err(e) => {
4575 let _ = sender.send(AsyncMessage::PluginProcessOutput {
4576 process_id: callback_id.as_u64(),
4577 stdout: String::new(),
4578 stderr: e.to_string(),
4579 exit_code: -1,
4580 });
4581 }
4582 }
4583 });
4584 } else {
4585 self.plugin_manager
4587 .reject_callback(callback_id, "Async runtime not available".to_string());
4588 }
4589 }
4590
4591 PluginCommand::SpawnProcessWait {
4592 process_id,
4593 callback_id,
4594 } => {
4595 tracing::warn!(
4598 "SpawnProcessWait not fully implemented - process_id={}",
4599 process_id
4600 );
4601 self.plugin_manager.reject_callback(
4602 callback_id,
4603 format!(
4604 "SpawnProcessWait not yet fully implemented for process_id={}",
4605 process_id
4606 ),
4607 );
4608 }
4609
4610 PluginCommand::Delay {
4611 callback_id,
4612 duration_ms,
4613 } => {
4614 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
4616 let sender = bridge.sender();
4617 let callback_id_u64 = callback_id.as_u64();
4618 runtime.spawn(async move {
4619 tokio::time::sleep(tokio::time::Duration::from_millis(duration_ms)).await;
4620 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
4621 fresh_core::api::PluginAsyncMessage::DelayComplete {
4622 callback_id: callback_id_u64,
4623 },
4624 ));
4625 });
4626 } else {
4627 std::thread::sleep(std::time::Duration::from_millis(duration_ms));
4629 self.plugin_manager
4630 .resolve_callback(callback_id, "null".to_string());
4631 }
4632 }
4633
4634 PluginCommand::SpawnBackgroundProcess {
4635 process_id,
4636 command,
4637 args,
4638 cwd,
4639 callback_id,
4640 } => {
4641 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
4643 use tokio::io::{AsyncBufReadExt, BufReader};
4644 use tokio::process::Command as TokioCommand;
4645
4646 let effective_cwd = cwd.unwrap_or_else(|| {
4647 std::env::current_dir()
4648 .map(|p| p.to_string_lossy().to_string())
4649 .unwrap_or_else(|_| ".".to_string())
4650 });
4651
4652 let sender = bridge.sender();
4653 let sender_stdout = sender.clone();
4654 let sender_stderr = sender.clone();
4655 let callback_id_u64 = callback_id.as_u64();
4656
4657 let handle = runtime.spawn(async move {
4658 let mut child = match TokioCommand::new(&command)
4659 .args(&args)
4660 .current_dir(&effective_cwd)
4661 .stdout(std::process::Stdio::piped())
4662 .stderr(std::process::Stdio::piped())
4663 .spawn()
4664 {
4665 Ok(child) => child,
4666 Err(e) => {
4667 let _ = sender.send(
4668 crate::services::async_bridge::AsyncMessage::Plugin(
4669 fresh_core::api::PluginAsyncMessage::ProcessExit {
4670 process_id,
4671 callback_id: callback_id_u64,
4672 exit_code: -1,
4673 },
4674 ),
4675 );
4676 tracing::error!("Failed to spawn background process: {}", e);
4677 return;
4678 }
4679 };
4680
4681 let stdout = child.stdout.take();
4683 let stderr = child.stderr.take();
4684 let pid = process_id;
4685
4686 if let Some(stdout) = stdout {
4688 let sender = sender_stdout;
4689 tokio::spawn(async move {
4690 let reader = BufReader::new(stdout);
4691 let mut lines = reader.lines();
4692 while let Ok(Some(line)) = lines.next_line().await {
4693 let _ = sender.send(
4694 crate::services::async_bridge::AsyncMessage::Plugin(
4695 fresh_core::api::PluginAsyncMessage::ProcessStdout {
4696 process_id: pid,
4697 data: line + "\n",
4698 },
4699 ),
4700 );
4701 }
4702 });
4703 }
4704
4705 if let Some(stderr) = stderr {
4707 let sender = sender_stderr;
4708 tokio::spawn(async move {
4709 let reader = BufReader::new(stderr);
4710 let mut lines = reader.lines();
4711 while let Ok(Some(line)) = lines.next_line().await {
4712 let _ = sender.send(
4713 crate::services::async_bridge::AsyncMessage::Plugin(
4714 fresh_core::api::PluginAsyncMessage::ProcessStderr {
4715 process_id: pid,
4716 data: line + "\n",
4717 },
4718 ),
4719 );
4720 }
4721 });
4722 }
4723
4724 let exit_code = match child.wait().await {
4726 Ok(status) => status.code().unwrap_or(-1),
4727 Err(_) => -1,
4728 };
4729
4730 let _ = sender.send(crate::services::async_bridge::AsyncMessage::Plugin(
4731 fresh_core::api::PluginAsyncMessage::ProcessExit {
4732 process_id,
4733 callback_id: callback_id_u64,
4734 exit_code,
4735 },
4736 ));
4737 });
4738
4739 self.background_process_handles
4741 .insert(process_id, handle.abort_handle());
4742 } else {
4743 self.plugin_manager
4745 .reject_callback(callback_id, "Async runtime not available".to_string());
4746 }
4747 }
4748
4749 PluginCommand::KillBackgroundProcess { process_id } => {
4750 if let Some(handle) = self.background_process_handles.remove(&process_id) {
4751 handle.abort();
4752 tracing::debug!("Killed background process {}", process_id);
4753 }
4754 }
4755
4756 PluginCommand::CreateVirtualBuffer {
4758 name,
4759 mode,
4760 read_only,
4761 } => {
4762 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
4763 tracing::info!(
4764 "Created virtual buffer '{}' with mode '{}' (id={:?})",
4765 name,
4766 mode,
4767 buffer_id
4768 );
4769 }
4771 PluginCommand::CreateVirtualBufferWithContent {
4772 name,
4773 mode,
4774 read_only,
4775 entries,
4776 show_line_numbers,
4777 show_cursors,
4778 editing_disabled,
4779 hidden_from_tabs,
4780 request_id,
4781 } => {
4782 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
4783 tracing::info!(
4784 "Created virtual buffer '{}' with mode '{}' (id={:?})",
4785 name,
4786 mode,
4787 buffer_id
4788 );
4789
4790 if let Some(state) = self.buffers.get_mut(&buffer_id) {
4792 state.margins.set_line_numbers(show_line_numbers);
4793 state.show_cursors = show_cursors;
4794 state.editing_disabled = editing_disabled;
4795 tracing::debug!(
4796 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
4797 buffer_id,
4798 show_line_numbers,
4799 show_cursors,
4800 editing_disabled
4801 );
4802 }
4803
4804 if hidden_from_tabs {
4806 if let Some(meta) = self.buffer_metadata.get_mut(&buffer_id) {
4807 meta.hidden_from_tabs = true;
4808 }
4809 }
4810
4811 match self.set_virtual_buffer_content(buffer_id, entries) {
4813 Ok(()) => {
4814 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
4815 self.set_active_buffer(buffer_id);
4817 tracing::debug!("Switched to virtual buffer {:?}", buffer_id);
4818
4819 if let Some(req_id) = request_id {
4821 tracing::info!(
4822 "CreateVirtualBufferWithContent: resolving callback for request_id={}, buffer_id={:?}",
4823 req_id,
4824 buffer_id
4825 );
4826 let result = fresh_core::api::VirtualBufferResult {
4828 buffer_id: buffer_id.0 as u64,
4829 split_id: None,
4830 };
4831 self.plugin_manager.resolve_callback(
4832 fresh_core::api::JsCallbackId::from(req_id),
4833 serde_json::to_string(&result).unwrap_or_default(),
4834 );
4835 tracing::info!("CreateVirtualBufferWithContent: resolve_callback sent for request_id={}", req_id);
4836 }
4837 }
4838 Err(e) => {
4839 tracing::error!("Failed to set virtual buffer content: {}", e);
4840 }
4841 }
4842 }
4843 PluginCommand::CreateVirtualBufferInSplit {
4844 name,
4845 mode,
4846 read_only,
4847 entries,
4848 ratio,
4849 direction,
4850 panel_id,
4851 show_line_numbers,
4852 show_cursors,
4853 editing_disabled,
4854 line_wrap,
4855 request_id,
4856 } => {
4857 if let Some(pid) = &panel_id {
4859 if let Some(&existing_buffer_id) = self.panel_ids.get(pid) {
4860 if self.buffers.contains_key(&existing_buffer_id) {
4862 if let Err(e) =
4864 self.set_virtual_buffer_content(existing_buffer_id, entries)
4865 {
4866 tracing::error!("Failed to update panel content: {}", e);
4867 } else {
4868 tracing::info!("Updated existing panel '{}' content", pid);
4869 }
4870
4871 let splits = self.split_manager.splits_for_buffer(existing_buffer_id);
4873 if let Some(&split_id) = splits.first() {
4874 self.split_manager.set_active_split(split_id);
4875 self.split_manager.set_active_buffer_id(existing_buffer_id);
4878 tracing::debug!(
4879 "Focused split {:?} containing panel buffer",
4880 split_id
4881 );
4882 }
4883
4884 if let Some(req_id) = request_id {
4886 let result = fresh_core::api::VirtualBufferResult {
4887 buffer_id: existing_buffer_id.0 as u64,
4888 split_id: splits.first().map(|s| s.0 as u64),
4889 };
4890 self.plugin_manager.resolve_callback(
4891 fresh_core::api::JsCallbackId::from(req_id),
4892 serde_json::to_string(&result).unwrap_or_default(),
4893 );
4894 }
4895 return Ok(());
4896 } else {
4897 tracing::warn!(
4899 "Removing stale panel_id '{}' pointing to non-existent buffer {:?}",
4900 pid,
4901 existing_buffer_id
4902 );
4903 self.panel_ids.remove(pid);
4904 }
4906 }
4907 }
4908
4909 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
4911 tracing::info!(
4912 "Created virtual buffer '{}' with mode '{}' in split (id={:?})",
4913 name,
4914 mode,
4915 buffer_id
4916 );
4917
4918 if let Some(state) = self.buffers.get_mut(&buffer_id) {
4920 state.margins.set_line_numbers(show_line_numbers);
4921 state.show_cursors = show_cursors;
4922 state.editing_disabled = editing_disabled;
4923 tracing::debug!(
4924 "Set buffer {:?} view options: show_line_numbers={}, show_cursors={}, editing_disabled={}",
4925 buffer_id,
4926 show_line_numbers,
4927 show_cursors,
4928 editing_disabled
4929 );
4930 }
4931
4932 if let Some(pid) = panel_id {
4934 self.panel_ids.insert(pid, buffer_id);
4935 }
4936
4937 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
4939 tracing::error!("Failed to set virtual buffer content: {}", e);
4940 return Ok(());
4941 }
4942
4943 self.save_current_split_view_state();
4945
4946 let split_dir = match direction.as_deref() {
4948 Some("vertical") => crate::model::event::SplitDirection::Vertical,
4949 _ => crate::model::event::SplitDirection::Horizontal,
4950 };
4951
4952 let created_split_id =
4954 match self.split_manager.split_active(split_dir, buffer_id, ratio) {
4955 Ok(new_split_id) => {
4956 let mut view_state = SplitViewState::with_buffer(
4958 self.terminal_width,
4959 self.terminal_height,
4960 buffer_id,
4961 );
4962 view_state.viewport.line_wrap_enabled =
4963 line_wrap.unwrap_or(self.config.editor.line_wrap);
4964 self.split_view_states.insert(new_split_id, view_state);
4965
4966 self.split_manager.set_active_split(new_split_id);
4968 tracing::info!(
4971 "Created {:?} split with virtual buffer {:?}",
4972 split_dir,
4973 buffer_id
4974 );
4975 Some(new_split_id)
4976 }
4977 Err(e) => {
4978 tracing::error!("Failed to create split: {}", e);
4979 self.set_active_buffer(buffer_id);
4981 None
4982 }
4983 };
4984
4985 if let Some(req_id) = request_id {
4988 tracing::trace!("CreateVirtualBufferInSplit: resolving callback for request_id={}, buffer_id={:?}, split_id={:?}", req_id, buffer_id, created_split_id);
4989 let result = fresh_core::api::VirtualBufferResult {
4990 buffer_id: buffer_id.0 as u64,
4991 split_id: created_split_id.map(|s| s.0 as u64),
4992 };
4993 self.plugin_manager.resolve_callback(
4994 fresh_core::api::JsCallbackId::from(req_id),
4995 serde_json::to_string(&result).unwrap_or_default(),
4996 );
4997 }
4998 }
4999 PluginCommand::SetVirtualBufferContent { buffer_id, entries } => {
5000 match self.set_virtual_buffer_content(buffer_id, entries) {
5001 Ok(()) => {
5002 tracing::debug!("Set virtual buffer content for {:?}", buffer_id);
5003 }
5004 Err(e) => {
5005 tracing::error!("Failed to set virtual buffer content: {}", e);
5006 }
5007 }
5008 }
5009 PluginCommand::GetTextPropertiesAtCursor { buffer_id } => {
5010 if let Some(state) = self.buffers.get(&buffer_id) {
5012 let cursor_pos = state.cursors.primary().position;
5013 let properties = state.text_properties.get_at(cursor_pos);
5014 tracing::debug!(
5015 "Text properties at cursor in {:?}: {} properties found",
5016 buffer_id,
5017 properties.len()
5018 );
5019 }
5021 }
5022 PluginCommand::CreateVirtualBufferInExistingSplit {
5023 name,
5024 mode,
5025 read_only,
5026 entries,
5027 split_id,
5028 show_line_numbers,
5029 show_cursors,
5030 editing_disabled,
5031 line_wrap,
5032 request_id,
5033 } => {
5034 let buffer_id = self.create_virtual_buffer(name.clone(), mode.clone(), read_only);
5036 tracing::info!(
5037 "Created virtual buffer '{}' with mode '{}' for existing split {:?} (id={:?})",
5038 name,
5039 mode,
5040 split_id,
5041 buffer_id
5042 );
5043
5044 if let Some(state) = self.buffers.get_mut(&buffer_id) {
5046 state.margins.set_line_numbers(show_line_numbers);
5047 state.show_cursors = show_cursors;
5048 state.editing_disabled = editing_disabled;
5049 }
5050
5051 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
5053 tracing::error!("Failed to set virtual buffer content: {}", e);
5054 return Ok(());
5055 }
5056
5057 if let Err(e) = self.split_manager.set_split_buffer(split_id, buffer_id) {
5059 tracing::error!("Failed to set buffer in split {:?}: {}", split_id, e);
5060 self.set_active_buffer(buffer_id);
5062 } else {
5063 self.split_manager.set_active_split(split_id);
5065 self.split_manager.set_active_buffer_id(buffer_id);
5066
5067 if let Some(wrap) = line_wrap {
5069 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
5070 view_state.viewport.line_wrap_enabled = wrap;
5071 }
5072 }
5073
5074 tracing::info!(
5075 "Displayed virtual buffer {:?} in split {:?}",
5076 buffer_id,
5077 split_id
5078 );
5079 }
5080
5081 if let Some(req_id) = request_id {
5083 let result = fresh_core::api::VirtualBufferResult {
5084 buffer_id: buffer_id.0 as u64,
5085 split_id: Some(split_id.0 as u64),
5086 };
5087 self.plugin_manager.resolve_callback(
5088 fresh_core::api::JsCallbackId::from(req_id),
5089 serde_json::to_string(&result).unwrap_or_default(),
5090 );
5091 }
5092 }
5093
5094 PluginCommand::SetContext { name, active } => {
5096 if active {
5097 self.active_custom_contexts.insert(name.clone());
5098 tracing::debug!("Set custom context: {}", name);
5099 } else {
5100 self.active_custom_contexts.remove(&name);
5101 tracing::debug!("Unset custom context: {}", name);
5102 }
5103 }
5104
5105 PluginCommand::SetReviewDiffHunks { hunks } => {
5107 self.review_hunks = hunks;
5108 tracing::debug!("Set {} review hunks", self.review_hunks.len());
5109 }
5110
5111 PluginCommand::ExecuteAction { action_name } => {
5113 self.handle_execute_action(action_name);
5114 }
5115 PluginCommand::ExecuteActions { actions } => {
5116 self.handle_execute_actions(actions);
5117 }
5118 PluginCommand::GetBufferText {
5119 buffer_id,
5120 start,
5121 end,
5122 request_id,
5123 } => {
5124 self.handle_get_buffer_text(buffer_id, start, end, request_id);
5125 }
5126 PluginCommand::GetLineStartPosition {
5127 buffer_id,
5128 line,
5129 request_id,
5130 } => {
5131 self.handle_get_line_start_position(buffer_id, line, request_id);
5132 }
5133 PluginCommand::SetEditorMode { mode } => {
5134 self.handle_set_editor_mode(mode);
5135 }
5136
5137 PluginCommand::ShowActionPopup {
5139 popup_id,
5140 title,
5141 message,
5142 actions,
5143 } => {
5144 tracing::info!(
5145 "Action popup requested: id={}, title={}, actions={}",
5146 popup_id,
5147 title,
5148 actions.len()
5149 );
5150
5151 let items: Vec<crate::model::event::PopupListItemData> = actions
5153 .iter()
5154 .map(|action| crate::model::event::PopupListItemData {
5155 text: action.label.clone(),
5156 detail: None,
5157 icon: None,
5158 data: Some(action.id.clone()),
5159 })
5160 .collect();
5161
5162 let action_ids: Vec<(String, String)> =
5164 actions.into_iter().map(|a| (a.id, a.label)).collect();
5165 self.active_action_popup = Some((popup_id.clone(), action_ids));
5166
5167 let popup = crate::model::event::PopupData {
5169 title: Some(title),
5170 description: Some(message),
5171 transient: false,
5172 content: crate::model::event::PopupContentData::List { items, selected: 0 },
5173 position: crate::model::event::PopupPositionData::BottomRight,
5174 width: 60,
5175 max_height: 15,
5176 bordered: true,
5177 };
5178
5179 self.show_popup(popup);
5180 tracing::info!(
5181 "Action popup shown: id={}, active_action_popup={:?}",
5182 popup_id,
5183 self.active_action_popup.as_ref().map(|(id, _)| id)
5184 );
5185 }
5186
5187 PluginCommand::DisableLspForLanguage { language } => {
5188 tracing::info!("Disabling LSP for language: {}", language);
5189
5190 if let Some(ref mut lsp) = self.lsp {
5192 lsp.shutdown_server(&language);
5193 tracing::info!("Stopped LSP server for {}", language);
5194 }
5195
5196 if let Some(lsp_config) = self.config.lsp.get_mut(&language) {
5198 lsp_config.enabled = false;
5199 lsp_config.auto_start = false;
5200 tracing::info!("Disabled LSP config for {}", language);
5201 }
5202
5203 if let Err(e) = self.save_config() {
5205 tracing::error!("Failed to save config: {}", e);
5206 self.status_message = Some(format!(
5207 "LSP disabled for {} (config save failed)",
5208 language
5209 ));
5210 } else {
5211 self.status_message = Some(format!("LSP disabled for {}", language));
5212 }
5213
5214 self.warning_domains.lsp.clear();
5216 }
5217
5218 PluginCommand::SetLspRootUri { language, uri } => {
5219 tracing::info!("Plugin setting LSP root URI for {}: {}", language, uri);
5220
5221 match uri.parse::<lsp_types::Uri>() {
5223 Ok(parsed_uri) => {
5224 if let Some(ref mut lsp) = self.lsp {
5225 let restarted = lsp.set_language_root_uri(&language, parsed_uri);
5226 if restarted {
5227 self.status_message = Some(format!(
5228 "LSP root updated for {} (restarting server)",
5229 language
5230 ));
5231 } else {
5232 self.status_message =
5233 Some(format!("LSP root set for {}", language));
5234 }
5235 }
5236 }
5237 Err(e) => {
5238 tracing::error!("Invalid LSP root URI '{}': {}", uri, e);
5239 self.status_message = Some(format!("Invalid LSP root URI: {}", e));
5240 }
5241 }
5242 }
5243
5244 PluginCommand::CreateScrollSyncGroup {
5246 group_id,
5247 left_split,
5248 right_split,
5249 } => {
5250 let success = self.scroll_sync_manager.create_group_with_id(
5251 group_id,
5252 left_split,
5253 right_split,
5254 );
5255 if success {
5256 tracing::debug!(
5257 "Created scroll sync group {} for splits {:?} and {:?}",
5258 group_id,
5259 left_split,
5260 right_split
5261 );
5262 } else {
5263 tracing::warn!(
5264 "Failed to create scroll sync group {} (ID already exists)",
5265 group_id
5266 );
5267 }
5268 }
5269 PluginCommand::SetScrollSyncAnchors { group_id, anchors } => {
5270 use crate::view::scroll_sync::SyncAnchor;
5271 let anchor_count = anchors.len();
5272 let sync_anchors: Vec<SyncAnchor> = anchors
5273 .into_iter()
5274 .map(|(left_line, right_line)| SyncAnchor {
5275 left_line,
5276 right_line,
5277 })
5278 .collect();
5279 self.scroll_sync_manager.set_anchors(group_id, sync_anchors);
5280 tracing::debug!(
5281 "Set {} anchors for scroll sync group {}",
5282 anchor_count,
5283 group_id
5284 );
5285 }
5286 PluginCommand::RemoveScrollSyncGroup { group_id } => {
5287 if self.scroll_sync_manager.remove_group(group_id) {
5288 tracing::debug!("Removed scroll sync group {}", group_id);
5289 } else {
5290 tracing::warn!("Scroll sync group {} not found", group_id);
5291 }
5292 }
5293
5294 PluginCommand::CreateCompositeBuffer {
5296 name,
5297 mode,
5298 layout,
5299 sources,
5300 hunks,
5301 request_id,
5302 } => {
5303 self.handle_create_composite_buffer(name, mode, layout, sources, hunks, request_id);
5304 }
5305 PluginCommand::UpdateCompositeAlignment { buffer_id, hunks } => {
5306 self.handle_update_composite_alignment(buffer_id, hunks);
5307 }
5308 PluginCommand::CloseCompositeBuffer { buffer_id } => {
5309 self.close_composite_buffer(buffer_id);
5310 }
5311
5312 PluginCommand::SaveBufferToPath { buffer_id, path } => {
5314 self.handle_save_buffer_to_path(buffer_id, path);
5315 }
5316
5317 #[cfg(feature = "plugins")]
5319 PluginCommand::LoadPlugin { path, callback_id } => {
5320 self.handle_load_plugin(path, callback_id);
5321 }
5322 #[cfg(feature = "plugins")]
5323 PluginCommand::UnloadPlugin { name, callback_id } => {
5324 self.handle_unload_plugin(name, callback_id);
5325 }
5326 #[cfg(feature = "plugins")]
5327 PluginCommand::ReloadPlugin { name, callback_id } => {
5328 self.handle_reload_plugin(name, callback_id);
5329 }
5330 #[cfg(feature = "plugins")]
5331 PluginCommand::ListPlugins { callback_id } => {
5332 self.handle_list_plugins(callback_id);
5333 }
5334 #[cfg(not(feature = "plugins"))]
5336 PluginCommand::LoadPlugin { .. }
5337 | PluginCommand::UnloadPlugin { .. }
5338 | PluginCommand::ReloadPlugin { .. }
5339 | PluginCommand::ListPlugins { .. } => {
5340 tracing::warn!("Plugin management commands require the 'plugins' feature");
5341 }
5342 }
5343 Ok(())
5344 }
5345
5346 fn handle_save_buffer_to_path(&mut self, buffer_id: BufferId, path: std::path::PathBuf) {
5348 if let Some(state) = self.buffers.get_mut(&buffer_id) {
5349 match state.buffer.save_to_file(&path) {
5351 Ok(()) => {
5352 state.buffer.set_file_path(path.clone());
5354 let _ = self.finalize_save(Some(path));
5356 tracing::debug!("Saved buffer {:?} to path", buffer_id);
5357 }
5358 Err(e) => {
5359 self.handle_set_status(format!("Error saving: {}", e));
5360 tracing::error!("Failed to save buffer to path: {}", e);
5361 }
5362 }
5363 } else {
5364 self.handle_set_status(format!("Buffer {:?} not found", buffer_id));
5365 tracing::warn!("SaveBufferToPath: buffer {:?} not found", buffer_id);
5366 }
5367 }
5368
5369 #[cfg(feature = "plugins")]
5371 fn handle_load_plugin(&mut self, path: std::path::PathBuf, callback_id: JsCallbackId) {
5372 match self.plugin_manager.load_plugin(&path) {
5373 Ok(()) => {
5374 tracing::info!("Loaded plugin from {:?}", path);
5375 self.plugin_manager
5376 .resolve_callback(callback_id, "true".to_string());
5377 }
5378 Err(e) => {
5379 tracing::error!("Failed to load plugin from {:?}: {}", path, e);
5380 self.plugin_manager
5381 .reject_callback(callback_id, format!("{}", e));
5382 }
5383 }
5384 }
5385
5386 #[cfg(feature = "plugins")]
5388 fn handle_unload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
5389 match self.plugin_manager.unload_plugin(&name) {
5390 Ok(()) => {
5391 tracing::info!("Unloaded plugin: {}", name);
5392 self.plugin_manager
5393 .resolve_callback(callback_id, "true".to_string());
5394 }
5395 Err(e) => {
5396 tracing::error!("Failed to unload plugin '{}': {}", name, e);
5397 self.plugin_manager
5398 .reject_callback(callback_id, format!("{}", e));
5399 }
5400 }
5401 }
5402
5403 #[cfg(feature = "plugins")]
5405 fn handle_reload_plugin(&mut self, name: String, callback_id: JsCallbackId) {
5406 match self.plugin_manager.reload_plugin(&name) {
5407 Ok(()) => {
5408 tracing::info!("Reloaded plugin: {}", name);
5409 self.plugin_manager
5410 .resolve_callback(callback_id, "true".to_string());
5411 }
5412 Err(e) => {
5413 tracing::error!("Failed to reload plugin '{}': {}", name, e);
5414 self.plugin_manager
5415 .reject_callback(callback_id, format!("{}", e));
5416 }
5417 }
5418 }
5419
5420 #[cfg(feature = "plugins")]
5422 fn handle_list_plugins(&mut self, callback_id: JsCallbackId) {
5423 let plugins = self.plugin_manager.list_plugins();
5424 let json_array: Vec<serde_json::Value> = plugins
5426 .iter()
5427 .map(|p| {
5428 serde_json::json!({
5429 "name": p.name,
5430 "path": p.path.to_string_lossy(),
5431 "enabled": p.enabled
5432 })
5433 })
5434 .collect();
5435 let json_str = serde_json::to_string(&json_array).unwrap_or_else(|_| "[]".to_string());
5436 self.plugin_manager.resolve_callback(callback_id, json_str);
5437 }
5438
5439 fn handle_execute_action(&mut self, action_name: String) {
5441 use crate::input::keybindings::Action;
5442 use std::collections::HashMap;
5443
5444 if let Some(action) = Action::from_str(&action_name, &HashMap::new()) {
5446 if let Err(e) = self.handle_action(action) {
5448 tracing::warn!("Failed to execute action '{}': {}", action_name, e);
5449 } else {
5450 tracing::debug!("Executed action: {}", action_name);
5451 }
5452 } else {
5453 tracing::warn!("Unknown action: {}", action_name);
5454 }
5455 }
5456
5457 fn handle_execute_actions(&mut self, actions: Vec<fresh_core::api::ActionSpec>) {
5460 use crate::input::keybindings::Action;
5461 use std::collections::HashMap;
5462
5463 for action_spec in actions {
5464 if let Some(action) = Action::from_str(&action_spec.action, &HashMap::new()) {
5465 for _ in 0..action_spec.count {
5467 if let Err(e) = self.handle_action(action.clone()) {
5468 tracing::warn!("Failed to execute action '{}': {}", action_spec.action, e);
5469 return; }
5471 }
5472 tracing::debug!(
5473 "Executed action '{}' {} time(s)",
5474 action_spec.action,
5475 action_spec.count
5476 );
5477 } else {
5478 tracing::warn!("Unknown action: {}", action_spec.action);
5479 return; }
5481 }
5482 }
5483
5484 fn handle_get_buffer_text(
5486 &mut self,
5487 buffer_id: BufferId,
5488 start: usize,
5489 end: usize,
5490 request_id: u64,
5491 ) {
5492 let result = if let Some(state) = self.buffers.get_mut(&buffer_id) {
5493 let len = state.buffer.len();
5495 if start <= end && end <= len {
5496 Ok(state.get_text_range(start, end))
5497 } else {
5498 Err(format!(
5499 "Invalid range {}..{} for buffer of length {}",
5500 start, end, len
5501 ))
5502 }
5503 } else {
5504 Err(format!("Buffer {:?} not found", buffer_id))
5505 };
5506
5507 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
5509 match result {
5510 Ok(text) => {
5511 let json = serde_json::to_string(&text).unwrap_or_else(|_| "null".to_string());
5513 self.plugin_manager.resolve_callback(callback_id, json);
5514 }
5515 Err(error) => {
5516 self.plugin_manager.reject_callback(callback_id, error);
5517 }
5518 }
5519 }
5520
5521 fn handle_set_editor_mode(&mut self, mode: Option<String>) {
5523 self.editor_mode = mode.clone();
5524 tracing::debug!("Set editor mode: {:?}", mode);
5525 }
5526
5527 fn handle_get_line_start_position(&mut self, buffer_id: BufferId, line: u32, request_id: u64) {
5529 let actual_buffer_id = if buffer_id.0 == 0 {
5531 self.active_buffer_id()
5532 } else {
5533 buffer_id
5534 };
5535
5536 let result = if let Some(state) = self.buffers.get_mut(&actual_buffer_id) {
5537 let line_number = line as usize;
5539 let buffer_len = state.buffer.len();
5540
5541 if line_number == 0 {
5542 Some(0)
5544 } else {
5545 let mut current_line = 0;
5547 let mut line_start = None;
5548
5549 let content = state.get_text_range(0, buffer_len);
5551 for (byte_idx, c) in content.char_indices() {
5552 if c == '\n' {
5553 current_line += 1;
5554 if current_line == line_number {
5555 line_start = Some(byte_idx + 1);
5557 break;
5558 }
5559 }
5560 }
5561 line_start
5562 }
5563 } else {
5564 None
5565 };
5566
5567 let callback_id = fresh_core::api::JsCallbackId::from(request_id);
5569 let json = serde_json::to_string(&result).unwrap_or_else(|_| "null".to_string());
5571 self.plugin_manager.resolve_callback(callback_id, json);
5572 }
5573}
5574
5575fn parse_key_string(key_str: &str) -> Option<(KeyCode, KeyModifiers)> {
5584 use crossterm::event::{KeyCode, KeyModifiers};
5585
5586 let mut modifiers = KeyModifiers::NONE;
5587 let mut remaining = key_str;
5588
5589 loop {
5591 if remaining.starts_with("C-") {
5592 modifiers |= KeyModifiers::CONTROL;
5593 remaining = &remaining[2..];
5594 } else if remaining.starts_with("M-") {
5595 modifiers |= KeyModifiers::ALT;
5596 remaining = &remaining[2..];
5597 } else if remaining.starts_with("S-") {
5598 modifiers |= KeyModifiers::SHIFT;
5599 remaining = &remaining[2..];
5600 } else {
5601 break;
5602 }
5603 }
5604
5605 let upper = remaining.to_uppercase();
5608 let code = match upper.as_str() {
5609 "RET" | "RETURN" | "ENTER" => KeyCode::Enter,
5610 "TAB" => KeyCode::Tab,
5611 "ESC" | "ESCAPE" => KeyCode::Esc,
5612 "SPC" | "SPACE" => KeyCode::Char(' '),
5613 "DEL" | "DELETE" => KeyCode::Delete,
5614 "BS" | "BACKSPACE" => KeyCode::Backspace,
5615 "UP" => KeyCode::Up,
5616 "DOWN" => KeyCode::Down,
5617 "LEFT" => KeyCode::Left,
5618 "RIGHT" => KeyCode::Right,
5619 "HOME" => KeyCode::Home,
5620 "END" => KeyCode::End,
5621 "PAGEUP" | "PGUP" => KeyCode::PageUp,
5622 "PAGEDOWN" | "PGDN" => KeyCode::PageDown,
5623 s if s.starts_with('F') && s.len() > 1 => {
5624 if let Ok(n) = s[1..].parse::<u8>() {
5626 KeyCode::F(n)
5627 } else {
5628 return None;
5629 }
5630 }
5631 _ if remaining.len() == 1 => {
5632 let c = remaining.chars().next()?;
5635 if c.is_ascii_uppercase() {
5636 modifiers |= KeyModifiers::SHIFT;
5637 }
5638 KeyCode::Char(c.to_ascii_lowercase())
5639 }
5640 _ => return None,
5641 };
5642
5643 Some((code, modifiers))
5644}
5645
5646#[cfg(test)]
5647mod tests {
5648 use super::*;
5649 use tempfile::TempDir;
5650
5651 fn test_dir_context() -> (DirectoryContext, TempDir) {
5653 let temp_dir = TempDir::new().unwrap();
5654 let dir_context = DirectoryContext::for_testing(temp_dir.path());
5655 (dir_context, temp_dir)
5656 }
5657
5658 fn test_filesystem() -> Arc<dyn FileSystem + Send + Sync> {
5660 Arc::new(crate::model::filesystem::StdFileSystem)
5661 }
5662
5663 #[test]
5664 fn test_editor_new() {
5665 let config = Config::default();
5666 let (dir_context, _temp) = test_dir_context();
5667 let editor = Editor::new(
5668 config,
5669 80,
5670 24,
5671 dir_context,
5672 crate::view::color_support::ColorCapability::TrueColor,
5673 test_filesystem(),
5674 )
5675 .unwrap();
5676
5677 assert_eq!(editor.buffers.len(), 1);
5678 assert!(!editor.should_quit());
5679 }
5680
5681 #[test]
5682 fn test_new_buffer() {
5683 let config = Config::default();
5684 let (dir_context, _temp) = test_dir_context();
5685 let mut editor = Editor::new(
5686 config,
5687 80,
5688 24,
5689 dir_context,
5690 crate::view::color_support::ColorCapability::TrueColor,
5691 test_filesystem(),
5692 )
5693 .unwrap();
5694
5695 let id = editor.new_buffer();
5696 assert_eq!(editor.buffers.len(), 2);
5697 assert_eq!(editor.active_buffer(), id);
5698 }
5699
5700 #[test]
5701 #[ignore]
5702 fn test_clipboard() {
5703 let config = Config::default();
5704 let (dir_context, _temp) = test_dir_context();
5705 let mut editor = Editor::new(
5706 config,
5707 80,
5708 24,
5709 dir_context,
5710 crate::view::color_support::ColorCapability::TrueColor,
5711 test_filesystem(),
5712 )
5713 .unwrap();
5714
5715 editor.clipboard.set_internal("test".to_string());
5717
5718 editor.paste();
5720
5721 let content = editor.active_state().buffer.to_string().unwrap();
5722 assert_eq!(content, "test");
5723 }
5724
5725 #[test]
5726 fn test_action_to_events_insert_char() {
5727 let config = Config::default();
5728 let (dir_context, _temp) = test_dir_context();
5729 let mut editor = Editor::new(
5730 config,
5731 80,
5732 24,
5733 dir_context,
5734 crate::view::color_support::ColorCapability::TrueColor,
5735 test_filesystem(),
5736 )
5737 .unwrap();
5738
5739 let events = editor.action_to_events(Action::InsertChar('a'));
5740 assert!(events.is_some());
5741
5742 let events = events.unwrap();
5743 assert_eq!(events.len(), 1);
5744
5745 match &events[0] {
5746 Event::Insert { position, text, .. } => {
5747 assert_eq!(*position, 0);
5748 assert_eq!(text, "a");
5749 }
5750 _ => panic!("Expected Insert event"),
5751 }
5752 }
5753
5754 #[test]
5755 fn test_action_to_events_move_right() {
5756 let config = Config::default();
5757 let (dir_context, _temp) = test_dir_context();
5758 let mut editor = Editor::new(
5759 config,
5760 80,
5761 24,
5762 dir_context,
5763 crate::view::color_support::ColorCapability::TrueColor,
5764 test_filesystem(),
5765 )
5766 .unwrap();
5767
5768 let state = editor.active_state_mut();
5770 state.apply(&Event::Insert {
5771 position: 0,
5772 text: "hello".to_string(),
5773 cursor_id: state.cursors.primary_id(),
5774 });
5775
5776 let events = editor.action_to_events(Action::MoveRight);
5777 assert!(events.is_some());
5778
5779 let events = events.unwrap();
5780 assert_eq!(events.len(), 1);
5781
5782 match &events[0] {
5783 Event::MoveCursor {
5784 new_position,
5785 new_anchor,
5786 ..
5787 } => {
5788 assert_eq!(*new_position, 5);
5790 assert_eq!(*new_anchor, None); }
5792 _ => panic!("Expected MoveCursor event"),
5793 }
5794 }
5795
5796 #[test]
5797 fn test_action_to_events_move_up_down() {
5798 let config = Config::default();
5799 let (dir_context, _temp) = test_dir_context();
5800 let mut editor = Editor::new(
5801 config,
5802 80,
5803 24,
5804 dir_context,
5805 crate::view::color_support::ColorCapability::TrueColor,
5806 test_filesystem(),
5807 )
5808 .unwrap();
5809
5810 let state = editor.active_state_mut();
5812 state.apply(&Event::Insert {
5813 position: 0,
5814 text: "line1\nline2\nline3".to_string(),
5815 cursor_id: state.cursors.primary_id(),
5816 });
5817
5818 state.apply(&Event::MoveCursor {
5820 cursor_id: state.cursors.primary_id(),
5821 old_position: 0, new_position: 6,
5823 old_anchor: None, new_anchor: None,
5825 old_sticky_column: 0,
5826 new_sticky_column: 0,
5827 });
5828
5829 let events = editor.action_to_events(Action::MoveUp);
5831 assert!(events.is_some());
5832 let events = events.unwrap();
5833 assert_eq!(events.len(), 1);
5834
5835 match &events[0] {
5836 Event::MoveCursor { new_position, .. } => {
5837 assert_eq!(*new_position, 0); }
5839 _ => panic!("Expected MoveCursor event"),
5840 }
5841 }
5842
5843 #[test]
5844 fn test_action_to_events_insert_newline() {
5845 let config = Config::default();
5846 let (dir_context, _temp) = test_dir_context();
5847 let mut editor = Editor::new(
5848 config,
5849 80,
5850 24,
5851 dir_context,
5852 crate::view::color_support::ColorCapability::TrueColor,
5853 test_filesystem(),
5854 )
5855 .unwrap();
5856
5857 let events = editor.action_to_events(Action::InsertNewline);
5858 assert!(events.is_some());
5859
5860 let events = events.unwrap();
5861 assert_eq!(events.len(), 1);
5862
5863 match &events[0] {
5864 Event::Insert { text, .. } => {
5865 assert_eq!(text, "\n");
5866 }
5867 _ => panic!("Expected Insert event"),
5868 }
5869 }
5870
5871 #[test]
5872 fn test_action_to_events_unimplemented() {
5873 let config = Config::default();
5874 let (dir_context, _temp) = test_dir_context();
5875 let mut editor = Editor::new(
5876 config,
5877 80,
5878 24,
5879 dir_context,
5880 crate::view::color_support::ColorCapability::TrueColor,
5881 test_filesystem(),
5882 )
5883 .unwrap();
5884
5885 assert!(editor.action_to_events(Action::Save).is_none());
5887 assert!(editor.action_to_events(Action::Quit).is_none());
5888 assert!(editor.action_to_events(Action::Undo).is_none());
5889 }
5890
5891 #[test]
5892 fn test_action_to_events_delete_backward() {
5893 let config = Config::default();
5894 let (dir_context, _temp) = test_dir_context();
5895 let mut editor = Editor::new(
5896 config,
5897 80,
5898 24,
5899 dir_context,
5900 crate::view::color_support::ColorCapability::TrueColor,
5901 test_filesystem(),
5902 )
5903 .unwrap();
5904
5905 let state = editor.active_state_mut();
5907 state.apply(&Event::Insert {
5908 position: 0,
5909 text: "hello".to_string(),
5910 cursor_id: state.cursors.primary_id(),
5911 });
5912
5913 let events = editor.action_to_events(Action::DeleteBackward);
5914 assert!(events.is_some());
5915
5916 let events = events.unwrap();
5917 assert_eq!(events.len(), 1);
5918
5919 match &events[0] {
5920 Event::Delete {
5921 range,
5922 deleted_text,
5923 ..
5924 } => {
5925 assert_eq!(range.clone(), 4..5); assert_eq!(deleted_text, "o");
5927 }
5928 _ => panic!("Expected Delete event"),
5929 }
5930 }
5931
5932 #[test]
5933 fn test_action_to_events_delete_forward() {
5934 let config = Config::default();
5935 let (dir_context, _temp) = test_dir_context();
5936 let mut editor = Editor::new(
5937 config,
5938 80,
5939 24,
5940 dir_context,
5941 crate::view::color_support::ColorCapability::TrueColor,
5942 test_filesystem(),
5943 )
5944 .unwrap();
5945
5946 let state = editor.active_state_mut();
5948 state.apply(&Event::Insert {
5949 position: 0,
5950 text: "hello".to_string(),
5951 cursor_id: state.cursors.primary_id(),
5952 });
5953
5954 state.apply(&Event::MoveCursor {
5956 cursor_id: state.cursors.primary_id(),
5957 old_position: 0, new_position: 0,
5959 old_anchor: None, new_anchor: None,
5961 old_sticky_column: 0,
5962 new_sticky_column: 0,
5963 });
5964
5965 let events = editor.action_to_events(Action::DeleteForward);
5966 assert!(events.is_some());
5967
5968 let events = events.unwrap();
5969 assert_eq!(events.len(), 1);
5970
5971 match &events[0] {
5972 Event::Delete {
5973 range,
5974 deleted_text,
5975 ..
5976 } => {
5977 assert_eq!(range.clone(), 0..1); assert_eq!(deleted_text, "h");
5979 }
5980 _ => panic!("Expected Delete event"),
5981 }
5982 }
5983
5984 #[test]
5985 fn test_action_to_events_select_right() {
5986 let config = Config::default();
5987 let (dir_context, _temp) = test_dir_context();
5988 let mut editor = Editor::new(
5989 config,
5990 80,
5991 24,
5992 dir_context,
5993 crate::view::color_support::ColorCapability::TrueColor,
5994 test_filesystem(),
5995 )
5996 .unwrap();
5997
5998 let state = editor.active_state_mut();
6000 state.apply(&Event::Insert {
6001 position: 0,
6002 text: "hello".to_string(),
6003 cursor_id: state.cursors.primary_id(),
6004 });
6005
6006 state.apply(&Event::MoveCursor {
6008 cursor_id: state.cursors.primary_id(),
6009 old_position: 0, new_position: 0,
6011 old_anchor: None, new_anchor: None,
6013 old_sticky_column: 0,
6014 new_sticky_column: 0,
6015 });
6016
6017 let events = editor.action_to_events(Action::SelectRight);
6018 assert!(events.is_some());
6019
6020 let events = events.unwrap();
6021 assert_eq!(events.len(), 1);
6022
6023 match &events[0] {
6024 Event::MoveCursor {
6025 new_position,
6026 new_anchor,
6027 ..
6028 } => {
6029 assert_eq!(*new_position, 1); assert_eq!(*new_anchor, Some(0)); }
6032 _ => panic!("Expected MoveCursor event"),
6033 }
6034 }
6035
6036 #[test]
6037 fn test_action_to_events_select_all() {
6038 let config = Config::default();
6039 let (dir_context, _temp) = test_dir_context();
6040 let mut editor = Editor::new(
6041 config,
6042 80,
6043 24,
6044 dir_context,
6045 crate::view::color_support::ColorCapability::TrueColor,
6046 test_filesystem(),
6047 )
6048 .unwrap();
6049
6050 let state = editor.active_state_mut();
6052 state.apply(&Event::Insert {
6053 position: 0,
6054 text: "hello world".to_string(),
6055 cursor_id: state.cursors.primary_id(),
6056 });
6057
6058 let events = editor.action_to_events(Action::SelectAll);
6059 assert!(events.is_some());
6060
6061 let events = events.unwrap();
6062 assert_eq!(events.len(), 1);
6063
6064 match &events[0] {
6065 Event::MoveCursor {
6066 new_position,
6067 new_anchor,
6068 ..
6069 } => {
6070 assert_eq!(*new_position, 11); assert_eq!(*new_anchor, Some(0)); }
6073 _ => panic!("Expected MoveCursor event"),
6074 }
6075 }
6076
6077 #[test]
6078 fn test_action_to_events_document_nav() {
6079 let config = Config::default();
6080 let (dir_context, _temp) = test_dir_context();
6081 let mut editor = Editor::new(
6082 config,
6083 80,
6084 24,
6085 dir_context,
6086 crate::view::color_support::ColorCapability::TrueColor,
6087 test_filesystem(),
6088 )
6089 .unwrap();
6090
6091 let state = editor.active_state_mut();
6093 state.apply(&Event::Insert {
6094 position: 0,
6095 text: "line1\nline2\nline3".to_string(),
6096 cursor_id: state.cursors.primary_id(),
6097 });
6098
6099 let events = editor.action_to_events(Action::MoveDocumentStart);
6101 assert!(events.is_some());
6102 let events = events.unwrap();
6103 match &events[0] {
6104 Event::MoveCursor { new_position, .. } => {
6105 assert_eq!(*new_position, 0);
6106 }
6107 _ => panic!("Expected MoveCursor event"),
6108 }
6109
6110 let events = editor.action_to_events(Action::MoveDocumentEnd);
6112 assert!(events.is_some());
6113 let events = events.unwrap();
6114 match &events[0] {
6115 Event::MoveCursor { new_position, .. } => {
6116 assert_eq!(*new_position, 17); }
6118 _ => panic!("Expected MoveCursor event"),
6119 }
6120 }
6121
6122 #[test]
6123 fn test_action_to_events_remove_secondary_cursors() {
6124 use crate::model::event::CursorId;
6125
6126 let config = Config::default();
6127 let (dir_context, _temp) = test_dir_context();
6128 let mut editor = Editor::new(
6129 config,
6130 80,
6131 24,
6132 dir_context,
6133 crate::view::color_support::ColorCapability::TrueColor,
6134 test_filesystem(),
6135 )
6136 .unwrap();
6137
6138 {
6140 let state = editor.active_state_mut();
6141 state.apply(&Event::Insert {
6142 position: 0,
6143 text: "hello world test".to_string(),
6144 cursor_id: state.cursors.primary_id(),
6145 });
6146 }
6147
6148 {
6150 let state = editor.active_state_mut();
6151 state.apply(&Event::AddCursor {
6152 cursor_id: CursorId(1),
6153 position: 5,
6154 anchor: None,
6155 });
6156 state.apply(&Event::AddCursor {
6157 cursor_id: CursorId(2),
6158 position: 10,
6159 anchor: None,
6160 });
6161
6162 assert_eq!(state.cursors.count(), 3);
6163 }
6164
6165 let first_id = editor
6167 .active_state()
6168 .cursors
6169 .iter()
6170 .map(|(id, _)| id)
6171 .min_by_key(|id| id.0)
6172 .expect("Should have at least one cursor");
6173
6174 let events = editor.action_to_events(Action::RemoveSecondaryCursors);
6176 assert!(events.is_some());
6177
6178 let events = events.unwrap();
6179 let remove_cursor_events: Vec<_> = events
6182 .iter()
6183 .filter_map(|e| match e {
6184 Event::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
6185 _ => None,
6186 })
6187 .collect();
6188
6189 assert_eq!(remove_cursor_events.len(), 2);
6191
6192 for cursor_id in &remove_cursor_events {
6193 assert_ne!(*cursor_id, first_id);
6195 }
6196 }
6197
6198 #[test]
6199 fn test_action_to_events_scroll() {
6200 let config = Config::default();
6201 let (dir_context, _temp) = test_dir_context();
6202 let mut editor = Editor::new(
6203 config,
6204 80,
6205 24,
6206 dir_context,
6207 crate::view::color_support::ColorCapability::TrueColor,
6208 test_filesystem(),
6209 )
6210 .unwrap();
6211
6212 let events = editor.action_to_events(Action::ScrollUp);
6214 assert!(events.is_some());
6215 let events = events.unwrap();
6216 assert_eq!(events.len(), 1);
6217 match &events[0] {
6218 Event::Scroll { line_offset } => {
6219 assert_eq!(*line_offset, -1);
6220 }
6221 _ => panic!("Expected Scroll event"),
6222 }
6223
6224 let events = editor.action_to_events(Action::ScrollDown);
6226 assert!(events.is_some());
6227 let events = events.unwrap();
6228 assert_eq!(events.len(), 1);
6229 match &events[0] {
6230 Event::Scroll { line_offset } => {
6231 assert_eq!(*line_offset, 1);
6232 }
6233 _ => panic!("Expected Scroll event"),
6234 }
6235 }
6236
6237 #[test]
6238 fn test_action_to_events_none() {
6239 let config = Config::default();
6240 let (dir_context, _temp) = test_dir_context();
6241 let mut editor = Editor::new(
6242 config,
6243 80,
6244 24,
6245 dir_context,
6246 crate::view::color_support::ColorCapability::TrueColor,
6247 test_filesystem(),
6248 )
6249 .unwrap();
6250
6251 let events = editor.action_to_events(Action::None);
6253 assert!(events.is_none());
6254 }
6255
6256 #[test]
6257 fn test_lsp_incremental_insert_generates_correct_range() {
6258 use crate::model::buffer::Buffer;
6261
6262 let buffer = Buffer::from_str_test("hello\nworld");
6263
6264 let position = 0;
6267 let (line, character) = buffer.position_to_lsp_position(position);
6268
6269 assert_eq!(line, 0, "Insertion at start should be line 0");
6270 assert_eq!(character, 0, "Insertion at start should be char 0");
6271
6272 let lsp_pos = Position::new(line as u32, character as u32);
6274 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
6275
6276 assert_eq!(lsp_range.start.line, 0);
6277 assert_eq!(lsp_range.start.character, 0);
6278 assert_eq!(lsp_range.end.line, 0);
6279 assert_eq!(lsp_range.end.character, 0);
6280 assert_eq!(
6281 lsp_range.start, lsp_range.end,
6282 "Insert should have zero-width range"
6283 );
6284
6285 let position = 3;
6287 let (line, character) = buffer.position_to_lsp_position(position);
6288
6289 assert_eq!(line, 0);
6290 assert_eq!(character, 3);
6291
6292 let position = 6;
6294 let (line, character) = buffer.position_to_lsp_position(position);
6295
6296 assert_eq!(line, 1, "Position after newline should be line 1");
6297 assert_eq!(character, 0, "Position at start of line 2 should be char 0");
6298 }
6299
6300 #[test]
6301 fn test_lsp_incremental_delete_generates_correct_range() {
6302 use crate::model::buffer::Buffer;
6305
6306 let buffer = Buffer::from_str_test("hello\nworld");
6307
6308 let range_start = 1;
6310 let range_end = 5;
6311
6312 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
6313 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
6314
6315 assert_eq!(start_line, 0);
6316 assert_eq!(start_char, 1);
6317 assert_eq!(end_line, 0);
6318 assert_eq!(end_char, 5);
6319
6320 let lsp_range = LspRange::new(
6321 Position::new(start_line as u32, start_char as u32),
6322 Position::new(end_line as u32, end_char as u32),
6323 );
6324
6325 assert_eq!(lsp_range.start.line, 0);
6326 assert_eq!(lsp_range.start.character, 1);
6327 assert_eq!(lsp_range.end.line, 0);
6328 assert_eq!(lsp_range.end.character, 5);
6329 assert_ne!(
6330 lsp_range.start, lsp_range.end,
6331 "Delete should have non-zero range"
6332 );
6333
6334 let range_start = 4;
6336 let range_end = 8;
6337
6338 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
6339 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
6340
6341 assert_eq!(start_line, 0, "Delete start on line 0");
6342 assert_eq!(start_char, 4, "Delete start at char 4");
6343 assert_eq!(end_line, 1, "Delete end on line 1");
6344 assert_eq!(end_char, 2, "Delete end at char 2 of line 1");
6345 }
6346
6347 #[test]
6348 fn test_lsp_incremental_utf16_encoding() {
6349 use crate::model::buffer::Buffer;
6352
6353 let buffer = Buffer::from_str_test("😀hello");
6355
6356 let (line, character) = buffer.position_to_lsp_position(4);
6358
6359 assert_eq!(line, 0);
6360 assert_eq!(character, 2, "Emoji should count as 2 UTF-16 code units");
6361
6362 let (line, character) = buffer.position_to_lsp_position(9);
6364
6365 assert_eq!(line, 0);
6366 assert_eq!(
6367 character, 7,
6368 "Should be 2 (emoji) + 5 (text) = 7 UTF-16 code units"
6369 );
6370
6371 let buffer = Buffer::from_str_test("café");
6373
6374 let (line, character) = buffer.position_to_lsp_position(3);
6376
6377 assert_eq!(line, 0);
6378 assert_eq!(character, 3);
6379
6380 let (line, character) = buffer.position_to_lsp_position(5);
6382
6383 assert_eq!(line, 0);
6384 assert_eq!(character, 4, "é should count as 1 UTF-16 code unit");
6385 }
6386
6387 #[test]
6388 fn test_lsp_content_change_event_structure() {
6389 let insert_change = TextDocumentContentChangeEvent {
6393 range: Some(LspRange::new(Position::new(0, 5), Position::new(0, 5))),
6394 range_length: None,
6395 text: "NEW".to_string(),
6396 };
6397
6398 assert!(insert_change.range.is_some());
6399 assert_eq!(insert_change.text, "NEW");
6400 let range = insert_change.range.unwrap();
6401 assert_eq!(
6402 range.start, range.end,
6403 "Insert should have zero-width range"
6404 );
6405
6406 let delete_change = TextDocumentContentChangeEvent {
6408 range: Some(LspRange::new(Position::new(0, 2), Position::new(0, 7))),
6409 range_length: None,
6410 text: String::new(),
6411 };
6412
6413 assert!(delete_change.range.is_some());
6414 assert_eq!(delete_change.text, "");
6415 let range = delete_change.range.unwrap();
6416 assert_ne!(range.start, range.end, "Delete should have non-zero range");
6417 assert_eq!(range.start.line, 0);
6418 assert_eq!(range.start.character, 2);
6419 assert_eq!(range.end.line, 0);
6420 assert_eq!(range.end.character, 7);
6421 }
6422
6423 #[test]
6424 fn test_goto_matching_bracket_forward() {
6425 let config = Config::default();
6426 let (dir_context, _temp) = test_dir_context();
6427 let mut editor = Editor::new(
6428 config,
6429 80,
6430 24,
6431 dir_context,
6432 crate::view::color_support::ColorCapability::TrueColor,
6433 test_filesystem(),
6434 )
6435 .unwrap();
6436
6437 let state = editor.active_state_mut();
6439 state.apply(&Event::Insert {
6440 position: 0,
6441 text: "fn main() { let x = (1 + 2); }".to_string(),
6442 cursor_id: state.cursors.primary_id(),
6443 });
6444
6445 state.apply(&Event::MoveCursor {
6447 cursor_id: state.cursors.primary_id(),
6448 old_position: 31,
6449 new_position: 10,
6450 old_anchor: None,
6451 new_anchor: None,
6452 old_sticky_column: 0,
6453 new_sticky_column: 0,
6454 });
6455
6456 assert_eq!(state.cursors.primary().position, 10);
6457
6458 editor.goto_matching_bracket();
6460
6461 assert_eq!(editor.active_state().cursors.primary().position, 29);
6466 }
6467
6468 #[test]
6469 fn test_goto_matching_bracket_backward() {
6470 let config = Config::default();
6471 let (dir_context, _temp) = test_dir_context();
6472 let mut editor = Editor::new(
6473 config,
6474 80,
6475 24,
6476 dir_context,
6477 crate::view::color_support::ColorCapability::TrueColor,
6478 test_filesystem(),
6479 )
6480 .unwrap();
6481
6482 let state = editor.active_state_mut();
6484 state.apply(&Event::Insert {
6485 position: 0,
6486 text: "fn main() { let x = (1 + 2); }".to_string(),
6487 cursor_id: state.cursors.primary_id(),
6488 });
6489
6490 state.apply(&Event::MoveCursor {
6492 cursor_id: state.cursors.primary_id(),
6493 old_position: 31,
6494 new_position: 26,
6495 old_anchor: None,
6496 new_anchor: None,
6497 old_sticky_column: 0,
6498 new_sticky_column: 0,
6499 });
6500
6501 editor.goto_matching_bracket();
6503
6504 assert_eq!(editor.active_state().cursors.primary().position, 20);
6506 }
6507
6508 #[test]
6509 fn test_goto_matching_bracket_nested() {
6510 let config = Config::default();
6511 let (dir_context, _temp) = test_dir_context();
6512 let mut editor = Editor::new(
6513 config,
6514 80,
6515 24,
6516 dir_context,
6517 crate::view::color_support::ColorCapability::TrueColor,
6518 test_filesystem(),
6519 )
6520 .unwrap();
6521
6522 let state = editor.active_state_mut();
6524 state.apply(&Event::Insert {
6525 position: 0,
6526 text: "{a{b{c}d}e}".to_string(),
6527 cursor_id: state.cursors.primary_id(),
6528 });
6529
6530 state.apply(&Event::MoveCursor {
6532 cursor_id: state.cursors.primary_id(),
6533 old_position: 11,
6534 new_position: 0,
6535 old_anchor: None,
6536 new_anchor: None,
6537 old_sticky_column: 0,
6538 new_sticky_column: 0,
6539 });
6540
6541 editor.goto_matching_bracket();
6543
6544 assert_eq!(editor.active_state().cursors.primary().position, 10);
6546 }
6547
6548 #[test]
6549 fn test_search_case_sensitive() {
6550 let config = Config::default();
6551 let (dir_context, _temp) = test_dir_context();
6552 let mut editor = Editor::new(
6553 config,
6554 80,
6555 24,
6556 dir_context,
6557 crate::view::color_support::ColorCapability::TrueColor,
6558 test_filesystem(),
6559 )
6560 .unwrap();
6561
6562 let state = editor.active_state_mut();
6564 state.apply(&Event::Insert {
6565 position: 0,
6566 text: "Hello hello HELLO".to_string(),
6567 cursor_id: state.cursors.primary_id(),
6568 });
6569
6570 editor.search_case_sensitive = false;
6572 editor.perform_search("hello");
6573
6574 let search_state = editor.search_state.as_ref().unwrap();
6575 assert_eq!(
6576 search_state.matches.len(),
6577 3,
6578 "Should find all 3 matches case-insensitively"
6579 );
6580
6581 editor.search_case_sensitive = true;
6583 editor.perform_search("hello");
6584
6585 let search_state = editor.search_state.as_ref().unwrap();
6586 assert_eq!(
6587 search_state.matches.len(),
6588 1,
6589 "Should find only 1 exact match"
6590 );
6591 assert_eq!(
6592 search_state.matches[0], 6,
6593 "Should find 'hello' at position 6"
6594 );
6595 }
6596
6597 #[test]
6598 fn test_search_whole_word() {
6599 let config = Config::default();
6600 let (dir_context, _temp) = test_dir_context();
6601 let mut editor = Editor::new(
6602 config,
6603 80,
6604 24,
6605 dir_context,
6606 crate::view::color_support::ColorCapability::TrueColor,
6607 test_filesystem(),
6608 )
6609 .unwrap();
6610
6611 let state = editor.active_state_mut();
6613 state.apply(&Event::Insert {
6614 position: 0,
6615 text: "test testing tested attest test".to_string(),
6616 cursor_id: state.cursors.primary_id(),
6617 });
6618
6619 editor.search_whole_word = false;
6621 editor.search_case_sensitive = true;
6622 editor.perform_search("test");
6623
6624 let search_state = editor.search_state.as_ref().unwrap();
6625 assert_eq!(
6626 search_state.matches.len(),
6627 5,
6628 "Should find 'test' in all occurrences"
6629 );
6630
6631 editor.search_whole_word = true;
6633 editor.perform_search("test");
6634
6635 let search_state = editor.search_state.as_ref().unwrap();
6636 assert_eq!(
6637 search_state.matches.len(),
6638 2,
6639 "Should find only whole word 'test'"
6640 );
6641 assert_eq!(search_state.matches[0], 0, "First match at position 0");
6642 assert_eq!(search_state.matches[1], 27, "Second match at position 27");
6643 }
6644
6645 #[test]
6646 fn test_bookmarks() {
6647 let config = Config::default();
6648 let (dir_context, _temp) = test_dir_context();
6649 let mut editor = Editor::new(
6650 config,
6651 80,
6652 24,
6653 dir_context,
6654 crate::view::color_support::ColorCapability::TrueColor,
6655 test_filesystem(),
6656 )
6657 .unwrap();
6658
6659 let state = editor.active_state_mut();
6661 state.apply(&Event::Insert {
6662 position: 0,
6663 text: "Line 1\nLine 2\nLine 3".to_string(),
6664 cursor_id: state.cursors.primary_id(),
6665 });
6666
6667 state.apply(&Event::MoveCursor {
6669 cursor_id: state.cursors.primary_id(),
6670 old_position: 21,
6671 new_position: 7,
6672 old_anchor: None,
6673 new_anchor: None,
6674 old_sticky_column: 0,
6675 new_sticky_column: 0,
6676 });
6677
6678 editor.set_bookmark('1');
6680 assert!(editor.bookmarks.contains_key(&'1'));
6681 assert_eq!(editor.bookmarks.get(&'1').unwrap().position, 7);
6682
6683 let state = editor.active_state_mut();
6685 state.apply(&Event::MoveCursor {
6686 cursor_id: state.cursors.primary_id(),
6687 old_position: 7,
6688 new_position: 14,
6689 old_anchor: None,
6690 new_anchor: None,
6691 old_sticky_column: 0,
6692 new_sticky_column: 0,
6693 });
6694
6695 editor.jump_to_bookmark('1');
6697 assert_eq!(editor.active_state().cursors.primary().position, 7);
6698
6699 editor.clear_bookmark('1');
6701 assert!(!editor.bookmarks.contains_key(&'1'));
6702 }
6703
6704 #[test]
6705 fn test_action_enum_new_variants() {
6706 use serde_json::json;
6708
6709 let args = HashMap::new();
6710 assert_eq!(
6711 Action::from_str("smart_home", &args),
6712 Some(Action::SmartHome)
6713 );
6714 assert_eq!(
6715 Action::from_str("dedent_selection", &args),
6716 Some(Action::DedentSelection)
6717 );
6718 assert_eq!(
6719 Action::from_str("toggle_comment", &args),
6720 Some(Action::ToggleComment)
6721 );
6722 assert_eq!(
6723 Action::from_str("goto_matching_bracket", &args),
6724 Some(Action::GoToMatchingBracket)
6725 );
6726 assert_eq!(
6727 Action::from_str("list_bookmarks", &args),
6728 Some(Action::ListBookmarks)
6729 );
6730 assert_eq!(
6731 Action::from_str("toggle_search_case_sensitive", &args),
6732 Some(Action::ToggleSearchCaseSensitive)
6733 );
6734 assert_eq!(
6735 Action::from_str("toggle_search_whole_word", &args),
6736 Some(Action::ToggleSearchWholeWord)
6737 );
6738
6739 let mut args_with_char = HashMap::new();
6741 args_with_char.insert("char".to_string(), json!("5"));
6742 assert_eq!(
6743 Action::from_str("set_bookmark", &args_with_char),
6744 Some(Action::SetBookmark('5'))
6745 );
6746 assert_eq!(
6747 Action::from_str("jump_to_bookmark", &args_with_char),
6748 Some(Action::JumpToBookmark('5'))
6749 );
6750 assert_eq!(
6751 Action::from_str("clear_bookmark", &args_with_char),
6752 Some(Action::ClearBookmark('5'))
6753 );
6754 }
6755
6756 #[test]
6757 fn test_keybinding_new_defaults() {
6758 use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
6759
6760 let mut config = Config::default();
6764 config.active_keybinding_map = crate::config::KeybindingMapName("default".to_string());
6765 let resolver = KeybindingResolver::new(&config);
6766
6767 let event = KeyEvent {
6769 code: KeyCode::Char('/'),
6770 modifiers: KeyModifiers::CONTROL,
6771 kind: KeyEventKind::Press,
6772 state: KeyEventState::NONE,
6773 };
6774 let action = resolver.resolve(&event, KeyContext::Normal);
6775 assert_eq!(action, Action::ToggleComment);
6776
6777 let event = KeyEvent {
6779 code: KeyCode::Char(']'),
6780 modifiers: KeyModifiers::CONTROL,
6781 kind: KeyEventKind::Press,
6782 state: KeyEventState::NONE,
6783 };
6784 let action = resolver.resolve(&event, KeyContext::Normal);
6785 assert_eq!(action, Action::GoToMatchingBracket);
6786
6787 let event = KeyEvent {
6789 code: KeyCode::Tab,
6790 modifiers: KeyModifiers::SHIFT,
6791 kind: KeyEventKind::Press,
6792 state: KeyEventState::NONE,
6793 };
6794 let action = resolver.resolve(&event, KeyContext::Normal);
6795 assert_eq!(action, Action::DedentSelection);
6796
6797 let event = KeyEvent {
6799 code: KeyCode::Char('g'),
6800 modifiers: KeyModifiers::CONTROL,
6801 kind: KeyEventKind::Press,
6802 state: KeyEventState::NONE,
6803 };
6804 let action = resolver.resolve(&event, KeyContext::Normal);
6805 assert_eq!(action, Action::GotoLine);
6806
6807 let event = KeyEvent {
6809 code: KeyCode::Char('5'),
6810 modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
6811 kind: KeyEventKind::Press,
6812 state: KeyEventState::NONE,
6813 };
6814 let action = resolver.resolve(&event, KeyContext::Normal);
6815 assert_eq!(action, Action::SetBookmark('5'));
6816
6817 let event = KeyEvent {
6818 code: KeyCode::Char('5'),
6819 modifiers: KeyModifiers::ALT,
6820 kind: KeyEventKind::Press,
6821 state: KeyEventState::NONE,
6822 };
6823 let action = resolver.resolve(&event, KeyContext::Normal);
6824 assert_eq!(action, Action::JumpToBookmark('5'));
6825 }
6826
6827 #[test]
6839 fn test_lsp_rename_didchange_positions_bug() {
6840 use crate::model::buffer::Buffer;
6841
6842 let config = Config::default();
6843 let (dir_context, _temp) = test_dir_context();
6844 let mut editor = Editor::new(
6845 config,
6846 80,
6847 24,
6848 dir_context,
6849 crate::view::color_support::ColorCapability::TrueColor,
6850 test_filesystem(),
6851 )
6852 .unwrap();
6853
6854 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
6858 editor.active_state_mut().buffer =
6859 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
6860
6861 let cursor_id = editor.active_state().cursors.primary_id();
6866
6867 let batch = Event::Batch {
6868 events: vec![
6869 Event::Delete {
6871 range: 23..26, deleted_text: "val".to_string(),
6873 cursor_id,
6874 },
6875 Event::Insert {
6876 position: 23,
6877 text: "value".to_string(),
6878 cursor_id,
6879 },
6880 Event::Delete {
6882 range: 7..10, deleted_text: "val".to_string(),
6884 cursor_id,
6885 },
6886 Event::Insert {
6887 position: 7,
6888 text: "value".to_string(),
6889 cursor_id,
6890 },
6891 ],
6892 description: "LSP Rename".to_string(),
6893 };
6894
6895 let lsp_changes_before = editor.collect_lsp_changes(&batch);
6897
6898 editor.active_state_mut().apply(&batch);
6900
6901 let lsp_changes_after = editor.collect_lsp_changes(&batch);
6904
6905 let final_content = editor.active_state().buffer.to_string().unwrap();
6907 assert_eq!(
6908 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
6909 "Buffer should have 'value' in both places"
6910 );
6911
6912 assert_eq!(lsp_changes_before.len(), 4, "Should have 4 changes");
6918
6919 let first_delete = &lsp_changes_before[0];
6920 let first_del_range = first_delete.range.unwrap();
6921 assert_eq!(
6922 first_del_range.start.line, 1,
6923 "First delete should be on line 1 (BEFORE)"
6924 );
6925 assert_eq!(
6926 first_del_range.start.character, 4,
6927 "First delete start should be at char 4 (BEFORE)"
6928 );
6929
6930 assert_eq!(lsp_changes_after.len(), 4, "Should have 4 changes");
6936
6937 let first_delete_after = &lsp_changes_after[0];
6938 let first_del_range_after = first_delete_after.range.unwrap();
6939
6940 eprintln!("BEFORE modification:");
6943 eprintln!(
6944 " Delete at line {}, char {}-{}",
6945 first_del_range.start.line,
6946 first_del_range.start.character,
6947 first_del_range.end.character
6948 );
6949 eprintln!("AFTER modification:");
6950 eprintln!(
6951 " Delete at line {}, char {}-{}",
6952 first_del_range_after.start.line,
6953 first_del_range_after.start.character,
6954 first_del_range_after.end.character
6955 );
6956
6957 assert_ne!(
6975 first_del_range_after.end.character, first_del_range.end.character,
6976 "BUG CONFIRMED: LSP positions are different when calculated after buffer modification!"
6977 );
6978
6979 eprintln!("\n=== BUG DEMONSTRATED ===");
6980 eprintln!("When collect_lsp_changes() is called AFTER buffer modification,");
6981 eprintln!("the positions are WRONG because they're calculated from the");
6982 eprintln!("modified buffer, not the original buffer.");
6983 eprintln!("This causes the second rename to fail with 'content modified' error.");
6984 eprintln!("========================\n");
6985 }
6986
6987 #[test]
6988 fn test_lsp_rename_preserves_cursor_position() {
6989 use crate::model::buffer::Buffer;
6990
6991 let config = Config::default();
6992 let (dir_context, _temp) = test_dir_context();
6993 let mut editor = Editor::new(
6994 config,
6995 80,
6996 24,
6997 dir_context,
6998 crate::view::color_support::ColorCapability::TrueColor,
6999 test_filesystem(),
7000 )
7001 .unwrap();
7002
7003 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
7007 editor.active_state_mut().buffer =
7008 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
7009
7010 let original_cursor_pos = 23;
7012 editor.active_state_mut().cursors.primary_mut().position = original_cursor_pos;
7013
7014 let buffer_text = editor.active_state().buffer.to_string().unwrap();
7016 let text_at_cursor = buffer_text[original_cursor_pos..original_cursor_pos + 3].to_string();
7017 assert_eq!(text_at_cursor, "val", "Cursor should be at 'val'");
7018
7019 let cursor_id = editor.active_state().cursors.primary_id();
7022 let buffer_id = editor.active_buffer();
7023
7024 let events = vec![
7025 Event::Delete {
7027 range: 23..26, deleted_text: "val".to_string(),
7029 cursor_id,
7030 },
7031 Event::Insert {
7032 position: 23,
7033 text: "value".to_string(),
7034 cursor_id,
7035 },
7036 Event::Delete {
7038 range: 7..10, deleted_text: "val".to_string(),
7040 cursor_id,
7041 },
7042 Event::Insert {
7043 position: 7,
7044 text: "value".to_string(),
7045 cursor_id,
7046 },
7047 ];
7048
7049 editor
7051 .apply_events_to_buffer_as_bulk_edit(buffer_id, events, "LSP Rename".to_string())
7052 .unwrap();
7053
7054 let final_content = editor.active_state().buffer.to_string().unwrap();
7056 assert_eq!(
7057 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
7058 "Buffer should have 'value' in both places"
7059 );
7060
7061 let final_cursor_pos = editor.active_state().cursors.primary().position;
7069 let expected_cursor_pos = 25; assert_eq!(
7072 final_cursor_pos, expected_cursor_pos,
7073 "Cursor should be at position {} (start of 'value' on line 1), but was at {}. \
7074 Original pos: {}, expected adjustment: +2 for first rename",
7075 expected_cursor_pos, final_cursor_pos, original_cursor_pos
7076 );
7077
7078 let text_at_new_cursor = &final_content[final_cursor_pos..final_cursor_pos + 5];
7080 assert_eq!(
7081 text_at_new_cursor, "value",
7082 "Cursor should be at the start of 'value' after rename"
7083 );
7084 }
7085
7086 #[test]
7087 fn test_lsp_rename_twice_consecutive() {
7088 use crate::model::buffer::Buffer;
7091
7092 let config = Config::default();
7093 let (dir_context, _temp) = test_dir_context();
7094 let mut editor = Editor::new(
7095 config,
7096 80,
7097 24,
7098 dir_context,
7099 crate::view::color_support::ColorCapability::TrueColor,
7100 test_filesystem(),
7101 )
7102 .unwrap();
7103
7104 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
7106 editor.active_state_mut().buffer =
7107 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
7108
7109 let cursor_id = editor.active_state().cursors.primary_id();
7110 let buffer_id = editor.active_buffer();
7111
7112 let events1 = vec![
7115 Event::Delete {
7117 range: 23..26,
7118 deleted_text: "val".to_string(),
7119 cursor_id,
7120 },
7121 Event::Insert {
7122 position: 23,
7123 text: "value".to_string(),
7124 cursor_id,
7125 },
7126 Event::Delete {
7128 range: 7..10,
7129 deleted_text: "val".to_string(),
7130 cursor_id,
7131 },
7132 Event::Insert {
7133 position: 7,
7134 text: "value".to_string(),
7135 cursor_id,
7136 },
7137 ];
7138
7139 let batch1 = Event::Batch {
7141 events: events1.clone(),
7142 description: "LSP Rename 1".to_string(),
7143 };
7144
7145 let lsp_changes1 = editor.collect_lsp_changes(&batch1);
7147
7148 assert_eq!(
7150 lsp_changes1.len(),
7151 4,
7152 "First rename should have 4 LSP changes"
7153 );
7154
7155 let first_del = &lsp_changes1[0];
7157 let first_del_range = first_del.range.unwrap();
7158 assert_eq!(first_del_range.start.line, 1, "First delete line");
7159 assert_eq!(
7160 first_del_range.start.character, 4,
7161 "First delete start char"
7162 );
7163 assert_eq!(first_del_range.end.character, 7, "First delete end char");
7164
7165 editor
7167 .apply_events_to_buffer_as_bulk_edit(buffer_id, events1, "LSP Rename 1".to_string())
7168 .unwrap();
7169
7170 let after_first = editor.active_state().buffer.to_string().unwrap();
7172 assert_eq!(
7173 after_first, "fn foo(value: i32) {\n value + 1\n}\n",
7174 "After first rename"
7175 );
7176
7177 let events2 = vec![
7187 Event::Delete {
7189 range: 25..30,
7190 deleted_text: "value".to_string(),
7191 cursor_id,
7192 },
7193 Event::Insert {
7194 position: 25,
7195 text: "x".to_string(),
7196 cursor_id,
7197 },
7198 Event::Delete {
7200 range: 7..12,
7201 deleted_text: "value".to_string(),
7202 cursor_id,
7203 },
7204 Event::Insert {
7205 position: 7,
7206 text: "x".to_string(),
7207 cursor_id,
7208 },
7209 ];
7210
7211 let batch2 = Event::Batch {
7213 events: events2.clone(),
7214 description: "LSP Rename 2".to_string(),
7215 };
7216
7217 let lsp_changes2 = editor.collect_lsp_changes(&batch2);
7219
7220 assert_eq!(
7224 lsp_changes2.len(),
7225 4,
7226 "Second rename should have 4 LSP changes"
7227 );
7228
7229 let second_first_del = &lsp_changes2[0];
7231 let second_first_del_range = second_first_del.range.unwrap();
7232 assert_eq!(
7233 second_first_del_range.start.line, 1,
7234 "Second rename first delete should be on line 1"
7235 );
7236 assert_eq!(
7237 second_first_del_range.start.character, 4,
7238 "Second rename first delete start should be at char 4"
7239 );
7240 assert_eq!(
7241 second_first_del_range.end.character, 9,
7242 "Second rename first delete end should be at char 9 (4 + 5 for 'value')"
7243 );
7244
7245 let second_third_del = &lsp_changes2[2];
7247 let second_third_del_range = second_third_del.range.unwrap();
7248 assert_eq!(
7249 second_third_del_range.start.line, 0,
7250 "Second rename third delete should be on line 0"
7251 );
7252 assert_eq!(
7253 second_third_del_range.start.character, 7,
7254 "Second rename third delete start should be at char 7"
7255 );
7256 assert_eq!(
7257 second_third_del_range.end.character, 12,
7258 "Second rename third delete end should be at char 12 (7 + 5 for 'value')"
7259 );
7260
7261 editor
7263 .apply_events_to_buffer_as_bulk_edit(buffer_id, events2, "LSP Rename 2".to_string())
7264 .unwrap();
7265
7266 let after_second = editor.active_state().buffer.to_string().unwrap();
7268 assert_eq!(
7269 after_second, "fn foo(x: i32) {\n x + 1\n}\n",
7270 "After second rename"
7271 );
7272 }
7273
7274 #[test]
7275 fn test_ensure_active_tab_visible_static_offset() {
7276 let config = Config::default();
7277 let (dir_context, _temp) = test_dir_context();
7278 let mut editor = Editor::new(
7279 config,
7280 80,
7281 24,
7282 dir_context,
7283 crate::view::color_support::ColorCapability::TrueColor,
7284 test_filesystem(),
7285 )
7286 .unwrap();
7287 let split_id = editor.split_manager.active_split();
7288
7289 let buf1 = editor.new_buffer();
7291 editor
7292 .buffers
7293 .get_mut(&buf1)
7294 .unwrap()
7295 .buffer
7296 .set_file_path(std::path::PathBuf::from("aaa_long_name_01.txt"));
7297 let buf2 = editor.new_buffer();
7298 editor
7299 .buffers
7300 .get_mut(&buf2)
7301 .unwrap()
7302 .buffer
7303 .set_file_path(std::path::PathBuf::from("bbb_long_name_02.txt"));
7304 let buf3 = editor.new_buffer();
7305 editor
7306 .buffers
7307 .get_mut(&buf3)
7308 .unwrap()
7309 .buffer
7310 .set_file_path(std::path::PathBuf::from("ccc_long_name_03.txt"));
7311
7312 {
7313 let view_state = editor.split_view_states.get_mut(&split_id).unwrap();
7314 view_state.open_buffers = vec![buf1, buf2, buf3];
7315 view_state.tab_scroll_offset = 50;
7316 }
7317
7318 editor.ensure_active_tab_visible(split_id, buf1, 25);
7322 assert_eq!(
7323 editor
7324 .split_view_states
7325 .get(&split_id)
7326 .unwrap()
7327 .tab_scroll_offset,
7328 0
7329 );
7330
7331 editor.ensure_active_tab_visible(split_id, buf3, 25);
7333 let view_state = editor.split_view_states.get(&split_id).unwrap();
7334 assert!(view_state.tab_scroll_offset > 0);
7335 let total_width: usize = view_state
7336 .open_buffers
7337 .iter()
7338 .enumerate()
7339 .map(|(idx, id)| {
7340 let state = editor.buffers.get(id).unwrap();
7341 let name_len = state
7342 .buffer
7343 .file_path()
7344 .and_then(|p| p.file_name())
7345 .and_then(|n| n.to_str())
7346 .map(|s| s.chars().count())
7347 .unwrap_or(0);
7348 let tab_width = 2 + name_len;
7349 if idx < view_state.open_buffers.len() - 1 {
7350 tab_width + 1 } else {
7352 tab_width
7353 }
7354 })
7355 .sum();
7356 assert!(view_state.tab_scroll_offset <= total_width);
7357 }
7358}