Skip to main content

fresh/app/
editor_init.rs

1//! Editor construction and initialization.
2//!
3//! `Editor::new` and friends — the entry points that take a configuration,
4//! terminal dimensions, color capability, and filesystem implementation
5//! and return a ready-to-use Editor with every field initialized.
6//!
7//! Also includes `start_background_grammar_build`, which kicks off the
8//! initial grammar registry build asynchronously so startup doesn't block.
9
10// Re-use everything mod.rs imports — the constructors touch every field
11// on Editor and most of the types in the module.
12use super::*;
13
14impl Editor {
15    /// Create a new editor with the given configuration and terminal dimensions
16    /// Uses system directories for state (recovery, sessions, etc.)
17    pub fn new(
18        config: Config,
19        width: u16,
20        height: u16,
21        dir_context: DirectoryContext,
22        color_capability: crate::view::color_support::ColorCapability,
23        filesystem: Arc<dyn FileSystem + Send + Sync>,
24    ) -> AnyhowResult<Self> {
25        Self::with_working_dir(
26            config,
27            width,
28            height,
29            None,
30            dir_context,
31            true,
32            color_capability,
33            filesystem,
34        )
35    }
36
37    /// Create a new editor with an explicit working directory
38    /// This is useful for testing with isolated temporary directories
39    #[allow(clippy::too_many_arguments)]
40    pub fn with_working_dir(
41        config: Config,
42        width: u16,
43        height: u16,
44        working_dir: Option<PathBuf>,
45        dir_context: DirectoryContext,
46        plugins_enabled: bool,
47        color_capability: crate::view::color_support::ColorCapability,
48        filesystem: Arc<dyn FileSystem + Send + Sync>,
49    ) -> AnyhowResult<Self> {
50        tracing::info!("Building default grammar registry...");
51        let start = std::time::Instant::now();
52        let mut grammar_registry = crate::primitives::grammar::GrammarRegistry::defaults_only();
53        // Merge user config so find_by_path respects user globs/filenames
54        // from the very first lookup. `defaults_only` just built the Arc, so
55        // we're the sole owner; get_mut is guaranteed to succeed. Assert
56        // rather than silently drop config — a failure here would leave the
57        // user wondering why their `*.conf → bash` rule doesn't highlight.
58        std::sync::Arc::get_mut(&mut grammar_registry)
59            .expect("defaults_only returned a shared Arc")
60            .apply_language_config(&config.languages);
61        tracing::info!("Default grammar registry built in {:?}", start.elapsed());
62        // Don't start background grammar build here — it's deferred to the
63        // first flush_pending_grammars() call so that plugin-registered grammars
64        // from the first event-loop tick are included in a single build.
65        Self::with_options(
66            config,
67            width,
68            height,
69            working_dir,
70            filesystem,
71            plugins_enabled,
72            dir_context,
73            None,
74            color_capability,
75            grammar_registry,
76        )
77    }
78
79    /// Create a new editor for testing with custom backends
80    ///
81    /// By default uses empty grammar registry for fast initialization.
82    /// Pass `Some(registry)` for tests that need syntax highlighting or shebang detection.
83    #[allow(clippy::too_many_arguments)]
84    pub fn for_test(
85        config: Config,
86        width: u16,
87        height: u16,
88        working_dir: Option<PathBuf>,
89        dir_context: DirectoryContext,
90        color_capability: crate::view::color_support::ColorCapability,
91        filesystem: Arc<dyn FileSystem + Send + Sync>,
92        time_source: Option<SharedTimeSource>,
93        grammar_registry: Option<Arc<crate::primitives::grammar::GrammarRegistry>>,
94    ) -> AnyhowResult<Self> {
95        let mut grammar_registry =
96            grammar_registry.unwrap_or_else(crate::primitives::grammar::GrammarRegistry::empty);
97        // Merge user `[languages]` config into the catalog — production code
98        // does this at startup and again after the background grammar build,
99        // tests need the same so config-declared grammars/extensions resolve
100        // through `find_by_path`. Both call sites that feed into `for_test`
101        // (`HarnessOptions::with_full_grammar_registry` and the default
102        // `GrammarRegistry::empty()`) hand us the sole Arc owner.
103        std::sync::Arc::get_mut(&mut grammar_registry)
104            .expect("grammar registry Arc must be uniquely owned at for_test entry")
105            .apply_language_config(&config.languages);
106        let mut editor = Self::with_options(
107            config,
108            width,
109            height,
110            working_dir,
111            filesystem,
112            true,
113            dir_context,
114            time_source,
115            color_capability,
116            grammar_registry,
117        )?;
118        // Tests typically have no async_bridge, so the deferred grammar build
119        // would just drain pending_grammars and early-return. Skip it entirely.
120        editor.needs_full_grammar_build = false;
121        Ok(editor)
122    }
123
124    /// Create a new editor with custom options
125    /// This is primarily used for testing with slow or mock backends
126    /// to verify editor behavior under various I/O conditions
127    #[allow(clippy::too_many_arguments)]
128    fn with_options(
129        mut config: Config,
130        width: u16,
131        height: u16,
132        working_dir: Option<PathBuf>,
133        filesystem: Arc<dyn FileSystem + Send + Sync>,
134        enable_plugins: bool,
135        dir_context: DirectoryContext,
136        time_source: Option<SharedTimeSource>,
137        color_capability: crate::view::color_support::ColorCapability,
138        grammar_registry: Arc<crate::primitives::grammar::GrammarRegistry>,
139    ) -> AnyhowResult<Self> {
140        // Use provided time_source or default to RealTimeSource
141        let time_source = time_source.unwrap_or_else(RealTimeSource::shared);
142        tracing::info!("Editor::new called with width={}, height={}", width, height);
143
144        // Use provided working_dir or capture from environment
145        let working_dir = working_dir
146            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
147
148        // Canonicalize working_dir to resolve symlinks and normalize path components
149        // This ensures consistent path comparisons throughout the editor
150        let working_dir = working_dir.canonicalize().unwrap_or(working_dir);
151
152        // Load all themes into registry
153        tracing::info!("Loading themes...");
154        let theme_loader = crate::view::theme::ThemeLoader::new(dir_context.themes_dir());
155        // Scan installed packages (language packs + bundles) before plugin loading.
156        // This replaces the JS loadInstalledPackages() — configs, grammars, plugin dirs,
157        // and theme dirs are all collected here and applied synchronously.
158        let scan_result =
159            crate::services::packages::scan_installed_packages(&dir_context.config_dir);
160
161        // Apply package language configs (user config takes priority via or_insert)
162        for (lang_id, lang_config) in &scan_result.language_configs {
163            config
164                .languages
165                .entry(lang_id.clone())
166                .or_insert_with(|| lang_config.clone());
167        }
168
169        // Apply package LSP configs (user config takes priority via or_insert)
170        for (lang_id, lsp_config) in &scan_result.lsp_configs {
171            config
172                .lsp
173                .entry(lang_id.clone())
174                .or_insert_with(|| LspLanguageConfig::Multi(vec![lsp_config.clone()]));
175        }
176
177        let theme_registry = theme_loader.load_all(&scan_result.bundle_theme_dirs);
178        tracing::info!("Themes loaded");
179
180        // Get active theme from registry, falling back to default if not found
181        let theme = theme_registry.get_cloned(&config.theme).unwrap_or_else(|| {
182            tracing::warn!(
183                "Theme '{}' not found, falling back to default theme",
184                config.theme.0
185            );
186            theme_registry
187                .get_cloned(&crate::config::ThemeName(
188                    crate::view::theme::THEME_HIGH_CONTRAST.to_string(),
189                ))
190                .expect("Default theme must exist")
191        });
192
193        // Set terminal cursor color to match theme
194        theme.set_terminal_cursor_color();
195
196        let keybindings = Arc::new(RwLock::new(KeybindingResolver::new(&config)));
197
198        // Create an empty initial buffer
199        let mut buffers = HashMap::new();
200        let mut event_logs = HashMap::new();
201
202        // Buffer IDs start at 1 (not 0) because the plugin API returns 0 to
203        // mean "no active buffer" from getActiveBufferId().  JavaScript treats
204        // 0 as falsy (`if (!bufferId)` would wrongly reject buffer 0), so
205        // using 1-based IDs avoids this entire class of bugs in plugins.
206        let buffer_id = BufferId(1);
207        let mut state = EditorState::new(
208            width,
209            height,
210            config.editor.large_file_threshold_bytes as usize,
211            Arc::clone(&filesystem),
212        );
213        // Configure initial buffer settings from config
214        state
215            .margins
216            .configure_for_line_numbers(config.editor.line_numbers);
217        state.buffer_settings.tab_size = config.editor.tab_size;
218        state.buffer_settings.auto_close = config.editor.auto_close;
219        // Note: line_wrap_enabled is now stored in SplitViewState.viewport
220        tracing::info!("EditorState created for buffer {:?}", buffer_id);
221        buffers.insert(buffer_id, state);
222        event_logs.insert(buffer_id, EventLog::new());
223
224        // Create metadata for the initial empty buffer
225        let mut buffer_metadata = HashMap::new();
226        buffer_metadata.insert(buffer_id, BufferMetadata::new());
227
228        // Initialize LSP manager with current working directory as root
229        let root_uri = types::file_path_to_lsp_uri(&working_dir);
230
231        // Create Tokio runtime for async I/O (LSP, file watching, git, etc.)
232        let tokio_runtime = tokio::runtime::Builder::new_multi_thread()
233            .worker_threads(2) // Small pool for I/O tasks
234            .thread_name("editor-async")
235            .enable_all()
236            .build()
237            .ok();
238
239        // Create async bridge for communication
240        let async_bridge = AsyncBridge::new();
241
242        if tokio_runtime.is_none() {
243            tracing::warn!("Failed to create Tokio runtime - async features disabled");
244        }
245
246        // Create LSP manager with async support
247        let mut lsp = LspManager::new(root_uri);
248
249        // Configure runtime and bridge if available
250        if let Some(ref runtime) = tokio_runtime {
251            lsp.set_runtime(runtime.handle().clone(), async_bridge.clone());
252        }
253
254        // Configure LSP servers from config
255        for (language, lsp_configs) in &config.lsp {
256            lsp.set_language_configs(language.clone(), lsp_configs.as_slice().to_vec());
257        }
258
259        // Configure universal (global) LSP servers — spawned once, shared across languages
260        let universal_servers: Vec<LspServerConfig> = config
261            .universal_lsp
262            .values()
263            .flat_map(|lc| lc.as_slice().to_vec())
264            .filter(|c| c.enabled)
265            .collect();
266        lsp.set_universal_configs(universal_servers);
267
268        // Auto-detect Deno projects: if deno.json or deno.jsonc exists in the
269        // workspace root, override JS/TS LSP to use `deno lsp` (#1191)
270        if working_dir.join("deno.json").exists() || working_dir.join("deno.jsonc").exists() {
271            tracing::info!("Detected Deno project (deno.json found), using deno lsp for JS/TS");
272            let deno_config = LspServerConfig {
273                command: "deno".to_string(),
274                args: vec!["lsp".to_string()],
275                enabled: true,
276                auto_start: false,
277                process_limits: ProcessLimits::default(),
278                initialization_options: Some(serde_json::json!({"enable": true})),
279                ..Default::default()
280            };
281            lsp.set_language_config("javascript".to_string(), deno_config.clone());
282            lsp.set_language_config("typescript".to_string(), deno_config);
283        }
284
285        // Initialize split manager with the initial buffer
286        let split_manager = SplitManager::new(buffer_id);
287
288        // Initialize per-split view state for the initial split
289        let mut split_view_states = HashMap::new();
290        let initial_split_id = split_manager.active_split();
291        let mut initial_view_state = SplitViewState::with_buffer(width, height, buffer_id);
292        initial_view_state.apply_config_defaults(
293            config.editor.line_numbers,
294            config.editor.highlight_current_line,
295            config.editor.line_wrap,
296            config.editor.wrap_indent,
297            config.editor.wrap_column,
298            config.editor.rulers.clone(),
299        );
300        split_view_states.insert(initial_split_id, initial_view_state);
301
302        // Initialize filesystem manager for file explorer
303        let fs_manager = Arc::new(FsManager::new(Arc::clone(&filesystem)));
304
305        // Initialize command registry (always available, used by both plugins and core)
306        let command_registry = Arc::new(RwLock::new(CommandRegistry::new()));
307
308        // Initialize Quick Open registry with all providers
309        let mut quick_open_registry = QuickOpenRegistry::new();
310        let process_spawner: Arc<dyn crate::services::remote::ProcessSpawner> =
311            Arc::new(crate::services::remote::LocalProcessSpawner);
312        quick_open_registry.register(Box::new(FileProvider::new(
313            Arc::clone(&filesystem),
314            Arc::clone(&process_spawner),
315            tokio_runtime.as_ref().map(|rt| rt.handle().clone()),
316            Some(async_bridge.sender()),
317        )));
318        quick_open_registry.register(Box::new(CommandProvider::new(
319            Arc::clone(&command_registry),
320            Arc::clone(&keybindings),
321        )));
322        quick_open_registry.register(Box::new(BufferProvider::new()));
323        quick_open_registry.register(Box::new(GotoLineProvider::new()));
324
325        // Build shared theme cache for plugin access
326        let theme_cache = Arc::new(RwLock::new(theme_registry.to_json_map()));
327
328        // Initialize plugin manager (handles both enabled and disabled cases internally)
329        let plugin_manager = PluginManager::new(
330            enable_plugins,
331            Arc::clone(&command_registry),
332            dir_context.clone(),
333            Arc::clone(&theme_cache),
334        );
335
336        // Update the plugin state snapshot with working_dir BEFORE loading plugins
337        // This ensures plugins can call getCwd() correctly during initialization
338        #[cfg(feature = "plugins")]
339        if let Some(snapshot_handle) = plugin_manager.state_snapshot_handle() {
340            let mut snapshot = snapshot_handle.write().unwrap();
341            snapshot.working_dir = working_dir.clone();
342        }
343
344        // Load TypeScript plugins from multiple directories:
345        // 1. Next to the executable (for cargo-dist installations)
346        // 2. In the working directory (for development/local usage)
347        // 3. From embedded plugins (for cargo-binstall, when embed-plugins feature is enabled)
348        // 4. User plugins directory (~/.config/fresh/plugins)
349        // 5. Package manager installed plugins (~/.config/fresh/plugins/packages/*)
350        if plugin_manager.is_active() {
351            let mut plugin_dirs: Vec<std::path::PathBuf> = vec![];
352
353            // Check next to executable first (for cargo-dist installations)
354            if let Ok(exe_path) = std::env::current_exe() {
355                if let Some(exe_dir) = exe_path.parent() {
356                    let exe_plugin_dir = exe_dir.join("plugins");
357                    if exe_plugin_dir.exists() {
358                        plugin_dirs.push(exe_plugin_dir);
359                    }
360                }
361            }
362
363            // Then check working directory (for development)
364            let working_plugin_dir = working_dir.join("plugins");
365            if working_plugin_dir.exists() && !plugin_dirs.contains(&working_plugin_dir) {
366                plugin_dirs.push(working_plugin_dir);
367            }
368
369            // If no disk plugins found, try embedded plugins (cargo-binstall builds)
370            #[cfg(feature = "embed-plugins")]
371            if plugin_dirs.is_empty() {
372                if let Some(embedded_dir) =
373                    crate::services::plugins::embedded::get_embedded_plugins_dir()
374                {
375                    tracing::info!("Using embedded plugins from: {:?}", embedded_dir);
376                    plugin_dirs.push(embedded_dir.clone());
377                }
378            }
379
380            // Always check user config plugins directory (~/.config/fresh/plugins)
381            let user_plugins_dir = dir_context.config_dir.join("plugins");
382            if user_plugins_dir.exists() && !plugin_dirs.contains(&user_plugins_dir) {
383                tracing::info!("Found user plugins directory: {:?}", user_plugins_dir);
384                plugin_dirs.push(user_plugins_dir.clone());
385            }
386
387            // Check for package manager installed plugins (~/.config/fresh/plugins/packages/*)
388            let packages_dir = dir_context.config_dir.join("plugins").join("packages");
389            if packages_dir.exists() {
390                if let Ok(entries) = std::fs::read_dir(&packages_dir) {
391                    for entry in entries.flatten() {
392                        let path = entry.path();
393                        // Skip hidden directories (like .index for registry cache)
394                        if path.is_dir() {
395                            if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
396                                if !name.starts_with('.') {
397                                    tracing::info!("Found package manager plugin: {:?}", path);
398                                    plugin_dirs.push(path);
399                                }
400                            }
401                        }
402                    }
403                }
404            }
405
406            // Add bundle plugin directories from package scan
407            for dir in &scan_result.bundle_plugin_dirs {
408                tracing::info!("Found bundle plugin directory: {:?}", dir);
409                plugin_dirs.push(dir.clone());
410            }
411
412            if plugin_dirs.is_empty() {
413                tracing::debug!(
414                    "No plugins directory found next to executable or in working dir: {:?}",
415                    working_dir
416                );
417            }
418
419            // Load from all found plugin directories, respecting config
420            for plugin_dir in plugin_dirs {
421                tracing::info!("Loading TypeScript plugins from: {:?}", plugin_dir);
422                let (errors, discovered_plugins) =
423                    plugin_manager.load_plugins_from_dir_with_config(&plugin_dir, &config.plugins);
424
425                // Merge discovered plugins into config
426                // discovered_plugins already contains the merged config (saved enabled state + discovered path)
427                for (name, plugin_config) in discovered_plugins {
428                    config.plugins.insert(name, plugin_config);
429                }
430
431                if !errors.is_empty() {
432                    for err in &errors {
433                        tracing::error!("TypeScript plugin load error: {}", err);
434                    }
435                    // In debug/test builds, panic to surface plugin loading errors
436                    #[cfg(debug_assertions)]
437                    panic!(
438                        "TypeScript plugin loading failed with {} error(s): {}",
439                        errors.len(),
440                        errors.join("; ")
441                    );
442                }
443            }
444        }
445
446        // Extract config values before moving config into the struct
447        let file_explorer_width = config.file_explorer.width;
448        let recovery_enabled = config.editor.recovery_enabled;
449        let check_for_updates = config.check_for_updates;
450        let show_menu_bar = config.editor.show_menu_bar;
451        let show_tab_bar = config.editor.show_tab_bar;
452        let show_status_bar = config.editor.show_status_bar;
453        let show_prompt_line = config.editor.show_prompt_line;
454
455        // Start periodic update checker if enabled (also sends daily telemetry)
456        let update_checker = if check_for_updates {
457            tracing::debug!("Update checking enabled, starting periodic checker");
458            Some(
459                crate::services::release_checker::start_periodic_update_check(
460                    crate::services::release_checker::DEFAULT_RELEASES_URL,
461                    time_source.clone(),
462                    dir_context.data_dir.clone(),
463                ),
464            )
465        } else {
466            tracing::debug!("Update checking disabled by config");
467            None
468        };
469
470        // Cache raw user config at startup (to avoid re-reading file every frame)
471        let user_config_raw = Config::read_user_config_raw(&working_dir);
472
473        // Wrap config in Arc and pre-seed the snapshot mirror + JSON cache.
474        // Doing this at construction means the strong count of the live
475        // `config` Arc starts at 2 and stays there: every `Arc::make_mut`
476        // call on `config` is forced to CoW, so no mutation path (direct or
477        // via `config_mut()`) can leave `config_cached_json` referring to
478        // stale memory.
479        let config_arc = Arc::new(config);
480        let config_cached_json =
481            Arc::new(serde_json::to_value(&*config_arc).unwrap_or(serde_json::Value::Null));
482        let config_snapshot_anchor = Arc::clone(&config_arc);
483
484        let mut editor = Editor {
485            buffers,
486            event_logs,
487            next_buffer_id: 2,
488            config: config_arc,
489            config_snapshot_anchor,
490            config_cached_json,
491            user_config_raw: Arc::new(user_config_raw),
492            dir_context: dir_context.clone(),
493            grammar_registry,
494            pending_grammars: scan_result
495                .additional_grammars
496                .iter()
497                .map(|g| PendingGrammar {
498                    language: g.language.clone(),
499                    grammar_path: g.path.to_string_lossy().to_string(),
500                    extensions: g.extensions.clone(),
501                })
502                .collect(),
503            grammar_reload_pending: false,
504            grammar_build_in_progress: false,
505            needs_full_grammar_build: true,
506            streaming_grep_cancellation: None,
507            pending_grammar_callbacks: Vec::new(),
508            theme,
509            theme_registry,
510            theme_cache,
511            ansi_background: None,
512            ansi_background_path: None,
513            background_fade: crate::primitives::ansi_background::DEFAULT_BACKGROUND_FADE,
514            keybindings,
515            clipboard: crate::services::clipboard::Clipboard::new(),
516            should_quit: false,
517            should_detach: false,
518            session_mode: false,
519            software_cursor_only: false,
520            session_name: None,
521            pending_escape_sequences: Vec::new(),
522            restart_with_dir: None,
523            status_message: None,
524            plugin_status_message: None,
525            plugin_errors: Vec::new(),
526            prompt: None,
527            terminal_width: width,
528            terminal_height: height,
529            lsp: Some(lsp),
530            buffer_metadata,
531            mode_registry: ModeRegistry::new(),
532            tokio_runtime,
533            async_bridge: Some(async_bridge),
534            split_manager,
535            split_view_states,
536            previous_viewports: HashMap::new(),
537            scroll_sync_manager: ScrollSyncManager::new(),
538            file_explorer: None,
539            preview: None,
540            suppress_position_history_once: false,
541            fs_manager,
542            filesystem,
543            local_filesystem: Arc::new(crate::model::filesystem::StdFileSystem),
544            process_spawner,
545            file_explorer_visible: false,
546            file_explorer_sync_in_progress: false,
547            file_explorer_width_percent: file_explorer_width,
548            pending_file_explorer_show_hidden: None,
549            pending_file_explorer_show_gitignored: None,
550            menu_bar_visible: show_menu_bar,
551            file_explorer_decorations: HashMap::new(),
552            file_explorer_decoration_cache:
553                crate::view::file_tree::FileExplorerDecorationCache::default(),
554            menu_bar_auto_shown: false,
555            tab_bar_visible: show_tab_bar,
556            status_bar_visible: show_status_bar,
557            prompt_line_visible: show_prompt_line,
558            mouse_enabled: true,
559            same_buffer_scroll_sync: false,
560            mouse_cursor_position: None,
561            gpm_active: false,
562            key_context: KeyContext::Normal,
563            menu_state: crate::view::ui::MenuState::new(dir_context.themes_dir()),
564            menus: crate::config::MenuConfig::translated(),
565            working_dir,
566            position_history: PositionHistory::new(),
567            in_navigation: false,
568            next_lsp_request_id: 0,
569            pending_completion_requests: HashSet::new(),
570            completion_items: None,
571            scheduled_completion_trigger: None,
572            completion_service: crate::services::completion::CompletionService::new(),
573            dabbrev_state: None,
574            pending_goto_definition_request: None,
575            hover: hover::HoverState::default(),
576            pending_references_request: None,
577            pending_references_symbol: String::new(),
578            pending_signature_help_request: None,
579            pending_code_actions_requests: HashSet::new(),
580            pending_code_actions_server_names: HashMap::new(),
581            pending_code_actions: None,
582            pending_inlay_hints_requests: HashMap::new(),
583            pending_folding_range_requests: HashMap::new(),
584            folding_ranges_in_flight: HashMap::new(),
585            folding_ranges_debounce: HashMap::new(),
586            pending_semantic_token_requests: HashMap::new(),
587            semantic_tokens_in_flight: HashMap::new(),
588            pending_semantic_token_range_requests: HashMap::new(),
589            semantic_tokens_range_in_flight: HashMap::new(),
590            semantic_tokens_range_last_request: HashMap::new(),
591            semantic_tokens_range_applied: HashMap::new(),
592            semantic_tokens_full_debounce: HashMap::new(),
593            search_state: None,
594            search_namespace: crate::view::overlay::OverlayNamespace::from_string(
595                "search".to_string(),
596            ),
597            lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace::from_string(
598                "lsp-diagnostic".to_string(),
599            ),
600            pending_search_range: None,
601            interactive_replace_state: None,
602            mouse_state: MouseState::default(),
603            tab_context_menu: None,
604            theme_info_popup: None,
605            cached_layout: CachedLayout::default(),
606            command_registry,
607            quick_open_registry,
608            plugin_manager,
609            plugin_dev_workspaces: HashMap::new(),
610            seen_byte_ranges: HashMap::new(),
611            panel_ids: HashMap::new(),
612            buffer_groups: HashMap::new(),
613            buffer_to_group: HashMap::new(),
614            next_buffer_group_id: 0,
615            grouped_subtrees: HashMap::new(),
616            background_process_handles: HashMap::new(),
617            prompt_histories: {
618                // Load prompt histories from disk if available
619                let mut histories = HashMap::new();
620                for history_name in ["search", "replace", "goto_line"] {
621                    let path = dir_context.prompt_history_path(history_name);
622                    let history = crate::input::input_history::InputHistory::load_from_file(&path)
623                        .unwrap_or_else(|e| {
624                            tracing::warn!("Failed to load {} history: {}", history_name, e);
625                            crate::input::input_history::InputHistory::new()
626                        });
627                    histories.insert(history_name.to_string(), history);
628                }
629                histories
630            },
631            pending_async_prompt_callback: None,
632            lsp_progress: std::collections::HashMap::new(),
633            lsp_server_statuses: std::collections::HashMap::new(),
634            lsp_window_messages: Vec::new(),
635            lsp_log_messages: Vec::new(),
636            diagnostic_result_ids: HashMap::new(),
637            scheduled_diagnostic_pull: None,
638            scheduled_inlay_hints_request: None,
639            stored_push_diagnostics: HashMap::new(),
640            stored_pull_diagnostics: HashMap::new(),
641            stored_diagnostics: Arc::new(HashMap::new()),
642            stored_folding_ranges: Arc::new(HashMap::new()),
643            event_broadcaster: crate::model::control_event::EventBroadcaster::default(),
644            bookmarks: bookmarks::BookmarkState::default(),
645            search_case_sensitive: true,
646            search_whole_word: false,
647            search_use_regex: false,
648            search_confirm_each: false,
649            macros: macros::MacroState::default(),
650            #[cfg(feature = "plugins")]
651            pending_plugin_actions: Vec::new(),
652            #[cfg(feature = "plugins")]
653            plugin_render_requested: false,
654            chord_state: Vec::new(),
655            pending_lsp_confirmation: None,
656            pending_lsp_status_popup: None,
657            user_dismissed_lsp_languages: std::collections::HashSet::new(),
658            pending_close_buffer: None,
659            auto_revert_enabled: true,
660            last_auto_revert_poll: time_source.now(),
661            last_file_tree_poll: time_source.now(),
662            git_index_resolved: false,
663            file_mod_times: HashMap::new(),
664            dir_mod_times: HashMap::new(),
665            pending_file_poll_rx: None,
666            pending_dir_poll_rx: None,
667            file_rapid_change_counts: HashMap::new(),
668            file_open_state: None,
669            file_browser_layout: None,
670            recovery_service: {
671                let recovery_config = RecoveryConfig {
672                    enabled: recovery_enabled,
673                    ..RecoveryConfig::default()
674                };
675                RecoveryService::with_config_and_dir(recovery_config, dir_context.recovery_dir())
676            },
677            full_redraw_requested: false,
678            time_source: time_source.clone(),
679            last_auto_recovery_save: time_source.now(),
680            last_persistent_auto_save: time_source.now(),
681            active_custom_contexts: HashSet::new(),
682            plugin_global_state: HashMap::new(),
683            editor_mode: None,
684            warning_log: None,
685            status_log_path: None,
686            warning_domains: WarningDomainRegistry::new(),
687            update_checker,
688            terminal_manager: crate::services::terminal::TerminalManager::new(),
689            terminal_buffers: HashMap::new(),
690            terminal_backing_files: HashMap::new(),
691            terminal_log_files: HashMap::new(),
692            terminal_mode: false,
693            keyboard_capture: false,
694            terminal_mode_resume: std::collections::HashSet::new(),
695            previous_click_time: None,
696            previous_click_position: None,
697            click_count: 0,
698            settings_state: None,
699            calibration_wizard: None,
700            event_debug: None,
701            keybinding_editor: None,
702            key_translator: crate::input::key_translator::KeyTranslator::load_from_config_dir(
703                &dir_context.config_dir,
704            )
705            .unwrap_or_default(),
706            color_capability,
707            pending_file_opens: Vec::new(),
708            pending_hot_exit_recovery: false,
709            wait_tracking: HashMap::new(),
710            completed_waits: Vec::new(),
711            stdin_stream: stdin_stream::StdinStream::default(),
712            line_scan: line_scan::LineScan::default(),
713            search_scan: search_scan::SearchScan::default(),
714            search_overlay_top_byte: None,
715            review_hunks: Vec::new(),
716            active_action_popup: None,
717            composite_buffers: HashMap::new(),
718            composite_view_states: HashMap::new(),
719        };
720
721        // Apply clipboard configuration
722        editor.clipboard.apply_config(&editor.config.clipboard);
723
724        #[cfg(feature = "plugins")]
725        {
726            editor.update_plugin_state_snapshot();
727            if editor.plugin_manager.is_active() {
728                editor.plugin_manager.run_hook(
729                    "editor_initialized",
730                    crate::services::plugins::hooks::HookArgs::EditorInitialized,
731                );
732            }
733        }
734
735        Ok(editor)
736    }
737
738    /// Get a reference to the event broadcaster
739    pub fn event_broadcaster(&self) -> &crate::model::control_event::EventBroadcaster {
740        &self.event_broadcaster
741    }
742
743    /// Spawn a background thread to build the full grammar registry
744    /// (embedded grammars, user grammars, language packs, and any plugin-registered grammars).
745    /// Called on the first event-loop tick (via `flush_pending_grammars`) so that
746    /// plugin grammars registered during init are included in a single build.
747    pub(super) fn start_background_grammar_build(
748        &mut self,
749        additional: Vec<crate::primitives::grammar::GrammarSpec>,
750        callback_ids: Vec<fresh_core::api::JsCallbackId>,
751    ) {
752        let Some(bridge) = &self.async_bridge else {
753            return;
754        };
755        self.grammar_build_in_progress = true;
756        let sender = bridge.sender();
757        let config_dir = self.dir_context.config_dir.clone();
758        tracing::info!(
759            "Spawning background grammar build thread ({} plugin grammars)...",
760            additional.len()
761        );
762        std::thread::Builder::new()
763            .name("grammar-build".to_string())
764            .spawn(move || {
765                tracing::info!("[grammar-build] Thread started");
766                let start = std::time::Instant::now();
767                let registry = if additional.is_empty() {
768                    crate::primitives::grammar::GrammarRegistry::for_editor(config_dir)
769                } else {
770                    crate::primitives::grammar::GrammarRegistry::for_editor_with_additional(
771                        config_dir,
772                        &additional,
773                    )
774                };
775                tracing::info!("[grammar-build] Complete in {:?}", start.elapsed());
776                drop(sender.send(
777                    crate::services::async_bridge::AsyncMessage::GrammarRegistryBuilt {
778                        registry,
779                        callback_ids,
780                    },
781                ));
782            })
783            .ok();
784    }
785}