1use super::*;
13
14struct InitTimer {
18 label: &'static str,
19 start: std::time::Instant,
20 last: std::time::Instant,
21 enabled: bool,
22}
23
24impl InitTimer {
25 fn start(label: &'static str) -> Self {
26 let enabled = std::env::var("FRESH_TEST_TIMING").is_ok_and(|v| !v.is_empty() && v != "0");
27 let now = std::time::Instant::now();
28 if enabled {
29 eprintln!("[timing] {label} start");
30 }
31 Self {
32 label,
33 start: now,
34 last: now,
35 enabled,
36 }
37 }
38 fn phase(&mut self, name: &str) {
39 if !self.enabled {
40 return;
41 }
42 let now = std::time::Instant::now();
43 let delta = now.duration_since(self.last);
44 let cumul = now.duration_since(self.start);
45 eprintln!(
46 "[timing] {name:<30} +{delta:>8.1}ms (cumul {cumul:.1}ms)",
47 name = name,
48 delta = delta.as_secs_f64() * 1000.0,
49 cumul = cumul.as_secs_f64() * 1000.0,
50 );
51 self.last = now;
52 }
53 fn finish(self) {
54 if !self.enabled {
55 return;
56 }
57 eprintln!(
58 "[timing] {label} total {total:.1}ms",
59 label = self.label,
60 total = self.start.elapsed().as_secs_f64() * 1000.0,
61 );
62 }
63}
64
65fn set_dot_path(root: &mut serde_json::Value, path: &str, value: serde_json::Value) {
68 let segments: Vec<&str> = path.split('.').filter(|s| !s.is_empty()).collect();
69 if segments.is_empty() {
70 return;
71 }
72 let mut cur = root;
73 for seg in &segments[..segments.len() - 1] {
74 if !cur.is_object() {
75 *cur = serde_json::Value::Object(serde_json::Map::new());
76 }
77 cur = cur
78 .as_object_mut()
79 .unwrap()
80 .entry((*seg).to_string())
81 .or_insert(serde_json::Value::Null);
82 }
83 let last = segments[segments.len() - 1];
84 if !cur.is_object() {
85 *cur = serde_json::Value::Object(serde_json::Map::new());
86 }
87 cur.as_object_mut().unwrap().insert(last.to_string(), value);
88}
89
90pub(super) struct EditorParts {
106 pub(super) config: Arc<Config>,
108 pub(super) config_snapshot_anchor: Arc<Config>,
109 pub(super) config_cached_json: Arc<serde_json::Value>,
110 pub(super) user_config_raw: Arc<serde_json::Value>,
111 pub(super) dir_context: DirectoryContext,
112 pub(super) working_dir: PathBuf,
113
114 pub(super) theme: Arc<RwLock<crate::view::theme::Theme>>,
116 pub(super) theme_registry: Arc<crate::view::theme::ThemeRegistry>,
117 pub(super) theme_cache: Arc<RwLock<HashMap<String, serde_json::Value>>>,
118
119 pub(super) grammar_registry: Arc<crate::primitives::grammar::GrammarRegistry>,
121 pub(super) pending_grammars: Vec<PendingGrammar>,
122 pub(super) needs_full_grammar_build: bool,
123
124 pub(super) keybindings: Arc<RwLock<KeybindingResolver>>,
126 pub(super) buffer_id_alloc: crate::app::window_resources::BufferIdAllocator,
127 pub(super) next_buffer_id: usize,
128
129 pub(super) terminal_width: u16,
131 pub(super) terminal_height: u16,
132 pub(super) color_capability: crate::view::color_support::ColorCapability,
133
134 pub(super) tokio_runtime: Option<Arc<tokio::runtime::Runtime>>,
136 pub(super) async_bridge: AsyncBridge,
137 pub(super) fs_manager: Arc<FsManager>,
138 pub(super) authority: crate::services::authority::Authority,
139 pub(super) local_filesystem: Arc<dyn FileSystem + Send + Sync>,
140
141 pub(super) windows: HashMap<fresh_core::WindowId, crate::app::window::Window>,
147 pub(super) active_window: fresh_core::WindowId,
148 pub(super) next_window_id: u64,
149
150 pub(super) command_registry: Arc<RwLock<CommandRegistry>>,
152 pub(super) quick_open_registry: QuickOpenRegistry,
153 pub(super) plugin_manager: Arc<RwLock<PluginManager>>,
154 pub(super) recovery_service: RecoveryService,
155 pub(super) key_translator: crate::input::key_translator::KeyTranslator,
156 pub(super) update_checker: Option<crate::services::release_checker::PeriodicUpdateChecker>,
157
158 pub(super) time_source: SharedTimeSource,
160
161 pub(super) plugin_global_state: HashMap<String, HashMap<String, serde_json::Value>>,
167
168 pub(super) plugin_schemas: HashMap<String, serde_json::Value>,
170
171 pub(super) event_broadcaster: crate::model::control_event::EventBroadcaster,
173}
174
175impl Editor {
176 pub(super) fn from_parts(parts: EditorParts) -> Self {
189 Editor {
190 next_buffer_id: parts.next_buffer_id,
192 buffer_id_alloc: parts.buffer_id_alloc,
193 config: parts.config,
194 config_snapshot_anchor: parts.config_snapshot_anchor,
195 config_cached_json: parts.config_cached_json,
196 user_config_raw: parts.user_config_raw,
197 dir_context: parts.dir_context.clone(),
198 grammar_registry: parts.grammar_registry,
199 pending_grammars: parts.pending_grammars,
200 needs_full_grammar_build: parts.needs_full_grammar_build,
201 theme: parts.theme,
202 theme_registry: parts.theme_registry,
203 theme_cache: parts.theme_cache,
204 keybindings: parts.keybindings,
205 terminal_width: parts.terminal_width,
206 terminal_height: parts.terminal_height,
207 tokio_runtime: parts.tokio_runtime,
208 async_bridge: Some(parts.async_bridge),
209 fs_manager: parts.fs_manager,
210 authority: parts.authority,
211 local_filesystem: parts.local_filesystem,
212 menu_state: crate::view::ui::MenuState::new(parts.dir_context.themes_dir()),
213 working_dir: parts.working_dir,
214 windows: parts.windows,
215 active_window: parts.active_window,
216 next_window_id: parts.next_window_id,
217 command_registry: parts.command_registry,
218 quick_open_registry: parts.quick_open_registry,
219 plugin_manager: parts.plugin_manager,
220 recovery_service: parts.recovery_service,
221 time_source: parts.time_source,
222 color_capability: parts.color_capability,
223 update_checker: parts.update_checker,
224 key_translator: parts.key_translator,
225
226 grammar_reload_pending: false,
228 grammar_build_in_progress: false,
229 pending_grammar_callbacks: Vec::new(),
230 expanded_menus_cache: crate::view::ui::ExpandedMenusCache::default(),
231 ansi_background: None,
232 ansi_background_path: None,
233 background_fade: crate::primitives::ansi_background::DEFAULT_BACKGROUND_FADE,
234 clipboard: crate::services::clipboard::Clipboard::new(),
235 should_quit: false,
236 workspace_trust_prompt_cancellable: false,
237 workspace_trust_markers: Vec::new(),
238 workspace_trust_scroll: 0,
239 should_detach: false,
240 session_mode: false,
241 software_cursor_only: false,
242 session_name: None,
243 pending_escape_sequences: Vec::new(),
244 restart_with_dir: None,
245 last_window_title: None,
246 mode_registry: ModeRegistry::new(),
247 pending_authority: None,
248 remote_indicator_override: None,
249 menus: crate::config::MenuConfig::translated(),
250 background_process_handles: HashMap::new(),
251 host_process_handles: HashMap::new(),
252 status_bar_token_registry: Mutex::new(HashMap::new()),
253 plugin_schemas: std::sync::Arc::new(std::sync::RwLock::new(parts.plugin_schemas)),
254 event_broadcaster: parts.event_broadcaster,
255 #[cfg(feature = "plugins")]
256 pending_plugin_actions: Vec::new(),
257 #[cfg(feature = "plugins")]
258 plugin_render_requested: false,
259 full_redraw_requested: false,
260 suspend_requested: false,
261 plugin_global_state: parts.plugin_global_state,
262 warning_log: None,
263 status_log_path: None,
264 file_watcher_manager: crate::services::file_watcher::FileWatcherManager::new(),
265 last_path_change_for_test: None,
266 last_watch_response_for_test: None,
267 preview_window_id: None,
268 settings_state: None,
269 calibration_wizard: None,
270 keybinding_editor: None,
272 stdin_stream: stdin_stream::StdinStream::default(),
273 global_popups: crate::view::popup::PopupManager::new(),
274 previous_cursor_screen_pos: None,
275 cursor_jump_animation: None,
276 pending_vb_animations: Vec::new(),
277 widget_registry: crate::widgets::WidgetRegistry::new(),
278 floating_widget_panel: None,
279 }
280 }
281
282 pub fn new(
285 config: Config,
286 width: u16,
287 height: u16,
288 dir_context: DirectoryContext,
289 color_capability: crate::view::color_support::ColorCapability,
290 filesystem: Arc<dyn FileSystem + Send + Sync>,
291 ) -> AnyhowResult<Self> {
292 Self::with_working_dir(
293 config,
294 width,
295 height,
296 None,
297 dir_context,
298 true,
299 color_capability,
300 filesystem,
301 )
302 }
303
304 #[allow(clippy::too_many_arguments)]
307 pub fn with_working_dir(
308 config: Config,
309 width: u16,
310 height: u16,
311 working_dir: Option<PathBuf>,
312 dir_context: DirectoryContext,
313 plugins_enabled: bool,
314 color_capability: crate::view::color_support::ColorCapability,
315 filesystem: Arc<dyn FileSystem + Send + Sync>,
316 ) -> AnyhowResult<Self> {
317 Self::with_working_dir_opts(
318 config,
319 width,
320 height,
321 working_dir,
322 dir_context,
323 plugins_enabled,
324 color_capability,
325 filesystem,
326 false,
327 )
328 }
329
330 #[allow(clippy::too_many_arguments)]
338 pub fn with_working_dir_opts(
339 config: Config,
340 width: u16,
341 height: u16,
342 working_dir: Option<PathBuf>,
343 dir_context: DirectoryContext,
344 plugins_enabled: bool,
345 color_capability: crate::view::color_support::ColorCapability,
346 filesystem: Arc<dyn FileSystem + Send + Sync>,
347 defer_plugin_load: bool,
348 ) -> AnyhowResult<Self> {
349 tracing::info!("Building default grammar registry...");
350 let start = std::time::Instant::now();
351 let mut grammar_registry = crate::primitives::grammar::GrammarRegistry::defaults_only();
352 std::sync::Arc::get_mut(&mut grammar_registry)
358 .expect("defaults_only returned a shared Arc")
359 .apply_language_config(&config.languages);
360 tracing::info!("Default grammar registry built in {:?}", start.elapsed());
361 Self::with_options(
365 config,
366 width,
367 height,
368 working_dir,
369 filesystem,
370 plugins_enabled,
371 true, dir_context,
373 None,
374 color_capability,
375 grammar_registry,
376 defer_plugin_load,
377 )
378 }
379
380 #[allow(clippy::too_many_arguments)]
391 pub fn for_test(
392 config: Config,
393 width: u16,
394 height: u16,
395 working_dir: Option<PathBuf>,
396 dir_context: DirectoryContext,
397 color_capability: crate::view::color_support::ColorCapability,
398 filesystem: Arc<dyn FileSystem + Send + Sync>,
399 time_source: Option<SharedTimeSource>,
400 grammar_registry: Option<Arc<crate::primitives::grammar::GrammarRegistry>>,
401 enable_plugins: bool,
402 enable_embedded_plugins: bool,
403 ) -> AnyhowResult<Self> {
404 let mut grammar_registry =
405 grammar_registry.unwrap_or_else(crate::primitives::grammar::GrammarRegistry::empty);
406 std::sync::Arc::get_mut(&mut grammar_registry)
413 .expect("grammar registry Arc must be uniquely owned at for_test entry")
414 .apply_language_config(&config.languages);
415 let mut editor = Self::with_options(
416 config,
417 width,
418 height,
419 working_dir,
420 filesystem,
421 enable_plugins,
422 enable_embedded_plugins,
423 dir_context,
424 time_source,
425 color_capability,
426 grammar_registry,
427 false,
428 )?;
429 editor.needs_full_grammar_build = false;
432 Ok(editor)
433 }
434
435 #[allow(clippy::too_many_arguments)]
439 fn with_options(
440 mut config: Config,
441 width: u16,
442 height: u16,
443 working_dir: Option<PathBuf>,
444 filesystem: Arc<dyn FileSystem + Send + Sync>,
445 enable_plugins: bool,
446 #[cfg_attr(not(feature = "embed-plugins"), allow(unused_variables))]
447 enable_embedded_plugins: bool,
448 dir_context: DirectoryContext,
449 time_source: Option<SharedTimeSource>,
450 color_capability: crate::view::color_support::ColorCapability,
451 grammar_registry: Arc<crate::primitives::grammar::GrammarRegistry>,
452 defer_plugin_load: bool,
453 ) -> AnyhowResult<Self> {
454 let mut t = InitTimer::start("Editor::with_options");
455 let time_source = time_source.unwrap_or_else(RealTimeSource::shared);
457 tracing::info!("Editor::new called with width={}, height={}", width, height);
458
459 let working_dir = working_dir
461 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
462
463 let working_dir = working_dir.canonicalize().unwrap_or(working_dir);
466
467 t.phase("preamble");
468 tracing::info!("Loading themes...");
470 let theme_loader = crate::view::theme::ThemeLoader::new(dir_context.themes_dir());
471 t.phase("ThemeLoader::new");
472 let scan_result =
476 crate::services::packages::scan_installed_packages(&dir_context.config_dir);
477 t.phase("scan_installed_packages");
478
479 for (lang_id, lang_config) in &scan_result.language_configs {
481 config
482 .languages
483 .entry(lang_id.clone())
484 .or_insert_with(|| lang_config.clone());
485 }
486
487 for (lang_id, lsp_config) in &scan_result.lsp_configs {
489 config
490 .lsp
491 .entry(lang_id.clone())
492 .or_insert_with(|| LspLanguageConfig::Multi(vec![lsp_config.clone()]));
493 }
494
495 let theme_registry = Arc::new(theme_loader.load_all(&scan_result.bundle_theme_dirs));
496 t.phase("theme_loader.load_all");
497 tracing::info!("Themes loaded");
498
499 let theme_inner = theme_registry.get_cloned(&config.theme).unwrap_or_else(|| {
501 tracing::warn!(
502 "Theme '{}' not found, falling back to default theme",
503 config.theme.0
504 );
505 theme_registry
506 .get_cloned(&crate::config::ThemeName(
507 crate::view::theme::THEME_HIGH_CONTRAST.to_string(),
508 ))
509 .expect("Default theme must exist")
510 });
511
512 theme_inner.set_terminal_cursor_color();
514 let theme = Arc::new(RwLock::new(theme_inner));
515
516 t.phase("theme_setup");
517 let keybindings = Arc::new(RwLock::new(KeybindingResolver::new(&config)));
518 t.phase("keybindings");
519
520 let mut buffers = crate::app::window::WindowBuffers::new();
522 let mut event_logs = HashMap::new();
523
524 let buffer_id = BufferId(1);
529 let mut state = EditorState::new(
530 width,
531 height,
532 config.editor.large_file_threshold_bytes as usize,
533 Arc::clone(&filesystem),
534 );
535 state
537 .margins
538 .configure_for_line_numbers(config.editor.line_numbers);
539 state.buffer_settings.tab_size = config.editor.tab_size;
540 state.buffer_settings.auto_close = config.editor.auto_close;
541 tracing::info!("EditorState created for buffer {:?}", buffer_id);
543 buffers.insert(buffer_id, state);
544 event_logs.insert(buffer_id, EventLog::new());
545
546 let mut buffer_metadata: HashMap<BufferId, BufferMetadata> = HashMap::new();
550 buffer_metadata.insert(buffer_id, BufferMetadata::new());
551
552 let persisted_env = crate::app::orchestrator_persistence::read_persisted_windows_env(
565 filesystem.as_ref(),
566 &dir_context.data_dir,
567 &working_dir,
568 );
569 let plugin_global_state = crate::app::orchestrator_persistence::read_persisted_plugin_state(
570 filesystem.as_ref(),
571 &dir_context.data_dir,
572 &working_dir,
573 );
574
575 let picked_active = crate::app::orchestrator_persistence::pick_active_window_for_cwd(
586 persisted_env.as_ref(),
587 &working_dir,
588 );
589 let (active_window_id, active_window_root) = picked_active
590 .map(|w| (fresh_core::WindowId(w.id), w.root.clone()))
591 .unwrap_or((fresh_core::WindowId(1), working_dir.clone()));
592
593 let root_uri = types::file_path_to_lsp_uri(&active_window_root);
595
596 t.phase("buffer_state");
597 let tokio_runtime = tokio::runtime::Builder::new_multi_thread()
599 .worker_threads(2) .thread_name("editor-async")
601 .enable_all()
602 .build()
603 .ok()
604 .map(Arc::new);
605 t.phase("tokio_runtime");
606
607 let async_bridge = AsyncBridge::new();
613 let event_broadcaster = crate::model::control_event::EventBroadcaster::default();
614
615 let base_window_bridge = AsyncBridge::new();
621
622 if tokio_runtime.is_none() {
623 tracing::warn!("Failed to create Tokio runtime - async features disabled");
624 }
625
626 let mut lsp = LspManager::new(active_window_id, root_uri);
631
632 if let Some(ref runtime) = tokio_runtime {
636 lsp.set_runtime(runtime.handle().clone(), base_window_bridge.clone());
637 }
638
639 for (language, lsp_configs) in &config.lsp {
641 lsp.set_language_configs(language.clone(), lsp_configs.as_slice().to_vec());
642 }
643
644 let universal_servers: Vec<LspServerConfig> = config
646 .universal_lsp
647 .values()
648 .flat_map(|lc| lc.as_slice().to_vec())
649 .filter(|c| c.enabled)
650 .collect();
651 lsp.set_universal_configs(universal_servers);
652
653 if active_window_root.join("deno.json").exists()
659 || active_window_root.join("deno.jsonc").exists()
660 {
661 tracing::info!("Detected Deno project (deno.json found), using deno lsp for JS/TS");
662 let deno_config = LspServerConfig {
663 command: "deno".to_string(),
664 args: vec!["lsp".to_string()],
665 enabled: true,
666 auto_start: false,
667 process_limits: ProcessLimits::default(),
668 initialization_options: Some(serde_json::json!({"enable": true})),
669 ..Default::default()
670 };
671 lsp.set_language_config("javascript".to_string(), deno_config.clone());
672 lsp.set_language_config("typescript".to_string(), deno_config);
673 }
674
675 t.phase("lsp_setup");
676 let split_manager = SplitManager::new(buffer_id);
678
679 let mut split_view_states = HashMap::new();
681 let initial_split_id = split_manager.active_split();
682 let mut initial_view_state = SplitViewState::with_buffer(width, height, buffer_id);
683 initial_view_state.apply_config_defaults(
684 config.editor.line_numbers,
685 config.editor.highlight_current_line,
686 config.editor.line_wrap,
687 config.editor.wrap_indent,
688 config.editor.wrap_column,
689 config.editor.rulers.clone(),
690 );
691 split_view_states.insert(initial_split_id, initial_view_state);
692
693 let fs_manager = Arc::new(FsManager::new(Arc::clone(&filesystem)));
695
696 let command_registry = Arc::new(RwLock::new(CommandRegistry::new()));
698
699 let authority = crate::services::authority::Authority {
709 filesystem: Arc::clone(&filesystem),
710 ..crate::services::authority::Authority::local(
711 Arc::new(crate::services::workspace_trust::WorkspaceTrust::permissive()),
712 Arc::new(crate::services::env_provider::EnvProvider::inactive()),
713 )
714 };
715 let process_spawner = Arc::clone(&authority.process_spawner);
716
717 let mut quick_open_registry = QuickOpenRegistry::new();
719 quick_open_registry.register(Box::new(FileProvider::new(
720 Arc::clone(&filesystem),
721 Arc::clone(&process_spawner),
722 tokio_runtime.as_ref().map(|rt| rt.handle().clone()),
723 Some(async_bridge.sender()),
724 )));
725 quick_open_registry.register(Box::new(CommandProvider::new(
726 Arc::clone(&command_registry),
727 Arc::clone(&keybindings),
728 )));
729 quick_open_registry.register(Box::new(BufferProvider::new()));
730 quick_open_registry.register(Box::new(GotoLineProvider::new()));
731
732 let theme_cache = Arc::new(RwLock::new(theme_registry.to_json_map()));
734
735 t.phase("split_quickopen_authority");
736 let plugin_manager = Arc::new(RwLock::new(PluginManager::new(
738 enable_plugins,
739 Arc::clone(&command_registry),
740 dir_context.clone(),
741 Arc::clone(&theme_cache),
742 )));
743 t.phase("PluginManager::new");
744
745 #[cfg(feature = "plugins")]
748 if let Some(snapshot_handle) = plugin_manager.read().unwrap().state_snapshot_handle() {
749 let mut snapshot = snapshot_handle.write().unwrap();
750 snapshot.working_dir = working_dir.clone();
751 populate_builtin_keybinding_labels(&mut snapshot, &keybindings);
759 if let Ok(json) = serde_json::to_value(&config) {
771 snapshot.config = std::sync::Arc::new(json);
772 }
773 }
774
775 let plugin_schemas: HashMap<String, serde_json::Value> = HashMap::new();
785 if plugin_manager.read().unwrap().is_active() {
786 let mut plugin_dirs: Vec<std::path::PathBuf> = vec![];
787
788 if let Ok(exe_path) = std::env::current_exe() {
790 if let Some(exe_dir) = exe_path.parent() {
791 let exe_plugin_dir = exe_dir.join("plugins");
792 if exe_plugin_dir.exists() {
793 plugin_dirs.push(exe_plugin_dir);
794 }
795 }
796 }
797
798 #[cfg(feature = "embed-plugins")]
809 if enable_embedded_plugins && plugin_dirs.is_empty() {
810 if let Some(embedded_dir) =
811 crate::services::plugins::embedded::get_embedded_plugins_dir()
812 {
813 tracing::info!("Using embedded plugins from: {:?}", embedded_dir);
814 plugin_dirs.push(embedded_dir.clone());
815 }
816 }
817
818 let user_plugins_dir = dir_context.config_dir.join("plugins");
820 if user_plugins_dir.exists() && !plugin_dirs.contains(&user_plugins_dir) {
821 tracing::info!("Found user plugins directory: {:?}", user_plugins_dir);
822 plugin_dirs.push(user_plugins_dir.clone());
823 }
824
825 let packages_dir = dir_context.config_dir.join("plugins").join("packages");
827 if packages_dir.exists() {
828 if let Ok(entries) = std::fs::read_dir(&packages_dir) {
829 for entry in entries.flatten() {
830 let path = entry.path();
831 if path.is_dir() {
833 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
834 if !name.starts_with('.') {
835 tracing::info!("Found package manager plugin: {:?}", path);
836 plugin_dirs.push(path);
837 }
838 }
839 }
840 }
841 }
842 }
843
844 for dir in &scan_result.bundle_plugin_dirs {
846 tracing::info!("Found bundle plugin directory: {:?}", dir);
847 plugin_dirs.push(dir.clone());
848 }
849
850 if plugin_dirs.is_empty() {
851 tracing::debug!(
852 "No plugins directory found next to executable or in working dir: {:?}",
853 working_dir
854 );
855 }
856
857 if defer_plugin_load {
858 #[cfg(feature = "plugins")]
869 {
870 let bridge = &async_bridge;
871 let mut dir_receivers: Vec<(
872 std::path::PathBuf,
873 fresh_plugin_runtime::thread::oneshot::Receiver<
874 fresh_plugin_runtime::thread::PluginsDirLoadResult,
875 >,
876 )> = Vec::with_capacity(plugin_dirs.len());
877 for plugin_dir in &plugin_dirs {
878 tracing::info!(
879 "Submitting async TypeScript plugin load for: {:?}",
880 plugin_dir
881 );
882 if let Some(rx) = plugin_manager
883 .read()
884 .unwrap()
885 .load_plugins_from_dir_with_config_request(plugin_dir, &config.plugins)
886 {
887 dir_receivers.push((plugin_dir.clone(), rx));
888 }
889 }
890 let declarations_rx = if !dir_receivers.is_empty() {
891 plugin_manager.read().unwrap().list_plugins_request()
892 } else {
893 None
894 };
895 if !dir_receivers.is_empty() {
896 let sender = bridge.sender();
897 std::thread::Builder::new()
898 .name("plugin-load-forwarder".to_string())
899 .spawn(move || {
900 for (dir, rx) in dir_receivers {
901 let load_start = std::time::Instant::now();
902 match rx.recv() {
903 Ok((errors, discovered_plugins)) => {
904 tracing::info!(
905 "Loaded TypeScript plugins from {:?} in {:?}",
906 dir,
907 load_start.elapsed()
908 );
909 drop(sender.send(
910 crate::services::async_bridge::AsyncMessage::PluginsDirLoaded {
911 dir,
912 errors,
913 discovered_plugins,
914 },
915 ));
916 }
917 Err(e) => {
918 tracing::warn!(
919 "plugin-load-forwarder: dir {:?} recv failed: {}",
920 dir,
921 e
922 );
923 }
924 }
925 }
926 if let Some(rx) = declarations_rx {
927 match rx.recv() {
928 Ok(plugin_infos) => {
929 let declarations: Vec<(String, String)> = plugin_infos
930 .into_iter()
931 .filter_map(|info| {
932 info.declarations.map(|d| (info.name, d))
933 })
934 .collect();
935 drop(sender.send(
936 crate::services::async_bridge::AsyncMessage::PluginDeclarationsReady {
937 declarations,
938 },
939 ));
940 }
941 Err(e) => {
942 tracing::warn!(
943 "plugin-load-forwarder: list_plugins recv failed: {}",
944 e
945 );
946 }
947 }
948 }
949 })
950 .ok();
951 }
952 }
953 } else {
954 for plugin_dir in plugin_dirs {
959 tracing::info!("Loading TypeScript plugins from: {:?}", plugin_dir);
960 let load_start = std::time::Instant::now();
961 let (errors, discovered_plugins) = plugin_manager
962 .read()
963 .unwrap()
964 .load_plugins_from_dir_with_config(&plugin_dir, &config.plugins);
965 tracing::info!(
966 "Loaded TypeScript plugins from {:?} in {:?}",
967 plugin_dir,
968 load_start.elapsed()
969 );
970
971 for (name, plugin_config) in discovered_plugins {
974 config.plugins.insert(name, plugin_config);
975 }
976
977 if !errors.is_empty() {
978 for err in &errors {
979 tracing::error!("TypeScript plugin load error: {}", err);
980 }
981 #[cfg(debug_assertions)]
983 panic!(
984 "TypeScript plugin loading failed with {} error(s): {}",
985 errors.len(),
986 errors.join("; ")
987 );
988 }
989 }
990
991 let declarations = plugin_manager.read().unwrap().plugin_declarations();
999 crate::init_script::write_plugin_declarations(
1000 &dir_context.config_dir,
1001 &declarations,
1002 );
1003 }
1004 }
1005
1006 t.phase("plugin_loading");
1007 let recovery_enabled = config.editor.recovery_enabled;
1009 let check_for_updates = config.check_for_updates;
1010
1011 let update_checker = if check_for_updates {
1013 tracing::debug!("Update checking enabled, starting periodic checker");
1014 Some(
1015 crate::services::release_checker::start_periodic_update_check(
1016 crate::services::release_checker::DEFAULT_RELEASES_URL,
1017 time_source.clone(),
1018 dir_context.data_dir.clone(),
1019 ),
1020 )
1021 } else {
1022 tracing::debug!("Update checking disabled by config");
1023 None
1024 };
1025
1026 let user_config_raw = Config::read_user_config_raw(&working_dir);
1028
1029 let config_arc = Arc::new(config);
1036 let config_cached_json =
1037 Arc::new(serde_json::to_value(&*config_arc).unwrap_or(serde_json::Value::Null));
1038 let config_snapshot_anchor = Arc::clone(&config_arc);
1039
1040 let buffer_id_alloc = crate::app::window_resources::BufferIdAllocator::new(2);
1046
1047 let local_filesystem: Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> =
1052 Arc::new(crate::model::filesystem::StdFileSystem);
1053
1054 let base_resources = crate::app::window_resources::WindowResources {
1060 config: Arc::clone(&config_arc),
1061 grammar_registry: Arc::clone(&grammar_registry),
1062 theme_registry: Arc::clone(&theme_registry),
1063 theme_cache: Arc::clone(&theme_cache),
1064 keybindings: Arc::clone(&keybindings),
1065 command_registry: Arc::clone(&command_registry),
1066 fs_manager: Arc::clone(&fs_manager),
1067 local_filesystem: Arc::clone(&local_filesystem),
1068 buffer_id_alloc: buffer_id_alloc.clone(),
1069 authority: authority.clone(),
1070 time_source: Arc::clone(&time_source),
1071 dir_context: dir_context.clone(),
1072 tokio_runtime: tokio_runtime.clone(),
1073 async_bridge: Some(async_bridge.clone()),
1074 plugin_manager: Arc::clone(&plugin_manager),
1075 theme: Arc::clone(&theme),
1076 event_broadcaster: event_broadcaster.clone(),
1077 };
1078
1079 let (active_label, active_root, active_plugin_state) = picked_active
1090 .map(|w| (w.label.clone(), w.root.clone(), w.plugin_state.clone()))
1091 .unwrap_or_else(|| (String::new(), working_dir.clone(), HashMap::new()));
1092
1093 let mut active_win = crate::app::window::Window::new(
1094 active_window_id,
1095 active_label,
1096 active_root,
1097 base_resources,
1098 );
1099 active_win.terminal_width = width;
1105 active_win.terminal_height = height;
1106 active_win.lsp = Some(lsp);
1110 active_win.buffers = buffers;
1111 active_win
1112 .buffers
1113 .set_splits((split_manager, split_view_states));
1114 active_win.buffer_metadata = buffer_metadata;
1115 active_win.event_logs = event_logs;
1116 active_win.plugin_state = active_plugin_state;
1117 active_win.bridge = base_window_bridge;
1123 for history_name in ["search", "replace", "goto_line"] {
1126 let path = dir_context.prompt_history_path(history_name);
1127 let history = crate::input::input_history::InputHistory::load_from_file(&path)
1128 .unwrap_or_else(|e| {
1129 tracing::warn!("Failed to load {} history: {}", history_name, e);
1130 crate::input::input_history::InputHistory::new()
1131 });
1132 active_win
1133 .prompt_histories
1134 .insert(history_name.to_string(), history);
1135 }
1136
1137 let mut windows = HashMap::new();
1141 if let Some(ref env) = persisted_env {
1142 for ps in &env.windows {
1143 let id = fresh_core::WindowId(ps.id);
1144 if id == active_window_id {
1145 continue;
1146 }
1147 let resources = crate::app::window_resources::WindowResources {
1148 config: Arc::clone(&config_arc),
1149 grammar_registry: Arc::clone(&grammar_registry),
1150 theme_registry: Arc::clone(&theme_registry),
1151 theme_cache: Arc::clone(&theme_cache),
1152 keybindings: Arc::clone(&keybindings),
1153 command_registry: Arc::clone(&command_registry),
1154 fs_manager: Arc::clone(&fs_manager),
1155 local_filesystem: Arc::clone(&local_filesystem),
1156 buffer_id_alloc: buffer_id_alloc.clone(),
1157 authority: authority.clone(),
1158 time_source: Arc::clone(&time_source),
1159 dir_context: dir_context.clone(),
1160 tokio_runtime: tokio_runtime.clone(),
1161 async_bridge: Some(async_bridge.clone()),
1162 plugin_manager: Arc::clone(&plugin_manager),
1163 theme: Arc::clone(&theme),
1164 event_broadcaster: event_broadcaster.clone(),
1165 };
1166 let mut shell = crate::app::window::Window::new(
1167 id,
1168 ps.label.clone(),
1169 ps.root.clone(),
1170 resources,
1171 );
1172 shell.terminal_width = width;
1173 shell.terminal_height = height;
1174 shell.plugin_state = ps.plugin_state.clone();
1175 windows.insert(id, shell);
1176 }
1177 }
1178 windows.insert(active_window_id, active_win);
1179
1180 let max_existing = windows.keys().map(|k| k.0).max().unwrap_or(0);
1186 let next_window_id = persisted_env
1187 .as_ref()
1188 .map(|env| env.next_id.max(max_existing + 1))
1189 .unwrap_or(2);
1190
1191 let recovery_service = {
1192 let recovery_config = RecoveryConfig {
1193 enabled: recovery_enabled,
1194 ..RecoveryConfig::default()
1195 };
1196 let scope = crate::services::recovery::RecoveryScope::Standalone {
1204 working_dir: working_dir.clone(),
1205 };
1206 RecoveryService::with_scope(recovery_config, &dir_context.recovery_dir(), &scope)
1207 };
1208
1209 let key_translator = crate::input::key_translator::KeyTranslator::load_from_config_dir(
1210 &dir_context.config_dir,
1211 )
1212 .unwrap_or_default();
1213
1214 let pending_grammars = scan_result
1215 .additional_grammars
1216 .iter()
1217 .map(|g| PendingGrammar {
1218 language: g.language.clone(),
1219 grammar_path: g.path.to_string_lossy().to_string(),
1220 extensions: g.extensions.clone(),
1221 })
1222 .collect();
1223
1224 let parts = EditorParts {
1225 config: config_arc,
1226 config_snapshot_anchor,
1227 config_cached_json,
1228 user_config_raw: Arc::new(user_config_raw),
1229 dir_context: dir_context.clone(),
1230 working_dir: working_dir.clone(),
1231 theme,
1232 theme_registry,
1233 theme_cache,
1234 grammar_registry,
1235 pending_grammars,
1236 needs_full_grammar_build: true,
1237 keybindings,
1238 buffer_id_alloc: buffer_id_alloc.clone(),
1239 next_buffer_id: 2,
1240 terminal_width: width,
1241 terminal_height: height,
1242 color_capability,
1243 tokio_runtime,
1244 async_bridge,
1245 fs_manager,
1246 authority,
1247 local_filesystem: Arc::clone(&local_filesystem),
1248 windows,
1249 active_window: active_window_id,
1250 next_window_id,
1251 command_registry,
1252 quick_open_registry,
1253 plugin_manager,
1254 recovery_service,
1255 key_translator,
1256 update_checker,
1257 time_source: time_source.clone(),
1258 plugin_global_state,
1259 plugin_schemas,
1260 event_broadcaster: event_broadcaster.clone(),
1261 };
1262
1263 let mut editor = Editor::from_parts(parts);
1264
1265 t.phase("editor_struct_assembly");
1266 editor.clipboard.apply_config(&editor.config.clipboard);
1268
1269 let needs_seed: Vec<fresh_core::WindowId> = editor
1277 .windows
1278 .iter()
1279 .filter(|(_, s)| s.buffers.splits().is_none() || s.buffers.len() == 0)
1280 .map(|(id, _)| *id)
1281 .collect();
1282 for id in needs_seed {
1283 if let Some((buf, state, metadata, event_log, mgr, vs)) =
1284 editor.build_fresh_layout_if_needed(id)
1285 {
1286 if let Some(s) = editor.windows.get_mut(&id) {
1287 s.buffers.set_splits((mgr, vs));
1288 s.buffers.insert(buf, state);
1289 s.buffer_metadata.insert(buf, metadata);
1290 s.event_logs.insert(buf, event_log);
1291 }
1292 }
1293 }
1294
1295 #[cfg(feature = "plugins")]
1296 {
1297 editor.update_plugin_state_snapshot();
1298 if editor.plugin_manager.read().unwrap().is_active() {
1299 editor.plugin_manager.read().unwrap().run_hook(
1300 "editor_initialized",
1301 crate::services::plugins::hooks::HookArgs::EditorInitialized {},
1302 );
1303 }
1304 }
1305 t.phase("post_struct_hooks");
1306 t.finish();
1307 Ok(editor)
1308 }
1309
1310 pub fn event_broadcaster(&self) -> &crate::model::control_event::EventBroadcaster {
1312 &self.event_broadcaster
1313 }
1314
1315 pub(super) fn start_background_grammar_build(
1320 &mut self,
1321 additional: Vec<crate::primitives::grammar::GrammarSpec>,
1322 callback_ids: Vec<fresh_core::api::JsCallbackId>,
1323 ) {
1324 let Some(bridge) = &self.async_bridge else {
1325 return;
1326 };
1327 self.grammar_build_in_progress = true;
1328 let sender = bridge.sender();
1329 let config_dir = self.dir_context.config_dir.clone();
1330 tracing::info!(
1331 "Spawning background grammar build thread ({} plugin grammars)...",
1332 additional.len()
1333 );
1334 std::thread::Builder::new()
1335 .name("grammar-build".to_string())
1336 .spawn(move || {
1337 tracing::info!("[grammar-build] Thread started");
1338 let start = std::time::Instant::now();
1339 let registry = if additional.is_empty() {
1340 crate::primitives::grammar::GrammarRegistry::for_editor(config_dir)
1341 } else {
1342 crate::primitives::grammar::GrammarRegistry::for_editor_with_additional(
1343 config_dir,
1344 &additional,
1345 )
1346 };
1347 tracing::info!("[grammar-build] Complete in {:?}", start.elapsed());
1348 drop(sender.send(
1349 crate::services::async_bridge::AsyncMessage::GrammarRegistryBuilt {
1350 registry,
1351 callback_ids,
1352 },
1353 ));
1354 })
1355 .ok();
1356 }
1357
1358 pub fn load_init_script(&mut self, enabled: bool) {
1365 use crate::init_script::{
1366 check, decide_load, describe, record_success, refresh_types_scaffolding, CheckSeverity,
1367 InitOutcome, LoadDecision,
1368 };
1369
1370 let config_dir = self.dir_context.config_dir.clone();
1371
1372 if enabled {
1373 refresh_types_scaffolding(&config_dir);
1377
1378 let report = check(&config_dir);
1383 if !report.ok {
1384 for d in &report.diagnostics {
1385 let level = match d.severity {
1386 CheckSeverity::Error => "error",
1387 CheckSeverity::Warning => "warning",
1388 };
1389 tracing::warn!(
1390 "init.ts pre-load {level} at {}:{}: {}",
1391 d.line,
1392 d.column,
1393 d.message
1394 );
1395 }
1396 }
1397 }
1398
1399 let outcome = match decide_load(&config_dir, enabled) {
1400 LoadDecision::Skip(outcome) => outcome,
1401 LoadDecision::Load { source } => {
1402 if !self.plugin_manager.read().unwrap().is_active() {
1403 InitOutcome::Failed {
1404 message: "plugin runtime inactive (--no-plugins); init.ts cannot run"
1405 .into(),
1406 }
1407 } else {
1408 match self.plugin_manager.read().unwrap().load_plugin_from_source(
1409 &source,
1410 crate::init_script::INIT_PLUGIN_NAME,
1411 true,
1412 ) {
1413 Ok(()) => {
1414 record_success(&config_dir);
1415 InitOutcome::Loaded
1416 }
1417 Err(e) => InitOutcome::Failed {
1418 message: format!("{e}"),
1419 },
1420 }
1421 }
1422 }
1423 };
1424
1425 let summary = describe(&outcome);
1426 match outcome {
1427 InitOutcome::NotFound | InitOutcome::Disabled => tracing::debug!("{}", summary),
1428 InitOutcome::Loaded => tracing::info!("{}", summary),
1429 InitOutcome::CrashFused { .. } | InitOutcome::Failed { .. } => {
1430 tracing::warn!("{}", summary);
1431 self.set_status_message(summary);
1432 }
1433 }
1434 }
1435
1436 pub fn load_init_script_async(&mut self, enabled: bool) {
1448 use crate::init_script::{
1449 check, decide_load, refresh_types_scaffolding, CheckSeverity, InitOutcome, LoadDecision,
1450 };
1451 use crate::services::async_bridge::PluginInitScriptOutcome;
1452
1453 let config_dir = self.dir_context.config_dir.clone();
1454
1455 if enabled {
1456 refresh_types_scaffolding(&config_dir);
1457 let report = check(&config_dir);
1458 if !report.ok {
1459 for d in &report.diagnostics {
1460 let level = match d.severity {
1461 CheckSeverity::Error => "error",
1462 CheckSeverity::Warning => "warning",
1463 };
1464 tracing::warn!(
1465 "init.ts pre-load {level} at {}:{}: {}",
1466 d.line,
1467 d.column,
1468 d.message
1469 );
1470 }
1471 }
1472 }
1473
1474 let outcome_now: Option<PluginInitScriptOutcome> = match decide_load(&config_dir, enabled) {
1475 LoadDecision::Skip(outcome) => Some(match outcome {
1476 InitOutcome::NotFound => PluginInitScriptOutcome::NotFound,
1477 InitOutcome::Disabled => PluginInitScriptOutcome::Disabled,
1478 InitOutcome::CrashFused { failures } => {
1479 PluginInitScriptOutcome::CrashFused { failures }
1480 }
1481 InitOutcome::Loaded => PluginInitScriptOutcome::Loaded,
1484 InitOutcome::Failed { message } => PluginInitScriptOutcome::Failed { message },
1485 }),
1486 LoadDecision::Load { source } => {
1487 if !self.plugin_manager.read().unwrap().is_active() {
1488 Some(PluginInitScriptOutcome::Failed {
1489 message: "plugin runtime inactive (--no-plugins); init.ts cannot run"
1490 .into(),
1491 })
1492 } else {
1493 self.spawn_init_script_forwarder(source);
1494 None
1495 }
1496 }
1497 };
1498
1499 if let Some(outcome) = outcome_now {
1500 if let Some(bridge) = &self.async_bridge {
1504 drop(bridge.sender().send(
1505 crate::services::async_bridge::AsyncMessage::PluginInitScriptLoaded(outcome),
1506 ));
1507 } else {
1508 self.handle_plugin_init_script_loaded(outcome);
1509 }
1510 }
1511 }
1512
1513 #[cfg(feature = "plugins")]
1514 fn spawn_init_script_forwarder(&self, source: String) {
1515 let Some(bridge) = &self.async_bridge else {
1516 return;
1517 };
1518 let Some(rx) = self
1519 .plugin_manager
1520 .read()
1521 .unwrap()
1522 .load_plugin_from_source_request(&source, crate::init_script::INIT_PLUGIN_NAME, true)
1523 else {
1524 return;
1525 };
1526 let sender = bridge.sender();
1527 std::thread::Builder::new()
1528 .name("plugin-init-forwarder".to_string())
1529 .spawn(move || {
1530 let outcome = match rx.recv() {
1531 Ok(Ok(())) => crate::services::async_bridge::PluginInitScriptOutcome::Loaded,
1532 Ok(Err(e)) => crate::services::async_bridge::PluginInitScriptOutcome::Failed {
1533 message: format!("{e}"),
1534 },
1535 Err(e) => crate::services::async_bridge::PluginInitScriptOutcome::Failed {
1536 message: format!("plugin thread closed: {e}"),
1537 },
1538 };
1539 drop(sender.send(
1540 crate::services::async_bridge::AsyncMessage::PluginInitScriptLoaded(outcome),
1541 ));
1542 })
1543 .ok();
1544 }
1545
1546 #[cfg(not(feature = "plugins"))]
1547 fn spawn_init_script_forwarder(&self, _source: String) {}
1548
1549 pub fn handle_set_setting(&mut self, path: String, value: serde_json::Value) {
1553 let mut json = serde_json::to_value(&*self.config).unwrap_or_default();
1554 set_dot_path(&mut json, &path, value);
1555 match serde_json::from_value::<crate::config::Config>(json) {
1556 Ok(new_config) => {
1557 let old_theme = self.config.theme.clone();
1558 self.config = Arc::new(new_config);
1559 if old_theme != self.config.theme {
1560 if let Some(theme) = self.theme_registry.get_cloned(&self.config.theme) {
1561 *self.theme.write().unwrap() = theme;
1562 }
1563 }
1564 *self.keybindings.write().unwrap() =
1565 crate::input::keybindings::KeybindingResolver::new(&self.config);
1566 self.clipboard.apply_config(&self.config.clipboard);
1567 {
1568 let cfg = self.config.editor.clone();
1569 let win = self.active_window_mut();
1570 win.menu_bar_visible = cfg.show_menu_bar;
1571 win.tab_bar_visible = cfg.show_tab_bar;
1572 win.status_bar_visible = cfg.show_status_bar;
1573 win.prompt_line_visible = cfg.show_prompt_line;
1574 }
1575 #[cfg(feature = "plugins")]
1576 self.update_plugin_state_snapshot();
1577 }
1578 Err(e) => {
1579 self.set_status_message(format!("setSetting({path}): {e}"));
1580 }
1581 }
1582 }
1583
1584 pub fn handle_add_plugin_config_field(
1594 &mut self,
1595 plugin_name: String,
1596 field_name: String,
1597 field_schema: serde_json::Value,
1598 ) {
1599 tracing::trace!(
1600 "Registering plugin config field: {}.{}",
1601 plugin_name,
1602 field_name
1603 );
1604 let updated_schema = {
1607 let schemas = self.plugin_schemas.read().ok();
1608 let existing = schemas.as_ref().and_then(|m| m.get(&plugin_name)).cloned();
1609 let mut schema = existing.unwrap_or_else(|| {
1610 serde_json::json!({
1611 "type": "object",
1612 "properties": {},
1613 })
1614 });
1615 if let Some(props) = schema
1616 .as_object_mut()
1617 .and_then(|o| o.get_mut("properties"))
1618 .and_then(|p| p.as_object_mut())
1619 {
1620 props.insert(field_name.clone(), field_schema.clone());
1621 }
1622 schema
1623 };
1624
1625 if let Err(msg) = crate::plugin_schemas::validate_plugin_schema(&updated_schema) {
1626 self.set_status_message(format!(
1629 "defineConfig({}.{}): {}",
1630 plugin_name, field_name, msg
1631 ));
1632 return;
1633 }
1634
1635 if let Some(default) = field_schema.get("default").cloned() {
1637 let cfg = std::sync::Arc::make_mut(&mut self.config);
1638 let entry = cfg.plugins.entry(plugin_name.clone()).or_default();
1639 let settings_obj = match &mut entry.settings {
1640 serde_json::Value::Object(_) => &mut entry.settings,
1641 slot => {
1642 *slot = serde_json::Value::Object(Default::default());
1643 slot
1644 }
1645 };
1646 if let serde_json::Value::Object(map) = settings_obj {
1647 map.entry(field_name.clone()).or_insert(default);
1648 }
1649 }
1650
1651 if let Ok(mut schemas) = self.plugin_schemas.write() {
1652 schemas.insert(plugin_name, updated_schema);
1653 }
1654
1655 #[cfg(feature = "plugins")]
1656 self.update_plugin_state_snapshot();
1657 }
1658
1659 pub(crate) fn handle_plugins_dir_loaded(
1664 &mut self,
1665 dir: std::path::PathBuf,
1666 errors: Vec<String>,
1667 discovered_plugins: std::collections::HashMap<String, fresh_core::config::PluginConfig>,
1668 ) {
1669 if !discovered_plugins.is_empty() {
1670 let cfg = std::sync::Arc::make_mut(&mut self.config);
1671 for (name, plugin_config) in discovered_plugins {
1672 cfg.plugins.insert(name, plugin_config);
1673 }
1674 }
1675 if !errors.is_empty() {
1676 for err in &errors {
1677 tracing::error!("TypeScript plugin load error: {}", err);
1678 }
1679 #[cfg(debug_assertions)]
1680 panic!(
1681 "TypeScript plugin loading failed for {:?} with {} error(s): {}",
1682 dir,
1683 errors.len(),
1684 errors.join("; ")
1685 );
1686 #[cfg(not(debug_assertions))]
1687 {
1688 let _ = dir;
1689 }
1690 }
1691 }
1692
1693 pub(crate) fn handle_plugin_declarations_ready(&self, declarations: Vec<(String, String)>) {
1697 crate::init_script::write_plugin_declarations(&self.dir_context.config_dir, &declarations);
1698 }
1699
1700 pub(crate) fn handle_plugin_init_script_loaded(
1704 &mut self,
1705 outcome: crate::services::async_bridge::PluginInitScriptOutcome,
1706 ) {
1707 use crate::init_script::{describe, record_success, InitOutcome};
1708 use crate::services::async_bridge::PluginInitScriptOutcome as O;
1709 let outcome = match outcome {
1710 O::NotFound => InitOutcome::NotFound,
1711 O::Disabled => InitOutcome::Disabled,
1712 O::CrashFused { failures } => InitOutcome::CrashFused { failures },
1713 O::Loaded => {
1714 record_success(&self.dir_context.config_dir);
1715 InitOutcome::Loaded
1716 }
1717 O::Failed { message } => InitOutcome::Failed { message },
1718 };
1719 let summary = describe(&outcome);
1720 match outcome {
1721 InitOutcome::NotFound | InitOutcome::Disabled => tracing::debug!("{}", summary),
1722 InitOutcome::Loaded => tracing::info!("{}", summary),
1723 InitOutcome::CrashFused { .. } | InitOutcome::Failed { .. } => {
1724 tracing::warn!("{}", summary);
1725 self.set_status_message(summary);
1726 }
1727 }
1728 }
1729
1730 pub fn fire_plugins_loaded_hook(&self) {
1732 #[cfg(feature = "plugins")]
1733 if self.plugin_manager.read().unwrap().is_active() {
1734 self.plugin_manager.read().unwrap().run_hook(
1735 "plugins_loaded",
1736 crate::services::plugins::hooks::HookArgs::PluginsLoaded {},
1737 );
1738 }
1739 }
1740
1741 pub fn fire_ready_hook(&self) {
1743 #[cfg(feature = "plugins")]
1744 if self.plugin_manager.read().unwrap().is_active() {
1745 self.plugin_manager
1746 .read()
1747 .unwrap()
1748 .run_hook("ready", crate::services::plugins::hooks::HookArgs::Ready {});
1749 }
1750 }
1751
1752 #[doc(hidden)]
1754 pub fn config_for_tests(&self) -> &crate::config::Config {
1755 &self.config
1756 }
1757
1758 #[doc(hidden)]
1760 pub fn dispatch_action_for_tests(&mut self, action: crate::input::keybindings::Action) {
1761 if let Err(e) = self.handle_action(action) {
1762 tracing::warn!("dispatch_action_for_tests: {e}");
1763 }
1764 }
1765
1766 #[doc(hidden)]
1768 pub fn live_grep_last_state_for_tests(
1769 &self,
1770 ) -> Option<&crate::services::live_grep_state::LiveGrepLastState> {
1771 self.active_window().live_grep_last_state.as_ref()
1772 }
1773
1774 #[doc(hidden)]
1776 pub fn set_live_grep_last_state_for_tests(
1777 &mut self,
1778 state: Option<crate::services::live_grep_state::LiveGrepLastState>,
1779 ) {
1780 self.active_window_mut().live_grep_last_state = state;
1781 }
1782
1783 #[doc(hidden)]
1786 pub fn split_manager_for_tests(&self) -> &crate::view::split::SplitManager {
1787 self.windows
1788 .get(&self.active_window)
1789 .and_then(|w| w.buffers.splits())
1790 .map(|(mgr, _)| mgr)
1791 .expect("active window must have a populated split layout")
1792 }
1793
1794 #[doc(hidden)]
1799 pub fn split_view_state_for_tests(
1800 &self,
1801 leaf: crate::model::event::LeafId,
1802 ) -> Option<&crate::view::split::SplitViewState> {
1803 self.windows
1804 .get(&self.active_window)
1805 .and_then(|w| w.buffers.splits())
1806 .map(|(_, vs)| vs)
1807 .expect("active window must have a populated split layout")
1808 .get(&leaf)
1809 }
1810
1811 #[cfg(feature = "plugins")]
1820 pub(crate) fn refresh_keybinding_labels_snapshot(&self) {
1821 if let Some(snapshot_handle) = self.plugin_manager.read().unwrap().state_snapshot_handle() {
1822 if let Ok(mut snapshot) = snapshot_handle.write() {
1823 populate_builtin_keybinding_labels(&mut snapshot, &self.keybindings);
1824 }
1825 }
1826 }
1827}
1828
1829#[cfg(feature = "plugins")]
1845fn populate_builtin_keybinding_labels(
1846 snapshot: &mut crate::services::plugins::api::EditorStateSnapshot,
1847 keybindings: &std::sync::Arc<std::sync::RwLock<crate::input::keybindings::KeybindingResolver>>,
1848) {
1849 use crate::input::keybindings::{Action, KeyContext};
1850 let Ok(resolver) = keybindings.read() else {
1851 return;
1852 };
1853 let contexts = [
1854 KeyContext::Normal,
1855 KeyContext::Prompt,
1856 KeyContext::Popup,
1857 KeyContext::Completion,
1858 KeyContext::FileExplorer,
1859 KeyContext::Menu,
1860 KeyContext::Terminal,
1861 KeyContext::Settings,
1862 KeyContext::CompositeBuffer,
1863 ];
1864 let known_suffixes: Vec<String> = contexts
1871 .iter()
1872 .map(|c| format!("\0{}", c.to_when_clause()))
1873 .collect();
1874 snapshot
1875 .keybinding_labels
1876 .retain(|k, _| !known_suffixes.iter().any(|s| k.ends_with(s)));
1877 for action_name in Action::all_action_names() {
1878 for ctx in &contexts {
1879 if let Some(label) = resolver.find_keybinding_for_action(&action_name, ctx.clone()) {
1880 let key = format!("{}\0{}", action_name, ctx.to_when_clause());
1881 snapshot.keybinding_labels.insert(key, label);
1882 }
1883 }
1884 }
1885}