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