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