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>>,
166
167 pub(super) event_broadcaster: crate::model::control_event::EventBroadcaster,
169}
170
171impl Editor {
172 pub(super) fn from_parts(parts: EditorParts) -> Self {
185 Editor {
186 next_buffer_id: parts.next_buffer_id,
188 buffer_id_alloc: parts.buffer_id_alloc,
189 config: parts.config,
190 config_snapshot_anchor: parts.config_snapshot_anchor,
191 config_cached_json: parts.config_cached_json,
192 user_config_raw: parts.user_config_raw,
193 dir_context: parts.dir_context.clone(),
194 grammar_registry: parts.grammar_registry,
195 pending_grammars: parts.pending_grammars,
196 needs_full_grammar_build: parts.needs_full_grammar_build,
197 theme: parts.theme,
198 theme_registry: parts.theme_registry,
199 theme_cache: parts.theme_cache,
200 keybindings: parts.keybindings,
201 terminal_width: parts.terminal_width,
202 terminal_height: parts.terminal_height,
203 tokio_runtime: parts.tokio_runtime,
204 async_bridge: Some(parts.async_bridge),
205 fs_manager: parts.fs_manager,
206 authority: parts.authority,
207 local_filesystem: parts.local_filesystem,
208 menu_state: crate::view::ui::MenuState::new(parts.dir_context.themes_dir()),
209 working_dir: parts.working_dir,
210 windows: parts.windows,
211 active_window: parts.active_window,
212 next_window_id: parts.next_window_id,
213 command_registry: parts.command_registry,
214 quick_open_registry: parts.quick_open_registry,
215 plugin_manager: parts.plugin_manager,
216 recovery_service: parts.recovery_service,
217 time_source: parts.time_source,
218 color_capability: parts.color_capability,
219 update_checker: parts.update_checker,
220 key_translator: parts.key_translator,
221
222 grammar_reload_pending: false,
224 grammar_build_in_progress: false,
225 pending_grammar_callbacks: Vec::new(),
226 expanded_menus_cache: crate::view::ui::ExpandedMenusCache::default(),
227 ansi_background: None,
228 ansi_background_path: None,
229 background_fade: crate::primitives::ansi_background::DEFAULT_BACKGROUND_FADE,
230 clipboard: crate::services::clipboard::Clipboard::new(),
231 should_quit: false,
232 should_detach: false,
233 session_mode: false,
234 software_cursor_only: false,
235 session_name: None,
236 pending_escape_sequences: Vec::new(),
237 restart_with_dir: None,
238 last_window_title: None,
239 mode_registry: ModeRegistry::new(),
240 pending_authority: None,
241 remote_indicator_override: None,
242 menus: crate::config::MenuConfig::translated(),
243 background_process_handles: HashMap::new(),
244 host_process_handles: HashMap::new(),
245 event_broadcaster: parts.event_broadcaster,
246 #[cfg(feature = "plugins")]
247 pending_plugin_actions: Vec::new(),
248 #[cfg(feature = "plugins")]
249 plugin_render_requested: false,
250 full_redraw_requested: false,
251 suspend_requested: false,
252 plugin_global_state: parts.plugin_global_state,
253 warning_log: None,
254 status_log_path: None,
255 file_watcher_manager: crate::services::file_watcher::FileWatcherManager::new(),
256 last_path_change_for_test: None,
257 last_watch_response_for_test: None,
258 preview_window_id: None,
259 settings_state: None,
260 calibration_wizard: None,
261 keybinding_editor: None,
263 stdin_stream: stdin_stream::StdinStream::default(),
264 global_popups: crate::view::popup::PopupManager::new(),
265 previous_cursor_screen_pos: None,
266 cursor_jump_animation: None,
267 pending_vb_animations: Vec::new(),
268 widget_registry: crate::widgets::WidgetRegistry::new(),
269 floating_widget_panel: None,
270 }
271 }
272
273 pub fn new(
276 config: Config,
277 width: u16,
278 height: u16,
279 dir_context: DirectoryContext,
280 color_capability: crate::view::color_support::ColorCapability,
281 filesystem: Arc<dyn FileSystem + Send + Sync>,
282 ) -> AnyhowResult<Self> {
283 Self::with_working_dir(
284 config,
285 width,
286 height,
287 None,
288 dir_context,
289 true,
290 color_capability,
291 filesystem,
292 )
293 }
294
295 #[allow(clippy::too_many_arguments)]
298 pub fn with_working_dir(
299 config: Config,
300 width: u16,
301 height: u16,
302 working_dir: Option<PathBuf>,
303 dir_context: DirectoryContext,
304 plugins_enabled: bool,
305 color_capability: crate::view::color_support::ColorCapability,
306 filesystem: Arc<dyn FileSystem + Send + Sync>,
307 ) -> AnyhowResult<Self> {
308 Self::with_working_dir_opts(
309 config,
310 width,
311 height,
312 working_dir,
313 dir_context,
314 plugins_enabled,
315 color_capability,
316 filesystem,
317 false,
318 )
319 }
320
321 #[allow(clippy::too_many_arguments)]
329 pub fn with_working_dir_opts(
330 config: Config,
331 width: u16,
332 height: u16,
333 working_dir: Option<PathBuf>,
334 dir_context: DirectoryContext,
335 plugins_enabled: bool,
336 color_capability: crate::view::color_support::ColorCapability,
337 filesystem: Arc<dyn FileSystem + Send + Sync>,
338 defer_plugin_load: bool,
339 ) -> AnyhowResult<Self> {
340 tracing::info!("Building default grammar registry...");
341 let start = std::time::Instant::now();
342 let mut grammar_registry = crate::primitives::grammar::GrammarRegistry::defaults_only();
343 std::sync::Arc::get_mut(&mut grammar_registry)
349 .expect("defaults_only returned a shared Arc")
350 .apply_language_config(&config.languages);
351 tracing::info!("Default grammar registry built in {:?}", start.elapsed());
352 Self::with_options(
356 config,
357 width,
358 height,
359 working_dir,
360 filesystem,
361 plugins_enabled,
362 true, dir_context,
364 None,
365 color_capability,
366 grammar_registry,
367 defer_plugin_load,
368 )
369 }
370
371 #[allow(clippy::too_many_arguments)]
382 pub fn for_test(
383 config: Config,
384 width: u16,
385 height: u16,
386 working_dir: Option<PathBuf>,
387 dir_context: DirectoryContext,
388 color_capability: crate::view::color_support::ColorCapability,
389 filesystem: Arc<dyn FileSystem + Send + Sync>,
390 time_source: Option<SharedTimeSource>,
391 grammar_registry: Option<Arc<crate::primitives::grammar::GrammarRegistry>>,
392 enable_plugins: bool,
393 enable_embedded_plugins: bool,
394 ) -> AnyhowResult<Self> {
395 let mut grammar_registry =
396 grammar_registry.unwrap_or_else(crate::primitives::grammar::GrammarRegistry::empty);
397 std::sync::Arc::get_mut(&mut grammar_registry)
404 .expect("grammar registry Arc must be uniquely owned at for_test entry")
405 .apply_language_config(&config.languages);
406 let mut editor = Self::with_options(
407 config,
408 width,
409 height,
410 working_dir,
411 filesystem,
412 enable_plugins,
413 enable_embedded_plugins,
414 dir_context,
415 time_source,
416 color_capability,
417 grammar_registry,
418 false,
419 )?;
420 editor.needs_full_grammar_build = false;
423 Ok(editor)
424 }
425
426 #[allow(clippy::too_many_arguments)]
430 fn with_options(
431 mut config: Config,
432 width: u16,
433 height: u16,
434 working_dir: Option<PathBuf>,
435 filesystem: Arc<dyn FileSystem + Send + Sync>,
436 enable_plugins: bool,
437 #[cfg_attr(not(feature = "embed-plugins"), allow(unused_variables))]
438 enable_embedded_plugins: bool,
439 dir_context: DirectoryContext,
440 time_source: Option<SharedTimeSource>,
441 color_capability: crate::view::color_support::ColorCapability,
442 grammar_registry: Arc<crate::primitives::grammar::GrammarRegistry>,
443 defer_plugin_load: bool,
444 ) -> AnyhowResult<Self> {
445 let mut t = InitTimer::start("Editor::with_options");
446 let time_source = time_source.unwrap_or_else(RealTimeSource::shared);
448 tracing::info!("Editor::new called with width={}, height={}", width, height);
449
450 let working_dir = working_dir
452 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
453
454 let working_dir = working_dir.canonicalize().unwrap_or(working_dir);
457
458 t.phase("preamble");
459 tracing::info!("Loading themes...");
461 let theme_loader = crate::view::theme::ThemeLoader::new(dir_context.themes_dir());
462 t.phase("ThemeLoader::new");
463 let scan_result =
467 crate::services::packages::scan_installed_packages(&dir_context.config_dir);
468 t.phase("scan_installed_packages");
469
470 for (lang_id, lang_config) in &scan_result.language_configs {
472 config
473 .languages
474 .entry(lang_id.clone())
475 .or_insert_with(|| lang_config.clone());
476 }
477
478 for (lang_id, lsp_config) in &scan_result.lsp_configs {
480 config
481 .lsp
482 .entry(lang_id.clone())
483 .or_insert_with(|| LspLanguageConfig::Multi(vec![lsp_config.clone()]));
484 }
485
486 let theme_registry = Arc::new(theme_loader.load_all(&scan_result.bundle_theme_dirs));
487 t.phase("theme_loader.load_all");
488 tracing::info!("Themes loaded");
489
490 let theme_inner = theme_registry.get_cloned(&config.theme).unwrap_or_else(|| {
492 tracing::warn!(
493 "Theme '{}' not found, falling back to default theme",
494 config.theme.0
495 );
496 theme_registry
497 .get_cloned(&crate::config::ThemeName(
498 crate::view::theme::THEME_HIGH_CONTRAST.to_string(),
499 ))
500 .expect("Default theme must exist")
501 });
502
503 theme_inner.set_terminal_cursor_color();
505 let theme = Arc::new(RwLock::new(theme_inner));
506
507 t.phase("theme_setup");
508 let keybindings = Arc::new(RwLock::new(KeybindingResolver::new(&config)));
509 t.phase("keybindings");
510
511 let mut buffers = crate::app::window::WindowBuffers::new();
513 let mut event_logs = HashMap::new();
514
515 let buffer_id = BufferId(1);
520 let mut state = EditorState::new(
521 width,
522 height,
523 config.editor.large_file_threshold_bytes as usize,
524 Arc::clone(&filesystem),
525 );
526 state
528 .margins
529 .configure_for_line_numbers(config.editor.line_numbers);
530 state.buffer_settings.tab_size = config.editor.tab_size;
531 state.buffer_settings.auto_close = config.editor.auto_close;
532 tracing::info!("EditorState created for buffer {:?}", buffer_id);
534 buffers.insert(buffer_id, state);
535 event_logs.insert(buffer_id, EventLog::new());
536
537 let mut buffer_metadata: HashMap<BufferId, BufferMetadata> = HashMap::new();
541 buffer_metadata.insert(buffer_id, BufferMetadata::new());
542
543 let persisted_env = crate::app::orchestrator_persistence::read_persisted_windows_env(
555 filesystem.as_ref(),
556 &working_dir,
557 );
558 let plugin_global_state = crate::app::orchestrator_persistence::read_persisted_plugin_state(
559 filesystem.as_ref(),
560 &working_dir,
561 );
562
563 let (active_window_id, active_window_root) = persisted_env
569 .as_ref()
570 .and_then(|env| {
571 env.windows
572 .iter()
573 .find(|w| w.id == env.active)
574 .map(|w| (fresh_core::WindowId(env.active), w.root.clone()))
575 })
576 .unwrap_or((fresh_core::WindowId(1), working_dir.clone()));
577
578 let root_uri = types::file_path_to_lsp_uri(&active_window_root);
580
581 t.phase("buffer_state");
582 let tokio_runtime = tokio::runtime::Builder::new_multi_thread()
584 .worker_threads(2) .thread_name("editor-async")
586 .enable_all()
587 .build()
588 .ok()
589 .map(Arc::new);
590 t.phase("tokio_runtime");
591
592 let async_bridge = AsyncBridge::new();
598 let event_broadcaster = crate::model::control_event::EventBroadcaster::default();
599
600 let base_window_bridge = AsyncBridge::new();
606
607 if tokio_runtime.is_none() {
608 tracing::warn!("Failed to create Tokio runtime - async features disabled");
609 }
610
611 let mut lsp = LspManager::new(active_window_id, root_uri);
616
617 if let Some(ref runtime) = tokio_runtime {
621 lsp.set_runtime(runtime.handle().clone(), base_window_bridge.clone());
622 }
623
624 for (language, lsp_configs) in &config.lsp {
626 lsp.set_language_configs(language.clone(), lsp_configs.as_slice().to_vec());
627 }
628
629 let universal_servers: Vec<LspServerConfig> = config
631 .universal_lsp
632 .values()
633 .flat_map(|lc| lc.as_slice().to_vec())
634 .filter(|c| c.enabled)
635 .collect();
636 lsp.set_universal_configs(universal_servers);
637
638 if active_window_root.join("deno.json").exists()
644 || active_window_root.join("deno.jsonc").exists()
645 {
646 tracing::info!("Detected Deno project (deno.json found), using deno lsp for JS/TS");
647 let deno_config = LspServerConfig {
648 command: "deno".to_string(),
649 args: vec!["lsp".to_string()],
650 enabled: true,
651 auto_start: false,
652 process_limits: ProcessLimits::default(),
653 initialization_options: Some(serde_json::json!({"enable": true})),
654 ..Default::default()
655 };
656 lsp.set_language_config("javascript".to_string(), deno_config.clone());
657 lsp.set_language_config("typescript".to_string(), deno_config);
658 }
659
660 t.phase("lsp_setup");
661 let split_manager = SplitManager::new(buffer_id);
663
664 let mut split_view_states = HashMap::new();
666 let initial_split_id = split_manager.active_split();
667 let mut initial_view_state = SplitViewState::with_buffer(width, height, buffer_id);
668 initial_view_state.apply_config_defaults(
669 config.editor.line_numbers,
670 config.editor.highlight_current_line,
671 config.editor.line_wrap,
672 config.editor.wrap_indent,
673 config.editor.wrap_column,
674 config.editor.rulers.clone(),
675 );
676 split_view_states.insert(initial_split_id, initial_view_state);
677
678 let fs_manager = Arc::new(FsManager::new(Arc::clone(&filesystem)));
680
681 let command_registry = Arc::new(RwLock::new(CommandRegistry::new()));
683
684 let authority = crate::services::authority::Authority {
690 filesystem: Arc::clone(&filesystem),
691 ..crate::services::authority::Authority::local()
692 };
693 let process_spawner = Arc::clone(&authority.process_spawner);
694
695 let mut quick_open_registry = QuickOpenRegistry::new();
697 quick_open_registry.register(Box::new(FileProvider::new(
698 Arc::clone(&filesystem),
699 Arc::clone(&process_spawner),
700 tokio_runtime.as_ref().map(|rt| rt.handle().clone()),
701 Some(async_bridge.sender()),
702 )));
703 quick_open_registry.register(Box::new(CommandProvider::new(
704 Arc::clone(&command_registry),
705 Arc::clone(&keybindings),
706 )));
707 quick_open_registry.register(Box::new(BufferProvider::new()));
708 quick_open_registry.register(Box::new(GotoLineProvider::new()));
709
710 let theme_cache = Arc::new(RwLock::new(theme_registry.to_json_map()));
712
713 t.phase("split_quickopen_authority");
714 let plugin_manager = Arc::new(RwLock::new(PluginManager::new(
716 enable_plugins,
717 Arc::clone(&command_registry),
718 dir_context.clone(),
719 Arc::clone(&theme_cache),
720 )));
721 t.phase("PluginManager::new");
722
723 #[cfg(feature = "plugins")]
726 if let Some(snapshot_handle) = plugin_manager.read().unwrap().state_snapshot_handle() {
727 let mut snapshot = snapshot_handle.write().unwrap();
728 snapshot.working_dir = working_dir.clone();
729 populate_builtin_keybinding_labels(&mut snapshot, &keybindings);
737 }
738
739 if plugin_manager.read().unwrap().is_active() {
746 let mut plugin_dirs: Vec<std::path::PathBuf> = vec![];
747
748 if let Ok(exe_path) = std::env::current_exe() {
750 if let Some(exe_dir) = exe_path.parent() {
751 let exe_plugin_dir = exe_dir.join("plugins");
752 if exe_plugin_dir.exists() {
753 plugin_dirs.push(exe_plugin_dir);
754 }
755 }
756 }
757
758 #[cfg(feature = "embed-plugins")]
769 if enable_embedded_plugins && plugin_dirs.is_empty() {
770 if let Some(embedded_dir) =
771 crate::services::plugins::embedded::get_embedded_plugins_dir()
772 {
773 tracing::info!("Using embedded plugins from: {:?}", embedded_dir);
774 plugin_dirs.push(embedded_dir.clone());
775 }
776 }
777
778 let user_plugins_dir = dir_context.config_dir.join("plugins");
780 if user_plugins_dir.exists() && !plugin_dirs.contains(&user_plugins_dir) {
781 tracing::info!("Found user plugins directory: {:?}", user_plugins_dir);
782 plugin_dirs.push(user_plugins_dir.clone());
783 }
784
785 let packages_dir = dir_context.config_dir.join("plugins").join("packages");
787 if packages_dir.exists() {
788 if let Ok(entries) = std::fs::read_dir(&packages_dir) {
789 for entry in entries.flatten() {
790 let path = entry.path();
791 if path.is_dir() {
793 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
794 if !name.starts_with('.') {
795 tracing::info!("Found package manager plugin: {:?}", path);
796 plugin_dirs.push(path);
797 }
798 }
799 }
800 }
801 }
802 }
803
804 for dir in &scan_result.bundle_plugin_dirs {
806 tracing::info!("Found bundle plugin directory: {:?}", dir);
807 plugin_dirs.push(dir.clone());
808 }
809
810 if plugin_dirs.is_empty() {
811 tracing::debug!(
812 "No plugins directory found next to executable or in working dir: {:?}",
813 working_dir
814 );
815 }
816
817 if defer_plugin_load {
818 #[cfg(feature = "plugins")]
829 {
830 let bridge = &async_bridge;
831 let mut dir_receivers: Vec<(
832 std::path::PathBuf,
833 fresh_plugin_runtime::thread::oneshot::Receiver<
834 fresh_plugin_runtime::thread::PluginsDirLoadResult,
835 >,
836 )> = Vec::with_capacity(plugin_dirs.len());
837 for plugin_dir in &plugin_dirs {
838 tracing::info!(
839 "Submitting async TypeScript plugin load for: {:?}",
840 plugin_dir
841 );
842 if let Some(rx) = plugin_manager
843 .read()
844 .unwrap()
845 .load_plugins_from_dir_with_config_request(plugin_dir, &config.plugins)
846 {
847 dir_receivers.push((plugin_dir.clone(), rx));
848 }
849 }
850 let declarations_rx = if !dir_receivers.is_empty() {
851 plugin_manager.read().unwrap().list_plugins_request()
852 } else {
853 None
854 };
855 if !dir_receivers.is_empty() {
856 let sender = bridge.sender();
857 std::thread::Builder::new()
858 .name("plugin-load-forwarder".to_string())
859 .spawn(move || {
860 for (dir, rx) in dir_receivers {
861 let load_start = std::time::Instant::now();
862 match rx.recv() {
863 Ok((errors, discovered_plugins)) => {
864 tracing::info!(
865 "Loaded TypeScript plugins from {:?} in {:?}",
866 dir,
867 load_start.elapsed()
868 );
869 drop(sender.send(
870 crate::services::async_bridge::AsyncMessage::PluginsDirLoaded {
871 dir,
872 errors,
873 discovered_plugins,
874 },
875 ));
876 }
877 Err(e) => {
878 tracing::warn!(
879 "plugin-load-forwarder: dir {:?} recv failed: {}",
880 dir,
881 e
882 );
883 }
884 }
885 }
886 if let Some(rx) = declarations_rx {
887 match rx.recv() {
888 Ok(plugin_infos) => {
889 let declarations: Vec<(String, String)> = plugin_infos
890 .into_iter()
891 .filter_map(|info| {
892 info.declarations.map(|d| (info.name, d))
893 })
894 .collect();
895 drop(sender.send(
896 crate::services::async_bridge::AsyncMessage::PluginDeclarationsReady {
897 declarations,
898 },
899 ));
900 }
901 Err(e) => {
902 tracing::warn!(
903 "plugin-load-forwarder: list_plugins recv failed: {}",
904 e
905 );
906 }
907 }
908 }
909 })
910 .ok();
911 }
912 }
913 } else {
914 for plugin_dir in plugin_dirs {
919 tracing::info!("Loading TypeScript plugins from: {:?}", plugin_dir);
920 let load_start = std::time::Instant::now();
921 let (errors, discovered_plugins) = plugin_manager
922 .read()
923 .unwrap()
924 .load_plugins_from_dir_with_config(&plugin_dir, &config.plugins);
925 tracing::info!(
926 "Loaded TypeScript plugins from {:?} in {:?}",
927 plugin_dir,
928 load_start.elapsed()
929 );
930
931 for (name, plugin_config) in discovered_plugins {
934 config.plugins.insert(name, plugin_config);
935 }
936
937 if !errors.is_empty() {
938 for err in &errors {
939 tracing::error!("TypeScript plugin load error: {}", err);
940 }
941 #[cfg(debug_assertions)]
943 panic!(
944 "TypeScript plugin loading failed with {} error(s): {}",
945 errors.len(),
946 errors.join("; ")
947 );
948 }
949 }
950
951 let declarations = plugin_manager.read().unwrap().plugin_declarations();
959 crate::init_script::write_plugin_declarations(
960 &dir_context.config_dir,
961 &declarations,
962 );
963 }
964 }
965
966 t.phase("plugin_loading");
967 let recovery_enabled = config.editor.recovery_enabled;
969 let check_for_updates = config.check_for_updates;
970
971 let update_checker = if check_for_updates {
973 tracing::debug!("Update checking enabled, starting periodic checker");
974 Some(
975 crate::services::release_checker::start_periodic_update_check(
976 crate::services::release_checker::DEFAULT_RELEASES_URL,
977 time_source.clone(),
978 dir_context.data_dir.clone(),
979 ),
980 )
981 } else {
982 tracing::debug!("Update checking disabled by config");
983 None
984 };
985
986 let user_config_raw = Config::read_user_config_raw(&working_dir);
988
989 let config_arc = Arc::new(config);
996 let config_cached_json =
997 Arc::new(serde_json::to_value(&*config_arc).unwrap_or(serde_json::Value::Null));
998 let config_snapshot_anchor = Arc::clone(&config_arc);
999
1000 let buffer_id_alloc = crate::app::window_resources::BufferIdAllocator::new(2);
1006
1007 let local_filesystem: Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> =
1012 Arc::new(crate::model::filesystem::StdFileSystem);
1013
1014 let base_resources = crate::app::window_resources::WindowResources {
1020 config: Arc::clone(&config_arc),
1021 grammar_registry: Arc::clone(&grammar_registry),
1022 theme_registry: Arc::clone(&theme_registry),
1023 theme_cache: Arc::clone(&theme_cache),
1024 keybindings: Arc::clone(&keybindings),
1025 command_registry: Arc::clone(&command_registry),
1026 fs_manager: Arc::clone(&fs_manager),
1027 local_filesystem: Arc::clone(&local_filesystem),
1028 buffer_id_alloc: buffer_id_alloc.clone(),
1029 authority: authority.clone(),
1030 time_source: Arc::clone(&time_source),
1031 dir_context: dir_context.clone(),
1032 tokio_runtime: tokio_runtime.clone(),
1033 async_bridge: Some(async_bridge.clone()),
1034 plugin_manager: Arc::clone(&plugin_manager),
1035 theme: Arc::clone(&theme),
1036 event_broadcaster: event_broadcaster.clone(),
1037 };
1038
1039 let (active_label, active_root, active_plugin_state) = persisted_env
1046 .as_ref()
1047 .and_then(|env| env.windows.iter().find(|w| w.id == active_window_id.0))
1048 .map(|w| (w.label.clone(), w.root.clone(), w.plugin_state.clone()))
1049 .unwrap_or_else(|| (String::new(), working_dir.clone(), HashMap::new()));
1050
1051 let mut active_win = crate::app::window::Window::new(
1052 active_window_id,
1053 active_label,
1054 active_root,
1055 base_resources,
1056 );
1057 active_win.terminal_width = width;
1063 active_win.terminal_height = height;
1064 active_win.lsp = Some(lsp);
1068 active_win.buffers = buffers;
1069 active_win
1070 .buffers
1071 .set_splits((split_manager, split_view_states));
1072 active_win.buffer_metadata = buffer_metadata;
1073 active_win.event_logs = event_logs;
1074 active_win.plugin_state = active_plugin_state;
1075 active_win.bridge = base_window_bridge;
1081 for history_name in ["search", "replace", "goto_line"] {
1084 let path = dir_context.prompt_history_path(history_name);
1085 let history = crate::input::input_history::InputHistory::load_from_file(&path)
1086 .unwrap_or_else(|e| {
1087 tracing::warn!("Failed to load {} history: {}", history_name, e);
1088 crate::input::input_history::InputHistory::new()
1089 });
1090 active_win
1091 .prompt_histories
1092 .insert(history_name.to_string(), history);
1093 }
1094
1095 let mut windows = HashMap::new();
1099 if let Some(ref env) = persisted_env {
1100 for ps in &env.windows {
1101 let id = fresh_core::WindowId(ps.id);
1102 if id == active_window_id {
1103 continue;
1104 }
1105 let resources = crate::app::window_resources::WindowResources {
1106 config: Arc::clone(&config_arc),
1107 grammar_registry: Arc::clone(&grammar_registry),
1108 theme_registry: Arc::clone(&theme_registry),
1109 theme_cache: Arc::clone(&theme_cache),
1110 keybindings: Arc::clone(&keybindings),
1111 command_registry: Arc::clone(&command_registry),
1112 fs_manager: Arc::clone(&fs_manager),
1113 local_filesystem: Arc::clone(&local_filesystem),
1114 buffer_id_alloc: buffer_id_alloc.clone(),
1115 authority: authority.clone(),
1116 time_source: Arc::clone(&time_source),
1117 dir_context: dir_context.clone(),
1118 tokio_runtime: tokio_runtime.clone(),
1119 async_bridge: Some(async_bridge.clone()),
1120 plugin_manager: Arc::clone(&plugin_manager),
1121 theme: Arc::clone(&theme),
1122 event_broadcaster: event_broadcaster.clone(),
1123 };
1124 let mut shell = crate::app::window::Window::new(
1125 id,
1126 ps.label.clone(),
1127 ps.root.clone(),
1128 resources,
1129 );
1130 shell.terminal_width = width;
1131 shell.terminal_height = height;
1132 shell.plugin_state = ps.plugin_state.clone();
1133 windows.insert(id, shell);
1134 }
1135 }
1136 windows.insert(active_window_id, active_win);
1137
1138 let max_existing = windows.keys().map(|k| k.0).max().unwrap_or(0);
1144 let next_window_id = persisted_env
1145 .as_ref()
1146 .map(|env| env.next_id.max(max_existing + 1))
1147 .unwrap_or(2);
1148
1149 let recovery_service = {
1150 let recovery_config = RecoveryConfig {
1151 enabled: recovery_enabled,
1152 ..RecoveryConfig::default()
1153 };
1154 let scope = crate::services::recovery::RecoveryScope::Standalone {
1162 working_dir: working_dir.clone(),
1163 };
1164 RecoveryService::with_scope(recovery_config, &dir_context.recovery_dir(), &scope)
1165 };
1166
1167 let key_translator = crate::input::key_translator::KeyTranslator::load_from_config_dir(
1168 &dir_context.config_dir,
1169 )
1170 .unwrap_or_default();
1171
1172 let pending_grammars = scan_result
1173 .additional_grammars
1174 .iter()
1175 .map(|g| PendingGrammar {
1176 language: g.language.clone(),
1177 grammar_path: g.path.to_string_lossy().to_string(),
1178 extensions: g.extensions.clone(),
1179 })
1180 .collect();
1181
1182 let parts = EditorParts {
1183 config: config_arc,
1184 config_snapshot_anchor,
1185 config_cached_json,
1186 user_config_raw: Arc::new(user_config_raw),
1187 dir_context: dir_context.clone(),
1188 working_dir: working_dir.clone(),
1189 theme,
1190 theme_registry,
1191 theme_cache,
1192 grammar_registry,
1193 pending_grammars,
1194 needs_full_grammar_build: true,
1195 keybindings,
1196 buffer_id_alloc: buffer_id_alloc.clone(),
1197 next_buffer_id: 2,
1198 terminal_width: width,
1199 terminal_height: height,
1200 color_capability,
1201 tokio_runtime,
1202 async_bridge,
1203 fs_manager,
1204 authority,
1205 local_filesystem: Arc::clone(&local_filesystem),
1206 windows,
1207 active_window: active_window_id,
1208 next_window_id,
1209 command_registry,
1210 quick_open_registry,
1211 plugin_manager,
1212 recovery_service,
1213 key_translator,
1214 update_checker,
1215 time_source: time_source.clone(),
1216 plugin_global_state,
1217 event_broadcaster: event_broadcaster.clone(),
1218 };
1219
1220 let mut editor = Editor::from_parts(parts);
1221
1222 t.phase("editor_struct_assembly");
1223 editor.clipboard.apply_config(&editor.config.clipboard);
1225
1226 let needs_seed: Vec<fresh_core::WindowId> = editor
1234 .windows
1235 .iter()
1236 .filter(|(_, s)| s.buffers.splits().is_none() || s.buffers.len() == 0)
1237 .map(|(id, _)| *id)
1238 .collect();
1239 for id in needs_seed {
1240 if let Some((buf, state, metadata, event_log, mgr, vs)) =
1241 editor.build_fresh_layout_if_needed(id)
1242 {
1243 if let Some(s) = editor.windows.get_mut(&id) {
1244 s.buffers.set_splits((mgr, vs));
1245 s.buffers.insert(buf, state);
1246 s.buffer_metadata.insert(buf, metadata);
1247 s.event_logs.insert(buf, event_log);
1248 }
1249 }
1250 }
1251
1252 #[cfg(feature = "plugins")]
1253 {
1254 editor.update_plugin_state_snapshot();
1255 if editor.plugin_manager.read().unwrap().is_active() {
1256 editor.plugin_manager.read().unwrap().run_hook(
1257 "editor_initialized",
1258 crate::services::plugins::hooks::HookArgs::EditorInitialized {},
1259 );
1260 }
1261 }
1262 t.phase("post_struct_hooks");
1263 t.finish();
1264 Ok(editor)
1265 }
1266
1267 pub fn event_broadcaster(&self) -> &crate::model::control_event::EventBroadcaster {
1269 &self.event_broadcaster
1270 }
1271
1272 pub(super) fn start_background_grammar_build(
1277 &mut self,
1278 additional: Vec<crate::primitives::grammar::GrammarSpec>,
1279 callback_ids: Vec<fresh_core::api::JsCallbackId>,
1280 ) {
1281 let Some(bridge) = &self.async_bridge else {
1282 return;
1283 };
1284 self.grammar_build_in_progress = true;
1285 let sender = bridge.sender();
1286 let config_dir = self.dir_context.config_dir.clone();
1287 tracing::info!(
1288 "Spawning background grammar build thread ({} plugin grammars)...",
1289 additional.len()
1290 );
1291 std::thread::Builder::new()
1292 .name("grammar-build".to_string())
1293 .spawn(move || {
1294 tracing::info!("[grammar-build] Thread started");
1295 let start = std::time::Instant::now();
1296 let registry = if additional.is_empty() {
1297 crate::primitives::grammar::GrammarRegistry::for_editor(config_dir)
1298 } else {
1299 crate::primitives::grammar::GrammarRegistry::for_editor_with_additional(
1300 config_dir,
1301 &additional,
1302 )
1303 };
1304 tracing::info!("[grammar-build] Complete in {:?}", start.elapsed());
1305 drop(sender.send(
1306 crate::services::async_bridge::AsyncMessage::GrammarRegistryBuilt {
1307 registry,
1308 callback_ids,
1309 },
1310 ));
1311 })
1312 .ok();
1313 }
1314
1315 pub fn load_init_script(&mut self, enabled: bool) {
1322 use crate::init_script::{
1323 check, decide_load, describe, record_success, refresh_types_scaffolding, CheckSeverity,
1324 InitOutcome, LoadDecision,
1325 };
1326
1327 let config_dir = self.dir_context.config_dir.clone();
1328
1329 if enabled {
1330 refresh_types_scaffolding(&config_dir);
1334
1335 let report = check(&config_dir);
1340 if !report.ok {
1341 for d in &report.diagnostics {
1342 let level = match d.severity {
1343 CheckSeverity::Error => "error",
1344 CheckSeverity::Warning => "warning",
1345 };
1346 tracing::warn!(
1347 "init.ts pre-load {level} at {}:{}: {}",
1348 d.line,
1349 d.column,
1350 d.message
1351 );
1352 }
1353 }
1354 }
1355
1356 let outcome = match decide_load(&config_dir, enabled) {
1357 LoadDecision::Skip(outcome) => outcome,
1358 LoadDecision::Load { source } => {
1359 if !self.plugin_manager.read().unwrap().is_active() {
1360 InitOutcome::Failed {
1361 message: "plugin runtime inactive (--no-plugins); init.ts cannot run"
1362 .into(),
1363 }
1364 } else {
1365 match self.plugin_manager.read().unwrap().load_plugin_from_source(
1366 &source,
1367 crate::init_script::INIT_PLUGIN_NAME,
1368 true,
1369 ) {
1370 Ok(()) => {
1371 record_success(&config_dir);
1372 InitOutcome::Loaded
1373 }
1374 Err(e) => InitOutcome::Failed {
1375 message: format!("{e}"),
1376 },
1377 }
1378 }
1379 }
1380 };
1381
1382 let summary = describe(&outcome);
1383 match outcome {
1384 InitOutcome::NotFound | InitOutcome::Disabled => tracing::debug!("{}", summary),
1385 InitOutcome::Loaded => tracing::info!("{}", summary),
1386 InitOutcome::CrashFused { .. } | InitOutcome::Failed { .. } => {
1387 tracing::warn!("{}", summary);
1388 self.set_status_message(summary);
1389 }
1390 }
1391 }
1392
1393 pub fn load_init_script_async(&mut self, enabled: bool) {
1405 use crate::init_script::{
1406 check, decide_load, refresh_types_scaffolding, CheckSeverity, InitOutcome, LoadDecision,
1407 };
1408 use crate::services::async_bridge::PluginInitScriptOutcome;
1409
1410 let config_dir = self.dir_context.config_dir.clone();
1411
1412 if enabled {
1413 refresh_types_scaffolding(&config_dir);
1414 let report = check(&config_dir);
1415 if !report.ok {
1416 for d in &report.diagnostics {
1417 let level = match d.severity {
1418 CheckSeverity::Error => "error",
1419 CheckSeverity::Warning => "warning",
1420 };
1421 tracing::warn!(
1422 "init.ts pre-load {level} at {}:{}: {}",
1423 d.line,
1424 d.column,
1425 d.message
1426 );
1427 }
1428 }
1429 }
1430
1431 let outcome_now: Option<PluginInitScriptOutcome> = match decide_load(&config_dir, enabled) {
1432 LoadDecision::Skip(outcome) => Some(match outcome {
1433 InitOutcome::NotFound => PluginInitScriptOutcome::NotFound,
1434 InitOutcome::Disabled => PluginInitScriptOutcome::Disabled,
1435 InitOutcome::CrashFused { failures } => {
1436 PluginInitScriptOutcome::CrashFused { failures }
1437 }
1438 InitOutcome::Loaded => PluginInitScriptOutcome::Loaded,
1441 InitOutcome::Failed { message } => PluginInitScriptOutcome::Failed { message },
1442 }),
1443 LoadDecision::Load { source } => {
1444 if !self.plugin_manager.read().unwrap().is_active() {
1445 Some(PluginInitScriptOutcome::Failed {
1446 message: "plugin runtime inactive (--no-plugins); init.ts cannot run"
1447 .into(),
1448 })
1449 } else {
1450 self.spawn_init_script_forwarder(source);
1451 None
1452 }
1453 }
1454 };
1455
1456 if let Some(outcome) = outcome_now {
1457 if let Some(bridge) = &self.async_bridge {
1461 drop(bridge.sender().send(
1462 crate::services::async_bridge::AsyncMessage::PluginInitScriptLoaded(outcome),
1463 ));
1464 } else {
1465 self.handle_plugin_init_script_loaded(outcome);
1466 }
1467 }
1468 }
1469
1470 #[cfg(feature = "plugins")]
1471 fn spawn_init_script_forwarder(&self, source: String) {
1472 let Some(bridge) = &self.async_bridge else {
1473 return;
1474 };
1475 let Some(rx) = self
1476 .plugin_manager
1477 .read()
1478 .unwrap()
1479 .load_plugin_from_source_request(&source, crate::init_script::INIT_PLUGIN_NAME, true)
1480 else {
1481 return;
1482 };
1483 let sender = bridge.sender();
1484 std::thread::Builder::new()
1485 .name("plugin-init-forwarder".to_string())
1486 .spawn(move || {
1487 let outcome = match rx.recv() {
1488 Ok(Ok(())) => crate::services::async_bridge::PluginInitScriptOutcome::Loaded,
1489 Ok(Err(e)) => crate::services::async_bridge::PluginInitScriptOutcome::Failed {
1490 message: format!("{e}"),
1491 },
1492 Err(e) => crate::services::async_bridge::PluginInitScriptOutcome::Failed {
1493 message: format!("plugin thread closed: {e}"),
1494 },
1495 };
1496 drop(sender.send(
1497 crate::services::async_bridge::AsyncMessage::PluginInitScriptLoaded(outcome),
1498 ));
1499 })
1500 .ok();
1501 }
1502
1503 #[cfg(not(feature = "plugins"))]
1504 fn spawn_init_script_forwarder(&self, _source: String) {}
1505
1506 pub fn handle_set_setting(&mut self, path: String, value: serde_json::Value) {
1510 let mut json = serde_json::to_value(&*self.config).unwrap_or_default();
1511 set_dot_path(&mut json, &path, value);
1512 match serde_json::from_value::<crate::config::Config>(json) {
1513 Ok(new_config) => {
1514 let old_theme = self.config.theme.clone();
1515 self.config = Arc::new(new_config);
1516 if old_theme != self.config.theme {
1517 if let Some(theme) = self.theme_registry.get_cloned(&self.config.theme) {
1518 *self.theme.write().unwrap() = theme;
1519 }
1520 }
1521 *self.keybindings.write().unwrap() =
1522 crate::input::keybindings::KeybindingResolver::new(&self.config);
1523 self.clipboard.apply_config(&self.config.clipboard);
1524 {
1525 let cfg = self.config.editor.clone();
1526 let win = self.active_window_mut();
1527 win.menu_bar_visible = cfg.show_menu_bar;
1528 win.tab_bar_visible = cfg.show_tab_bar;
1529 win.status_bar_visible = cfg.show_status_bar;
1530 win.prompt_line_visible = cfg.show_prompt_line;
1531 }
1532 #[cfg(feature = "plugins")]
1533 self.update_plugin_state_snapshot();
1534 }
1535 Err(e) => {
1536 self.set_status_message(format!("setSetting({path}): {e}"));
1537 }
1538 }
1539 }
1540
1541 pub(crate) fn handle_plugins_dir_loaded(
1546 &mut self,
1547 dir: std::path::PathBuf,
1548 errors: Vec<String>,
1549 discovered_plugins: std::collections::HashMap<String, fresh_core::config::PluginConfig>,
1550 ) {
1551 if !discovered_plugins.is_empty() {
1552 let cfg = std::sync::Arc::make_mut(&mut self.config);
1553 for (name, plugin_config) in discovered_plugins {
1554 cfg.plugins.insert(name, plugin_config);
1555 }
1556 }
1557 if !errors.is_empty() {
1558 for err in &errors {
1559 tracing::error!("TypeScript plugin load error: {}", err);
1560 }
1561 #[cfg(debug_assertions)]
1562 panic!(
1563 "TypeScript plugin loading failed for {:?} with {} error(s): {}",
1564 dir,
1565 errors.len(),
1566 errors.join("; ")
1567 );
1568 #[cfg(not(debug_assertions))]
1569 {
1570 let _ = dir;
1571 }
1572 }
1573 }
1574
1575 pub(crate) fn handle_plugin_declarations_ready(&self, declarations: Vec<(String, String)>) {
1579 crate::init_script::write_plugin_declarations(&self.dir_context.config_dir, &declarations);
1580 }
1581
1582 pub(crate) fn handle_plugin_init_script_loaded(
1586 &mut self,
1587 outcome: crate::services::async_bridge::PluginInitScriptOutcome,
1588 ) {
1589 use crate::init_script::{describe, record_success, InitOutcome};
1590 use crate::services::async_bridge::PluginInitScriptOutcome as O;
1591 let outcome = match outcome {
1592 O::NotFound => InitOutcome::NotFound,
1593 O::Disabled => InitOutcome::Disabled,
1594 O::CrashFused { failures } => InitOutcome::CrashFused { failures },
1595 O::Loaded => {
1596 record_success(&self.dir_context.config_dir);
1597 InitOutcome::Loaded
1598 }
1599 O::Failed { message } => InitOutcome::Failed { message },
1600 };
1601 let summary = describe(&outcome);
1602 match outcome {
1603 InitOutcome::NotFound | InitOutcome::Disabled => tracing::debug!("{}", summary),
1604 InitOutcome::Loaded => tracing::info!("{}", summary),
1605 InitOutcome::CrashFused { .. } | InitOutcome::Failed { .. } => {
1606 tracing::warn!("{}", summary);
1607 self.set_status_message(summary);
1608 }
1609 }
1610 }
1611
1612 pub fn fire_plugins_loaded_hook(&self) {
1614 #[cfg(feature = "plugins")]
1615 if self.plugin_manager.read().unwrap().is_active() {
1616 self.plugin_manager.read().unwrap().run_hook(
1617 "plugins_loaded",
1618 crate::services::plugins::hooks::HookArgs::PluginsLoaded {},
1619 );
1620 }
1621 }
1622
1623 pub fn fire_ready_hook(&self) {
1625 #[cfg(feature = "plugins")]
1626 if self.plugin_manager.read().unwrap().is_active() {
1627 self.plugin_manager
1628 .read()
1629 .unwrap()
1630 .run_hook("ready", crate::services::plugins::hooks::HookArgs::Ready {});
1631 }
1632 }
1633
1634 #[doc(hidden)]
1636 pub fn config_for_tests(&self) -> &crate::config::Config {
1637 &self.config
1638 }
1639
1640 #[doc(hidden)]
1642 pub fn dispatch_action_for_tests(&mut self, action: crate::input::keybindings::Action) {
1643 if let Err(e) = self.handle_action(action) {
1644 tracing::warn!("dispatch_action_for_tests: {e}");
1645 }
1646 }
1647
1648 #[doc(hidden)]
1650 pub fn live_grep_last_state_for_tests(
1651 &self,
1652 ) -> Option<&crate::services::live_grep_state::LiveGrepLastState> {
1653 self.active_window().live_grep_last_state.as_ref()
1654 }
1655
1656 #[doc(hidden)]
1658 pub fn set_live_grep_last_state_for_tests(
1659 &mut self,
1660 state: Option<crate::services::live_grep_state::LiveGrepLastState>,
1661 ) {
1662 self.active_window_mut().live_grep_last_state = state;
1663 }
1664
1665 #[doc(hidden)]
1668 pub fn split_manager_for_tests(&self) -> &crate::view::split::SplitManager {
1669 self.windows
1670 .get(&self.active_window)
1671 .and_then(|w| w.buffers.splits())
1672 .map(|(mgr, _)| mgr)
1673 .expect("active window must have a populated split layout")
1674 }
1675
1676 #[doc(hidden)]
1681 pub fn split_view_state_for_tests(
1682 &self,
1683 leaf: crate::model::event::LeafId,
1684 ) -> Option<&crate::view::split::SplitViewState> {
1685 self.windows
1686 .get(&self.active_window)
1687 .and_then(|w| w.buffers.splits())
1688 .map(|(_, vs)| vs)
1689 .expect("active window must have a populated split layout")
1690 .get(&leaf)
1691 }
1692
1693 #[cfg(feature = "plugins")]
1702 pub(crate) fn refresh_keybinding_labels_snapshot(&self) {
1703 if let Some(snapshot_handle) = self.plugin_manager.read().unwrap().state_snapshot_handle() {
1704 if let Ok(mut snapshot) = snapshot_handle.write() {
1705 populate_builtin_keybinding_labels(&mut snapshot, &self.keybindings);
1706 }
1707 }
1708 }
1709}
1710
1711#[cfg(feature = "plugins")]
1727fn populate_builtin_keybinding_labels(
1728 snapshot: &mut crate::services::plugins::api::EditorStateSnapshot,
1729 keybindings: &std::sync::Arc<std::sync::RwLock<crate::input::keybindings::KeybindingResolver>>,
1730) {
1731 use crate::input::keybindings::{Action, KeyContext};
1732 let Ok(resolver) = keybindings.read() else {
1733 return;
1734 };
1735 let contexts = [
1736 KeyContext::Normal,
1737 KeyContext::Prompt,
1738 KeyContext::Popup,
1739 KeyContext::Completion,
1740 KeyContext::FileExplorer,
1741 KeyContext::Menu,
1742 KeyContext::Terminal,
1743 KeyContext::Settings,
1744 KeyContext::CompositeBuffer,
1745 ];
1746 let known_suffixes: Vec<String> = contexts
1753 .iter()
1754 .map(|c| format!("\0{}", c.to_when_clause()))
1755 .collect();
1756 snapshot
1757 .keybinding_labels
1758 .retain(|k, _| !known_suffixes.iter().any(|s| k.ends_with(s)));
1759 for action_name in Action::all_action_names() {
1760 for ctx in &contexts {
1761 if let Some(label) = resolver.find_keybinding_for_action(&action_name, ctx.clone()) {
1762 let key = format!("{}\0{}", action_name, ctx.to_when_clause());
1763 snapshot.keybinding_labels.insert(key, label);
1764 }
1765 }
1766 }
1767}