1use super::*;
13
14fn set_dot_path(root: &mut serde_json::Value, path: &str, value: serde_json::Value) {
17 let segments: Vec<&str> = path.split('.').filter(|s| !s.is_empty()).collect();
18 if segments.is_empty() {
19 return;
20 }
21 let mut cur = root;
22 for seg in &segments[..segments.len() - 1] {
23 if !cur.is_object() {
24 *cur = serde_json::Value::Object(serde_json::Map::new());
25 }
26 cur = cur
27 .as_object_mut()
28 .unwrap()
29 .entry((*seg).to_string())
30 .or_insert(serde_json::Value::Null);
31 }
32 let last = segments[segments.len() - 1];
33 if !cur.is_object() {
34 *cur = serde_json::Value::Object(serde_json::Map::new());
35 }
36 cur.as_object_mut().unwrap().insert(last.to_string(), value);
37}
38
39impl Editor {
40 pub fn new(
43 config: Config,
44 width: u16,
45 height: u16,
46 dir_context: DirectoryContext,
47 color_capability: crate::view::color_support::ColorCapability,
48 filesystem: Arc<dyn FileSystem + Send + Sync>,
49 ) -> AnyhowResult<Self> {
50 Self::with_working_dir(
51 config,
52 width,
53 height,
54 None,
55 dir_context,
56 true,
57 color_capability,
58 filesystem,
59 )
60 }
61
62 #[allow(clippy::too_many_arguments)]
65 pub fn with_working_dir(
66 config: Config,
67 width: u16,
68 height: u16,
69 working_dir: Option<PathBuf>,
70 dir_context: DirectoryContext,
71 plugins_enabled: bool,
72 color_capability: crate::view::color_support::ColorCapability,
73 filesystem: Arc<dyn FileSystem + Send + Sync>,
74 ) -> AnyhowResult<Self> {
75 tracing::info!("Building default grammar registry...");
76 let start = std::time::Instant::now();
77 let mut grammar_registry = crate::primitives::grammar::GrammarRegistry::defaults_only();
78 std::sync::Arc::get_mut(&mut grammar_registry)
84 .expect("defaults_only returned a shared Arc")
85 .apply_language_config(&config.languages);
86 tracing::info!("Default grammar registry built in {:?}", start.elapsed());
87 Self::with_options(
91 config,
92 width,
93 height,
94 working_dir,
95 filesystem,
96 plugins_enabled,
97 dir_context,
98 None,
99 color_capability,
100 grammar_registry,
101 )
102 }
103
104 #[allow(clippy::too_many_arguments)]
109 pub fn for_test(
110 config: Config,
111 width: u16,
112 height: u16,
113 working_dir: Option<PathBuf>,
114 dir_context: DirectoryContext,
115 color_capability: crate::view::color_support::ColorCapability,
116 filesystem: Arc<dyn FileSystem + Send + Sync>,
117 time_source: Option<SharedTimeSource>,
118 grammar_registry: Option<Arc<crate::primitives::grammar::GrammarRegistry>>,
119 ) -> AnyhowResult<Self> {
120 let mut grammar_registry =
121 grammar_registry.unwrap_or_else(crate::primitives::grammar::GrammarRegistry::empty);
122 std::sync::Arc::get_mut(&mut grammar_registry)
129 .expect("grammar registry Arc must be uniquely owned at for_test entry")
130 .apply_language_config(&config.languages);
131 let mut editor = Self::with_options(
132 config,
133 width,
134 height,
135 working_dir,
136 filesystem,
137 true,
138 dir_context,
139 time_source,
140 color_capability,
141 grammar_registry,
142 )?;
143 editor.needs_full_grammar_build = false;
146 Ok(editor)
147 }
148
149 #[allow(clippy::too_many_arguments)]
153 fn with_options(
154 mut config: Config,
155 width: u16,
156 height: u16,
157 working_dir: Option<PathBuf>,
158 filesystem: Arc<dyn FileSystem + Send + Sync>,
159 enable_plugins: bool,
160 dir_context: DirectoryContext,
161 time_source: Option<SharedTimeSource>,
162 color_capability: crate::view::color_support::ColorCapability,
163 grammar_registry: Arc<crate::primitives::grammar::GrammarRegistry>,
164 ) -> AnyhowResult<Self> {
165 let time_source = time_source.unwrap_or_else(RealTimeSource::shared);
167 tracing::info!("Editor::new called with width={}, height={}", width, height);
168
169 let working_dir = working_dir
171 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
172
173 let working_dir = working_dir.canonicalize().unwrap_or(working_dir);
176
177 tracing::info!("Loading themes...");
179 let theme_loader = crate::view::theme::ThemeLoader::new(dir_context.themes_dir());
180 let scan_result =
184 crate::services::packages::scan_installed_packages(&dir_context.config_dir);
185
186 for (lang_id, lang_config) in &scan_result.language_configs {
188 config
189 .languages
190 .entry(lang_id.clone())
191 .or_insert_with(|| lang_config.clone());
192 }
193
194 for (lang_id, lsp_config) in &scan_result.lsp_configs {
196 config
197 .lsp
198 .entry(lang_id.clone())
199 .or_insert_with(|| LspLanguageConfig::Multi(vec![lsp_config.clone()]));
200 }
201
202 let theme_registry = theme_loader.load_all(&scan_result.bundle_theme_dirs);
203 tracing::info!("Themes loaded");
204
205 let theme = theme_registry.get_cloned(&config.theme).unwrap_or_else(|| {
207 tracing::warn!(
208 "Theme '{}' not found, falling back to default theme",
209 config.theme.0
210 );
211 theme_registry
212 .get_cloned(&crate::config::ThemeName(
213 crate::view::theme::THEME_HIGH_CONTRAST.to_string(),
214 ))
215 .expect("Default theme must exist")
216 });
217
218 theme.set_terminal_cursor_color();
220
221 let keybindings = Arc::new(RwLock::new(KeybindingResolver::new(&config)));
222
223 let mut buffers = HashMap::new();
225 let mut event_logs = HashMap::new();
226
227 let buffer_id = BufferId(1);
232 let mut state = EditorState::new(
233 width,
234 height,
235 config.editor.large_file_threshold_bytes as usize,
236 Arc::clone(&filesystem),
237 );
238 state
240 .margins
241 .configure_for_line_numbers(config.editor.line_numbers);
242 state.buffer_settings.tab_size = config.editor.tab_size;
243 state.buffer_settings.auto_close = config.editor.auto_close;
244 tracing::info!("EditorState created for buffer {:?}", buffer_id);
246 buffers.insert(buffer_id, state);
247 event_logs.insert(buffer_id, EventLog::new());
248
249 let mut buffer_metadata = HashMap::new();
251 buffer_metadata.insert(buffer_id, BufferMetadata::new());
252
253 let root_uri = types::file_path_to_lsp_uri(&working_dir);
255
256 let tokio_runtime = tokio::runtime::Builder::new_multi_thread()
258 .worker_threads(2) .thread_name("editor-async")
260 .enable_all()
261 .build()
262 .ok();
263
264 let async_bridge = AsyncBridge::new();
266
267 if tokio_runtime.is_none() {
268 tracing::warn!("Failed to create Tokio runtime - async features disabled");
269 }
270
271 let mut lsp = LspManager::new(root_uri);
273
274 if let Some(ref runtime) = tokio_runtime {
276 lsp.set_runtime(runtime.handle().clone(), async_bridge.clone());
277 }
278
279 for (language, lsp_configs) in &config.lsp {
281 lsp.set_language_configs(language.clone(), lsp_configs.as_slice().to_vec());
282 }
283
284 let universal_servers: Vec<LspServerConfig> = config
286 .universal_lsp
287 .values()
288 .flat_map(|lc| lc.as_slice().to_vec())
289 .filter(|c| c.enabled)
290 .collect();
291 lsp.set_universal_configs(universal_servers);
292
293 if working_dir.join("deno.json").exists() || working_dir.join("deno.jsonc").exists() {
296 tracing::info!("Detected Deno project (deno.json found), using deno lsp for JS/TS");
297 let deno_config = LspServerConfig {
298 command: "deno".to_string(),
299 args: vec!["lsp".to_string()],
300 enabled: true,
301 auto_start: false,
302 process_limits: ProcessLimits::default(),
303 initialization_options: Some(serde_json::json!({"enable": true})),
304 ..Default::default()
305 };
306 lsp.set_language_config("javascript".to_string(), deno_config.clone());
307 lsp.set_language_config("typescript".to_string(), deno_config);
308 }
309
310 let split_manager = SplitManager::new(buffer_id);
312
313 let mut split_view_states = HashMap::new();
315 let initial_split_id = split_manager.active_split();
316 let mut initial_view_state = SplitViewState::with_buffer(width, height, buffer_id);
317 initial_view_state.apply_config_defaults(
318 config.editor.line_numbers,
319 config.editor.highlight_current_line,
320 config.editor.line_wrap,
321 config.editor.wrap_indent,
322 config.editor.wrap_column,
323 config.editor.rulers.clone(),
324 );
325 split_view_states.insert(initial_split_id, initial_view_state);
326
327 let fs_manager = Arc::new(FsManager::new(Arc::clone(&filesystem)));
329
330 let command_registry = Arc::new(RwLock::new(CommandRegistry::new()));
332
333 let authority = crate::services::authority::Authority {
339 filesystem: Arc::clone(&filesystem),
340 ..crate::services::authority::Authority::local()
341 };
342 let process_spawner = Arc::clone(&authority.process_spawner);
343
344 let mut quick_open_registry = QuickOpenRegistry::new();
346 quick_open_registry.register(Box::new(FileProvider::new(
347 Arc::clone(&filesystem),
348 Arc::clone(&process_spawner),
349 tokio_runtime.as_ref().map(|rt| rt.handle().clone()),
350 Some(async_bridge.sender()),
351 )));
352 quick_open_registry.register(Box::new(CommandProvider::new(
353 Arc::clone(&command_registry),
354 Arc::clone(&keybindings),
355 )));
356 quick_open_registry.register(Box::new(BufferProvider::new()));
357 quick_open_registry.register(Box::new(GotoLineProvider::new()));
358
359 let theme_cache = Arc::new(RwLock::new(theme_registry.to_json_map()));
361
362 let plugin_manager = PluginManager::new(
364 enable_plugins,
365 Arc::clone(&command_registry),
366 dir_context.clone(),
367 Arc::clone(&theme_cache),
368 );
369
370 #[cfg(feature = "plugins")]
373 if let Some(snapshot_handle) = plugin_manager.state_snapshot_handle() {
374 let mut snapshot = snapshot_handle.write().unwrap();
375 snapshot.working_dir = working_dir.clone();
376 }
377
378 if plugin_manager.is_active() {
385 let mut plugin_dirs: Vec<std::path::PathBuf> = vec![];
386
387 if let Ok(exe_path) = std::env::current_exe() {
389 if let Some(exe_dir) = exe_path.parent() {
390 let exe_plugin_dir = exe_dir.join("plugins");
391 if exe_plugin_dir.exists() {
392 plugin_dirs.push(exe_plugin_dir);
393 }
394 }
395 }
396
397 let working_plugin_dir = working_dir.join("plugins");
399 if working_plugin_dir.exists() && !plugin_dirs.contains(&working_plugin_dir) {
400 plugin_dirs.push(working_plugin_dir);
401 }
402
403 #[cfg(feature = "embed-plugins")]
405 if plugin_dirs.is_empty() {
406 if let Some(embedded_dir) =
407 crate::services::plugins::embedded::get_embedded_plugins_dir()
408 {
409 tracing::info!("Using embedded plugins from: {:?}", embedded_dir);
410 plugin_dirs.push(embedded_dir.clone());
411 }
412 }
413
414 let user_plugins_dir = dir_context.config_dir.join("plugins");
416 if user_plugins_dir.exists() && !plugin_dirs.contains(&user_plugins_dir) {
417 tracing::info!("Found user plugins directory: {:?}", user_plugins_dir);
418 plugin_dirs.push(user_plugins_dir.clone());
419 }
420
421 let packages_dir = dir_context.config_dir.join("plugins").join("packages");
423 if packages_dir.exists() {
424 if let Ok(entries) = std::fs::read_dir(&packages_dir) {
425 for entry in entries.flatten() {
426 let path = entry.path();
427 if path.is_dir() {
429 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
430 if !name.starts_with('.') {
431 tracing::info!("Found package manager plugin: {:?}", path);
432 plugin_dirs.push(path);
433 }
434 }
435 }
436 }
437 }
438 }
439
440 for dir in &scan_result.bundle_plugin_dirs {
442 tracing::info!("Found bundle plugin directory: {:?}", dir);
443 plugin_dirs.push(dir.clone());
444 }
445
446 if plugin_dirs.is_empty() {
447 tracing::debug!(
448 "No plugins directory found next to executable or in working dir: {:?}",
449 working_dir
450 );
451 }
452
453 for plugin_dir in plugin_dirs {
455 tracing::info!("Loading TypeScript plugins from: {:?}", plugin_dir);
456 let (errors, discovered_plugins) =
457 plugin_manager.load_plugins_from_dir_with_config(&plugin_dir, &config.plugins);
458
459 for (name, plugin_config) in discovered_plugins {
462 config.plugins.insert(name, plugin_config);
463 }
464
465 if !errors.is_empty() {
466 for err in &errors {
467 tracing::error!("TypeScript plugin load error: {}", err);
468 }
469 #[cfg(debug_assertions)]
471 panic!(
472 "TypeScript plugin loading failed with {} error(s): {}",
473 errors.len(),
474 errors.join("; ")
475 );
476 }
477 }
478
479 let declarations = plugin_manager.plugin_declarations();
487 crate::init_script::write_plugin_declarations(&dir_context.config_dir, &declarations);
488 }
489
490 let file_explorer_width = config.file_explorer.width;
492 let recovery_enabled = config.editor.recovery_enabled;
493 let check_for_updates = config.check_for_updates;
494 let show_menu_bar = config.editor.show_menu_bar;
495 let show_tab_bar = config.editor.show_tab_bar;
496 let show_status_bar = config.editor.show_status_bar;
497 let show_prompt_line = config.editor.show_prompt_line;
498
499 let update_checker = if check_for_updates {
501 tracing::debug!("Update checking enabled, starting periodic checker");
502 Some(
503 crate::services::release_checker::start_periodic_update_check(
504 crate::services::release_checker::DEFAULT_RELEASES_URL,
505 time_source.clone(),
506 dir_context.data_dir.clone(),
507 ),
508 )
509 } else {
510 tracing::debug!("Update checking disabled by config");
511 None
512 };
513
514 let user_config_raw = Config::read_user_config_raw(&working_dir);
516
517 let config_arc = Arc::new(config);
524 let config_cached_json =
525 Arc::new(serde_json::to_value(&*config_arc).unwrap_or(serde_json::Value::Null));
526 let config_snapshot_anchor = Arc::clone(&config_arc);
527
528 let mut editor = Editor {
529 buffers,
530 event_logs,
531 next_buffer_id: 2,
532 config: config_arc,
533 config_snapshot_anchor,
534 config_cached_json,
535 user_config_raw: Arc::new(user_config_raw),
536 dir_context: dir_context.clone(),
537 grammar_registry,
538 pending_grammars: scan_result
539 .additional_grammars
540 .iter()
541 .map(|g| PendingGrammar {
542 language: g.language.clone(),
543 grammar_path: g.path.to_string_lossy().to_string(),
544 extensions: g.extensions.clone(),
545 })
546 .collect(),
547 grammar_reload_pending: false,
548 grammar_build_in_progress: false,
549 needs_full_grammar_build: true,
550 streaming_grep_cancellation: None,
551 pending_grammar_callbacks: Vec::new(),
552 theme,
553 theme_registry,
554 theme_cache,
555 ansi_background: None,
556 ansi_background_path: None,
557 background_fade: crate::primitives::ansi_background::DEFAULT_BACKGROUND_FADE,
558 keybindings,
559 clipboard: crate::services::clipboard::Clipboard::new(),
560 should_quit: false,
561 should_detach: false,
562 session_mode: false,
563 software_cursor_only: false,
564 session_name: None,
565 pending_escape_sequences: Vec::new(),
566 restart_with_dir: None,
567 status_message: None,
568 plugin_status_message: None,
569 last_window_title: None,
570 plugin_errors: Vec::new(),
571 prompt: None,
572 terminal_width: width,
573 terminal_height: height,
574 lsp: Some(lsp),
575 buffer_metadata,
576 mode_registry: ModeRegistry::new(),
577 tokio_runtime,
578 async_bridge: Some(async_bridge),
579 split_manager,
580 split_view_states,
581 previous_viewports: HashMap::new(),
582 scroll_sync_manager: ScrollSyncManager::new(),
583 file_explorer: None,
584 preview: None,
585 suppress_position_history_once: false,
586 fs_manager,
587 authority,
588 pending_authority: None,
589 remote_indicator_override: None,
590 local_filesystem: Arc::new(crate::model::filesystem::StdFileSystem),
591 file_explorer_visible: false,
592 file_explorer_sync_in_progress: false,
593 file_explorer_width,
594 pending_file_explorer_show_hidden: None,
595 pending_file_explorer_show_gitignored: None,
596 menu_bar_visible: show_menu_bar,
597 file_explorer_decorations: HashMap::new(),
598 file_explorer_decoration_cache:
599 crate::view::file_tree::FileExplorerDecorationCache::default(),
600 file_explorer_clipboard: None,
601 menu_bar_auto_shown: false,
602 tab_bar_visible: show_tab_bar,
603 status_bar_visible: show_status_bar,
604 prompt_line_visible: show_prompt_line,
605 mouse_enabled: true,
606 same_buffer_scroll_sync: false,
607 mouse_cursor_position: None,
608 gpm_active: false,
609 key_context: KeyContext::Normal,
610 menu_state: crate::view::ui::MenuState::new(dir_context.themes_dir()),
611 menus: crate::config::MenuConfig::translated(),
612 working_dir,
613 position_history: PositionHistory::new(),
614 in_navigation: false,
615 next_lsp_request_id: 0,
616 pending_completion_requests: HashSet::new(),
617 completion_items: None,
618 scheduled_completion_trigger: None,
619 completion_service: crate::services::completion::CompletionService::new(),
620 dabbrev_state: None,
621 pending_goto_definition_request: None,
622 hover: hover::HoverState::default(),
623 pending_references_request: None,
624 pending_references_symbol: String::new(),
625 pending_signature_help_request: None,
626 pending_code_actions_requests: HashSet::new(),
627 pending_code_actions_server_names: HashMap::new(),
628 pending_code_actions: None,
629 pending_inlay_hints_requests: HashMap::new(),
630 pending_folding_range_requests: HashMap::new(),
631 folding_ranges_in_flight: HashMap::new(),
632 folding_ranges_debounce: HashMap::new(),
633 pending_semantic_token_requests: HashMap::new(),
634 semantic_tokens_in_flight: HashMap::new(),
635 pending_semantic_token_range_requests: HashMap::new(),
636 semantic_tokens_range_in_flight: HashMap::new(),
637 semantic_tokens_range_last_request: HashMap::new(),
638 semantic_tokens_range_applied: HashMap::new(),
639 semantic_tokens_full_debounce: HashMap::new(),
640 search_state: None,
641 search_namespace: crate::view::overlay::OverlayNamespace::from_string(
642 "search".to_string(),
643 ),
644 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace::from_string(
645 "lsp-diagnostic".to_string(),
646 ),
647 pending_search_range: None,
648 interactive_replace_state: None,
649 mouse_state: MouseState::default(),
650 tab_context_menu: None,
651 file_explorer_context_menu: None,
652 theme_info_popup: None,
653 cached_layout: CachedLayout::default(),
654 command_registry,
655 quick_open_registry,
656 plugin_manager,
657 plugin_dev_workspaces: HashMap::new(),
658 seen_byte_ranges: HashMap::new(),
659 panel_ids: HashMap::new(),
660 buffer_groups: HashMap::new(),
661 buffer_to_group: HashMap::new(),
662 next_buffer_group_id: 0,
663 grouped_subtrees: HashMap::new(),
664 background_process_handles: HashMap::new(),
665 host_process_handles: HashMap::new(),
666 prompt_histories: {
667 let mut histories = HashMap::new();
669 for history_name in ["search", "replace", "goto_line"] {
670 let path = dir_context.prompt_history_path(history_name);
671 let history = crate::input::input_history::InputHistory::load_from_file(&path)
672 .unwrap_or_else(|e| {
673 tracing::warn!("Failed to load {} history: {}", history_name, e);
674 crate::input::input_history::InputHistory::new()
675 });
676 histories.insert(history_name.to_string(), history);
677 }
678 histories
679 },
680 pending_async_prompt_callback: None,
681 goto_line_preview: None,
682 lsp_progress: std::collections::HashMap::new(),
683 lsp_server_statuses: std::collections::HashMap::new(),
684 lsp_window_messages: Vec::new(),
685 lsp_log_messages: Vec::new(),
686 diagnostic_result_ids: HashMap::new(),
687 scheduled_diagnostic_pull: None,
688 scheduled_inlay_hints_request: None,
689 stored_push_diagnostics: HashMap::new(),
690 stored_pull_diagnostics: HashMap::new(),
691 stored_diagnostics: Arc::new(HashMap::new()),
692 stored_folding_ranges: Arc::new(HashMap::new()),
693 event_broadcaster: crate::model::control_event::EventBroadcaster::default(),
694 bookmarks: bookmarks::BookmarkState::default(),
695 search_case_sensitive: true,
696 search_whole_word: false,
697 search_use_regex: false,
698 search_confirm_each: false,
699 macros: macros::MacroState::default(),
700 #[cfg(feature = "plugins")]
701 pending_plugin_actions: Vec::new(),
702 #[cfg(feature = "plugins")]
703 plugin_render_requested: false,
704 chord_state: Vec::new(),
705 user_dismissed_lsp_languages: std::collections::HashSet::new(),
706 auto_start_prompted_languages: std::collections::HashSet::new(),
707 pending_auto_start_prompts: std::collections::HashSet::new(),
708 lsp_auto_prompt_enabled: super::lsp_auto_prompt::default_enabled(),
709 pending_close_buffer: None,
710 auto_revert_enabled: true,
711 last_auto_revert_poll: time_source.now(),
712 last_file_tree_poll: time_source.now(),
713 git_index_resolved: false,
714 file_mod_times: HashMap::new(),
715 dir_mod_times: HashMap::new(),
716 pending_file_poll_rx: None,
717 pending_dir_poll_rx: None,
718 file_rapid_change_counts: HashMap::new(),
719 file_open_state: None,
720 file_browser_layout: None,
721 recovery_service: {
722 let recovery_config = RecoveryConfig {
723 enabled: recovery_enabled,
724 ..RecoveryConfig::default()
725 };
726 RecoveryService::with_config_and_dir(recovery_config, dir_context.recovery_dir())
727 },
728 full_redraw_requested: false,
729 suspend_requested: false,
730 time_source: time_source.clone(),
731 last_auto_recovery_save: time_source.now(),
732 last_persistent_auto_save: time_source.now(),
733 active_custom_contexts: HashSet::new(),
734 plugin_global_state: HashMap::new(),
735 editor_mode: None,
736 warning_log: None,
737 status_log_path: None,
738 warning_domains: WarningDomainRegistry::new(),
739 update_checker,
740 terminal_manager: crate::services::terminal::TerminalManager::new(),
741 terminal_buffers: HashMap::new(),
742 terminal_backing_files: HashMap::new(),
743 terminal_log_files: HashMap::new(),
744 ephemeral_terminals: std::collections::HashSet::new(),
745 terminal_mode: false,
746 keyboard_capture: false,
747 terminal_mode_resume: std::collections::HashSet::new(),
748 previous_click_time: None,
749 previous_click_position: None,
750 click_count: 0,
751 settings_state: None,
752 calibration_wizard: None,
753 event_debug: None,
754 keybinding_editor: None,
755 key_translator: crate::input::key_translator::KeyTranslator::load_from_config_dir(
756 &dir_context.config_dir,
757 )
758 .unwrap_or_default(),
759 color_capability,
760 pending_file_opens: Vec::new(),
761 pending_hot_exit_recovery: false,
762 wait_tracking: HashMap::new(),
763 completed_waits: Vec::new(),
764 stdin_stream: stdin_stream::StdinStream::default(),
765 line_scan: line_scan::LineScan::default(),
766 search_scan: search_scan::SearchScan::default(),
767 search_overlay_top_byte: None,
768 review_hunks: Vec::new(),
769 global_popups: crate::view::popup::PopupManager::new(),
770 composite_buffers: HashMap::new(),
771 composite_view_states: HashMap::new(),
772 };
773
774 editor.clipboard.apply_config(&editor.config.clipboard);
776
777 #[cfg(feature = "plugins")]
778 {
779 editor.update_plugin_state_snapshot();
780 if editor.plugin_manager.is_active() {
781 editor.plugin_manager.run_hook(
782 "editor_initialized",
783 crate::services::plugins::hooks::HookArgs::EditorInitialized,
784 );
785 }
786 }
787
788 Ok(editor)
789 }
790
791 pub fn event_broadcaster(&self) -> &crate::model::control_event::EventBroadcaster {
793 &self.event_broadcaster
794 }
795
796 pub(super) fn start_background_grammar_build(
801 &mut self,
802 additional: Vec<crate::primitives::grammar::GrammarSpec>,
803 callback_ids: Vec<fresh_core::api::JsCallbackId>,
804 ) {
805 let Some(bridge) = &self.async_bridge else {
806 return;
807 };
808 self.grammar_build_in_progress = true;
809 let sender = bridge.sender();
810 let config_dir = self.dir_context.config_dir.clone();
811 tracing::info!(
812 "Spawning background grammar build thread ({} plugin grammars)...",
813 additional.len()
814 );
815 std::thread::Builder::new()
816 .name("grammar-build".to_string())
817 .spawn(move || {
818 tracing::info!("[grammar-build] Thread started");
819 let start = std::time::Instant::now();
820 let registry = if additional.is_empty() {
821 crate::primitives::grammar::GrammarRegistry::for_editor(config_dir)
822 } else {
823 crate::primitives::grammar::GrammarRegistry::for_editor_with_additional(
824 config_dir,
825 &additional,
826 )
827 };
828 tracing::info!("[grammar-build] Complete in {:?}", start.elapsed());
829 drop(sender.send(
830 crate::services::async_bridge::AsyncMessage::GrammarRegistryBuilt {
831 registry,
832 callback_ids,
833 },
834 ));
835 })
836 .ok();
837 }
838
839 pub fn load_init_script(&mut self, enabled: bool) {
846 use crate::init_script::{
847 check, decide_load, describe, record_success, refresh_types_scaffolding, CheckSeverity,
848 InitOutcome, LoadDecision,
849 };
850
851 let config_dir = self.dir_context.config_dir.clone();
852
853 if enabled {
854 refresh_types_scaffolding(&config_dir);
858
859 let report = check(&config_dir);
864 if !report.ok {
865 for d in &report.diagnostics {
866 let level = match d.severity {
867 CheckSeverity::Error => "error",
868 CheckSeverity::Warning => "warning",
869 };
870 tracing::warn!(
871 "init.ts pre-load {level} at {}:{}: {}",
872 d.line,
873 d.column,
874 d.message
875 );
876 }
877 }
878 }
879
880 let outcome = match decide_load(&config_dir, enabled) {
881 LoadDecision::Skip(outcome) => outcome,
882 LoadDecision::Load { source } => {
883 if !self.plugin_manager.is_active() {
884 InitOutcome::Failed {
885 message: "plugin runtime inactive (--no-plugins); init.ts cannot run"
886 .into(),
887 }
888 } else {
889 match self.plugin_manager.load_plugin_from_source(
890 &source,
891 crate::init_script::INIT_PLUGIN_NAME,
892 true,
893 ) {
894 Ok(()) => {
895 record_success(&config_dir);
896 InitOutcome::Loaded
897 }
898 Err(e) => InitOutcome::Failed {
899 message: format!("{e}"),
900 },
901 }
902 }
903 }
904 };
905
906 let summary = describe(&outcome);
907 match outcome {
908 InitOutcome::NotFound | InitOutcome::Disabled => tracing::debug!("{}", summary),
909 InitOutcome::Loaded => tracing::info!("{}", summary),
910 InitOutcome::CrashFused { .. } | InitOutcome::Failed { .. } => {
911 tracing::warn!("{}", summary);
912 self.set_status_message(summary);
913 }
914 }
915 }
916
917 pub fn handle_set_setting(&mut self, path: String, value: serde_json::Value) {
921 let mut json = serde_json::to_value(&*self.config).unwrap_or_default();
922 set_dot_path(&mut json, &path, value);
923 match serde_json::from_value::<crate::config::Config>(json) {
924 Ok(new_config) => {
925 let old_theme = self.config.theme.clone();
926 self.config = Arc::new(new_config);
927 if old_theme != self.config.theme {
928 if let Some(theme) = self.theme_registry.get_cloned(&self.config.theme) {
929 self.theme = theme;
930 }
931 }
932 *self.keybindings.write().unwrap() =
933 crate::input::keybindings::KeybindingResolver::new(&self.config);
934 self.clipboard.apply_config(&self.config.clipboard);
935 self.menu_bar_visible = self.config.editor.show_menu_bar;
936 self.tab_bar_visible = self.config.editor.show_tab_bar;
937 self.status_bar_visible = self.config.editor.show_status_bar;
938 self.prompt_line_visible = self.config.editor.show_prompt_line;
939 #[cfg(feature = "plugins")]
940 self.update_plugin_state_snapshot();
941 }
942 Err(e) => {
943 self.set_status_message(format!("setSetting({path}): {e}"));
944 }
945 }
946 }
947
948 pub fn fire_plugins_loaded_hook(&self) {
950 #[cfg(feature = "plugins")]
951 if self.plugin_manager.is_active() {
952 self.plugin_manager.run_hook(
953 "plugins_loaded",
954 crate::services::plugins::hooks::HookArgs::PluginsLoaded,
955 );
956 }
957 }
958
959 pub fn fire_ready_hook(&self) {
961 #[cfg(feature = "plugins")]
962 if self.plugin_manager.is_active() {
963 self.plugin_manager
964 .run_hook("ready", crate::services::plugins::hooks::HookArgs::Ready);
965 }
966 }
967
968 #[doc(hidden)]
970 pub fn config_for_tests(&self) -> &crate::config::Config {
971 &self.config
972 }
973
974 #[doc(hidden)]
976 pub fn dispatch_action_for_tests(&mut self, action: crate::input::keybindings::Action) {
977 if let Err(e) = self.handle_action(action) {
978 tracing::warn!("dispatch_action_for_tests: {e}");
979 }
980 }
981}