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
113 pub(super) theme: Arc<RwLock<crate::view::theme::Theme>>,
115 pub(super) theme_registry: Arc<crate::view::theme::ThemeRegistry>,
116 pub(super) theme_cache: Arc<RwLock<HashMap<String, serde_json::Value>>>,
117
118 pub(super) grammar_registry: Arc<crate::primitives::grammar::GrammarRegistry>,
120 pub(super) pending_grammars: Vec<PendingGrammar>,
121 pub(super) needs_full_grammar_build: bool,
122
123 pub(super) keybindings: Arc<RwLock<KeybindingResolver>>,
125 pub(super) buffer_id_alloc: crate::app::window_resources::BufferIdAllocator,
126 pub(super) next_buffer_id: usize,
127
128 pub(super) terminal_width: u16,
130 pub(super) terminal_height: u16,
131 pub(super) color_capability: crate::view::color_support::ColorCapability,
132
133 pub(super) tokio_runtime: Option<Arc<tokio::runtime::Runtime>>,
135 pub(super) async_bridge: AsyncBridge,
136 pub(super) fs_manager: Arc<FsManager>,
137 pub(super) authority: crate::services::authority::Authority,
138 pub(super) local_filesystem: Arc<dyn FileSystem + Send + Sync>,
139
140 pub(super) windows: HashMap<fresh_core::WindowId, crate::app::window::Window>,
146 pub(super) active_window: fresh_core::WindowId,
147 pub(super) next_window_id: u64,
148
149 pub(super) command_registry: Arc<RwLock<CommandRegistry>>,
151 pub(super) quick_open_registry: QuickOpenRegistry,
152 pub(super) plugin_manager: Arc<RwLock<PluginManager>>,
153 pub(super) recovery_service: Arc<std::sync::Mutex<RecoveryService>>,
154 pub(super) key_translator: crate::input::key_translator::KeyTranslator,
155 pub(super) update_checker: Option<crate::services::release_checker::PeriodicUpdateChecker>,
156
157 pub(super) time_source: SharedTimeSource,
159
160 pub(super) plugin_global_state: HashMap<String, HashMap<String, serde_json::Value>>,
166
167 pub(super) plugin_schemas: HashMap<String, serde_json::Value>,
169
170 pub(super) event_broadcaster: crate::model::control_event::EventBroadcaster,
172}
173
174impl Editor {
175 pub(super) fn from_parts(parts: EditorParts) -> Self {
188 Editor {
189 next_buffer_id: parts.next_buffer_id,
191 buffer_id_alloc: parts.buffer_id_alloc,
192 config: parts.config,
193 config_snapshot_anchor: parts.config_snapshot_anchor,
194 config_cached_json: parts.config_cached_json,
195 user_config_raw: parts.user_config_raw,
196 dir_context: parts.dir_context.clone(),
197 grammar_registry: parts.grammar_registry,
198 pending_grammars: parts.pending_grammars,
199 needs_full_grammar_build: parts.needs_full_grammar_build,
200 theme: parts.theme,
201 theme_registry: parts.theme_registry,
202 theme_cache: parts.theme_cache,
203 keybindings: parts.keybindings,
204 terminal_width: parts.terminal_width,
205 terminal_height: parts.terminal_height,
206 tokio_runtime: parts.tokio_runtime,
207 async_bridge: Some(parts.async_bridge),
208 fs_manager: parts.fs_manager,
209 authority: parts.authority,
210 local_filesystem: parts.local_filesystem,
211 menu_state: crate::view::ui::MenuState::new(parts.dir_context.themes_dir()),
212 windows: parts.windows,
213 active_window: parts.active_window,
214 next_window_id: parts.next_window_id,
215 command_registry: parts.command_registry,
216 quick_open_registry: parts.quick_open_registry,
217 plugin_manager: parts.plugin_manager,
218 recovery_service: parts.recovery_service,
219 time_source: parts.time_source,
220 color_capability: parts.color_capability,
221 update_checker: parts.update_checker,
222 key_translator: parts.key_translator,
223
224 grammar_reload_pending: false,
226 grammar_build_in_progress: false,
227 pending_grammar_callbacks: Vec::new(),
228 expanded_menus_cache: crate::view::ui::ExpandedMenusCache::default(),
229 ansi_background: None,
230 ansi_background_path: None,
231 background_fade: crate::primitives::ansi_background::DEFAULT_BACKGROUND_FADE,
232 clipboard: crate::services::clipboard::Clipboard::new(),
233 should_quit: false,
234 workspace_trust_prompt_cancellable: false,
235 workspace_trust_markers: Vec::new(),
236 workspace_trust_scroll: 0,
237 should_detach: false,
238 session_mode: false,
239 software_cursor_only: false,
240 session_name: None,
241 pending_escape_sequences: Vec::new(),
242 restart_with_dir: None,
243 last_window_title: None,
244 mode_registry: ModeRegistry::new(),
245 pending_authority: None,
246 remote_indicator_override: None,
247 menus: crate::config::MenuConfig::translated(),
248 background_process_handles: HashMap::new(),
249 host_process_handles: HashMap::new(),
250 status_bar_token_registry: Mutex::new(HashMap::new()),
251 plugin_schemas: std::sync::Arc::new(std::sync::RwLock::new(parts.plugin_schemas)),
252 event_broadcaster: parts.event_broadcaster,
253 #[cfg(feature = "plugins")]
254 pending_plugin_actions: Vec::new(),
255 #[cfg(feature = "plugins")]
256 plugin_render_requested: false,
257 full_redraw_requested: false,
258 suspend_requested: false,
259 plugin_global_state: parts.plugin_global_state,
260 warning_log: None,
261 status_log_path: None,
262 file_watcher_manager: crate::services::file_watcher::FileWatcherManager::new(),
263 last_path_change_for_test: None,
264 last_watch_response_for_test: None,
265 preview_window_id: None,
266 settings_state: None,
267 calibration_wizard: None,
268 keybinding_editor: None,
270 stdin_stream: stdin_stream::StdinStream::default(),
271 global_popups: crate::view::popup::PopupManager::new(),
272 previous_cursor_screen_pos: None,
273 cursor_jump_animation: None,
274 pending_vb_animations: Vec::new(),
275 widget_registry: crate::widgets::WidgetRegistry::new(),
276 floating_widget_panel: None,
277 }
278 }
279
280 pub fn new(
283 config: Config,
284 width: u16,
285 height: u16,
286 dir_context: DirectoryContext,
287 color_capability: crate::view::color_support::ColorCapability,
288 filesystem: Arc<dyn FileSystem + Send + Sync>,
289 ) -> AnyhowResult<Self> {
290 Self::with_working_dir(
291 config,
292 width,
293 height,
294 None,
295 dir_context,
296 true,
297 color_capability,
298 filesystem,
299 )
300 }
301
302 #[allow(clippy::too_many_arguments)]
305 pub fn with_working_dir(
306 config: Config,
307 width: u16,
308 height: u16,
309 working_dir: Option<PathBuf>,
310 dir_context: DirectoryContext,
311 plugins_enabled: bool,
312 color_capability: crate::view::color_support::ColorCapability,
313 filesystem: Arc<dyn FileSystem + Send + Sync>,
314 ) -> AnyhowResult<Self> {
315 Self::with_working_dir_opts(
316 config,
317 width,
318 height,
319 working_dir,
320 dir_context,
321 plugins_enabled,
322 color_capability,
323 filesystem,
324 false,
325 )
326 }
327
328 #[allow(clippy::too_many_arguments)]
336 pub fn with_working_dir_opts(
337 config: Config,
338 width: u16,
339 height: u16,
340 working_dir: Option<PathBuf>,
341 dir_context: DirectoryContext,
342 plugins_enabled: bool,
343 color_capability: crate::view::color_support::ColorCapability,
344 filesystem: Arc<dyn FileSystem + Send + Sync>,
345 defer_plugin_load: bool,
346 ) -> AnyhowResult<Self> {
347 tracing::info!("Building default grammar registry...");
348 let start = std::time::Instant::now();
349 let mut grammar_registry = crate::primitives::grammar::GrammarRegistry::defaults_only();
350 std::sync::Arc::get_mut(&mut grammar_registry)
356 .expect("defaults_only returned a shared Arc")
357 .apply_language_config(&config.languages);
358 tracing::info!("Default grammar registry built in {:?}", start.elapsed());
359 Self::with_options(
363 config,
364 width,
365 height,
366 working_dir,
367 filesystem,
368 plugins_enabled,
369 true, dir_context,
371 None,
372 color_capability,
373 grammar_registry,
374 defer_plugin_load,
375 )
376 }
377
378 #[allow(clippy::too_many_arguments)]
389 pub fn for_test(
390 config: Config,
391 width: u16,
392 height: u16,
393 working_dir: Option<PathBuf>,
394 dir_context: DirectoryContext,
395 color_capability: crate::view::color_support::ColorCapability,
396 filesystem: Arc<dyn FileSystem + Send + Sync>,
397 time_source: Option<SharedTimeSource>,
398 grammar_registry: Option<Arc<crate::primitives::grammar::GrammarRegistry>>,
399 enable_plugins: bool,
400 enable_embedded_plugins: bool,
401 ) -> AnyhowResult<Self> {
402 let mut grammar_registry =
403 grammar_registry.unwrap_or_else(crate::primitives::grammar::GrammarRegistry::empty);
404 std::sync::Arc::get_mut(&mut grammar_registry)
411 .expect("grammar registry Arc must be uniquely owned at for_test entry")
412 .apply_language_config(&config.languages);
413 let mut editor = Self::with_options(
414 config,
415 width,
416 height,
417 working_dir,
418 filesystem,
419 enable_plugins,
420 enable_embedded_plugins,
421 dir_context,
422 time_source,
423 color_capability,
424 grammar_registry,
425 false,
426 )?;
427 editor.needs_full_grammar_build = false;
430 Ok(editor)
431 }
432
433 #[allow(clippy::too_many_arguments)]
437 fn with_options(
438 mut config: Config,
439 width: u16,
440 height: u16,
441 working_dir: Option<PathBuf>,
442 filesystem: Arc<dyn FileSystem + Send + Sync>,
443 enable_plugins: bool,
444 #[cfg_attr(not(feature = "embed-plugins"), allow(unused_variables))]
445 enable_embedded_plugins: bool,
446 dir_context: DirectoryContext,
447 time_source: Option<SharedTimeSource>,
448 color_capability: crate::view::color_support::ColorCapability,
449 grammar_registry: Arc<crate::primitives::grammar::GrammarRegistry>,
450 defer_plugin_load: bool,
451 ) -> AnyhowResult<Self> {
452 let mut t = InitTimer::start("Editor::with_options");
453 let time_source = time_source.unwrap_or_else(RealTimeSource::shared);
455 tracing::info!("Editor::new called with width={}, height={}", width, height);
456
457 let working_dir = working_dir
459 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
460
461 let working_dir = working_dir.canonicalize().unwrap_or(working_dir);
464
465 t.phase("preamble");
466 tracing::info!("Loading themes...");
468 let theme_loader = crate::view::theme::ThemeLoader::new(dir_context.themes_dir());
469 t.phase("ThemeLoader::new");
470 let scan_result =
474 crate::services::packages::scan_installed_packages(&dir_context.config_dir);
475 t.phase("scan_installed_packages");
476
477 for (lang_id, lang_config) in &scan_result.language_configs {
479 config
480 .languages
481 .entry(lang_id.clone())
482 .or_insert_with(|| lang_config.clone());
483 }
484
485 for (lang_id, lsp_config) in &scan_result.lsp_configs {
487 config
488 .lsp
489 .entry(lang_id.clone())
490 .or_insert_with(|| LspLanguageConfig::Multi(vec![lsp_config.clone()]));
491 }
492
493 let theme_registry = Arc::new(theme_loader.load_all(&scan_result.bundle_theme_dirs));
494 t.phase("theme_loader.load_all");
495 tracing::info!("Themes loaded");
496
497 let theme_inner = theme_registry.get_cloned(&config.theme).unwrap_or_else(|| {
499 tracing::warn!(
500 "Theme '{}' not found, falling back to default theme",
501 config.theme.0
502 );
503 theme_registry
504 .get_cloned(&crate::config::ThemeName(
505 crate::view::theme::THEME_HIGH_CONTRAST.to_string(),
506 ))
507 .expect("Default theme must exist")
508 });
509
510 theme_inner.set_terminal_cursor_color();
512 let theme = Arc::new(RwLock::new(theme_inner));
513
514 t.phase("theme_setup");
515 let keybindings = Arc::new(RwLock::new(KeybindingResolver::new(&config)));
516 t.phase("keybindings");
517
518 let mut buffers = crate::app::window::WindowBuffers::new();
520 let mut event_logs = HashMap::new();
521
522 let buffer_id = BufferId(1);
527 let mut state = EditorState::new(
528 width,
529 height,
530 config.editor.large_file_threshold_bytes as usize,
531 Arc::clone(&filesystem),
532 );
533 state
535 .margins
536 .configure_for_line_numbers(config.editor.line_numbers);
537 state.buffer_settings.tab_size = config.editor.tab_size;
538 state.buffer_settings.auto_close = config.editor.auto_close;
539 tracing::info!("EditorState created for buffer {:?}", buffer_id);
541 buffers.insert(buffer_id, state);
542 event_logs.insert(buffer_id, EventLog::new());
543
544 let mut buffer_metadata: HashMap<BufferId, BufferMetadata> = HashMap::new();
548 buffer_metadata.insert(buffer_id, BufferMetadata::new());
549
550 let persisted_env = crate::app::orchestrator_persistence::read_persisted_windows_env(
563 filesystem.as_ref(),
564 &dir_context.data_dir,
565 &working_dir,
566 );
567 let plugin_global_state = crate::app::orchestrator_persistence::read_persisted_plugin_state(
568 filesystem.as_ref(),
569 &dir_context.data_dir,
570 &working_dir,
571 );
572
573 let picked_active = crate::app::orchestrator_persistence::pick_active_window_for_cwd(
584 persisted_env.as_ref(),
585 &working_dir,
586 );
587 let (active_window_id, active_window_root) = picked_active
588 .map(|w| (fresh_core::WindowId(w.id), w.root.clone()))
589 .unwrap_or((fresh_core::WindowId(1), working_dir.clone()));
590
591 let root_uri = types::file_path_to_lsp_uri(&active_window_root);
593
594 t.phase("buffer_state");
595 let tokio_runtime = tokio::runtime::Builder::new_multi_thread()
597 .worker_threads(2) .thread_name("editor-async")
599 .enable_all()
600 .build()
601 .ok()
602 .map(Arc::new);
603 t.phase("tokio_runtime");
604
605 let async_bridge = AsyncBridge::new();
611 let event_broadcaster = crate::model::control_event::EventBroadcaster::default();
612
613 let base_window_bridge = AsyncBridge::new();
619
620 if tokio_runtime.is_none() {
621 tracing::warn!("Failed to create Tokio runtime - async features disabled");
622 }
623
624 let mut lsp = LspManager::new(active_window_id, root_uri);
629
630 if let Some(ref runtime) = tokio_runtime {
634 lsp.set_runtime(runtime.handle().clone(), base_window_bridge.clone());
635 }
636
637 for (language, lsp_configs) in &config.lsp {
639 lsp.set_language_configs(language.clone(), lsp_configs.as_slice().to_vec());
640 }
641
642 let universal_servers: Vec<LspServerConfig> = config
644 .universal_lsp
645 .values()
646 .flat_map(|lc| lc.as_slice().to_vec())
647 .filter(|c| c.enabled)
648 .collect();
649 lsp.set_universal_configs(universal_servers);
650
651 if active_window_root.join("deno.json").exists()
657 || active_window_root.join("deno.jsonc").exists()
658 {
659 tracing::info!("Detected Deno project (deno.json found), using deno lsp for JS/TS");
660 let deno_config = LspServerConfig {
661 command: "deno".to_string(),
662 args: vec!["lsp".to_string()],
663 enabled: true,
664 auto_start: false,
665 process_limits: ProcessLimits::default(),
666 initialization_options: Some(serde_json::json!({"enable": true})),
667 ..Default::default()
668 };
669 lsp.set_language_config("javascript".to_string(), deno_config.clone());
670 lsp.set_language_config("typescript".to_string(), deno_config);
671 }
672
673 t.phase("lsp_setup");
674 let split_manager = SplitManager::new(buffer_id);
676
677 let mut split_view_states = HashMap::new();
679 let initial_split_id = split_manager.active_split();
680 let mut initial_view_state = SplitViewState::with_buffer(width, height, buffer_id);
681 initial_view_state.apply_config_defaults(
682 config.editor.line_numbers,
683 config.editor.highlight_current_line,
684 config.editor.line_wrap,
685 config.editor.wrap_indent,
686 config.editor.wrap_column,
687 config.editor.rulers.clone(),
688 );
689 split_view_states.insert(initial_split_id, initial_view_state);
690
691 let fs_manager = Arc::new(FsManager::new(Arc::clone(&filesystem)));
693
694 let command_registry = Arc::new(RwLock::new(CommandRegistry::new()));
696
697 let authority = crate::services::authority::Authority {
707 filesystem: Arc::clone(&filesystem),
708 ..crate::services::authority::Authority::local(
709 Arc::new(crate::services::workspace_trust::WorkspaceTrust::permissive()),
710 Arc::new(crate::services::env_provider::EnvProvider::inactive()),
711 )
712 };
713 let process_spawner = Arc::clone(&authority.process_spawner);
714
715 let mut quick_open_registry = QuickOpenRegistry::new();
717 quick_open_registry.register(Box::new(FileProvider::new(
718 Arc::clone(&filesystem),
719 Arc::clone(&process_spawner),
720 tokio_runtime.as_ref().map(|rt| rt.handle().clone()),
721 Some(async_bridge.sender()),
722 )));
723 quick_open_registry.register(Box::new(CommandProvider::new(
724 Arc::clone(&command_registry),
725 Arc::clone(&keybindings),
726 )));
727 quick_open_registry.register(Box::new(BufferProvider::new()));
728 quick_open_registry.register(Box::new(GotoLineProvider::new()));
729
730 let theme_cache = Arc::new(RwLock::new(theme_registry.to_json_map()));
732
733 t.phase("split_quickopen_authority");
734 let plugin_manager = Arc::new(RwLock::new(PluginManager::new(
736 enable_plugins,
737 Arc::clone(&command_registry),
738 dir_context.clone(),
739 Arc::clone(&theme_cache),
740 )));
741 t.phase("PluginManager::new");
742
743 #[cfg(feature = "plugins")]
746 if let Some(snapshot_handle) = plugin_manager.read().unwrap().state_snapshot_handle() {
747 let mut snapshot = snapshot_handle.write().unwrap();
748 snapshot.working_dir = working_dir.clone();
749 populate_builtin_keybinding_labels(&mut snapshot, &keybindings);
757 if let Ok(json) = serde_json::to_value(&config) {
769 snapshot.config = std::sync::Arc::new(json);
770 }
771 }
772
773 let plugin_schemas: HashMap<String, serde_json::Value> = HashMap::new();
783 if plugin_manager.read().unwrap().is_active() {
784 let mut plugin_dirs: Vec<std::path::PathBuf> = vec![];
785
786 if let Ok(exe_path) = std::env::current_exe() {
788 if let Some(exe_dir) = exe_path.parent() {
789 let exe_plugin_dir = exe_dir.join("plugins");
790 if exe_plugin_dir.exists() {
791 plugin_dirs.push(exe_plugin_dir);
792 }
793 }
794 }
795
796 #[cfg(feature = "embed-plugins")]
807 if enable_embedded_plugins && plugin_dirs.is_empty() {
808 if let Some(embedded_dir) =
809 crate::services::plugins::embedded::get_embedded_plugins_dir()
810 {
811 tracing::info!("Using embedded plugins from: {:?}", embedded_dir);
812 plugin_dirs.push(embedded_dir.clone());
813 }
814 }
815
816 let user_plugins_dir = dir_context.config_dir.join("plugins");
818 if user_plugins_dir.exists() && !plugin_dirs.contains(&user_plugins_dir) {
819 tracing::info!("Found user plugins directory: {:?}", user_plugins_dir);
820 plugin_dirs.push(user_plugins_dir.clone());
821 }
822
823 let packages_dir = dir_context.config_dir.join("plugins").join("packages");
825 if packages_dir.exists() {
826 if let Ok(entries) = std::fs::read_dir(&packages_dir) {
827 for entry in entries.flatten() {
828 let path = entry.path();
829 if path.is_dir() {
831 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
832 if !name.starts_with('.') {
833 tracing::info!("Found package manager plugin: {:?}", path);
834 plugin_dirs.push(path);
835 }
836 }
837 }
838 }
839 }
840 }
841
842 for dir in &scan_result.bundle_plugin_dirs {
844 tracing::info!("Found bundle plugin directory: {:?}", dir);
845 plugin_dirs.push(dir.clone());
846 }
847
848 if plugin_dirs.is_empty() {
849 tracing::debug!(
850 "No plugins directory found next to executable or in working dir: {:?}",
851 working_dir
852 );
853 }
854
855 if defer_plugin_load {
856 #[cfg(feature = "plugins")]
867 {
868 let bridge = &async_bridge;
869 let mut dir_receivers: Vec<(
870 std::path::PathBuf,
871 fresh_plugin_runtime::thread::oneshot::Receiver<
872 fresh_plugin_runtime::thread::PluginsDirLoadResult,
873 >,
874 )> = Vec::with_capacity(plugin_dirs.len());
875 for plugin_dir in &plugin_dirs {
876 tracing::info!(
877 "Submitting async TypeScript plugin load for: {:?}",
878 plugin_dir
879 );
880 if let Some(rx) = plugin_manager
881 .read()
882 .unwrap()
883 .load_plugins_from_dir_with_config_request(plugin_dir, &config.plugins)
884 {
885 dir_receivers.push((plugin_dir.clone(), rx));
886 }
887 }
888 let declarations_rx = if !dir_receivers.is_empty() {
889 plugin_manager.read().unwrap().list_plugins_request()
890 } else {
891 None
892 };
893 if !dir_receivers.is_empty() {
894 let sender = bridge.sender();
895 std::thread::Builder::new()
896 .name("plugin-load-forwarder".to_string())
897 .spawn(move || {
898 for (dir, rx) in dir_receivers {
899 let load_start = std::time::Instant::now();
900 match rx.recv() {
901 Ok((errors, discovered_plugins)) => {
902 tracing::info!(
903 "Loaded TypeScript plugins from {:?} in {:?}",
904 dir,
905 load_start.elapsed()
906 );
907 drop(sender.send(
908 crate::services::async_bridge::AsyncMessage::PluginsDirLoaded {
909 dir,
910 errors,
911 discovered_plugins,
912 },
913 ));
914 }
915 Err(e) => {
916 tracing::warn!(
917 "plugin-load-forwarder: dir {:?} recv failed: {}",
918 dir,
919 e
920 );
921 }
922 }
923 }
924 if let Some(rx) = declarations_rx {
925 match rx.recv() {
926 Ok(plugin_infos) => {
927 let declarations: Vec<(String, String)> = plugin_infos
928 .into_iter()
929 .filter_map(|info| {
930 info.declarations.map(|d| (info.name, d))
931 })
932 .collect();
933 drop(sender.send(
934 crate::services::async_bridge::AsyncMessage::PluginDeclarationsReady {
935 declarations,
936 },
937 ));
938 }
939 Err(e) => {
940 tracing::warn!(
941 "plugin-load-forwarder: list_plugins recv failed: {}",
942 e
943 );
944 }
945 }
946 }
947 })
948 .ok();
949 }
950 }
951 } else {
952 for plugin_dir in plugin_dirs {
957 tracing::info!("Loading TypeScript plugins from: {:?}", plugin_dir);
958 let load_start = std::time::Instant::now();
959 let (errors, discovered_plugins) = plugin_manager
960 .read()
961 .unwrap()
962 .load_plugins_from_dir_with_config(&plugin_dir, &config.plugins);
963 tracing::info!(
964 "Loaded TypeScript plugins from {:?} in {:?}",
965 plugin_dir,
966 load_start.elapsed()
967 );
968
969 for (name, plugin_config) in discovered_plugins {
972 config.plugins.insert(name, plugin_config);
973 }
974
975 if !errors.is_empty() {
976 for err in &errors {
977 tracing::error!("TypeScript plugin load error: {}", err);
978 }
979 #[cfg(debug_assertions)]
981 panic!(
982 "TypeScript plugin loading failed with {} error(s): {}",
983 errors.len(),
984 errors.join("; ")
985 );
986 }
987 }
988
989 let declarations = plugin_manager.read().unwrap().plugin_declarations();
997 crate::init_script::write_plugin_declarations(
998 &dir_context.config_dir,
999 &declarations,
1000 );
1001 }
1002 }
1003
1004 t.phase("plugin_loading");
1005 let recovery_enabled = config.editor.recovery_enabled;
1007 let check_for_updates = config.check_for_updates;
1008
1009 let update_checker = if check_for_updates {
1011 tracing::debug!("Update checking enabled, starting periodic checker");
1012 Some(
1013 crate::services::release_checker::start_periodic_update_check(
1014 crate::services::release_checker::DEFAULT_RELEASES_URL,
1015 time_source.clone(),
1016 dir_context.data_dir.clone(),
1017 ),
1018 )
1019 } else {
1020 tracing::debug!("Update checking disabled by config");
1021 None
1022 };
1023
1024 let user_config_raw = Config::read_user_config_raw(&working_dir);
1026
1027 let config_arc = Arc::new(config);
1034 let config_cached_json =
1035 Arc::new(serde_json::to_value(&*config_arc).unwrap_or(serde_json::Value::Null));
1036 let config_snapshot_anchor = Arc::clone(&config_arc);
1037
1038 let buffer_id_alloc = crate::app::window_resources::BufferIdAllocator::new(2);
1044
1045 let local_filesystem: Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> =
1050 Arc::new(crate::model::filesystem::StdFileSystem);
1051
1052 let recovery_service = {
1056 let recovery_config = RecoveryConfig {
1057 enabled: recovery_enabled,
1058 ..RecoveryConfig::default()
1059 };
1060 let scope = crate::services::recovery::RecoveryScope::Standalone {
1068 working_dir: working_dir.clone(),
1069 };
1070 std::sync::Arc::new(std::sync::Mutex::new(RecoveryService::with_scope(
1071 recovery_config,
1072 &dir_context.recovery_dir(),
1073 &scope,
1074 )))
1075 };
1076
1077 let base_resources = crate::app::window_resources::WindowResources {
1083 config: Arc::clone(&config_arc),
1084 grammar_registry: Arc::clone(&grammar_registry),
1085 theme_registry: Arc::clone(&theme_registry),
1086 theme_cache: Arc::clone(&theme_cache),
1087 keybindings: Arc::clone(&keybindings),
1088 command_registry: Arc::clone(&command_registry),
1089 fs_manager: Arc::clone(&fs_manager),
1090 local_filesystem: Arc::clone(&local_filesystem),
1091 buffer_id_alloc: buffer_id_alloc.clone(),
1092 authority: authority.clone(),
1093 time_source: Arc::clone(&time_source),
1094 dir_context: dir_context.clone(),
1095 tokio_runtime: tokio_runtime.clone(),
1096 async_bridge: Some(async_bridge.clone()),
1097 plugin_manager: Arc::clone(&plugin_manager),
1098 theme: Arc::clone(&theme),
1099 event_broadcaster: event_broadcaster.clone(),
1100 recovery_service: Arc::clone(&recovery_service),
1101 };
1102
1103 let (active_label, active_root, active_plugin_state) = picked_active
1114 .map(|w| (w.label.clone(), w.root.clone(), w.plugin_state.clone()))
1115 .unwrap_or_else(|| (String::new(), working_dir.clone(), HashMap::new()));
1116
1117 let mut active_win = crate::app::window::Window::new(
1118 active_window_id,
1119 active_label,
1120 active_root,
1121 base_resources,
1122 );
1123 active_win.terminal_width = width;
1129 active_win.terminal_height = height;
1130 active_win.lsp = Some(lsp);
1134 active_win.buffers = buffers;
1135 active_win
1136 .buffers
1137 .set_splits((split_manager, split_view_states));
1138 active_win.buffer_metadata = buffer_metadata;
1139 active_win.event_logs = event_logs;
1140 active_win.plugin_state = active_plugin_state;
1141 active_win.bridge = base_window_bridge;
1147 for history_name in ["search", "replace", "goto_line"] {
1150 let path = dir_context.prompt_history_path(history_name);
1151 let history = crate::input::input_history::InputHistory::load_from_file(&path)
1152 .unwrap_or_else(|e| {
1153 tracing::warn!("Failed to load {} history: {}", history_name, e);
1154 crate::input::input_history::InputHistory::new()
1155 });
1156 active_win
1157 .prompt_histories
1158 .insert(history_name.to_string(), history);
1159 }
1160
1161 let mut windows = HashMap::new();
1165 if let Some(ref env) = persisted_env {
1166 let active_came_from_pick = picked_active.is_some();
1175 let mut next_fresh_id = env
1176 .next_id
1177 .max(env.windows.iter().map(|w| w.id).max().unwrap_or(0) + 1)
1178 .max(active_window_id.0 + 1);
1179 for ps in &env.windows {
1180 if active_came_from_pick && ps.id == active_window_id.0 {
1181 continue;
1182 }
1183 let id = if ps.id == active_window_id.0 {
1184 let fresh = fresh_core::WindowId(next_fresh_id);
1185 next_fresh_id += 1;
1186 fresh
1187 } else {
1188 fresh_core::WindowId(ps.id)
1189 };
1190 let resources = crate::app::window_resources::WindowResources {
1191 config: Arc::clone(&config_arc),
1192 grammar_registry: Arc::clone(&grammar_registry),
1193 theme_registry: Arc::clone(&theme_registry),
1194 theme_cache: Arc::clone(&theme_cache),
1195 keybindings: Arc::clone(&keybindings),
1196 command_registry: Arc::clone(&command_registry),
1197 fs_manager: Arc::clone(&fs_manager),
1198 local_filesystem: Arc::clone(&local_filesystem),
1199 buffer_id_alloc: buffer_id_alloc.clone(),
1200 authority: authority.clone(),
1201 time_source: Arc::clone(&time_source),
1202 dir_context: dir_context.clone(),
1203 tokio_runtime: tokio_runtime.clone(),
1204 async_bridge: Some(async_bridge.clone()),
1205 plugin_manager: Arc::clone(&plugin_manager),
1206 theme: Arc::clone(&theme),
1207 event_broadcaster: event_broadcaster.clone(),
1208 recovery_service: Arc::clone(&recovery_service),
1209 };
1210 let mut shell = crate::app::window::Window::new(
1211 id,
1212 ps.label.clone(),
1213 ps.root.clone(),
1214 resources,
1215 );
1216 shell.terminal_width = width;
1217 shell.terminal_height = height;
1218 shell.plugin_state = ps.plugin_state.clone();
1219 windows.insert(id, shell);
1220 }
1221 }
1222 windows.insert(active_window_id, active_win);
1223
1224 let max_existing = windows.keys().map(|k| k.0).max().unwrap_or(0);
1230 let next_window_id = persisted_env
1231 .as_ref()
1232 .map(|env| env.next_id.max(max_existing + 1))
1233 .unwrap_or(2);
1234
1235 let key_translator = crate::input::key_translator::KeyTranslator::load_from_config_dir(
1236 &dir_context.config_dir,
1237 )
1238 .unwrap_or_default();
1239
1240 let pending_grammars = scan_result
1241 .additional_grammars
1242 .iter()
1243 .map(|g| PendingGrammar {
1244 language: g.language.clone(),
1245 grammar_path: g.path.to_string_lossy().to_string(),
1246 extensions: g.extensions.clone(),
1247 })
1248 .collect();
1249
1250 let parts = EditorParts {
1251 config: config_arc,
1252 config_snapshot_anchor,
1253 config_cached_json,
1254 user_config_raw: Arc::new(user_config_raw),
1255 dir_context: dir_context.clone(),
1256 theme,
1257 theme_registry,
1258 theme_cache,
1259 grammar_registry,
1260 pending_grammars,
1261 needs_full_grammar_build: true,
1262 keybindings,
1263 buffer_id_alloc: buffer_id_alloc.clone(),
1264 next_buffer_id: 2,
1265 terminal_width: width,
1266 terminal_height: height,
1267 color_capability,
1268 tokio_runtime,
1269 async_bridge,
1270 fs_manager,
1271 authority,
1272 local_filesystem: Arc::clone(&local_filesystem),
1273 windows,
1274 active_window: active_window_id,
1275 next_window_id,
1276 command_registry,
1277 quick_open_registry,
1278 plugin_manager,
1279 recovery_service,
1280 key_translator,
1281 update_checker,
1282 time_source: time_source.clone(),
1283 plugin_global_state,
1284 plugin_schemas,
1285 event_broadcaster: event_broadcaster.clone(),
1286 };
1287
1288 let mut editor = Editor::from_parts(parts);
1289
1290 t.phase("editor_struct_assembly");
1291 editor.clipboard.apply_config(&editor.config.clipboard);
1293
1294 let needs_seed: Vec<fresh_core::WindowId> = editor
1302 .windows
1303 .iter()
1304 .filter(|(_, s)| s.buffers.splits().is_none() || s.buffers.len() == 0)
1305 .map(|(id, _)| *id)
1306 .collect();
1307 for id in needs_seed {
1308 if let Some((buf, state, metadata, event_log, mgr, vs)) =
1309 editor.build_fresh_layout_if_needed(id)
1310 {
1311 if let Some(s) = editor.windows.get_mut(&id) {
1312 s.buffers.set_splits((mgr, vs));
1313 s.buffers.insert(buf, state);
1314 s.buffer_metadata.insert(buf, metadata);
1315 s.event_logs.insert(buf, event_log);
1316 }
1317 }
1318 }
1319
1320 #[cfg(feature = "plugins")]
1321 {
1322 editor.update_plugin_state_snapshot();
1323 if editor.plugin_manager.read().unwrap().is_active() {
1324 editor.plugin_manager.read().unwrap().run_hook(
1325 "editor_initialized",
1326 crate::services::plugins::hooks::HookArgs::EditorInitialized {},
1327 );
1328 }
1329 }
1330 t.phase("post_struct_hooks");
1331 t.finish();
1332 Ok(editor)
1333 }
1334
1335 pub fn event_broadcaster(&self) -> &crate::model::control_event::EventBroadcaster {
1337 &self.event_broadcaster
1338 }
1339
1340 pub(super) fn start_background_grammar_build(
1345 &mut self,
1346 additional: Vec<crate::primitives::grammar::GrammarSpec>,
1347 callback_ids: Vec<fresh_core::api::JsCallbackId>,
1348 ) {
1349 let Some(bridge) = &self.async_bridge else {
1350 return;
1351 };
1352 self.grammar_build_in_progress = true;
1353 let sender = bridge.sender();
1354 let config_dir = self.dir_context.config_dir.clone();
1355 tracing::info!(
1356 "Spawning background grammar build thread ({} plugin grammars)...",
1357 additional.len()
1358 );
1359 std::thread::Builder::new()
1360 .name("grammar-build".to_string())
1361 .spawn(move || {
1362 tracing::info!("[grammar-build] Thread started");
1363 let start = std::time::Instant::now();
1364 let registry = if additional.is_empty() {
1365 crate::primitives::grammar::GrammarRegistry::for_editor(config_dir)
1366 } else {
1367 crate::primitives::grammar::GrammarRegistry::for_editor_with_additional(
1368 config_dir,
1369 &additional,
1370 )
1371 };
1372 tracing::info!("[grammar-build] Complete in {:?}", start.elapsed());
1373 drop(sender.send(
1374 crate::services::async_bridge::AsyncMessage::GrammarRegistryBuilt {
1375 registry,
1376 callback_ids,
1377 },
1378 ));
1379 })
1380 .ok();
1381 }
1382
1383 pub fn load_init_script(&mut self, enabled: bool) {
1390 use crate::init_script::{
1391 check, decide_load, describe, record_success, refresh_types_scaffolding, CheckSeverity,
1392 InitOutcome, LoadDecision,
1393 };
1394
1395 let config_dir = self.dir_context.config_dir.clone();
1396
1397 if enabled {
1398 refresh_types_scaffolding(&config_dir);
1402
1403 let report = check(&config_dir);
1408 if !report.ok {
1409 for d in &report.diagnostics {
1410 let level = match d.severity {
1411 CheckSeverity::Error => "error",
1412 CheckSeverity::Warning => "warning",
1413 };
1414 tracing::warn!(
1415 "init.ts pre-load {level} at {}:{}: {}",
1416 d.line,
1417 d.column,
1418 d.message
1419 );
1420 }
1421 }
1422 }
1423
1424 let outcome = match decide_load(&config_dir, enabled) {
1425 LoadDecision::Skip(outcome) => outcome,
1426 LoadDecision::Load { source } => {
1427 if !self.plugin_manager.read().unwrap().is_active() {
1428 InitOutcome::Failed {
1429 message: "plugin runtime inactive (--no-plugins); init.ts cannot run"
1430 .into(),
1431 }
1432 } else {
1433 match self.plugin_manager.read().unwrap().load_plugin_from_source(
1434 &source,
1435 crate::init_script::INIT_PLUGIN_NAME,
1436 true,
1437 ) {
1438 Ok(()) => {
1439 record_success(&config_dir);
1440 InitOutcome::Loaded
1441 }
1442 Err(e) => InitOutcome::Failed {
1443 message: format!("{e}"),
1444 },
1445 }
1446 }
1447 }
1448 };
1449
1450 let summary = describe(&outcome);
1451 match outcome {
1452 InitOutcome::NotFound | InitOutcome::Disabled => tracing::debug!("{}", summary),
1453 InitOutcome::Loaded => tracing::info!("{}", summary),
1454 InitOutcome::CrashFused { .. } | InitOutcome::Failed { .. } => {
1455 tracing::warn!("{}", summary);
1456 self.set_status_message(summary);
1457 }
1458 }
1459 }
1460
1461 pub fn load_init_script_async(&mut self, enabled: bool) {
1473 use crate::init_script::{
1474 check, decide_load, refresh_types_scaffolding, CheckSeverity, InitOutcome, LoadDecision,
1475 };
1476 use crate::services::async_bridge::PluginInitScriptOutcome;
1477
1478 let config_dir = self.dir_context.config_dir.clone();
1479
1480 if enabled {
1481 refresh_types_scaffolding(&config_dir);
1482 let report = check(&config_dir);
1483 if !report.ok {
1484 for d in &report.diagnostics {
1485 let level = match d.severity {
1486 CheckSeverity::Error => "error",
1487 CheckSeverity::Warning => "warning",
1488 };
1489 tracing::warn!(
1490 "init.ts pre-load {level} at {}:{}: {}",
1491 d.line,
1492 d.column,
1493 d.message
1494 );
1495 }
1496 }
1497 }
1498
1499 let outcome_now: Option<PluginInitScriptOutcome> = match decide_load(&config_dir, enabled) {
1500 LoadDecision::Skip(outcome) => Some(match outcome {
1501 InitOutcome::NotFound => PluginInitScriptOutcome::NotFound,
1502 InitOutcome::Disabled => PluginInitScriptOutcome::Disabled,
1503 InitOutcome::CrashFused { failures } => {
1504 PluginInitScriptOutcome::CrashFused { failures }
1505 }
1506 InitOutcome::Loaded => PluginInitScriptOutcome::Loaded,
1509 InitOutcome::Failed { message } => PluginInitScriptOutcome::Failed { message },
1510 }),
1511 LoadDecision::Load { source } => {
1512 if !self.plugin_manager.read().unwrap().is_active() {
1513 Some(PluginInitScriptOutcome::Failed {
1514 message: "plugin runtime inactive (--no-plugins); init.ts cannot run"
1515 .into(),
1516 })
1517 } else {
1518 self.spawn_init_script_forwarder(source);
1519 None
1520 }
1521 }
1522 };
1523
1524 if let Some(outcome) = outcome_now {
1525 if let Some(bridge) = &self.async_bridge {
1529 drop(bridge.sender().send(
1530 crate::services::async_bridge::AsyncMessage::PluginInitScriptLoaded(outcome),
1531 ));
1532 } else {
1533 self.handle_plugin_init_script_loaded(outcome);
1534 }
1535 }
1536 }
1537
1538 #[cfg(feature = "plugins")]
1539 fn spawn_init_script_forwarder(&self, source: String) {
1540 let Some(bridge) = &self.async_bridge else {
1541 return;
1542 };
1543 let Some(rx) = self
1544 .plugin_manager
1545 .read()
1546 .unwrap()
1547 .load_plugin_from_source_request(&source, crate::init_script::INIT_PLUGIN_NAME, true)
1548 else {
1549 return;
1550 };
1551 let sender = bridge.sender();
1552 std::thread::Builder::new()
1553 .name("plugin-init-forwarder".to_string())
1554 .spawn(move || {
1555 let outcome = match rx.recv() {
1556 Ok(Ok(())) => crate::services::async_bridge::PluginInitScriptOutcome::Loaded,
1557 Ok(Err(e)) => crate::services::async_bridge::PluginInitScriptOutcome::Failed {
1558 message: format!("{e}"),
1559 },
1560 Err(e) => crate::services::async_bridge::PluginInitScriptOutcome::Failed {
1561 message: format!("plugin thread closed: {e}"),
1562 },
1563 };
1564 drop(sender.send(
1565 crate::services::async_bridge::AsyncMessage::PluginInitScriptLoaded(outcome),
1566 ));
1567 })
1568 .ok();
1569 }
1570
1571 #[cfg(not(feature = "plugins"))]
1572 fn spawn_init_script_forwarder(&self, _source: String) {}
1573
1574 pub fn handle_set_setting(&mut self, path: String, value: serde_json::Value) {
1578 let mut json = serde_json::to_value(&*self.config).unwrap_or_default();
1579 set_dot_path(&mut json, &path, value);
1580 match serde_json::from_value::<crate::config::Config>(json) {
1581 Ok(new_config) => {
1582 let old_theme = self.config.theme.clone();
1583 self.config = Arc::new(new_config);
1584 if old_theme != self.config.theme {
1585 if let Some(theme) = self.theme_registry.get_cloned(&self.config.theme) {
1586 *self.theme.write().unwrap() = theme;
1587 }
1588 }
1589 *self.keybindings.write().unwrap() =
1590 crate::input::keybindings::KeybindingResolver::new(&self.config);
1591 self.clipboard.apply_config(&self.config.clipboard);
1592 {
1593 let cfg = self.config.editor.clone();
1594 let win = self.active_window_mut();
1595 win.menu_bar_visible = cfg.show_menu_bar;
1596 win.tab_bar_visible = cfg.show_tab_bar;
1597 win.status_bar_visible = cfg.show_status_bar;
1598 win.prompt_line_visible = cfg.show_prompt_line;
1599 }
1600 #[cfg(feature = "plugins")]
1601 self.update_plugin_state_snapshot();
1602 }
1603 Err(e) => {
1604 self.set_status_message(format!("setSetting({path}): {e}"));
1605 }
1606 }
1607 }
1608
1609 pub fn handle_add_plugin_config_field(
1619 &mut self,
1620 plugin_name: String,
1621 field_name: String,
1622 field_schema: serde_json::Value,
1623 ) {
1624 tracing::trace!(
1625 "Registering plugin config field: {}.{}",
1626 plugin_name,
1627 field_name
1628 );
1629 let updated_schema = {
1632 let schemas = self.plugin_schemas.read().ok();
1633 let existing = schemas.as_ref().and_then(|m| m.get(&plugin_name)).cloned();
1634 let mut schema = existing.unwrap_or_else(|| {
1635 serde_json::json!({
1636 "type": "object",
1637 "properties": {},
1638 })
1639 });
1640 if let Some(props) = schema
1641 .as_object_mut()
1642 .and_then(|o| o.get_mut("properties"))
1643 .and_then(|p| p.as_object_mut())
1644 {
1645 props.insert(field_name.clone(), field_schema.clone());
1646 }
1647 schema
1648 };
1649
1650 if let Err(msg) = crate::plugin_schemas::validate_plugin_schema(&updated_schema) {
1651 self.set_status_message(format!(
1654 "defineConfig({}.{}): {}",
1655 plugin_name, field_name, msg
1656 ));
1657 return;
1658 }
1659
1660 if let Some(default) = field_schema.get("default").cloned() {
1662 let cfg = std::sync::Arc::make_mut(&mut self.config);
1663 let entry = cfg.plugins.entry(plugin_name.clone()).or_default();
1664 let settings_obj = match &mut entry.settings {
1665 serde_json::Value::Object(_) => &mut entry.settings,
1666 slot => {
1667 *slot = serde_json::Value::Object(Default::default());
1668 slot
1669 }
1670 };
1671 if let serde_json::Value::Object(map) = settings_obj {
1672 map.entry(field_name.clone()).or_insert(default);
1673 }
1674 }
1675
1676 if let Ok(mut schemas) = self.plugin_schemas.write() {
1677 schemas.insert(plugin_name, updated_schema);
1678 }
1679
1680 #[cfg(feature = "plugins")]
1681 self.update_plugin_state_snapshot();
1682 }
1683
1684 pub(crate) fn handle_plugins_dir_loaded(
1689 &mut self,
1690 dir: std::path::PathBuf,
1691 errors: Vec<String>,
1692 discovered_plugins: std::collections::HashMap<String, fresh_core::config::PluginConfig>,
1693 ) {
1694 if !discovered_plugins.is_empty() {
1695 let cfg = std::sync::Arc::make_mut(&mut self.config);
1696 for (name, plugin_config) in discovered_plugins {
1697 cfg.plugins.insert(name, plugin_config);
1698 }
1699 }
1700 if !errors.is_empty() {
1701 for err in &errors {
1702 tracing::error!("TypeScript plugin load error: {}", err);
1703 }
1704 #[cfg(debug_assertions)]
1705 panic!(
1706 "TypeScript plugin loading failed for {:?} with {} error(s): {}",
1707 dir,
1708 errors.len(),
1709 errors.join("; ")
1710 );
1711 #[cfg(not(debug_assertions))]
1712 {
1713 let _ = dir;
1714 }
1715 }
1716 }
1717
1718 pub(crate) fn handle_plugin_declarations_ready(&self, declarations: Vec<(String, String)>) {
1722 crate::init_script::write_plugin_declarations(&self.dir_context.config_dir, &declarations);
1723 }
1724
1725 pub(crate) fn handle_plugin_init_script_loaded(
1729 &mut self,
1730 outcome: crate::services::async_bridge::PluginInitScriptOutcome,
1731 ) {
1732 use crate::init_script::{describe, record_success, InitOutcome};
1733 use crate::services::async_bridge::PluginInitScriptOutcome as O;
1734 let outcome = match outcome {
1735 O::NotFound => InitOutcome::NotFound,
1736 O::Disabled => InitOutcome::Disabled,
1737 O::CrashFused { failures } => InitOutcome::CrashFused { failures },
1738 O::Loaded => {
1739 record_success(&self.dir_context.config_dir);
1740 InitOutcome::Loaded
1741 }
1742 O::Failed { message } => InitOutcome::Failed { message },
1743 };
1744 let summary = describe(&outcome);
1745 match outcome {
1746 InitOutcome::NotFound | InitOutcome::Disabled => tracing::debug!("{}", summary),
1747 InitOutcome::Loaded => tracing::info!("{}", summary),
1748 InitOutcome::CrashFused { .. } | InitOutcome::Failed { .. } => {
1749 tracing::warn!("{}", summary);
1750 self.set_status_message(summary);
1751 }
1752 }
1753 }
1754
1755 pub fn fire_plugins_loaded_hook(&self) {
1757 #[cfg(feature = "plugins")]
1758 if self.plugin_manager.read().unwrap().is_active() {
1759 self.plugin_manager.read().unwrap().run_hook(
1760 "plugins_loaded",
1761 crate::services::plugins::hooks::HookArgs::PluginsLoaded {},
1762 );
1763 }
1764 }
1765
1766 pub fn fire_ready_hook(&self) {
1768 #[cfg(feature = "plugins")]
1769 if self.plugin_manager.read().unwrap().is_active() {
1770 self.plugin_manager
1771 .read()
1772 .unwrap()
1773 .run_hook("ready", crate::services::plugins::hooks::HookArgs::Ready {});
1774 }
1775 }
1776
1777 #[doc(hidden)]
1779 pub fn config_for_tests(&self) -> &crate::config::Config {
1780 &self.config
1781 }
1782
1783 #[doc(hidden)]
1785 pub fn dispatch_action_for_tests(&mut self, action: crate::input::keybindings::Action) {
1786 if let Err(e) = self.handle_action(action) {
1787 tracing::warn!("dispatch_action_for_tests: {e}");
1788 }
1789 }
1790
1791 #[doc(hidden)]
1793 pub fn live_grep_last_state_for_tests(
1794 &self,
1795 ) -> Option<&crate::services::live_grep_state::LiveGrepLastState> {
1796 self.active_window().live_grep_last_state.as_ref()
1797 }
1798
1799 #[doc(hidden)]
1801 pub fn set_live_grep_last_state_for_tests(
1802 &mut self,
1803 state: Option<crate::services::live_grep_state::LiveGrepLastState>,
1804 ) {
1805 self.active_window_mut().live_grep_last_state = state;
1806 }
1807
1808 #[doc(hidden)]
1811 pub fn split_manager_for_tests(&self) -> &crate::view::split::SplitManager {
1812 self.windows
1813 .get(&self.active_window)
1814 .and_then(|w| w.buffers.splits())
1815 .map(|(mgr, _)| mgr)
1816 .expect("active window must have a populated split layout")
1817 }
1818
1819 #[doc(hidden)]
1824 pub fn split_view_state_for_tests(
1825 &self,
1826 leaf: crate::model::event::LeafId,
1827 ) -> Option<&crate::view::split::SplitViewState> {
1828 self.windows
1829 .get(&self.active_window)
1830 .and_then(|w| w.buffers.splits())
1831 .map(|(_, vs)| vs)
1832 .expect("active window must have a populated split layout")
1833 .get(&leaf)
1834 }
1835
1836 #[cfg(feature = "plugins")]
1845 pub(crate) fn refresh_keybinding_labels_snapshot(&self) {
1846 if let Some(snapshot_handle) = self.plugin_manager.read().unwrap().state_snapshot_handle() {
1847 if let Ok(mut snapshot) = snapshot_handle.write() {
1848 populate_builtin_keybinding_labels(&mut snapshot, &self.keybindings);
1849 }
1850 }
1851 }
1852}
1853
1854#[cfg(feature = "plugins")]
1870fn populate_builtin_keybinding_labels(
1871 snapshot: &mut crate::services::plugins::api::EditorStateSnapshot,
1872 keybindings: &std::sync::Arc<std::sync::RwLock<crate::input::keybindings::KeybindingResolver>>,
1873) {
1874 use crate::input::keybindings::{Action, KeyContext};
1875 let Ok(resolver) = keybindings.read() else {
1876 return;
1877 };
1878 let contexts = [
1879 KeyContext::Normal,
1880 KeyContext::Prompt,
1881 KeyContext::Popup,
1882 KeyContext::Completion,
1883 KeyContext::FileExplorer,
1884 KeyContext::Menu,
1885 KeyContext::Terminal,
1886 KeyContext::Settings,
1887 KeyContext::CompositeBuffer,
1888 ];
1889 let known_suffixes: Vec<String> = contexts
1896 .iter()
1897 .map(|c| format!("\0{}", c.to_when_clause()))
1898 .collect();
1899 snapshot
1900 .keybinding_labels
1901 .retain(|k, _| !known_suffixes.iter().any(|s| k.ends_with(s)));
1902 let plugin_action_names = resolver.bound_plugin_action_names();
1906 let action_names = Action::all_action_names()
1907 .into_iter()
1908 .chain(plugin_action_names);
1909 for action_name in action_names {
1910 for ctx in &contexts {
1911 if let Some(label) = resolver.find_keybinding_for_action(&action_name, ctx.clone()) {
1912 let key = format!("{}\0{}", action_name, ctx.to_when_clause());
1913 snapshot.keybinding_labels.insert(key, label);
1914 }
1915 }
1916 }
1917}