1use super::*;
13
14fn set_dot_path(root: &mut serde_json::Value, path: &str, value: serde_json::Value) {
17 let segments: Vec<&str> = path.split('.').filter(|s| !s.is_empty()).collect();
18 if segments.is_empty() {
19 return;
20 }
21 let mut cur = root;
22 for seg in &segments[..segments.len() - 1] {
23 if !cur.is_object() {
24 *cur = serde_json::Value::Object(serde_json::Map::new());
25 }
26 cur = cur
27 .as_object_mut()
28 .unwrap()
29 .entry((*seg).to_string())
30 .or_insert(serde_json::Value::Null);
31 }
32 let last = segments[segments.len() - 1];
33 if !cur.is_object() {
34 *cur = serde_json::Value::Object(serde_json::Map::new());
35 }
36 cur.as_object_mut().unwrap().insert(last.to_string(), value);
37}
38
39impl Editor {
40 pub fn new(
43 config: Config,
44 width: u16,
45 height: u16,
46 dir_context: DirectoryContext,
47 color_capability: crate::view::color_support::ColorCapability,
48 filesystem: Arc<dyn FileSystem + Send + Sync>,
49 ) -> AnyhowResult<Self> {
50 Self::with_working_dir(
51 config,
52 width,
53 height,
54 None,
55 dir_context,
56 true,
57 color_capability,
58 filesystem,
59 )
60 }
61
62 #[allow(clippy::too_many_arguments)]
65 pub fn with_working_dir(
66 config: Config,
67 width: u16,
68 height: u16,
69 working_dir: Option<PathBuf>,
70 dir_context: DirectoryContext,
71 plugins_enabled: bool,
72 color_capability: crate::view::color_support::ColorCapability,
73 filesystem: Arc<dyn FileSystem + Send + Sync>,
74 ) -> AnyhowResult<Self> {
75 Self::with_working_dir_opts(
76 config,
77 width,
78 height,
79 working_dir,
80 dir_context,
81 plugins_enabled,
82 color_capability,
83 filesystem,
84 false,
85 )
86 }
87
88 #[allow(clippy::too_many_arguments)]
96 pub fn with_working_dir_opts(
97 config: Config,
98 width: u16,
99 height: u16,
100 working_dir: Option<PathBuf>,
101 dir_context: DirectoryContext,
102 plugins_enabled: bool,
103 color_capability: crate::view::color_support::ColorCapability,
104 filesystem: Arc<dyn FileSystem + Send + Sync>,
105 defer_plugin_load: bool,
106 ) -> AnyhowResult<Self> {
107 tracing::info!("Building default grammar registry...");
108 let start = std::time::Instant::now();
109 let mut grammar_registry = crate::primitives::grammar::GrammarRegistry::defaults_only();
110 std::sync::Arc::get_mut(&mut grammar_registry)
116 .expect("defaults_only returned a shared Arc")
117 .apply_language_config(&config.languages);
118 tracing::info!("Default grammar registry built in {:?}", start.elapsed());
119 Self::with_options(
123 config,
124 width,
125 height,
126 working_dir,
127 filesystem,
128 plugins_enabled,
129 true, dir_context,
131 None,
132 color_capability,
133 grammar_registry,
134 defer_plugin_load,
135 )
136 }
137
138 #[allow(clippy::too_many_arguments)]
149 pub fn for_test(
150 config: Config,
151 width: u16,
152 height: u16,
153 working_dir: Option<PathBuf>,
154 dir_context: DirectoryContext,
155 color_capability: crate::view::color_support::ColorCapability,
156 filesystem: Arc<dyn FileSystem + Send + Sync>,
157 time_source: Option<SharedTimeSource>,
158 grammar_registry: Option<Arc<crate::primitives::grammar::GrammarRegistry>>,
159 enable_plugins: bool,
160 enable_embedded_plugins: bool,
161 ) -> AnyhowResult<Self> {
162 let mut grammar_registry =
163 grammar_registry.unwrap_or_else(crate::primitives::grammar::GrammarRegistry::empty);
164 std::sync::Arc::get_mut(&mut grammar_registry)
171 .expect("grammar registry Arc must be uniquely owned at for_test entry")
172 .apply_language_config(&config.languages);
173 let mut editor = Self::with_options(
174 config,
175 width,
176 height,
177 working_dir,
178 filesystem,
179 enable_plugins,
180 enable_embedded_plugins,
181 dir_context,
182 time_source,
183 color_capability,
184 grammar_registry,
185 false,
186 )?;
187 editor.needs_full_grammar_build = false;
190 Ok(editor)
191 }
192
193 #[allow(clippy::too_many_arguments)]
197 fn with_options(
198 mut config: Config,
199 width: u16,
200 height: u16,
201 working_dir: Option<PathBuf>,
202 filesystem: Arc<dyn FileSystem + Send + Sync>,
203 enable_plugins: bool,
204 #[cfg_attr(not(feature = "embed-plugins"), allow(unused_variables))]
205 enable_embedded_plugins: bool,
206 dir_context: DirectoryContext,
207 time_source: Option<SharedTimeSource>,
208 color_capability: crate::view::color_support::ColorCapability,
209 grammar_registry: Arc<crate::primitives::grammar::GrammarRegistry>,
210 defer_plugin_load: bool,
211 ) -> AnyhowResult<Self> {
212 let time_source = time_source.unwrap_or_else(RealTimeSource::shared);
214 tracing::info!("Editor::new called with width={}, height={}", width, height);
215
216 let working_dir = working_dir
218 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
219
220 let working_dir = working_dir.canonicalize().unwrap_or(working_dir);
223
224 tracing::info!("Loading themes...");
226 let theme_loader = crate::view::theme::ThemeLoader::new(dir_context.themes_dir());
227 let scan_result =
231 crate::services::packages::scan_installed_packages(&dir_context.config_dir);
232
233 for (lang_id, lang_config) in &scan_result.language_configs {
235 config
236 .languages
237 .entry(lang_id.clone())
238 .or_insert_with(|| lang_config.clone());
239 }
240
241 for (lang_id, lsp_config) in &scan_result.lsp_configs {
243 config
244 .lsp
245 .entry(lang_id.clone())
246 .or_insert_with(|| LspLanguageConfig::Multi(vec![lsp_config.clone()]));
247 }
248
249 let theme_registry = Arc::new(theme_loader.load_all(&scan_result.bundle_theme_dirs));
250 tracing::info!("Themes loaded");
251
252 let theme = theme_registry.get_cloned(&config.theme).unwrap_or_else(|| {
254 tracing::warn!(
255 "Theme '{}' not found, falling back to default theme",
256 config.theme.0
257 );
258 theme_registry
259 .get_cloned(&crate::config::ThemeName(
260 crate::view::theme::THEME_HIGH_CONTRAST.to_string(),
261 ))
262 .expect("Default theme must exist")
263 });
264
265 theme.set_terminal_cursor_color();
267
268 let keybindings = Arc::new(RwLock::new(KeybindingResolver::new(&config)));
269
270 let mut buffers = HashMap::new();
272 let mut event_logs = HashMap::new();
273
274 let buffer_id = BufferId(1);
279 let mut state = EditorState::new(
280 width,
281 height,
282 config.editor.large_file_threshold_bytes as usize,
283 Arc::clone(&filesystem),
284 );
285 state
287 .margins
288 .configure_for_line_numbers(config.editor.line_numbers);
289 state.buffer_settings.tab_size = config.editor.tab_size;
290 state.buffer_settings.auto_close = config.editor.auto_close;
291 tracing::info!("EditorState created for buffer {:?}", buffer_id);
293 buffers.insert(buffer_id, state);
294 event_logs.insert(buffer_id, EventLog::new());
295
296 let mut buffer_metadata = HashMap::new();
298 buffer_metadata.insert(buffer_id, BufferMetadata::new());
299
300 let root_uri = types::file_path_to_lsp_uri(&working_dir);
302
303 let tokio_runtime = tokio::runtime::Builder::new_multi_thread()
305 .worker_threads(2) .thread_name("editor-async")
307 .enable_all()
308 .build()
309 .ok();
310
311 let async_bridge = AsyncBridge::new();
313
314 if tokio_runtime.is_none() {
315 tracing::warn!("Failed to create Tokio runtime - async features disabled");
316 }
317
318 let mut lsp = LspManager::new(root_uri);
320
321 if let Some(ref runtime) = tokio_runtime {
323 lsp.set_runtime(runtime.handle().clone(), async_bridge.clone());
324 }
325
326 for (language, lsp_configs) in &config.lsp {
328 lsp.set_language_configs(language.clone(), lsp_configs.as_slice().to_vec());
329 }
330
331 let universal_servers: Vec<LspServerConfig> = config
333 .universal_lsp
334 .values()
335 .flat_map(|lc| lc.as_slice().to_vec())
336 .filter(|c| c.enabled)
337 .collect();
338 lsp.set_universal_configs(universal_servers);
339
340 if working_dir.join("deno.json").exists() || working_dir.join("deno.jsonc").exists() {
343 tracing::info!("Detected Deno project (deno.json found), using deno lsp for JS/TS");
344 let deno_config = LspServerConfig {
345 command: "deno".to_string(),
346 args: vec!["lsp".to_string()],
347 enabled: true,
348 auto_start: false,
349 process_limits: ProcessLimits::default(),
350 initialization_options: Some(serde_json::json!({"enable": true})),
351 ..Default::default()
352 };
353 lsp.set_language_config("javascript".to_string(), deno_config.clone());
354 lsp.set_language_config("typescript".to_string(), deno_config);
355 }
356
357 let split_manager = SplitManager::new(buffer_id);
359
360 let mut split_view_states = HashMap::new();
362 let initial_split_id = split_manager.active_split();
363 let mut initial_view_state = SplitViewState::with_buffer(width, height, buffer_id);
364 initial_view_state.apply_config_defaults(
365 config.editor.line_numbers,
366 config.editor.highlight_current_line,
367 config.editor.line_wrap,
368 config.editor.wrap_indent,
369 config.editor.wrap_column,
370 config.editor.rulers.clone(),
371 );
372 split_view_states.insert(initial_split_id, initial_view_state);
373
374 let fs_manager = Arc::new(FsManager::new(Arc::clone(&filesystem)));
376
377 let command_registry = Arc::new(RwLock::new(CommandRegistry::new()));
379
380 let authority = crate::services::authority::Authority {
386 filesystem: Arc::clone(&filesystem),
387 ..crate::services::authority::Authority::local()
388 };
389 let process_spawner = Arc::clone(&authority.process_spawner);
390
391 let mut quick_open_registry = QuickOpenRegistry::new();
393 quick_open_registry.register(Box::new(FileProvider::new(
394 Arc::clone(&filesystem),
395 Arc::clone(&process_spawner),
396 tokio_runtime.as_ref().map(|rt| rt.handle().clone()),
397 Some(async_bridge.sender()),
398 )));
399 quick_open_registry.register(Box::new(CommandProvider::new(
400 Arc::clone(&command_registry),
401 Arc::clone(&keybindings),
402 )));
403 quick_open_registry.register(Box::new(BufferProvider::new()));
404 quick_open_registry.register(Box::new(GotoLineProvider::new()));
405
406 let theme_cache = Arc::new(RwLock::new(theme_registry.to_json_map()));
408
409 let plugin_manager = PluginManager::new(
411 enable_plugins,
412 Arc::clone(&command_registry),
413 dir_context.clone(),
414 Arc::clone(&theme_cache),
415 );
416
417 #[cfg(feature = "plugins")]
420 if let Some(snapshot_handle) = plugin_manager.state_snapshot_handle() {
421 let mut snapshot = snapshot_handle.write().unwrap();
422 snapshot.working_dir = working_dir.clone();
423 }
424
425 if plugin_manager.is_active() {
432 let mut plugin_dirs: Vec<std::path::PathBuf> = vec![];
433
434 if let Ok(exe_path) = std::env::current_exe() {
436 if let Some(exe_dir) = exe_path.parent() {
437 let exe_plugin_dir = exe_dir.join("plugins");
438 if exe_plugin_dir.exists() {
439 plugin_dirs.push(exe_plugin_dir);
440 }
441 }
442 }
443
444 #[cfg(feature = "embed-plugins")]
455 if enable_embedded_plugins && plugin_dirs.is_empty() {
456 if let Some(embedded_dir) =
457 crate::services::plugins::embedded::get_embedded_plugins_dir()
458 {
459 tracing::info!("Using embedded plugins from: {:?}", embedded_dir);
460 plugin_dirs.push(embedded_dir.clone());
461 }
462 }
463
464 let user_plugins_dir = dir_context.config_dir.join("plugins");
466 if user_plugins_dir.exists() && !plugin_dirs.contains(&user_plugins_dir) {
467 tracing::info!("Found user plugins directory: {:?}", user_plugins_dir);
468 plugin_dirs.push(user_plugins_dir.clone());
469 }
470
471 let packages_dir = dir_context.config_dir.join("plugins").join("packages");
473 if packages_dir.exists() {
474 if let Ok(entries) = std::fs::read_dir(&packages_dir) {
475 for entry in entries.flatten() {
476 let path = entry.path();
477 if path.is_dir() {
479 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
480 if !name.starts_with('.') {
481 tracing::info!("Found package manager plugin: {:?}", path);
482 plugin_dirs.push(path);
483 }
484 }
485 }
486 }
487 }
488 }
489
490 for dir in &scan_result.bundle_plugin_dirs {
492 tracing::info!("Found bundle plugin directory: {:?}", dir);
493 plugin_dirs.push(dir.clone());
494 }
495
496 if plugin_dirs.is_empty() {
497 tracing::debug!(
498 "No plugins directory found next to executable or in working dir: {:?}",
499 working_dir
500 );
501 }
502
503 if defer_plugin_load {
504 #[cfg(feature = "plugins")]
515 {
516 let bridge = &async_bridge;
517 let mut dir_receivers: Vec<(
518 std::path::PathBuf,
519 fresh_plugin_runtime::thread::oneshot::Receiver<
520 fresh_plugin_runtime::thread::PluginsDirLoadResult,
521 >,
522 )> = Vec::with_capacity(plugin_dirs.len());
523 for plugin_dir in &plugin_dirs {
524 tracing::info!(
525 "Submitting async TypeScript plugin load for: {:?}",
526 plugin_dir
527 );
528 if let Some(rx) = plugin_manager
529 .load_plugins_from_dir_with_config_request(plugin_dir, &config.plugins)
530 {
531 dir_receivers.push((plugin_dir.clone(), rx));
532 }
533 }
534 let declarations_rx = if !dir_receivers.is_empty() {
535 plugin_manager.list_plugins_request()
536 } else {
537 None
538 };
539 if !dir_receivers.is_empty() {
540 let sender = bridge.sender();
541 std::thread::Builder::new()
542 .name("plugin-load-forwarder".to_string())
543 .spawn(move || {
544 for (dir, rx) in dir_receivers {
545 let load_start = std::time::Instant::now();
546 match rx.recv() {
547 Ok((errors, discovered_plugins)) => {
548 tracing::info!(
549 "Loaded TypeScript plugins from {:?} in {:?}",
550 dir,
551 load_start.elapsed()
552 );
553 drop(sender.send(
554 crate::services::async_bridge::AsyncMessage::PluginsDirLoaded {
555 dir,
556 errors,
557 discovered_plugins,
558 },
559 ));
560 }
561 Err(e) => {
562 tracing::warn!(
563 "plugin-load-forwarder: dir {:?} recv failed: {}",
564 dir,
565 e
566 );
567 }
568 }
569 }
570 if let Some(rx) = declarations_rx {
571 match rx.recv() {
572 Ok(plugin_infos) => {
573 let declarations: Vec<(String, String)> = plugin_infos
574 .into_iter()
575 .filter_map(|info| {
576 info.declarations.map(|d| (info.name, d))
577 })
578 .collect();
579 drop(sender.send(
580 crate::services::async_bridge::AsyncMessage::PluginDeclarationsReady {
581 declarations,
582 },
583 ));
584 }
585 Err(e) => {
586 tracing::warn!(
587 "plugin-load-forwarder: list_plugins recv failed: {}",
588 e
589 );
590 }
591 }
592 }
593 })
594 .ok();
595 }
596 }
597 } else {
598 for plugin_dir in plugin_dirs {
603 tracing::info!("Loading TypeScript plugins from: {:?}", plugin_dir);
604 let load_start = std::time::Instant::now();
605 let (errors, discovered_plugins) = plugin_manager
606 .load_plugins_from_dir_with_config(&plugin_dir, &config.plugins);
607 tracing::info!(
608 "Loaded TypeScript plugins from {:?} in {:?}",
609 plugin_dir,
610 load_start.elapsed()
611 );
612
613 for (name, plugin_config) in discovered_plugins {
616 config.plugins.insert(name, plugin_config);
617 }
618
619 if !errors.is_empty() {
620 for err in &errors {
621 tracing::error!("TypeScript plugin load error: {}", err);
622 }
623 #[cfg(debug_assertions)]
625 panic!(
626 "TypeScript plugin loading failed with {} error(s): {}",
627 errors.len(),
628 errors.join("; ")
629 );
630 }
631 }
632
633 let declarations = plugin_manager.plugin_declarations();
641 crate::init_script::write_plugin_declarations(
642 &dir_context.config_dir,
643 &declarations,
644 );
645 }
646 }
647
648 let file_explorer_width = config.file_explorer.width;
650 let file_explorer_side = config.file_explorer.side;
651 let recovery_enabled = config.editor.recovery_enabled;
652 let check_for_updates = config.check_for_updates;
653 let show_menu_bar = config.editor.show_menu_bar;
654 let show_tab_bar = config.editor.show_tab_bar;
655 let show_status_bar = config.editor.show_status_bar;
656 let show_prompt_line = config.editor.show_prompt_line;
657
658 let update_checker = if check_for_updates {
660 tracing::debug!("Update checking enabled, starting periodic checker");
661 Some(
662 crate::services::release_checker::start_periodic_update_check(
663 crate::services::release_checker::DEFAULT_RELEASES_URL,
664 time_source.clone(),
665 dir_context.data_dir.clone(),
666 ),
667 )
668 } else {
669 tracing::debug!("Update checking disabled by config");
670 None
671 };
672
673 let user_config_raw = Config::read_user_config_raw(&working_dir);
675
676 let config_arc = Arc::new(config);
683 let config_cached_json =
684 Arc::new(serde_json::to_value(&*config_arc).unwrap_or(serde_json::Value::Null));
685 let config_snapshot_anchor = Arc::clone(&config_arc);
686
687 let mut editor = Editor {
688 buffers,
689 event_logs,
690 next_buffer_id: 2,
691 config: config_arc,
692 config_snapshot_anchor,
693 config_cached_json,
694 user_config_raw: Arc::new(user_config_raw),
695 dir_context: dir_context.clone(),
696 grammar_registry,
697 pending_grammars: scan_result
698 .additional_grammars
699 .iter()
700 .map(|g| PendingGrammar {
701 language: g.language.clone(),
702 grammar_path: g.path.to_string_lossy().to_string(),
703 extensions: g.extensions.clone(),
704 })
705 .collect(),
706 grammar_reload_pending: false,
707 grammar_build_in_progress: false,
708 needs_full_grammar_build: true,
709 streaming_grep_cancellation: None,
710 pending_grammar_callbacks: Vec::new(),
711 theme,
712 theme_registry,
713 expanded_menus_cache: crate::view::ui::ExpandedMenusCache::default(),
714 theme_cache,
715 ansi_background: None,
716 ansi_background_path: None,
717 background_fade: crate::primitives::ansi_background::DEFAULT_BACKGROUND_FADE,
718 keybindings,
719 clipboard: crate::services::clipboard::Clipboard::new(),
720 should_quit: false,
721 should_detach: false,
722 session_mode: false,
723 software_cursor_only: false,
724 session_name: None,
725 pending_escape_sequences: Vec::new(),
726 restart_with_dir: None,
727 status_message: None,
728 plugin_status_message: None,
729 last_window_title: None,
730 plugin_errors: Vec::new(),
731 prompt: None,
732 terminal_width: width,
733 terminal_height: height,
734 lsp: Some(lsp),
735 buffer_metadata,
736 mode_registry: ModeRegistry::new(),
737 tokio_runtime,
738 async_bridge: Some(async_bridge),
739 split_manager,
740 split_view_states,
741 previous_viewports: HashMap::new(),
742 scroll_sync_manager: ScrollSyncManager::new(),
743 file_explorer: None,
744 preview: None,
745 suppress_position_history_once: false,
746 fs_manager,
747 authority,
748 pending_authority: None,
749 remote_indicator_override: None,
750 local_filesystem: Arc::new(crate::model::filesystem::StdFileSystem),
751 file_explorer_visible: false,
752 file_explorer_sync_in_progress: false,
753 file_explorer_width,
754 file_explorer_side,
755 pending_file_explorer_show_hidden: None,
756 pending_file_explorer_show_gitignored: None,
757 menu_bar_visible: show_menu_bar,
758 file_explorer_decorations: HashMap::new(),
759 file_explorer_decoration_cache:
760 crate::view::file_tree::FileExplorerDecorationCache::default(),
761 file_explorer_clipboard: None,
762 menu_bar_auto_shown: false,
763 tab_bar_visible: show_tab_bar,
764 status_bar_visible: show_status_bar,
765 prompt_line_visible: show_prompt_line,
766 mouse_enabled: true,
767 same_buffer_scroll_sync: false,
768 mouse_cursor_position: None,
769 gpm_active: false,
770 key_context: KeyContext::Normal,
771 menu_state: crate::view::ui::MenuState::new(dir_context.themes_dir()),
772 menus: crate::config::MenuConfig::translated(),
773 working_dir,
774 position_history: PositionHistory::new(),
775 in_navigation: false,
776 next_lsp_request_id: 0,
777 pending_completion_requests: HashSet::new(),
778 completion_items: None,
779 scheduled_completion_trigger: None,
780 completion_service: crate::services::completion::CompletionService::new(),
781 dabbrev_state: None,
782 pending_goto_definition_request: None,
783 hover: hover::HoverState::default(),
784 pending_references_request: None,
785 pending_references_symbol: String::new(),
786 pending_signature_help_request: None,
787 pending_code_actions_requests: HashSet::new(),
788 pending_code_actions_server_names: HashMap::new(),
789 pending_code_actions: None,
790 pending_inlay_hints_requests: HashMap::new(),
791 pending_folding_range_requests: HashMap::new(),
792 folding_ranges_in_flight: HashMap::new(),
793 folding_ranges_debounce: HashMap::new(),
794 pending_semantic_token_requests: HashMap::new(),
795 semantic_tokens_in_flight: HashMap::new(),
796 pending_semantic_token_range_requests: HashMap::new(),
797 semantic_tokens_range_in_flight: HashMap::new(),
798 semantic_tokens_range_last_request: HashMap::new(),
799 semantic_tokens_range_applied: HashMap::new(),
800 semantic_tokens_full_debounce: HashMap::new(),
801 search_state: None,
802 search_namespace: crate::view::overlay::OverlayNamespace::from_string(
803 "search".to_string(),
804 ),
805 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace::from_string(
806 "lsp-diagnostic".to_string(),
807 ),
808 pending_search_range: None,
809 interactive_replace_state: None,
810 mouse_state: MouseState::default(),
811 tab_context_menu: None,
812 file_explorer_context_menu: None,
813 theme_info_popup: None,
814 cached_layout: CachedLayout::default(),
815 command_registry,
816 quick_open_registry,
817 plugin_manager,
818 plugin_dev_workspaces: HashMap::new(),
819 seen_byte_ranges: HashMap::new(),
820 panel_ids: HashMap::new(),
821 buffer_groups: HashMap::new(),
822 buffer_to_group: HashMap::new(),
823 next_buffer_group_id: 0,
824 grouped_subtrees: HashMap::new(),
825 background_process_handles: HashMap::new(),
826 host_process_handles: HashMap::new(),
827 prompt_histories: {
828 let mut histories = HashMap::new();
830 for history_name in ["search", "replace", "goto_line"] {
831 let path = dir_context.prompt_history_path(history_name);
832 let history = crate::input::input_history::InputHistory::load_from_file(&path)
833 .unwrap_or_else(|e| {
834 tracing::warn!("Failed to load {} history: {}", history_name, e);
835 crate::input::input_history::InputHistory::new()
836 });
837 histories.insert(history_name.to_string(), history);
838 }
839 histories
840 },
841 pending_async_prompt_callback: None,
842 pending_next_key_callbacks: std::collections::VecDeque::new(),
843 key_capture_active: false,
844 pending_key_capture_buffer: std::collections::VecDeque::new(),
845 goto_line_preview: None,
846 lsp_progress: std::collections::HashMap::new(),
847 lsp_server_statuses: std::collections::HashMap::new(),
848 lsp_window_messages: Vec::new(),
849 lsp_log_messages: Vec::new(),
850 diagnostic_result_ids: HashMap::new(),
851 scheduled_diagnostic_pull: None,
852 scheduled_inlay_hints_request: None,
853 stored_push_diagnostics: HashMap::new(),
854 stored_pull_diagnostics: HashMap::new(),
855 stored_diagnostics: Arc::new(HashMap::new()),
856 stored_folding_ranges: Arc::new(HashMap::new()),
857 event_broadcaster: crate::model::control_event::EventBroadcaster::default(),
858 bookmarks: bookmarks::BookmarkState::default(),
859 search_case_sensitive: true,
860 search_whole_word: false,
861 search_use_regex: false,
862 search_confirm_each: false,
863 macros: macros::MacroState::default(),
864 #[cfg(feature = "plugins")]
865 pending_plugin_actions: Vec::new(),
866 #[cfg(feature = "plugins")]
867 plugin_render_requested: false,
868 chord_state: Vec::new(),
869 user_dismissed_lsp_languages: std::collections::HashSet::new(),
870 auto_start_prompted_languages: std::collections::HashSet::new(),
871 pending_auto_start_prompts: std::collections::HashSet::new(),
872 lsp_auto_prompt_enabled: super::lsp_auto_prompt::default_enabled(),
873 pending_close_buffer: None,
874 auto_revert_enabled: true,
875 last_auto_revert_poll: time_source.now(),
876 last_file_tree_poll: time_source.now(),
877 git_index_resolved: false,
878 file_mod_times: HashMap::new(),
879 dir_mod_times: HashMap::new(),
880 pending_file_poll_rx: None,
881 pending_dir_poll_rx: None,
882 file_rapid_change_counts: HashMap::new(),
883 file_open_state: None,
884 file_browser_layout: None,
885 recovery_service: {
886 let recovery_config = RecoveryConfig {
887 enabled: recovery_enabled,
888 ..RecoveryConfig::default()
889 };
890 RecoveryService::with_config_and_dir(recovery_config, dir_context.recovery_dir())
891 },
892 full_redraw_requested: false,
893 suspend_requested: false,
894 time_source: time_source.clone(),
895 last_auto_recovery_save: time_source.now(),
896 last_persistent_auto_save: time_source.now(),
897 active_custom_contexts: HashSet::new(),
898 plugin_global_state: HashMap::new(),
899 editor_mode: None,
900 warning_log: None,
901 status_log_path: None,
902 warning_domains: WarningDomainRegistry::new(),
903 update_checker,
904 terminal_manager: crate::services::terminal::TerminalManager::new(),
905 terminal_buffers: HashMap::new(),
906 terminal_backing_files: HashMap::new(),
907 terminal_log_files: HashMap::new(),
908 ephemeral_terminals: std::collections::HashSet::new(),
909 terminal_mode: false,
910 keyboard_capture: false,
911 terminal_mode_resume: std::collections::HashSet::new(),
912 previous_click_time: None,
913 previous_click_position: None,
914 click_count: 0,
915 settings_state: None,
916 calibration_wizard: None,
917 event_debug: None,
918 keybinding_editor: None,
919 key_translator: crate::input::key_translator::KeyTranslator::load_from_config_dir(
920 &dir_context.config_dir,
921 )
922 .unwrap_or_default(),
923 color_capability,
924 pending_file_opens: Vec::new(),
925 pending_hot_exit_recovery: false,
926 wait_tracking: HashMap::new(),
927 completed_waits: Vec::new(),
928 stdin_stream: stdin_stream::StdinStream::default(),
929 line_scan: line_scan::LineScan::default(),
930 search_scan: search_scan::SearchScan::default(),
931 search_overlay_top_byte: None,
932 review_hunks: Vec::new(),
933 global_popups: crate::view::popup::PopupManager::new(),
934 composite_buffers: HashMap::new(),
935 composite_view_states: HashMap::new(),
936 animations: crate::view::animation::AnimationRunner::new(),
937 previous_cursor_screen_pos: None,
938 cursor_jump_animation: None,
939 pending_vb_animations: Vec::new(),
940 };
941
942 editor.clipboard.apply_config(&editor.config.clipboard);
944
945 #[cfg(feature = "plugins")]
946 {
947 editor.update_plugin_state_snapshot();
948 if editor.plugin_manager.is_active() {
949 editor.plugin_manager.run_hook(
950 "editor_initialized",
951 crate::services::plugins::hooks::HookArgs::EditorInitialized {},
952 );
953 }
954 }
955
956 Ok(editor)
957 }
958
959 pub fn event_broadcaster(&self) -> &crate::model::control_event::EventBroadcaster {
961 &self.event_broadcaster
962 }
963
964 pub(super) fn start_background_grammar_build(
969 &mut self,
970 additional: Vec<crate::primitives::grammar::GrammarSpec>,
971 callback_ids: Vec<fresh_core::api::JsCallbackId>,
972 ) {
973 let Some(bridge) = &self.async_bridge else {
974 return;
975 };
976 self.grammar_build_in_progress = true;
977 let sender = bridge.sender();
978 let config_dir = self.dir_context.config_dir.clone();
979 tracing::info!(
980 "Spawning background grammar build thread ({} plugin grammars)...",
981 additional.len()
982 );
983 std::thread::Builder::new()
984 .name("grammar-build".to_string())
985 .spawn(move || {
986 tracing::info!("[grammar-build] Thread started");
987 let start = std::time::Instant::now();
988 let registry = if additional.is_empty() {
989 crate::primitives::grammar::GrammarRegistry::for_editor(config_dir)
990 } else {
991 crate::primitives::grammar::GrammarRegistry::for_editor_with_additional(
992 config_dir,
993 &additional,
994 )
995 };
996 tracing::info!("[grammar-build] Complete in {:?}", start.elapsed());
997 drop(sender.send(
998 crate::services::async_bridge::AsyncMessage::GrammarRegistryBuilt {
999 registry,
1000 callback_ids,
1001 },
1002 ));
1003 })
1004 .ok();
1005 }
1006
1007 pub fn load_init_script(&mut self, enabled: bool) {
1014 use crate::init_script::{
1015 check, decide_load, describe, record_success, refresh_types_scaffolding, CheckSeverity,
1016 InitOutcome, LoadDecision,
1017 };
1018
1019 let config_dir = self.dir_context.config_dir.clone();
1020
1021 if enabled {
1022 refresh_types_scaffolding(&config_dir);
1026
1027 let report = check(&config_dir);
1032 if !report.ok {
1033 for d in &report.diagnostics {
1034 let level = match d.severity {
1035 CheckSeverity::Error => "error",
1036 CheckSeverity::Warning => "warning",
1037 };
1038 tracing::warn!(
1039 "init.ts pre-load {level} at {}:{}: {}",
1040 d.line,
1041 d.column,
1042 d.message
1043 );
1044 }
1045 }
1046 }
1047
1048 let outcome = match decide_load(&config_dir, enabled) {
1049 LoadDecision::Skip(outcome) => outcome,
1050 LoadDecision::Load { source } => {
1051 if !self.plugin_manager.is_active() {
1052 InitOutcome::Failed {
1053 message: "plugin runtime inactive (--no-plugins); init.ts cannot run"
1054 .into(),
1055 }
1056 } else {
1057 match self.plugin_manager.load_plugin_from_source(
1058 &source,
1059 crate::init_script::INIT_PLUGIN_NAME,
1060 true,
1061 ) {
1062 Ok(()) => {
1063 record_success(&config_dir);
1064 InitOutcome::Loaded
1065 }
1066 Err(e) => InitOutcome::Failed {
1067 message: format!("{e}"),
1068 },
1069 }
1070 }
1071 }
1072 };
1073
1074 let summary = describe(&outcome);
1075 match outcome {
1076 InitOutcome::NotFound | InitOutcome::Disabled => tracing::debug!("{}", summary),
1077 InitOutcome::Loaded => tracing::info!("{}", summary),
1078 InitOutcome::CrashFused { .. } | InitOutcome::Failed { .. } => {
1079 tracing::warn!("{}", summary);
1080 self.set_status_message(summary);
1081 }
1082 }
1083 }
1084
1085 pub fn load_init_script_async(&mut self, enabled: bool) {
1097 use crate::init_script::{
1098 check, decide_load, refresh_types_scaffolding, CheckSeverity, InitOutcome, LoadDecision,
1099 };
1100 use crate::services::async_bridge::PluginInitScriptOutcome;
1101
1102 let config_dir = self.dir_context.config_dir.clone();
1103
1104 if enabled {
1105 refresh_types_scaffolding(&config_dir);
1106 let report = check(&config_dir);
1107 if !report.ok {
1108 for d in &report.diagnostics {
1109 let level = match d.severity {
1110 CheckSeverity::Error => "error",
1111 CheckSeverity::Warning => "warning",
1112 };
1113 tracing::warn!(
1114 "init.ts pre-load {level} at {}:{}: {}",
1115 d.line,
1116 d.column,
1117 d.message
1118 );
1119 }
1120 }
1121 }
1122
1123 let outcome_now: Option<PluginInitScriptOutcome> = match decide_load(&config_dir, enabled) {
1124 LoadDecision::Skip(outcome) => Some(match outcome {
1125 InitOutcome::NotFound => PluginInitScriptOutcome::NotFound,
1126 InitOutcome::Disabled => PluginInitScriptOutcome::Disabled,
1127 InitOutcome::CrashFused { failures } => {
1128 PluginInitScriptOutcome::CrashFused { failures }
1129 }
1130 InitOutcome::Loaded => PluginInitScriptOutcome::Loaded,
1133 InitOutcome::Failed { message } => PluginInitScriptOutcome::Failed { message },
1134 }),
1135 LoadDecision::Load { source } => {
1136 if !self.plugin_manager.is_active() {
1137 Some(PluginInitScriptOutcome::Failed {
1138 message: "plugin runtime inactive (--no-plugins); init.ts cannot run"
1139 .into(),
1140 })
1141 } else {
1142 self.spawn_init_script_forwarder(source);
1143 None
1144 }
1145 }
1146 };
1147
1148 if let Some(outcome) = outcome_now {
1149 if let Some(bridge) = &self.async_bridge {
1153 drop(bridge.sender().send(
1154 crate::services::async_bridge::AsyncMessage::PluginInitScriptLoaded(outcome),
1155 ));
1156 } else {
1157 self.handle_plugin_init_script_loaded(outcome);
1158 }
1159 }
1160 }
1161
1162 #[cfg(feature = "plugins")]
1163 fn spawn_init_script_forwarder(&self, source: String) {
1164 let Some(bridge) = &self.async_bridge else {
1165 return;
1166 };
1167 let Some(rx) = self.plugin_manager.load_plugin_from_source_request(
1168 &source,
1169 crate::init_script::INIT_PLUGIN_NAME,
1170 true,
1171 ) else {
1172 return;
1173 };
1174 let sender = bridge.sender();
1175 std::thread::Builder::new()
1176 .name("plugin-init-forwarder".to_string())
1177 .spawn(move || {
1178 let outcome = match rx.recv() {
1179 Ok(Ok(())) => crate::services::async_bridge::PluginInitScriptOutcome::Loaded,
1180 Ok(Err(e)) => crate::services::async_bridge::PluginInitScriptOutcome::Failed {
1181 message: format!("{e}"),
1182 },
1183 Err(e) => crate::services::async_bridge::PluginInitScriptOutcome::Failed {
1184 message: format!("plugin thread closed: {e}"),
1185 },
1186 };
1187 drop(sender.send(
1188 crate::services::async_bridge::AsyncMessage::PluginInitScriptLoaded(outcome),
1189 ));
1190 })
1191 .ok();
1192 }
1193
1194 #[cfg(not(feature = "plugins"))]
1195 fn spawn_init_script_forwarder(&self, _source: String) {}
1196
1197 pub fn handle_set_setting(&mut self, path: String, value: serde_json::Value) {
1201 let mut json = serde_json::to_value(&*self.config).unwrap_or_default();
1202 set_dot_path(&mut json, &path, value);
1203 match serde_json::from_value::<crate::config::Config>(json) {
1204 Ok(new_config) => {
1205 let old_theme = self.config.theme.clone();
1206 self.config = Arc::new(new_config);
1207 if old_theme != self.config.theme {
1208 if let Some(theme) = self.theme_registry.get_cloned(&self.config.theme) {
1209 self.theme = theme;
1210 }
1211 }
1212 *self.keybindings.write().unwrap() =
1213 crate::input::keybindings::KeybindingResolver::new(&self.config);
1214 self.clipboard.apply_config(&self.config.clipboard);
1215 self.menu_bar_visible = self.config.editor.show_menu_bar;
1216 self.tab_bar_visible = self.config.editor.show_tab_bar;
1217 self.status_bar_visible = self.config.editor.show_status_bar;
1218 self.prompt_line_visible = self.config.editor.show_prompt_line;
1219 #[cfg(feature = "plugins")]
1220 self.update_plugin_state_snapshot();
1221 }
1222 Err(e) => {
1223 self.set_status_message(format!("setSetting({path}): {e}"));
1224 }
1225 }
1226 }
1227
1228 pub(crate) fn handle_plugins_dir_loaded(
1233 &mut self,
1234 dir: std::path::PathBuf,
1235 errors: Vec<String>,
1236 discovered_plugins: std::collections::HashMap<String, fresh_core::config::PluginConfig>,
1237 ) {
1238 if !discovered_plugins.is_empty() {
1239 let cfg = std::sync::Arc::make_mut(&mut self.config);
1240 for (name, plugin_config) in discovered_plugins {
1241 cfg.plugins.insert(name, plugin_config);
1242 }
1243 }
1244 if !errors.is_empty() {
1245 for err in &errors {
1246 tracing::error!("TypeScript plugin load error: {}", err);
1247 }
1248 #[cfg(debug_assertions)]
1249 panic!(
1250 "TypeScript plugin loading failed for {:?} with {} error(s): {}",
1251 dir,
1252 errors.len(),
1253 errors.join("; ")
1254 );
1255 #[cfg(not(debug_assertions))]
1256 {
1257 let _ = dir;
1258 }
1259 }
1260 }
1261
1262 pub(crate) fn handle_plugin_declarations_ready(&self, declarations: Vec<(String, String)>) {
1266 crate::init_script::write_plugin_declarations(&self.dir_context.config_dir, &declarations);
1267 }
1268
1269 pub(crate) fn handle_plugin_init_script_loaded(
1273 &mut self,
1274 outcome: crate::services::async_bridge::PluginInitScriptOutcome,
1275 ) {
1276 use crate::init_script::{describe, record_success, InitOutcome};
1277 use crate::services::async_bridge::PluginInitScriptOutcome as O;
1278 let outcome = match outcome {
1279 O::NotFound => InitOutcome::NotFound,
1280 O::Disabled => InitOutcome::Disabled,
1281 O::CrashFused { failures } => InitOutcome::CrashFused { failures },
1282 O::Loaded => {
1283 record_success(&self.dir_context.config_dir);
1284 InitOutcome::Loaded
1285 }
1286 O::Failed { message } => InitOutcome::Failed { message },
1287 };
1288 let summary = describe(&outcome);
1289 match outcome {
1290 InitOutcome::NotFound | InitOutcome::Disabled => tracing::debug!("{}", summary),
1291 InitOutcome::Loaded => tracing::info!("{}", summary),
1292 InitOutcome::CrashFused { .. } | InitOutcome::Failed { .. } => {
1293 tracing::warn!("{}", summary);
1294 self.set_status_message(summary);
1295 }
1296 }
1297 }
1298
1299 pub fn fire_plugins_loaded_hook(&self) {
1301 #[cfg(feature = "plugins")]
1302 if self.plugin_manager.is_active() {
1303 self.plugin_manager.run_hook(
1304 "plugins_loaded",
1305 crate::services::plugins::hooks::HookArgs::PluginsLoaded {},
1306 );
1307 }
1308 }
1309
1310 pub fn fire_ready_hook(&self) {
1312 #[cfg(feature = "plugins")]
1313 if self.plugin_manager.is_active() {
1314 self.plugin_manager
1315 .run_hook("ready", crate::services::plugins::hooks::HookArgs::Ready {});
1316 }
1317 }
1318
1319 #[doc(hidden)]
1321 pub fn config_for_tests(&self) -> &crate::config::Config {
1322 &self.config
1323 }
1324
1325 #[doc(hidden)]
1327 pub fn dispatch_action_for_tests(&mut self, action: crate::input::keybindings::Action) {
1328 if let Err(e) = self.handle_action(action) {
1329 tracing::warn!("dispatch_action_for_tests: {e}");
1330 }
1331 }
1332}