1use super::*;
13
14struct InitTimer {
18 label: &'static str,
19 start: std::time::Instant,
20 last: std::time::Instant,
21 enabled: bool,
22}
23
24impl InitTimer {
25 fn start(label: &'static str) -> Self {
26 let enabled = std::env::var("FRESH_TEST_TIMING").is_ok_and(|v| !v.is_empty() && v != "0");
27 let now = std::time::Instant::now();
28 if enabled {
29 eprintln!("[timing] {label} start");
30 }
31 Self {
32 label,
33 start: now,
34 last: now,
35 enabled,
36 }
37 }
38 fn phase(&mut self, name: &str) {
39 if !self.enabled {
40 return;
41 }
42 let now = std::time::Instant::now();
43 let delta = now.duration_since(self.last);
44 let cumul = now.duration_since(self.start);
45 eprintln!(
46 "[timing] {name:<30} +{delta:>8.1}ms (cumul {cumul:.1}ms)",
47 name = name,
48 delta = delta.as_secs_f64() * 1000.0,
49 cumul = cumul.as_secs_f64() * 1000.0,
50 );
51 self.last = now;
52 }
53 fn finish(self) {
54 if !self.enabled {
55 return;
56 }
57 eprintln!(
58 "[timing] {label} total {total:.1}ms",
59 label = self.label,
60 total = self.start.elapsed().as_secs_f64() * 1000.0,
61 );
62 }
63}
64
65fn set_dot_path(root: &mut serde_json::Value, path: &str, value: serde_json::Value) {
68 let segments: Vec<&str> = path.split('.').filter(|s| !s.is_empty()).collect();
69 if segments.is_empty() {
70 return;
71 }
72 let mut cur = root;
73 for seg in &segments[..segments.len() - 1] {
74 if !cur.is_object() {
75 *cur = serde_json::Value::Object(serde_json::Map::new());
76 }
77 cur = cur
78 .as_object_mut()
79 .unwrap()
80 .entry((*seg).to_string())
81 .or_insert(serde_json::Value::Null);
82 }
83 let last = segments[segments.len() - 1];
84 if !cur.is_object() {
85 *cur = serde_json::Value::Object(serde_json::Map::new());
86 }
87 cur.as_object_mut().unwrap().insert(last.to_string(), value);
88}
89
90pub(super) struct EditorParts {
106 pub(super) config: Arc<Config>,
108 pub(super) config_snapshot_anchor: Arc<Config>,
109 pub(super) config_cached_json: Arc<serde_json::Value>,
110 pub(super) user_config_raw: Arc<serde_json::Value>,
111 pub(super) dir_context: DirectoryContext,
112 pub(super) working_dir: PathBuf,
113
114 pub(super) theme: Arc<RwLock<crate::view::theme::Theme>>,
116 pub(super) theme_registry: Arc<crate::view::theme::ThemeRegistry>,
117 pub(super) theme_cache: Arc<RwLock<HashMap<String, serde_json::Value>>>,
118
119 pub(super) grammar_registry: Arc<crate::primitives::grammar::GrammarRegistry>,
121 pub(super) pending_grammars: Vec<PendingGrammar>,
122 pub(super) needs_full_grammar_build: bool,
123
124 pub(super) keybindings: Arc<RwLock<KeybindingResolver>>,
126 pub(super) buffer_id_alloc: crate::app::window_resources::BufferIdAllocator,
127 pub(super) next_buffer_id: usize,
128
129 pub(super) terminal_width: u16,
131 pub(super) terminal_height: u16,
132 pub(super) color_capability: crate::view::color_support::ColorCapability,
133
134 pub(super) tokio_runtime: Option<Arc<tokio::runtime::Runtime>>,
136 pub(super) async_bridge: AsyncBridge,
137 pub(super) fs_manager: Arc<FsManager>,
138 pub(super) authority: crate::services::authority::Authority,
139 pub(super) local_filesystem: Arc<dyn FileSystem + Send + Sync>,
140
141 pub(super) windows: HashMap<fresh_core::WindowId, crate::app::window::Window>,
147 pub(super) active_window: fresh_core::WindowId,
148 pub(super) next_window_id: u64,
149
150 pub(super) command_registry: Arc<RwLock<CommandRegistry>>,
152 pub(super) quick_open_registry: QuickOpenRegistry,
153 pub(super) plugin_manager: Arc<RwLock<PluginManager>>,
154 pub(super) recovery_service: RecoveryService,
155 pub(super) key_translator: crate::input::key_translator::KeyTranslator,
156 pub(super) update_checker: Option<crate::services::release_checker::PeriodicUpdateChecker>,
157
158 pub(super) time_source: SharedTimeSource,
160
161 pub(super) plugin_global_state: HashMap<String, HashMap<String, serde_json::Value>>,
167
168 pub(super) plugin_schemas: HashMap<String, serde_json::Value>,
170
171 pub(super) event_broadcaster: crate::model::control_event::EventBroadcaster,
173}
174
175impl Editor {
176 pub(super) fn from_parts(parts: EditorParts) -> Self {
189 Editor {
190 next_buffer_id: parts.next_buffer_id,
192 buffer_id_alloc: parts.buffer_id_alloc,
193 config: parts.config,
194 config_snapshot_anchor: parts.config_snapshot_anchor,
195 config_cached_json: parts.config_cached_json,
196 user_config_raw: parts.user_config_raw,
197 dir_context: parts.dir_context.clone(),
198 grammar_registry: parts.grammar_registry,
199 pending_grammars: parts.pending_grammars,
200 needs_full_grammar_build: parts.needs_full_grammar_build,
201 theme: parts.theme,
202 theme_registry: parts.theme_registry,
203 theme_cache: parts.theme_cache,
204 keybindings: parts.keybindings,
205 terminal_width: parts.terminal_width,
206 terminal_height: parts.terminal_height,
207 tokio_runtime: parts.tokio_runtime,
208 async_bridge: Some(parts.async_bridge),
209 fs_manager: parts.fs_manager,
210 authority: parts.authority,
211 local_filesystem: parts.local_filesystem,
212 menu_state: crate::view::ui::MenuState::new(parts.dir_context.themes_dir()),
213 working_dir: parts.working_dir,
214 windows: parts.windows,
215 active_window: parts.active_window,
216 next_window_id: parts.next_window_id,
217 command_registry: parts.command_registry,
218 quick_open_registry: parts.quick_open_registry,
219 plugin_manager: parts.plugin_manager,
220 recovery_service: parts.recovery_service,
221 time_source: parts.time_source,
222 color_capability: parts.color_capability,
223 update_checker: parts.update_checker,
224 key_translator: parts.key_translator,
225
226 grammar_reload_pending: false,
228 grammar_build_in_progress: false,
229 pending_grammar_callbacks: Vec::new(),
230 expanded_menus_cache: crate::view::ui::ExpandedMenusCache::default(),
231 ansi_background: None,
232 ansi_background_path: None,
233 background_fade: crate::primitives::ansi_background::DEFAULT_BACKGROUND_FADE,
234 clipboard: crate::services::clipboard::Clipboard::new(),
235 should_quit: false,
236 should_detach: false,
237 session_mode: false,
238 software_cursor_only: false,
239 session_name: None,
240 pending_escape_sequences: Vec::new(),
241 restart_with_dir: None,
242 last_window_title: None,
243 mode_registry: ModeRegistry::new(),
244 pending_authority: None,
245 remote_indicator_override: None,
246 menus: crate::config::MenuConfig::translated(),
247 background_process_handles: HashMap::new(),
248 host_process_handles: HashMap::new(),
249 status_bar_token_registry: Mutex::new(HashMap::new()),
250 plugin_schemas: std::sync::Arc::new(std::sync::RwLock::new(parts.plugin_schemas)),
251 event_broadcaster: parts.event_broadcaster,
252 #[cfg(feature = "plugins")]
253 pending_plugin_actions: Vec::new(),
254 #[cfg(feature = "plugins")]
255 plugin_render_requested: false,
256 full_redraw_requested: false,
257 suspend_requested: false,
258 plugin_global_state: parts.plugin_global_state,
259 warning_log: None,
260 status_log_path: None,
261 file_watcher_manager: crate::services::file_watcher::FileWatcherManager::new(),
262 last_path_change_for_test: None,
263 last_watch_response_for_test: None,
264 preview_window_id: None,
265 settings_state: None,
266 calibration_wizard: None,
267 keybinding_editor: None,
269 stdin_stream: stdin_stream::StdinStream::default(),
270 global_popups: crate::view::popup::PopupManager::new(),
271 previous_cursor_screen_pos: None,
272 cursor_jump_animation: None,
273 pending_vb_animations: Vec::new(),
274 widget_registry: crate::widgets::WidgetRegistry::new(),
275 floating_widget_panel: None,
276 }
277 }
278
279 pub fn new(
282 config: Config,
283 width: u16,
284 height: u16,
285 dir_context: DirectoryContext,
286 color_capability: crate::view::color_support::ColorCapability,
287 filesystem: Arc<dyn FileSystem + Send + Sync>,
288 ) -> AnyhowResult<Self> {
289 Self::with_working_dir(
290 config,
291 width,
292 height,
293 None,
294 dir_context,
295 true,
296 color_capability,
297 filesystem,
298 )
299 }
300
301 #[allow(clippy::too_many_arguments)]
304 pub fn with_working_dir(
305 config: Config,
306 width: u16,
307 height: u16,
308 working_dir: Option<PathBuf>,
309 dir_context: DirectoryContext,
310 plugins_enabled: bool,
311 color_capability: crate::view::color_support::ColorCapability,
312 filesystem: Arc<dyn FileSystem + Send + Sync>,
313 ) -> AnyhowResult<Self> {
314 Self::with_working_dir_opts(
315 config,
316 width,
317 height,
318 working_dir,
319 dir_context,
320 plugins_enabled,
321 color_capability,
322 filesystem,
323 false,
324 )
325 }
326
327 #[allow(clippy::too_many_arguments)]
335 pub fn with_working_dir_opts(
336 config: Config,
337 width: u16,
338 height: u16,
339 working_dir: Option<PathBuf>,
340 dir_context: DirectoryContext,
341 plugins_enabled: bool,
342 color_capability: crate::view::color_support::ColorCapability,
343 filesystem: Arc<dyn FileSystem + Send + Sync>,
344 defer_plugin_load: bool,
345 ) -> AnyhowResult<Self> {
346 tracing::info!("Building default grammar registry...");
347 let start = std::time::Instant::now();
348 let mut grammar_registry = crate::primitives::grammar::GrammarRegistry::defaults_only();
349 std::sync::Arc::get_mut(&mut grammar_registry)
355 .expect("defaults_only returned a shared Arc")
356 .apply_language_config(&config.languages);
357 tracing::info!("Default grammar registry built in {:?}", start.elapsed());
358 Self::with_options(
362 config,
363 width,
364 height,
365 working_dir,
366 filesystem,
367 plugins_enabled,
368 true, dir_context,
370 None,
371 color_capability,
372 grammar_registry,
373 defer_plugin_load,
374 )
375 }
376
377 #[allow(clippy::too_many_arguments)]
388 pub fn for_test(
389 config: Config,
390 width: u16,
391 height: u16,
392 working_dir: Option<PathBuf>,
393 dir_context: DirectoryContext,
394 color_capability: crate::view::color_support::ColorCapability,
395 filesystem: Arc<dyn FileSystem + Send + Sync>,
396 time_source: Option<SharedTimeSource>,
397 grammar_registry: Option<Arc<crate::primitives::grammar::GrammarRegistry>>,
398 enable_plugins: bool,
399 enable_embedded_plugins: bool,
400 ) -> AnyhowResult<Self> {
401 let mut grammar_registry =
402 grammar_registry.unwrap_or_else(crate::primitives::grammar::GrammarRegistry::empty);
403 std::sync::Arc::get_mut(&mut grammar_registry)
410 .expect("grammar registry Arc must be uniquely owned at for_test entry")
411 .apply_language_config(&config.languages);
412 let mut editor = Self::with_options(
413 config,
414 width,
415 height,
416 working_dir,
417 filesystem,
418 enable_plugins,
419 enable_embedded_plugins,
420 dir_context,
421 time_source,
422 color_capability,
423 grammar_registry,
424 false,
425 )?;
426 editor.needs_full_grammar_build = false;
429 Ok(editor)
430 }
431
432 #[allow(clippy::too_many_arguments)]
436 fn with_options(
437 mut config: Config,
438 width: u16,
439 height: u16,
440 working_dir: Option<PathBuf>,
441 filesystem: Arc<dyn FileSystem + Send + Sync>,
442 enable_plugins: bool,
443 #[cfg_attr(not(feature = "embed-plugins"), allow(unused_variables))]
444 enable_embedded_plugins: bool,
445 dir_context: DirectoryContext,
446 time_source: Option<SharedTimeSource>,
447 color_capability: crate::view::color_support::ColorCapability,
448 grammar_registry: Arc<crate::primitives::grammar::GrammarRegistry>,
449 defer_plugin_load: bool,
450 ) -> AnyhowResult<Self> {
451 let mut t = InitTimer::start("Editor::with_options");
452 let time_source = time_source.unwrap_or_else(RealTimeSource::shared);
454 tracing::info!("Editor::new called with width={}, height={}", width, height);
455
456 let working_dir = working_dir
458 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
459
460 let working_dir = working_dir.canonicalize().unwrap_or(working_dir);
463
464 t.phase("preamble");
465 tracing::info!("Loading themes...");
467 let theme_loader = crate::view::theme::ThemeLoader::new(dir_context.themes_dir());
468 t.phase("ThemeLoader::new");
469 let scan_result =
473 crate::services::packages::scan_installed_packages(&dir_context.config_dir);
474 t.phase("scan_installed_packages");
475
476 for (lang_id, lang_config) in &scan_result.language_configs {
478 config
479 .languages
480 .entry(lang_id.clone())
481 .or_insert_with(|| lang_config.clone());
482 }
483
484 for (lang_id, lsp_config) in &scan_result.lsp_configs {
486 config
487 .lsp
488 .entry(lang_id.clone())
489 .or_insert_with(|| LspLanguageConfig::Multi(vec![lsp_config.clone()]));
490 }
491
492 let theme_registry = Arc::new(theme_loader.load_all(&scan_result.bundle_theme_dirs));
493 t.phase("theme_loader.load_all");
494 tracing::info!("Themes loaded");
495
496 let theme_inner = theme_registry.get_cloned(&config.theme).unwrap_or_else(|| {
498 tracing::warn!(
499 "Theme '{}' not found, falling back to default theme",
500 config.theme.0
501 );
502 theme_registry
503 .get_cloned(&crate::config::ThemeName(
504 crate::view::theme::THEME_HIGH_CONTRAST.to_string(),
505 ))
506 .expect("Default theme must exist")
507 });
508
509 theme_inner.set_terminal_cursor_color();
511 let theme = Arc::new(RwLock::new(theme_inner));
512
513 t.phase("theme_setup");
514 let keybindings = Arc::new(RwLock::new(KeybindingResolver::new(&config)));
515 t.phase("keybindings");
516
517 let mut buffers = crate::app::window::WindowBuffers::new();
519 let mut event_logs = HashMap::new();
520
521 let buffer_id = BufferId(1);
526 let mut state = EditorState::new(
527 width,
528 height,
529 config.editor.large_file_threshold_bytes as usize,
530 Arc::clone(&filesystem),
531 );
532 state
534 .margins
535 .configure_for_line_numbers(config.editor.line_numbers);
536 state.buffer_settings.tab_size = config.editor.tab_size;
537 state.buffer_settings.auto_close = config.editor.auto_close;
538 tracing::info!("EditorState created for buffer {:?}", buffer_id);
540 buffers.insert(buffer_id, state);
541 event_logs.insert(buffer_id, EventLog::new());
542
543 let mut buffer_metadata: HashMap<BufferId, BufferMetadata> = HashMap::new();
547 buffer_metadata.insert(buffer_id, BufferMetadata::new());
548
549 let persisted_env = crate::app::orchestrator_persistence::read_persisted_windows_env(
562 filesystem.as_ref(),
563 &dir_context.data_dir,
564 &working_dir,
565 );
566 let plugin_global_state = crate::app::orchestrator_persistence::read_persisted_plugin_state(
567 filesystem.as_ref(),
568 &dir_context.data_dir,
569 &working_dir,
570 );
571
572 let (active_window_id, active_window_root) =
582 crate::app::orchestrator_persistence::pick_active_window_for_cwd(
583 persisted_env.as_ref(),
584 &working_dir,
585 )
586 .map(|w| (fresh_core::WindowId(w.id), w.root.clone()))
587 .unwrap_or((fresh_core::WindowId(1), working_dir.clone()));
588
589 let root_uri = types::file_path_to_lsp_uri(&active_window_root);
591
592 t.phase("buffer_state");
593 let tokio_runtime = tokio::runtime::Builder::new_multi_thread()
595 .worker_threads(2) .thread_name("editor-async")
597 .enable_all()
598 .build()
599 .ok()
600 .map(Arc::new);
601 t.phase("tokio_runtime");
602
603 let async_bridge = AsyncBridge::new();
609 let event_broadcaster = crate::model::control_event::EventBroadcaster::default();
610
611 let base_window_bridge = AsyncBridge::new();
617
618 if tokio_runtime.is_none() {
619 tracing::warn!("Failed to create Tokio runtime - async features disabled");
620 }
621
622 let mut lsp = LspManager::new(active_window_id, root_uri);
627
628 if let Some(ref runtime) = tokio_runtime {
632 lsp.set_runtime(runtime.handle().clone(), base_window_bridge.clone());
633 }
634
635 for (language, lsp_configs) in &config.lsp {
637 lsp.set_language_configs(language.clone(), lsp_configs.as_slice().to_vec());
638 }
639
640 let universal_servers: Vec<LspServerConfig> = config
642 .universal_lsp
643 .values()
644 .flat_map(|lc| lc.as_slice().to_vec())
645 .filter(|c| c.enabled)
646 .collect();
647 lsp.set_universal_configs(universal_servers);
648
649 if active_window_root.join("deno.json").exists()
655 || active_window_root.join("deno.jsonc").exists()
656 {
657 tracing::info!("Detected Deno project (deno.json found), using deno lsp for JS/TS");
658 let deno_config = LspServerConfig {
659 command: "deno".to_string(),
660 args: vec!["lsp".to_string()],
661 enabled: true,
662 auto_start: false,
663 process_limits: ProcessLimits::default(),
664 initialization_options: Some(serde_json::json!({"enable": true})),
665 ..Default::default()
666 };
667 lsp.set_language_config("javascript".to_string(), deno_config.clone());
668 lsp.set_language_config("typescript".to_string(), deno_config);
669 }
670
671 t.phase("lsp_setup");
672 let split_manager = SplitManager::new(buffer_id);
674
675 let mut split_view_states = HashMap::new();
677 let initial_split_id = split_manager.active_split();
678 let mut initial_view_state = SplitViewState::with_buffer(width, height, buffer_id);
679 initial_view_state.apply_config_defaults(
680 config.editor.line_numbers,
681 config.editor.highlight_current_line,
682 config.editor.line_wrap,
683 config.editor.wrap_indent,
684 config.editor.wrap_column,
685 config.editor.rulers.clone(),
686 );
687 split_view_states.insert(initial_split_id, initial_view_state);
688
689 let fs_manager = Arc::new(FsManager::new(Arc::clone(&filesystem)));
691
692 let command_registry = Arc::new(RwLock::new(CommandRegistry::new()));
694
695 let authority = crate::services::authority::Authority {
701 filesystem: Arc::clone(&filesystem),
702 ..crate::services::authority::Authority::local()
703 };
704 let process_spawner = Arc::clone(&authority.process_spawner);
705
706 let mut quick_open_registry = QuickOpenRegistry::new();
708 quick_open_registry.register(Box::new(FileProvider::new(
709 Arc::clone(&filesystem),
710 Arc::clone(&process_spawner),
711 tokio_runtime.as_ref().map(|rt| rt.handle().clone()),
712 Some(async_bridge.sender()),
713 )));
714 quick_open_registry.register(Box::new(CommandProvider::new(
715 Arc::clone(&command_registry),
716 Arc::clone(&keybindings),
717 )));
718 quick_open_registry.register(Box::new(BufferProvider::new()));
719 quick_open_registry.register(Box::new(GotoLineProvider::new()));
720
721 let theme_cache = Arc::new(RwLock::new(theme_registry.to_json_map()));
723
724 t.phase("split_quickopen_authority");
725 let plugin_manager = Arc::new(RwLock::new(PluginManager::new(
727 enable_plugins,
728 Arc::clone(&command_registry),
729 dir_context.clone(),
730 Arc::clone(&theme_cache),
731 )));
732 t.phase("PluginManager::new");
733
734 #[cfg(feature = "plugins")]
737 if let Some(snapshot_handle) = plugin_manager.read().unwrap().state_snapshot_handle() {
738 let mut snapshot = snapshot_handle.write().unwrap();
739 snapshot.working_dir = working_dir.clone();
740 populate_builtin_keybinding_labels(&mut snapshot, &keybindings);
748 if let Ok(json) = serde_json::to_value(&config) {
760 snapshot.config = std::sync::Arc::new(json);
761 }
762 }
763
764 let plugin_schemas: HashMap<String, serde_json::Value> = HashMap::new();
774 if plugin_manager.read().unwrap().is_active() {
775 let mut plugin_dirs: Vec<std::path::PathBuf> = vec![];
776
777 if let Ok(exe_path) = std::env::current_exe() {
779 if let Some(exe_dir) = exe_path.parent() {
780 let exe_plugin_dir = exe_dir.join("plugins");
781 if exe_plugin_dir.exists() {
782 plugin_dirs.push(exe_plugin_dir);
783 }
784 }
785 }
786
787 #[cfg(feature = "embed-plugins")]
798 if enable_embedded_plugins && plugin_dirs.is_empty() {
799 if let Some(embedded_dir) =
800 crate::services::plugins::embedded::get_embedded_plugins_dir()
801 {
802 tracing::info!("Using embedded plugins from: {:?}", embedded_dir);
803 plugin_dirs.push(embedded_dir.clone());
804 }
805 }
806
807 let user_plugins_dir = dir_context.config_dir.join("plugins");
809 if user_plugins_dir.exists() && !plugin_dirs.contains(&user_plugins_dir) {
810 tracing::info!("Found user plugins directory: {:?}", user_plugins_dir);
811 plugin_dirs.push(user_plugins_dir.clone());
812 }
813
814 let packages_dir = dir_context.config_dir.join("plugins").join("packages");
816 if packages_dir.exists() {
817 if let Ok(entries) = std::fs::read_dir(&packages_dir) {
818 for entry in entries.flatten() {
819 let path = entry.path();
820 if path.is_dir() {
822 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
823 if !name.starts_with('.') {
824 tracing::info!("Found package manager plugin: {:?}", path);
825 plugin_dirs.push(path);
826 }
827 }
828 }
829 }
830 }
831 }
832
833 for dir in &scan_result.bundle_plugin_dirs {
835 tracing::info!("Found bundle plugin directory: {:?}", dir);
836 plugin_dirs.push(dir.clone());
837 }
838
839 if plugin_dirs.is_empty() {
840 tracing::debug!(
841 "No plugins directory found next to executable or in working dir: {:?}",
842 working_dir
843 );
844 }
845
846 if defer_plugin_load {
847 #[cfg(feature = "plugins")]
858 {
859 let bridge = &async_bridge;
860 let mut dir_receivers: Vec<(
861 std::path::PathBuf,
862 fresh_plugin_runtime::thread::oneshot::Receiver<
863 fresh_plugin_runtime::thread::PluginsDirLoadResult,
864 >,
865 )> = Vec::with_capacity(plugin_dirs.len());
866 for plugin_dir in &plugin_dirs {
867 tracing::info!(
868 "Submitting async TypeScript plugin load for: {:?}",
869 plugin_dir
870 );
871 if let Some(rx) = plugin_manager
872 .read()
873 .unwrap()
874 .load_plugins_from_dir_with_config_request(plugin_dir, &config.plugins)
875 {
876 dir_receivers.push((plugin_dir.clone(), rx));
877 }
878 }
879 let declarations_rx = if !dir_receivers.is_empty() {
880 plugin_manager.read().unwrap().list_plugins_request()
881 } else {
882 None
883 };
884 if !dir_receivers.is_empty() {
885 let sender = bridge.sender();
886 std::thread::Builder::new()
887 .name("plugin-load-forwarder".to_string())
888 .spawn(move || {
889 for (dir, rx) in dir_receivers {
890 let load_start = std::time::Instant::now();
891 match rx.recv() {
892 Ok((errors, discovered_plugins)) => {
893 tracing::info!(
894 "Loaded TypeScript plugins from {:?} in {:?}",
895 dir,
896 load_start.elapsed()
897 );
898 drop(sender.send(
899 crate::services::async_bridge::AsyncMessage::PluginsDirLoaded {
900 dir,
901 errors,
902 discovered_plugins,
903 },
904 ));
905 }
906 Err(e) => {
907 tracing::warn!(
908 "plugin-load-forwarder: dir {:?} recv failed: {}",
909 dir,
910 e
911 );
912 }
913 }
914 }
915 if let Some(rx) = declarations_rx {
916 match rx.recv() {
917 Ok(plugin_infos) => {
918 let declarations: Vec<(String, String)> = plugin_infos
919 .into_iter()
920 .filter_map(|info| {
921 info.declarations.map(|d| (info.name, d))
922 })
923 .collect();
924 drop(sender.send(
925 crate::services::async_bridge::AsyncMessage::PluginDeclarationsReady {
926 declarations,
927 },
928 ));
929 }
930 Err(e) => {
931 tracing::warn!(
932 "plugin-load-forwarder: list_plugins recv failed: {}",
933 e
934 );
935 }
936 }
937 }
938 })
939 .ok();
940 }
941 }
942 } else {
943 for plugin_dir in plugin_dirs {
948 tracing::info!("Loading TypeScript plugins from: {:?}", plugin_dir);
949 let load_start = std::time::Instant::now();
950 let (errors, discovered_plugins) = plugin_manager
951 .read()
952 .unwrap()
953 .load_plugins_from_dir_with_config(&plugin_dir, &config.plugins);
954 tracing::info!(
955 "Loaded TypeScript plugins from {:?} in {:?}",
956 plugin_dir,
957 load_start.elapsed()
958 );
959
960 for (name, plugin_config) in discovered_plugins {
963 config.plugins.insert(name, plugin_config);
964 }
965
966 if !errors.is_empty() {
967 for err in &errors {
968 tracing::error!("TypeScript plugin load error: {}", err);
969 }
970 #[cfg(debug_assertions)]
972 panic!(
973 "TypeScript plugin loading failed with {} error(s): {}",
974 errors.len(),
975 errors.join("; ")
976 );
977 }
978 }
979
980 let declarations = plugin_manager.read().unwrap().plugin_declarations();
988 crate::init_script::write_plugin_declarations(
989 &dir_context.config_dir,
990 &declarations,
991 );
992 }
993 }
994
995 t.phase("plugin_loading");
996 let recovery_enabled = config.editor.recovery_enabled;
998 let check_for_updates = config.check_for_updates;
999
1000 let update_checker = if check_for_updates {
1002 tracing::debug!("Update checking enabled, starting periodic checker");
1003 Some(
1004 crate::services::release_checker::start_periodic_update_check(
1005 crate::services::release_checker::DEFAULT_RELEASES_URL,
1006 time_source.clone(),
1007 dir_context.data_dir.clone(),
1008 ),
1009 )
1010 } else {
1011 tracing::debug!("Update checking disabled by config");
1012 None
1013 };
1014
1015 let user_config_raw = Config::read_user_config_raw(&working_dir);
1017
1018 let config_arc = Arc::new(config);
1025 let config_cached_json =
1026 Arc::new(serde_json::to_value(&*config_arc).unwrap_or(serde_json::Value::Null));
1027 let config_snapshot_anchor = Arc::clone(&config_arc);
1028
1029 let buffer_id_alloc = crate::app::window_resources::BufferIdAllocator::new(2);
1035
1036 let local_filesystem: Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> =
1041 Arc::new(crate::model::filesystem::StdFileSystem);
1042
1043 let base_resources = crate::app::window_resources::WindowResources {
1049 config: Arc::clone(&config_arc),
1050 grammar_registry: Arc::clone(&grammar_registry),
1051 theme_registry: Arc::clone(&theme_registry),
1052 theme_cache: Arc::clone(&theme_cache),
1053 keybindings: Arc::clone(&keybindings),
1054 command_registry: Arc::clone(&command_registry),
1055 fs_manager: Arc::clone(&fs_manager),
1056 local_filesystem: Arc::clone(&local_filesystem),
1057 buffer_id_alloc: buffer_id_alloc.clone(),
1058 authority: authority.clone(),
1059 time_source: Arc::clone(&time_source),
1060 dir_context: dir_context.clone(),
1061 tokio_runtime: tokio_runtime.clone(),
1062 async_bridge: Some(async_bridge.clone()),
1063 plugin_manager: Arc::clone(&plugin_manager),
1064 theme: Arc::clone(&theme),
1065 event_broadcaster: event_broadcaster.clone(),
1066 };
1067
1068 let (active_label, active_root, active_plugin_state) = persisted_env
1075 .as_ref()
1076 .and_then(|env| env.windows.iter().find(|w| w.id == active_window_id.0))
1077 .map(|w| (w.label.clone(), w.root.clone(), w.plugin_state.clone()))
1078 .unwrap_or_else(|| (String::new(), working_dir.clone(), HashMap::new()));
1079
1080 let mut active_win = crate::app::window::Window::new(
1081 active_window_id,
1082 active_label,
1083 active_root,
1084 base_resources,
1085 );
1086 active_win.terminal_width = width;
1092 active_win.terminal_height = height;
1093 active_win.lsp = Some(lsp);
1097 active_win.buffers = buffers;
1098 active_win
1099 .buffers
1100 .set_splits((split_manager, split_view_states));
1101 active_win.buffer_metadata = buffer_metadata;
1102 active_win.event_logs = event_logs;
1103 active_win.plugin_state = active_plugin_state;
1104 active_win.bridge = base_window_bridge;
1110 for history_name in ["search", "replace", "goto_line"] {
1113 let path = dir_context.prompt_history_path(history_name);
1114 let history = crate::input::input_history::InputHistory::load_from_file(&path)
1115 .unwrap_or_else(|e| {
1116 tracing::warn!("Failed to load {} history: {}", history_name, e);
1117 crate::input::input_history::InputHistory::new()
1118 });
1119 active_win
1120 .prompt_histories
1121 .insert(history_name.to_string(), history);
1122 }
1123
1124 let mut windows = HashMap::new();
1128 if let Some(ref env) = persisted_env {
1129 for ps in &env.windows {
1130 let id = fresh_core::WindowId(ps.id);
1131 if id == active_window_id {
1132 continue;
1133 }
1134 let resources = crate::app::window_resources::WindowResources {
1135 config: Arc::clone(&config_arc),
1136 grammar_registry: Arc::clone(&grammar_registry),
1137 theme_registry: Arc::clone(&theme_registry),
1138 theme_cache: Arc::clone(&theme_cache),
1139 keybindings: Arc::clone(&keybindings),
1140 command_registry: Arc::clone(&command_registry),
1141 fs_manager: Arc::clone(&fs_manager),
1142 local_filesystem: Arc::clone(&local_filesystem),
1143 buffer_id_alloc: buffer_id_alloc.clone(),
1144 authority: authority.clone(),
1145 time_source: Arc::clone(&time_source),
1146 dir_context: dir_context.clone(),
1147 tokio_runtime: tokio_runtime.clone(),
1148 async_bridge: Some(async_bridge.clone()),
1149 plugin_manager: Arc::clone(&plugin_manager),
1150 theme: Arc::clone(&theme),
1151 event_broadcaster: event_broadcaster.clone(),
1152 };
1153 let mut shell = crate::app::window::Window::new(
1154 id,
1155 ps.label.clone(),
1156 ps.root.clone(),
1157 resources,
1158 );
1159 shell.terminal_width = width;
1160 shell.terminal_height = height;
1161 shell.plugin_state = ps.plugin_state.clone();
1162 windows.insert(id, shell);
1163 }
1164 }
1165 windows.insert(active_window_id, active_win);
1166
1167 let max_existing = windows.keys().map(|k| k.0).max().unwrap_or(0);
1173 let next_window_id = persisted_env
1174 .as_ref()
1175 .map(|env| env.next_id.max(max_existing + 1))
1176 .unwrap_or(2);
1177
1178 let recovery_service = {
1179 let recovery_config = RecoveryConfig {
1180 enabled: recovery_enabled,
1181 ..RecoveryConfig::default()
1182 };
1183 let scope = crate::services::recovery::RecoveryScope::Standalone {
1191 working_dir: working_dir.clone(),
1192 };
1193 RecoveryService::with_scope(recovery_config, &dir_context.recovery_dir(), &scope)
1194 };
1195
1196 let key_translator = crate::input::key_translator::KeyTranslator::load_from_config_dir(
1197 &dir_context.config_dir,
1198 )
1199 .unwrap_or_default();
1200
1201 let pending_grammars = scan_result
1202 .additional_grammars
1203 .iter()
1204 .map(|g| PendingGrammar {
1205 language: g.language.clone(),
1206 grammar_path: g.path.to_string_lossy().to_string(),
1207 extensions: g.extensions.clone(),
1208 })
1209 .collect();
1210
1211 let parts = EditorParts {
1212 config: config_arc,
1213 config_snapshot_anchor,
1214 config_cached_json,
1215 user_config_raw: Arc::new(user_config_raw),
1216 dir_context: dir_context.clone(),
1217 working_dir: working_dir.clone(),
1218 theme,
1219 theme_registry,
1220 theme_cache,
1221 grammar_registry,
1222 pending_grammars,
1223 needs_full_grammar_build: true,
1224 keybindings,
1225 buffer_id_alloc: buffer_id_alloc.clone(),
1226 next_buffer_id: 2,
1227 terminal_width: width,
1228 terminal_height: height,
1229 color_capability,
1230 tokio_runtime,
1231 async_bridge,
1232 fs_manager,
1233 authority,
1234 local_filesystem: Arc::clone(&local_filesystem),
1235 windows,
1236 active_window: active_window_id,
1237 next_window_id,
1238 command_registry,
1239 quick_open_registry,
1240 plugin_manager,
1241 recovery_service,
1242 key_translator,
1243 update_checker,
1244 time_source: time_source.clone(),
1245 plugin_global_state,
1246 plugin_schemas,
1247 event_broadcaster: event_broadcaster.clone(),
1248 };
1249
1250 let mut editor = Editor::from_parts(parts);
1251
1252 t.phase("editor_struct_assembly");
1253 editor.clipboard.apply_config(&editor.config.clipboard);
1255
1256 let needs_seed: Vec<fresh_core::WindowId> = editor
1264 .windows
1265 .iter()
1266 .filter(|(_, s)| s.buffers.splits().is_none() || s.buffers.len() == 0)
1267 .map(|(id, _)| *id)
1268 .collect();
1269 for id in needs_seed {
1270 if let Some((buf, state, metadata, event_log, mgr, vs)) =
1271 editor.build_fresh_layout_if_needed(id)
1272 {
1273 if let Some(s) = editor.windows.get_mut(&id) {
1274 s.buffers.set_splits((mgr, vs));
1275 s.buffers.insert(buf, state);
1276 s.buffer_metadata.insert(buf, metadata);
1277 s.event_logs.insert(buf, event_log);
1278 }
1279 }
1280 }
1281
1282 #[cfg(feature = "plugins")]
1283 {
1284 editor.update_plugin_state_snapshot();
1285 if editor.plugin_manager.read().unwrap().is_active() {
1286 editor.plugin_manager.read().unwrap().run_hook(
1287 "editor_initialized",
1288 crate::services::plugins::hooks::HookArgs::EditorInitialized {},
1289 );
1290 }
1291 }
1292 t.phase("post_struct_hooks");
1293 t.finish();
1294 Ok(editor)
1295 }
1296
1297 pub fn event_broadcaster(&self) -> &crate::model::control_event::EventBroadcaster {
1299 &self.event_broadcaster
1300 }
1301
1302 pub(super) fn start_background_grammar_build(
1307 &mut self,
1308 additional: Vec<crate::primitives::grammar::GrammarSpec>,
1309 callback_ids: Vec<fresh_core::api::JsCallbackId>,
1310 ) {
1311 let Some(bridge) = &self.async_bridge else {
1312 return;
1313 };
1314 self.grammar_build_in_progress = true;
1315 let sender = bridge.sender();
1316 let config_dir = self.dir_context.config_dir.clone();
1317 tracing::info!(
1318 "Spawning background grammar build thread ({} plugin grammars)...",
1319 additional.len()
1320 );
1321 std::thread::Builder::new()
1322 .name("grammar-build".to_string())
1323 .spawn(move || {
1324 tracing::info!("[grammar-build] Thread started");
1325 let start = std::time::Instant::now();
1326 let registry = if additional.is_empty() {
1327 crate::primitives::grammar::GrammarRegistry::for_editor(config_dir)
1328 } else {
1329 crate::primitives::grammar::GrammarRegistry::for_editor_with_additional(
1330 config_dir,
1331 &additional,
1332 )
1333 };
1334 tracing::info!("[grammar-build] Complete in {:?}", start.elapsed());
1335 drop(sender.send(
1336 crate::services::async_bridge::AsyncMessage::GrammarRegistryBuilt {
1337 registry,
1338 callback_ids,
1339 },
1340 ));
1341 })
1342 .ok();
1343 }
1344
1345 pub fn load_init_script(&mut self, enabled: bool) {
1352 use crate::init_script::{
1353 check, decide_load, describe, record_success, refresh_types_scaffolding, CheckSeverity,
1354 InitOutcome, LoadDecision,
1355 };
1356
1357 let config_dir = self.dir_context.config_dir.clone();
1358
1359 if enabled {
1360 refresh_types_scaffolding(&config_dir);
1364
1365 let report = check(&config_dir);
1370 if !report.ok {
1371 for d in &report.diagnostics {
1372 let level = match d.severity {
1373 CheckSeverity::Error => "error",
1374 CheckSeverity::Warning => "warning",
1375 };
1376 tracing::warn!(
1377 "init.ts pre-load {level} at {}:{}: {}",
1378 d.line,
1379 d.column,
1380 d.message
1381 );
1382 }
1383 }
1384 }
1385
1386 let outcome = match decide_load(&config_dir, enabled) {
1387 LoadDecision::Skip(outcome) => outcome,
1388 LoadDecision::Load { source } => {
1389 if !self.plugin_manager.read().unwrap().is_active() {
1390 InitOutcome::Failed {
1391 message: "plugin runtime inactive (--no-plugins); init.ts cannot run"
1392 .into(),
1393 }
1394 } else {
1395 match self.plugin_manager.read().unwrap().load_plugin_from_source(
1396 &source,
1397 crate::init_script::INIT_PLUGIN_NAME,
1398 true,
1399 ) {
1400 Ok(()) => {
1401 record_success(&config_dir);
1402 InitOutcome::Loaded
1403 }
1404 Err(e) => InitOutcome::Failed {
1405 message: format!("{e}"),
1406 },
1407 }
1408 }
1409 }
1410 };
1411
1412 let summary = describe(&outcome);
1413 match outcome {
1414 InitOutcome::NotFound | InitOutcome::Disabled => tracing::debug!("{}", summary),
1415 InitOutcome::Loaded => tracing::info!("{}", summary),
1416 InitOutcome::CrashFused { .. } | InitOutcome::Failed { .. } => {
1417 tracing::warn!("{}", summary);
1418 self.set_status_message(summary);
1419 }
1420 }
1421 }
1422
1423 pub fn load_init_script_async(&mut self, enabled: bool) {
1435 use crate::init_script::{
1436 check, decide_load, refresh_types_scaffolding, CheckSeverity, InitOutcome, LoadDecision,
1437 };
1438 use crate::services::async_bridge::PluginInitScriptOutcome;
1439
1440 let config_dir = self.dir_context.config_dir.clone();
1441
1442 if enabled {
1443 refresh_types_scaffolding(&config_dir);
1444 let report = check(&config_dir);
1445 if !report.ok {
1446 for d in &report.diagnostics {
1447 let level = match d.severity {
1448 CheckSeverity::Error => "error",
1449 CheckSeverity::Warning => "warning",
1450 };
1451 tracing::warn!(
1452 "init.ts pre-load {level} at {}:{}: {}",
1453 d.line,
1454 d.column,
1455 d.message
1456 );
1457 }
1458 }
1459 }
1460
1461 let outcome_now: Option<PluginInitScriptOutcome> = match decide_load(&config_dir, enabled) {
1462 LoadDecision::Skip(outcome) => Some(match outcome {
1463 InitOutcome::NotFound => PluginInitScriptOutcome::NotFound,
1464 InitOutcome::Disabled => PluginInitScriptOutcome::Disabled,
1465 InitOutcome::CrashFused { failures } => {
1466 PluginInitScriptOutcome::CrashFused { failures }
1467 }
1468 InitOutcome::Loaded => PluginInitScriptOutcome::Loaded,
1471 InitOutcome::Failed { message } => PluginInitScriptOutcome::Failed { message },
1472 }),
1473 LoadDecision::Load { source } => {
1474 if !self.plugin_manager.read().unwrap().is_active() {
1475 Some(PluginInitScriptOutcome::Failed {
1476 message: "plugin runtime inactive (--no-plugins); init.ts cannot run"
1477 .into(),
1478 })
1479 } else {
1480 self.spawn_init_script_forwarder(source);
1481 None
1482 }
1483 }
1484 };
1485
1486 if let Some(outcome) = outcome_now {
1487 if let Some(bridge) = &self.async_bridge {
1491 drop(bridge.sender().send(
1492 crate::services::async_bridge::AsyncMessage::PluginInitScriptLoaded(outcome),
1493 ));
1494 } else {
1495 self.handle_plugin_init_script_loaded(outcome);
1496 }
1497 }
1498 }
1499
1500 #[cfg(feature = "plugins")]
1501 fn spawn_init_script_forwarder(&self, source: String) {
1502 let Some(bridge) = &self.async_bridge else {
1503 return;
1504 };
1505 let Some(rx) = self
1506 .plugin_manager
1507 .read()
1508 .unwrap()
1509 .load_plugin_from_source_request(&source, crate::init_script::INIT_PLUGIN_NAME, true)
1510 else {
1511 return;
1512 };
1513 let sender = bridge.sender();
1514 std::thread::Builder::new()
1515 .name("plugin-init-forwarder".to_string())
1516 .spawn(move || {
1517 let outcome = match rx.recv() {
1518 Ok(Ok(())) => crate::services::async_bridge::PluginInitScriptOutcome::Loaded,
1519 Ok(Err(e)) => crate::services::async_bridge::PluginInitScriptOutcome::Failed {
1520 message: format!("{e}"),
1521 },
1522 Err(e) => crate::services::async_bridge::PluginInitScriptOutcome::Failed {
1523 message: format!("plugin thread closed: {e}"),
1524 },
1525 };
1526 drop(sender.send(
1527 crate::services::async_bridge::AsyncMessage::PluginInitScriptLoaded(outcome),
1528 ));
1529 })
1530 .ok();
1531 }
1532
1533 #[cfg(not(feature = "plugins"))]
1534 fn spawn_init_script_forwarder(&self, _source: String) {}
1535
1536 pub fn handle_set_setting(&mut self, path: String, value: serde_json::Value) {
1540 let mut json = serde_json::to_value(&*self.config).unwrap_or_default();
1541 set_dot_path(&mut json, &path, value);
1542 match serde_json::from_value::<crate::config::Config>(json) {
1543 Ok(new_config) => {
1544 let old_theme = self.config.theme.clone();
1545 self.config = Arc::new(new_config);
1546 if old_theme != self.config.theme {
1547 if let Some(theme) = self.theme_registry.get_cloned(&self.config.theme) {
1548 *self.theme.write().unwrap() = theme;
1549 }
1550 }
1551 *self.keybindings.write().unwrap() =
1552 crate::input::keybindings::KeybindingResolver::new(&self.config);
1553 self.clipboard.apply_config(&self.config.clipboard);
1554 {
1555 let cfg = self.config.editor.clone();
1556 let win = self.active_window_mut();
1557 win.menu_bar_visible = cfg.show_menu_bar;
1558 win.tab_bar_visible = cfg.show_tab_bar;
1559 win.status_bar_visible = cfg.show_status_bar;
1560 win.prompt_line_visible = cfg.show_prompt_line;
1561 }
1562 #[cfg(feature = "plugins")]
1563 self.update_plugin_state_snapshot();
1564 }
1565 Err(e) => {
1566 self.set_status_message(format!("setSetting({path}): {e}"));
1567 }
1568 }
1569 }
1570
1571 pub fn handle_add_plugin_config_field(
1581 &mut self,
1582 plugin_name: String,
1583 field_name: String,
1584 field_schema: serde_json::Value,
1585 ) {
1586 tracing::trace!(
1587 "Registering plugin config field: {}.{}",
1588 plugin_name,
1589 field_name
1590 );
1591 let updated_schema = {
1594 let schemas = self.plugin_schemas.read().ok();
1595 let existing = schemas.as_ref().and_then(|m| m.get(&plugin_name)).cloned();
1596 let mut schema = existing.unwrap_or_else(|| {
1597 serde_json::json!({
1598 "type": "object",
1599 "properties": {},
1600 })
1601 });
1602 if let Some(props) = schema
1603 .as_object_mut()
1604 .and_then(|o| o.get_mut("properties"))
1605 .and_then(|p| p.as_object_mut())
1606 {
1607 props.insert(field_name.clone(), field_schema.clone());
1608 }
1609 schema
1610 };
1611
1612 if let Err(msg) = crate::plugin_schemas::validate_plugin_schema(&updated_schema) {
1613 self.set_status_message(format!(
1616 "defineConfig({}.{}): {}",
1617 plugin_name, field_name, msg
1618 ));
1619 return;
1620 }
1621
1622 if let Some(default) = field_schema.get("default").cloned() {
1624 let cfg = std::sync::Arc::make_mut(&mut self.config);
1625 let entry = cfg.plugins.entry(plugin_name.clone()).or_default();
1626 let settings_obj = match &mut entry.settings {
1627 serde_json::Value::Object(_) => &mut entry.settings,
1628 slot => {
1629 *slot = serde_json::Value::Object(Default::default());
1630 slot
1631 }
1632 };
1633 if let serde_json::Value::Object(map) = settings_obj {
1634 map.entry(field_name.clone()).or_insert(default);
1635 }
1636 }
1637
1638 if let Ok(mut schemas) = self.plugin_schemas.write() {
1639 schemas.insert(plugin_name, updated_schema);
1640 }
1641
1642 #[cfg(feature = "plugins")]
1643 self.update_plugin_state_snapshot();
1644 }
1645
1646 pub(crate) fn handle_plugins_dir_loaded(
1651 &mut self,
1652 dir: std::path::PathBuf,
1653 errors: Vec<String>,
1654 discovered_plugins: std::collections::HashMap<String, fresh_core::config::PluginConfig>,
1655 ) {
1656 if !discovered_plugins.is_empty() {
1657 let cfg = std::sync::Arc::make_mut(&mut self.config);
1658 for (name, plugin_config) in discovered_plugins {
1659 cfg.plugins.insert(name, plugin_config);
1660 }
1661 }
1662 if !errors.is_empty() {
1663 for err in &errors {
1664 tracing::error!("TypeScript plugin load error: {}", err);
1665 }
1666 #[cfg(debug_assertions)]
1667 panic!(
1668 "TypeScript plugin loading failed for {:?} with {} error(s): {}",
1669 dir,
1670 errors.len(),
1671 errors.join("; ")
1672 );
1673 #[cfg(not(debug_assertions))]
1674 {
1675 let _ = dir;
1676 }
1677 }
1678 }
1679
1680 pub(crate) fn handle_plugin_declarations_ready(&self, declarations: Vec<(String, String)>) {
1684 crate::init_script::write_plugin_declarations(&self.dir_context.config_dir, &declarations);
1685 }
1686
1687 pub(crate) fn handle_plugin_init_script_loaded(
1691 &mut self,
1692 outcome: crate::services::async_bridge::PluginInitScriptOutcome,
1693 ) {
1694 use crate::init_script::{describe, record_success, InitOutcome};
1695 use crate::services::async_bridge::PluginInitScriptOutcome as O;
1696 let outcome = match outcome {
1697 O::NotFound => InitOutcome::NotFound,
1698 O::Disabled => InitOutcome::Disabled,
1699 O::CrashFused { failures } => InitOutcome::CrashFused { failures },
1700 O::Loaded => {
1701 record_success(&self.dir_context.config_dir);
1702 InitOutcome::Loaded
1703 }
1704 O::Failed { message } => InitOutcome::Failed { message },
1705 };
1706 let summary = describe(&outcome);
1707 match outcome {
1708 InitOutcome::NotFound | InitOutcome::Disabled => tracing::debug!("{}", summary),
1709 InitOutcome::Loaded => tracing::info!("{}", summary),
1710 InitOutcome::CrashFused { .. } | InitOutcome::Failed { .. } => {
1711 tracing::warn!("{}", summary);
1712 self.set_status_message(summary);
1713 }
1714 }
1715 }
1716
1717 pub fn fire_plugins_loaded_hook(&self) {
1719 #[cfg(feature = "plugins")]
1720 if self.plugin_manager.read().unwrap().is_active() {
1721 self.plugin_manager.read().unwrap().run_hook(
1722 "plugins_loaded",
1723 crate::services::plugins::hooks::HookArgs::PluginsLoaded {},
1724 );
1725 }
1726 }
1727
1728 pub fn fire_ready_hook(&self) {
1730 #[cfg(feature = "plugins")]
1731 if self.plugin_manager.read().unwrap().is_active() {
1732 self.plugin_manager
1733 .read()
1734 .unwrap()
1735 .run_hook("ready", crate::services::plugins::hooks::HookArgs::Ready {});
1736 }
1737 }
1738
1739 #[doc(hidden)]
1741 pub fn config_for_tests(&self) -> &crate::config::Config {
1742 &self.config
1743 }
1744
1745 #[doc(hidden)]
1747 pub fn dispatch_action_for_tests(&mut self, action: crate::input::keybindings::Action) {
1748 if let Err(e) = self.handle_action(action) {
1749 tracing::warn!("dispatch_action_for_tests: {e}");
1750 }
1751 }
1752
1753 #[doc(hidden)]
1755 pub fn live_grep_last_state_for_tests(
1756 &self,
1757 ) -> Option<&crate::services::live_grep_state::LiveGrepLastState> {
1758 self.active_window().live_grep_last_state.as_ref()
1759 }
1760
1761 #[doc(hidden)]
1763 pub fn set_live_grep_last_state_for_tests(
1764 &mut self,
1765 state: Option<crate::services::live_grep_state::LiveGrepLastState>,
1766 ) {
1767 self.active_window_mut().live_grep_last_state = state;
1768 }
1769
1770 #[doc(hidden)]
1773 pub fn split_manager_for_tests(&self) -> &crate::view::split::SplitManager {
1774 self.windows
1775 .get(&self.active_window)
1776 .and_then(|w| w.buffers.splits())
1777 .map(|(mgr, _)| mgr)
1778 .expect("active window must have a populated split layout")
1779 }
1780
1781 #[doc(hidden)]
1786 pub fn split_view_state_for_tests(
1787 &self,
1788 leaf: crate::model::event::LeafId,
1789 ) -> Option<&crate::view::split::SplitViewState> {
1790 self.windows
1791 .get(&self.active_window)
1792 .and_then(|w| w.buffers.splits())
1793 .map(|(_, vs)| vs)
1794 .expect("active window must have a populated split layout")
1795 .get(&leaf)
1796 }
1797
1798 #[cfg(feature = "plugins")]
1807 pub(crate) fn refresh_keybinding_labels_snapshot(&self) {
1808 if let Some(snapshot_handle) = self.plugin_manager.read().unwrap().state_snapshot_handle() {
1809 if let Ok(mut snapshot) = snapshot_handle.write() {
1810 populate_builtin_keybinding_labels(&mut snapshot, &self.keybindings);
1811 }
1812 }
1813 }
1814}
1815
1816#[cfg(feature = "plugins")]
1832fn populate_builtin_keybinding_labels(
1833 snapshot: &mut crate::services::plugins::api::EditorStateSnapshot,
1834 keybindings: &std::sync::Arc<std::sync::RwLock<crate::input::keybindings::KeybindingResolver>>,
1835) {
1836 use crate::input::keybindings::{Action, KeyContext};
1837 let Ok(resolver) = keybindings.read() else {
1838 return;
1839 };
1840 let contexts = [
1841 KeyContext::Normal,
1842 KeyContext::Prompt,
1843 KeyContext::Popup,
1844 KeyContext::Completion,
1845 KeyContext::FileExplorer,
1846 KeyContext::Menu,
1847 KeyContext::Terminal,
1848 KeyContext::Settings,
1849 KeyContext::CompositeBuffer,
1850 ];
1851 let known_suffixes: Vec<String> = contexts
1858 .iter()
1859 .map(|c| format!("\0{}", c.to_when_clause()))
1860 .collect();
1861 snapshot
1862 .keybinding_labels
1863 .retain(|k, _| !known_suffixes.iter().any(|s| k.ends_with(s)));
1864 for action_name in Action::all_action_names() {
1865 for ctx in &contexts {
1866 if let Some(label) = resolver.find_keybinding_for_action(&action_name, ctx.clone()) {
1867 let key = format!("{}\0{}", action_name, ctx.to_when_clause());
1868 snapshot.keybinding_labels.insert(key, label);
1869 }
1870 }
1871 }
1872}