1use super::*;
13
14struct InitTimer {
18 label: &'static str,
19 start: std::time::Instant,
20 last: std::time::Instant,
21 enabled: bool,
22}
23
24impl InitTimer {
25 fn start(label: &'static str) -> Self {
26 let enabled = std::env::var("FRESH_TEST_TIMING").is_ok_and(|v| !v.is_empty() && v != "0");
27 let now = std::time::Instant::now();
28 if enabled {
29 eprintln!("[timing] {label} start");
30 }
31 Self {
32 label,
33 start: now,
34 last: now,
35 enabled,
36 }
37 }
38 fn phase(&mut self, name: &str) {
39 if !self.enabled {
40 return;
41 }
42 let now = std::time::Instant::now();
43 let delta = now.duration_since(self.last);
44 let cumul = now.duration_since(self.start);
45 eprintln!(
46 "[timing] {name:<30} +{delta:>8.1}ms (cumul {cumul:.1}ms)",
47 name = name,
48 delta = delta.as_secs_f64() * 1000.0,
49 cumul = cumul.as_secs_f64() * 1000.0,
50 );
51 self.last = now;
52 }
53 fn finish(self) {
54 if !self.enabled {
55 return;
56 }
57 eprintln!(
58 "[timing] {label} total {total:.1}ms",
59 label = self.label,
60 total = self.start.elapsed().as_secs_f64() * 1000.0,
61 );
62 }
63}
64
65fn set_dot_path(root: &mut serde_json::Value, path: &str, value: serde_json::Value) {
68 let segments: Vec<&str> = path.split('.').filter(|s| !s.is_empty()).collect();
69 if segments.is_empty() {
70 return;
71 }
72 let mut cur = root;
73 for seg in &segments[..segments.len() - 1] {
74 if !cur.is_object() {
75 *cur = serde_json::Value::Object(serde_json::Map::new());
76 }
77 cur = cur
78 .as_object_mut()
79 .unwrap()
80 .entry((*seg).to_string())
81 .or_insert(serde_json::Value::Null);
82 }
83 let last = segments[segments.len() - 1];
84 if !cur.is_object() {
85 *cur = serde_json::Value::Object(serde_json::Map::new());
86 }
87 cur.as_object_mut().unwrap().insert(last.to_string(), value);
88}
89
90impl Editor {
91 pub fn new(
94 config: Config,
95 width: u16,
96 height: u16,
97 dir_context: DirectoryContext,
98 color_capability: crate::view::color_support::ColorCapability,
99 filesystem: Arc<dyn FileSystem + Send + Sync>,
100 ) -> AnyhowResult<Self> {
101 Self::with_working_dir(
102 config,
103 width,
104 height,
105 None,
106 dir_context,
107 true,
108 color_capability,
109 filesystem,
110 )
111 }
112
113 #[allow(clippy::too_many_arguments)]
116 pub fn with_working_dir(
117 config: Config,
118 width: u16,
119 height: u16,
120 working_dir: Option<PathBuf>,
121 dir_context: DirectoryContext,
122 plugins_enabled: bool,
123 color_capability: crate::view::color_support::ColorCapability,
124 filesystem: Arc<dyn FileSystem + Send + Sync>,
125 ) -> AnyhowResult<Self> {
126 Self::with_working_dir_opts(
127 config,
128 width,
129 height,
130 working_dir,
131 dir_context,
132 plugins_enabled,
133 color_capability,
134 filesystem,
135 false,
136 )
137 }
138
139 #[allow(clippy::too_many_arguments)]
147 pub fn with_working_dir_opts(
148 config: Config,
149 width: u16,
150 height: u16,
151 working_dir: Option<PathBuf>,
152 dir_context: DirectoryContext,
153 plugins_enabled: bool,
154 color_capability: crate::view::color_support::ColorCapability,
155 filesystem: Arc<dyn FileSystem + Send + Sync>,
156 defer_plugin_load: bool,
157 ) -> AnyhowResult<Self> {
158 tracing::info!("Building default grammar registry...");
159 let start = std::time::Instant::now();
160 let mut grammar_registry = crate::primitives::grammar::GrammarRegistry::defaults_only();
161 std::sync::Arc::get_mut(&mut grammar_registry)
167 .expect("defaults_only returned a shared Arc")
168 .apply_language_config(&config.languages);
169 tracing::info!("Default grammar registry built in {:?}", start.elapsed());
170 Self::with_options(
174 config,
175 width,
176 height,
177 working_dir,
178 filesystem,
179 plugins_enabled,
180 true, dir_context,
182 None,
183 color_capability,
184 grammar_registry,
185 defer_plugin_load,
186 )
187 }
188
189 #[allow(clippy::too_many_arguments)]
200 pub fn for_test(
201 config: Config,
202 width: u16,
203 height: u16,
204 working_dir: Option<PathBuf>,
205 dir_context: DirectoryContext,
206 color_capability: crate::view::color_support::ColorCapability,
207 filesystem: Arc<dyn FileSystem + Send + Sync>,
208 time_source: Option<SharedTimeSource>,
209 grammar_registry: Option<Arc<crate::primitives::grammar::GrammarRegistry>>,
210 enable_plugins: bool,
211 enable_embedded_plugins: bool,
212 ) -> AnyhowResult<Self> {
213 let mut grammar_registry =
214 grammar_registry.unwrap_or_else(crate::primitives::grammar::GrammarRegistry::empty);
215 std::sync::Arc::get_mut(&mut grammar_registry)
222 .expect("grammar registry Arc must be uniquely owned at for_test entry")
223 .apply_language_config(&config.languages);
224 let mut editor = Self::with_options(
225 config,
226 width,
227 height,
228 working_dir,
229 filesystem,
230 enable_plugins,
231 enable_embedded_plugins,
232 dir_context,
233 time_source,
234 color_capability,
235 grammar_registry,
236 false,
237 )?;
238 editor.needs_full_grammar_build = false;
241 Ok(editor)
242 }
243
244 #[allow(clippy::too_many_arguments)]
248 fn with_options(
249 mut config: Config,
250 width: u16,
251 height: u16,
252 working_dir: Option<PathBuf>,
253 filesystem: Arc<dyn FileSystem + Send + Sync>,
254 enable_plugins: bool,
255 #[cfg_attr(not(feature = "embed-plugins"), allow(unused_variables))]
256 enable_embedded_plugins: bool,
257 dir_context: DirectoryContext,
258 time_source: Option<SharedTimeSource>,
259 color_capability: crate::view::color_support::ColorCapability,
260 grammar_registry: Arc<crate::primitives::grammar::GrammarRegistry>,
261 defer_plugin_load: bool,
262 ) -> AnyhowResult<Self> {
263 let mut t = InitTimer::start("Editor::with_options");
264 let time_source = time_source.unwrap_or_else(RealTimeSource::shared);
266 tracing::info!("Editor::new called with width={}, height={}", width, height);
267
268 let working_dir = working_dir
270 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
271
272 let working_dir = working_dir.canonicalize().unwrap_or(working_dir);
275
276 t.phase("preamble");
277 tracing::info!("Loading themes...");
279 let theme_loader = crate::view::theme::ThemeLoader::new(dir_context.themes_dir());
280 t.phase("ThemeLoader::new");
281 let scan_result =
285 crate::services::packages::scan_installed_packages(&dir_context.config_dir);
286 t.phase("scan_installed_packages");
287
288 for (lang_id, lang_config) in &scan_result.language_configs {
290 config
291 .languages
292 .entry(lang_id.clone())
293 .or_insert_with(|| lang_config.clone());
294 }
295
296 for (lang_id, lsp_config) in &scan_result.lsp_configs {
298 config
299 .lsp
300 .entry(lang_id.clone())
301 .or_insert_with(|| LspLanguageConfig::Multi(vec![lsp_config.clone()]));
302 }
303
304 let theme_registry = Arc::new(theme_loader.load_all(&scan_result.bundle_theme_dirs));
305 t.phase("theme_loader.load_all");
306 tracing::info!("Themes loaded");
307
308 let theme = theme_registry.get_cloned(&config.theme).unwrap_or_else(|| {
310 tracing::warn!(
311 "Theme '{}' not found, falling back to default theme",
312 config.theme.0
313 );
314 theme_registry
315 .get_cloned(&crate::config::ThemeName(
316 crate::view::theme::THEME_HIGH_CONTRAST.to_string(),
317 ))
318 .expect("Default theme must exist")
319 });
320
321 theme.set_terminal_cursor_color();
323
324 t.phase("theme_setup");
325 let keybindings = Arc::new(RwLock::new(KeybindingResolver::new(&config)));
326 t.phase("keybindings");
327
328 let mut buffers = HashMap::new();
330 let mut event_logs = HashMap::new();
331
332 let buffer_id = BufferId(1);
337 let mut state = EditorState::new(
338 width,
339 height,
340 config.editor.large_file_threshold_bytes as usize,
341 Arc::clone(&filesystem),
342 );
343 state
345 .margins
346 .configure_for_line_numbers(config.editor.line_numbers);
347 state.buffer_settings.tab_size = config.editor.tab_size;
348 state.buffer_settings.auto_close = config.editor.auto_close;
349 tracing::info!("EditorState created for buffer {:?}", buffer_id);
351 buffers.insert(buffer_id, state);
352 event_logs.insert(buffer_id, EventLog::new());
353
354 let mut buffer_metadata = HashMap::new();
356 buffer_metadata.insert(buffer_id, BufferMetadata::new());
357
358 let root_uri = types::file_path_to_lsp_uri(&working_dir);
360
361 t.phase("buffer_state");
362 let tokio_runtime = tokio::runtime::Builder::new_multi_thread()
364 .worker_threads(2) .thread_name("editor-async")
366 .enable_all()
367 .build()
368 .ok();
369 t.phase("tokio_runtime");
370
371 let async_bridge = AsyncBridge::new();
373
374 if tokio_runtime.is_none() {
375 tracing::warn!("Failed to create Tokio runtime - async features disabled");
376 }
377
378 let mut lsp = LspManager::new(root_uri);
380
381 if let Some(ref runtime) = tokio_runtime {
383 lsp.set_runtime(runtime.handle().clone(), async_bridge.clone());
384 }
385
386 for (language, lsp_configs) in &config.lsp {
388 lsp.set_language_configs(language.clone(), lsp_configs.as_slice().to_vec());
389 }
390
391 let universal_servers: Vec<LspServerConfig> = config
393 .universal_lsp
394 .values()
395 .flat_map(|lc| lc.as_slice().to_vec())
396 .filter(|c| c.enabled)
397 .collect();
398 lsp.set_universal_configs(universal_servers);
399
400 if working_dir.join("deno.json").exists() || working_dir.join("deno.jsonc").exists() {
403 tracing::info!("Detected Deno project (deno.json found), using deno lsp for JS/TS");
404 let deno_config = LspServerConfig {
405 command: "deno".to_string(),
406 args: vec!["lsp".to_string()],
407 enabled: true,
408 auto_start: false,
409 process_limits: ProcessLimits::default(),
410 initialization_options: Some(serde_json::json!({"enable": true})),
411 ..Default::default()
412 };
413 lsp.set_language_config("javascript".to_string(), deno_config.clone());
414 lsp.set_language_config("typescript".to_string(), deno_config);
415 }
416
417 t.phase("lsp_setup");
418 let split_manager = SplitManager::new(buffer_id);
420
421 let mut split_view_states = HashMap::new();
423 let initial_split_id = split_manager.active_split();
424 let mut initial_view_state = SplitViewState::with_buffer(width, height, buffer_id);
425 initial_view_state.apply_config_defaults(
426 config.editor.line_numbers,
427 config.editor.highlight_current_line,
428 config.editor.line_wrap,
429 config.editor.wrap_indent,
430 config.editor.wrap_column,
431 config.editor.rulers.clone(),
432 );
433 split_view_states.insert(initial_split_id, initial_view_state);
434
435 let fs_manager = Arc::new(FsManager::new(Arc::clone(&filesystem)));
437
438 let command_registry = Arc::new(RwLock::new(CommandRegistry::new()));
440
441 let authority = crate::services::authority::Authority {
447 filesystem: Arc::clone(&filesystem),
448 ..crate::services::authority::Authority::local()
449 };
450 let process_spawner = Arc::clone(&authority.process_spawner);
451
452 let mut quick_open_registry = QuickOpenRegistry::new();
454 quick_open_registry.register(Box::new(FileProvider::new(
455 Arc::clone(&filesystem),
456 Arc::clone(&process_spawner),
457 tokio_runtime.as_ref().map(|rt| rt.handle().clone()),
458 Some(async_bridge.sender()),
459 )));
460 quick_open_registry.register(Box::new(CommandProvider::new(
461 Arc::clone(&command_registry),
462 Arc::clone(&keybindings),
463 )));
464 quick_open_registry.register(Box::new(BufferProvider::new()));
465 quick_open_registry.register(Box::new(GotoLineProvider::new()));
466
467 let theme_cache = Arc::new(RwLock::new(theme_registry.to_json_map()));
469
470 t.phase("split_quickopen_authority");
471 let plugin_manager = PluginManager::new(
473 enable_plugins,
474 Arc::clone(&command_registry),
475 dir_context.clone(),
476 Arc::clone(&theme_cache),
477 );
478 t.phase("PluginManager::new");
479
480 #[cfg(feature = "plugins")]
483 if let Some(snapshot_handle) = plugin_manager.state_snapshot_handle() {
484 let mut snapshot = snapshot_handle.write().unwrap();
485 snapshot.working_dir = working_dir.clone();
486 populate_builtin_keybinding_labels(&mut snapshot, &keybindings);
494 }
495
496 if plugin_manager.is_active() {
503 let mut plugin_dirs: Vec<std::path::PathBuf> = vec![];
504
505 if let Ok(exe_path) = std::env::current_exe() {
507 if let Some(exe_dir) = exe_path.parent() {
508 let exe_plugin_dir = exe_dir.join("plugins");
509 if exe_plugin_dir.exists() {
510 plugin_dirs.push(exe_plugin_dir);
511 }
512 }
513 }
514
515 #[cfg(feature = "embed-plugins")]
526 if enable_embedded_plugins && plugin_dirs.is_empty() {
527 if let Some(embedded_dir) =
528 crate::services::plugins::embedded::get_embedded_plugins_dir()
529 {
530 tracing::info!("Using embedded plugins from: {:?}", embedded_dir);
531 plugin_dirs.push(embedded_dir.clone());
532 }
533 }
534
535 let user_plugins_dir = dir_context.config_dir.join("plugins");
537 if user_plugins_dir.exists() && !plugin_dirs.contains(&user_plugins_dir) {
538 tracing::info!("Found user plugins directory: {:?}", user_plugins_dir);
539 plugin_dirs.push(user_plugins_dir.clone());
540 }
541
542 let packages_dir = dir_context.config_dir.join("plugins").join("packages");
544 if packages_dir.exists() {
545 if let Ok(entries) = std::fs::read_dir(&packages_dir) {
546 for entry in entries.flatten() {
547 let path = entry.path();
548 if path.is_dir() {
550 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
551 if !name.starts_with('.') {
552 tracing::info!("Found package manager plugin: {:?}", path);
553 plugin_dirs.push(path);
554 }
555 }
556 }
557 }
558 }
559 }
560
561 for dir in &scan_result.bundle_plugin_dirs {
563 tracing::info!("Found bundle plugin directory: {:?}", dir);
564 plugin_dirs.push(dir.clone());
565 }
566
567 if plugin_dirs.is_empty() {
568 tracing::debug!(
569 "No plugins directory found next to executable or in working dir: {:?}",
570 working_dir
571 );
572 }
573
574 if defer_plugin_load {
575 #[cfg(feature = "plugins")]
586 {
587 let bridge = &async_bridge;
588 let mut dir_receivers: Vec<(
589 std::path::PathBuf,
590 fresh_plugin_runtime::thread::oneshot::Receiver<
591 fresh_plugin_runtime::thread::PluginsDirLoadResult,
592 >,
593 )> = Vec::with_capacity(plugin_dirs.len());
594 for plugin_dir in &plugin_dirs {
595 tracing::info!(
596 "Submitting async TypeScript plugin load for: {:?}",
597 plugin_dir
598 );
599 if let Some(rx) = plugin_manager
600 .load_plugins_from_dir_with_config_request(plugin_dir, &config.plugins)
601 {
602 dir_receivers.push((plugin_dir.clone(), rx));
603 }
604 }
605 let declarations_rx = if !dir_receivers.is_empty() {
606 plugin_manager.list_plugins_request()
607 } else {
608 None
609 };
610 if !dir_receivers.is_empty() {
611 let sender = bridge.sender();
612 std::thread::Builder::new()
613 .name("plugin-load-forwarder".to_string())
614 .spawn(move || {
615 for (dir, rx) in dir_receivers {
616 let load_start = std::time::Instant::now();
617 match rx.recv() {
618 Ok((errors, discovered_plugins)) => {
619 tracing::info!(
620 "Loaded TypeScript plugins from {:?} in {:?}",
621 dir,
622 load_start.elapsed()
623 );
624 drop(sender.send(
625 crate::services::async_bridge::AsyncMessage::PluginsDirLoaded {
626 dir,
627 errors,
628 discovered_plugins,
629 },
630 ));
631 }
632 Err(e) => {
633 tracing::warn!(
634 "plugin-load-forwarder: dir {:?} recv failed: {}",
635 dir,
636 e
637 );
638 }
639 }
640 }
641 if let Some(rx) = declarations_rx {
642 match rx.recv() {
643 Ok(plugin_infos) => {
644 let declarations: Vec<(String, String)> = plugin_infos
645 .into_iter()
646 .filter_map(|info| {
647 info.declarations.map(|d| (info.name, d))
648 })
649 .collect();
650 drop(sender.send(
651 crate::services::async_bridge::AsyncMessage::PluginDeclarationsReady {
652 declarations,
653 },
654 ));
655 }
656 Err(e) => {
657 tracing::warn!(
658 "plugin-load-forwarder: list_plugins recv failed: {}",
659 e
660 );
661 }
662 }
663 }
664 })
665 .ok();
666 }
667 }
668 } else {
669 for plugin_dir in plugin_dirs {
674 tracing::info!("Loading TypeScript plugins from: {:?}", plugin_dir);
675 let load_start = std::time::Instant::now();
676 let (errors, discovered_plugins) = plugin_manager
677 .load_plugins_from_dir_with_config(&plugin_dir, &config.plugins);
678 tracing::info!(
679 "Loaded TypeScript plugins from {:?} in {:?}",
680 plugin_dir,
681 load_start.elapsed()
682 );
683
684 for (name, plugin_config) in discovered_plugins {
687 config.plugins.insert(name, plugin_config);
688 }
689
690 if !errors.is_empty() {
691 for err in &errors {
692 tracing::error!("TypeScript plugin load error: {}", err);
693 }
694 #[cfg(debug_assertions)]
696 panic!(
697 "TypeScript plugin loading failed with {} error(s): {}",
698 errors.len(),
699 errors.join("; ")
700 );
701 }
702 }
703
704 let declarations = plugin_manager.plugin_declarations();
712 crate::init_script::write_plugin_declarations(
713 &dir_context.config_dir,
714 &declarations,
715 );
716 }
717 }
718
719 t.phase("plugin_loading");
720 let file_explorer_width = config.file_explorer.width;
722 let file_explorer_side = config.file_explorer.side;
723 let recovery_enabled = config.editor.recovery_enabled;
724 let check_for_updates = config.check_for_updates;
725 let show_menu_bar = config.editor.show_menu_bar;
726 let show_tab_bar = config.editor.show_tab_bar;
727 let show_status_bar = config.editor.show_status_bar;
728 let show_prompt_line = config.editor.show_prompt_line;
729
730 let update_checker = if check_for_updates {
732 tracing::debug!("Update checking enabled, starting periodic checker");
733 Some(
734 crate::services::release_checker::start_periodic_update_check(
735 crate::services::release_checker::DEFAULT_RELEASES_URL,
736 time_source.clone(),
737 dir_context.data_dir.clone(),
738 ),
739 )
740 } else {
741 tracing::debug!("Update checking disabled by config");
742 None
743 };
744
745 let user_config_raw = Config::read_user_config_raw(&working_dir);
747
748 let config_arc = Arc::new(config);
755 let config_cached_json =
756 Arc::new(serde_json::to_value(&*config_arc).unwrap_or(serde_json::Value::Null));
757 let config_snapshot_anchor = Arc::clone(&config_arc);
758
759 let mut editor = Editor {
760 buffers,
761 event_logs,
762 next_buffer_id: 2,
763 config: config_arc,
764 config_snapshot_anchor,
765 config_cached_json,
766 user_config_raw: Arc::new(user_config_raw),
767 dir_context: dir_context.clone(),
768 grammar_registry,
769 pending_grammars: scan_result
770 .additional_grammars
771 .iter()
772 .map(|g| PendingGrammar {
773 language: g.language.clone(),
774 grammar_path: g.path.to_string_lossy().to_string(),
775 extensions: g.extensions.clone(),
776 })
777 .collect(),
778 grammar_reload_pending: false,
779 grammar_build_in_progress: false,
780 needs_full_grammar_build: true,
781 streaming_grep_cancellation: None,
782 pending_grammar_callbacks: Vec::new(),
783 theme,
784 theme_registry,
785 expanded_menus_cache: crate::view::ui::ExpandedMenusCache::default(),
786 theme_cache,
787 ansi_background: None,
788 ansi_background_path: None,
789 background_fade: crate::primitives::ansi_background::DEFAULT_BACKGROUND_FADE,
790 keybindings,
791 clipboard: crate::services::clipboard::Clipboard::new(),
792 should_quit: false,
793 should_detach: false,
794 session_mode: false,
795 software_cursor_only: false,
796 session_name: None,
797 pending_escape_sequences: Vec::new(),
798 restart_with_dir: None,
799 status_message: None,
800 plugin_status_message: None,
801 last_window_title: None,
802 plugin_errors: Vec::new(),
803 prompt: None,
804 terminal_width: width,
805 terminal_height: height,
806 lsp: Some(lsp),
807 buffer_metadata,
808 mode_registry: ModeRegistry::new(),
809 tokio_runtime,
810 async_bridge: Some(async_bridge),
811 split_manager,
812 split_view_states,
813 previous_viewports: HashMap::new(),
814 scroll_sync_manager: ScrollSyncManager::new(),
815 file_explorer: None,
816 preview: None,
817 suppress_position_history_once: false,
818 fs_manager,
819 authority,
820 pending_authority: None,
821 remote_indicator_override: None,
822 local_filesystem: Arc::new(crate::model::filesystem::StdFileSystem),
823 file_explorer_visible: false,
824 file_explorer_sync_in_progress: false,
825 file_explorer_width,
826 file_explorer_side,
827 pending_file_explorer_show_hidden: None,
828 pending_file_explorer_show_gitignored: None,
829 menu_bar_visible: show_menu_bar,
830 file_explorer_decorations: HashMap::new(),
831 file_explorer_decoration_cache:
832 crate::view::file_tree::FileExplorerDecorationCache::default(),
833 file_explorer_clipboard: None,
834 menu_bar_auto_shown: false,
835 tab_bar_visible: show_tab_bar,
836 status_bar_visible: show_status_bar,
837 prompt_line_visible: show_prompt_line,
838 mouse_enabled: true,
839 same_buffer_scroll_sync: false,
840 mouse_cursor_position: None,
841 gpm_active: false,
842 key_context: KeyContext::Normal,
843 menu_state: crate::view::ui::MenuState::new(dir_context.themes_dir()),
844 menus: crate::config::MenuConfig::translated(),
845 working_dir: working_dir.clone(),
846 position_history: PositionHistory::new(),
847 in_navigation: false,
848 next_lsp_request_id: 0,
849 pending_completion_requests: HashSet::new(),
850 completion_items: None,
851 scheduled_completion_trigger: None,
852 completion_service: crate::services::completion::CompletionService::new(),
853 dabbrev_state: None,
854 pending_goto_definition_request: None,
855 hover: hover::HoverState::default(),
856 pending_references_request: None,
857 pending_references_symbol: String::new(),
858 pending_signature_help_request: None,
859 pending_code_actions_requests: HashSet::new(),
860 pending_code_actions_server_names: HashMap::new(),
861 pending_code_actions: None,
862 pending_inlay_hints_requests: HashMap::new(),
863 pending_folding_range_requests: HashMap::new(),
864 folding_ranges_in_flight: HashMap::new(),
865 folding_ranges_debounce: HashMap::new(),
866 pending_semantic_token_requests: HashMap::new(),
867 semantic_tokens_in_flight: HashMap::new(),
868 pending_semantic_token_range_requests: HashMap::new(),
869 semantic_tokens_range_in_flight: HashMap::new(),
870 semantic_tokens_range_last_request: HashMap::new(),
871 semantic_tokens_range_applied: HashMap::new(),
872 semantic_tokens_full_debounce: HashMap::new(),
873 search_state: None,
874 search_namespace: crate::view::overlay::OverlayNamespace::from_string(
875 "search".to_string(),
876 ),
877 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace::from_string(
878 "lsp-diagnostic".to_string(),
879 ),
880 pending_search_range: None,
881 interactive_replace_state: None,
882 mouse_state: MouseState::default(),
883 tab_context_menu: None,
884 file_explorer_context_menu: None,
885 theme_info_popup: None,
886 cached_layout: CachedLayout::default(),
887 command_registry,
888 quick_open_registry,
889 plugin_manager,
890 plugin_dev_workspaces: HashMap::new(),
891 seen_byte_ranges: HashMap::new(),
892 panel_ids: HashMap::new(),
893 live_grep_last_state: None,
894 overlay_preview_state: None,
895 buffer_groups: HashMap::new(),
896 buffer_to_group: HashMap::new(),
897 next_buffer_group_id: 0,
898 grouped_subtrees: HashMap::new(),
899 background_process_handles: HashMap::new(),
900 host_process_handles: HashMap::new(),
901 prompt_histories: {
902 let mut histories = HashMap::new();
904 for history_name in ["search", "replace", "goto_line"] {
905 let path = dir_context.prompt_history_path(history_name);
906 let history = crate::input::input_history::InputHistory::load_from_file(&path)
907 .unwrap_or_else(|e| {
908 tracing::warn!("Failed to load {} history: {}", history_name, e);
909 crate::input::input_history::InputHistory::new()
910 });
911 histories.insert(history_name.to_string(), history);
912 }
913 histories
914 },
915 pending_async_prompt_callback: None,
916 pending_next_key_callbacks: std::collections::VecDeque::new(),
917 key_capture_active: false,
918 pending_key_capture_buffer: std::collections::VecDeque::new(),
919 goto_line_preview: None,
920 lsp_progress: std::collections::HashMap::new(),
921 lsp_server_statuses: std::collections::HashMap::new(),
922 lsp_window_messages: Vec::new(),
923 lsp_log_messages: Vec::new(),
924 diagnostic_result_ids: HashMap::new(),
925 scheduled_diagnostic_pull: None,
926 scheduled_inlay_hints_request: None,
927 stored_push_diagnostics: HashMap::new(),
928 stored_pull_diagnostics: HashMap::new(),
929 stored_diagnostics: Arc::new(HashMap::new()),
930 stored_folding_ranges: Arc::new(HashMap::new()),
931 event_broadcaster: crate::model::control_event::EventBroadcaster::default(),
932 bookmarks: bookmarks::BookmarkState::default(),
933 search_case_sensitive: true,
934 search_whole_word: false,
935 search_use_regex: false,
936 search_confirm_each: false,
937 macros: macros::MacroState::default(),
938 #[cfg(feature = "plugins")]
939 pending_plugin_actions: Vec::new(),
940 #[cfg(feature = "plugins")]
941 plugin_render_requested: false,
942 chord_state: Vec::new(),
943 user_dismissed_lsp_languages: std::collections::HashSet::new(),
944 pending_close_buffer: None,
945 pending_quit_unnamed_save: Vec::new(),
946 auto_revert_enabled: true,
947 last_auto_revert_poll: time_source.now(),
948 last_file_tree_poll: time_source.now(),
949 git_index_resolved: false,
950 file_mod_times: HashMap::new(),
951 dir_mod_times: HashMap::new(),
952 pending_file_poll_rx: None,
953 pending_dir_poll_rx: None,
954 file_rapid_change_counts: HashMap::new(),
955 file_open_state: None,
956 file_browser_layout: None,
957 recovery_service: {
958 let recovery_config = RecoveryConfig {
959 enabled: recovery_enabled,
960 ..RecoveryConfig::default()
961 };
962 let scope = crate::services::recovery::RecoveryScope::Standalone {
970 working_dir: working_dir.clone(),
971 };
972 RecoveryService::with_scope(recovery_config, &dir_context.recovery_dir(), &scope)
973 },
974 full_redraw_requested: false,
975 suspend_requested: false,
976 time_source: time_source.clone(),
977 last_auto_recovery_save: time_source.now(),
978 last_persistent_auto_save: time_source.now(),
979 active_custom_contexts: HashSet::new(),
980 plugin_global_state: HashMap::new(),
981 editor_mode: None,
982 warning_log: None,
983 status_log_path: None,
984 warning_domains: WarningDomainRegistry::new(),
985 update_checker,
986 terminal_manager: crate::services::terminal::TerminalManager::new(),
987 terminal_buffers: HashMap::new(),
988 terminal_backing_files: HashMap::new(),
989 terminal_log_files: HashMap::new(),
990 ephemeral_terminals: std::collections::HashSet::new(),
991 terminal_mode: false,
992 keyboard_capture: false,
993 terminal_mode_resume: std::collections::HashSet::new(),
994 previous_click_time: None,
995 previous_click_position: None,
996 click_count: 0,
997 settings_state: None,
998 calibration_wizard: None,
999 event_debug: None,
1000 keybinding_editor: None,
1001 key_translator: crate::input::key_translator::KeyTranslator::load_from_config_dir(
1002 &dir_context.config_dir,
1003 )
1004 .unwrap_or_default(),
1005 color_capability,
1006 pending_file_opens: Vec::new(),
1007 pending_hot_exit_recovery: false,
1008 wait_tracking: HashMap::new(),
1009 completed_waits: Vec::new(),
1010 stdin_stream: stdin_stream::StdinStream::default(),
1011 line_scan: line_scan::LineScan::default(),
1012 search_scan: search_scan::SearchScan::default(),
1013 search_overlay_top_byte: None,
1014 review_hunks: Vec::new(),
1015 global_popups: crate::view::popup::PopupManager::new(),
1016 composite_buffers: HashMap::new(),
1017 composite_view_states: HashMap::new(),
1018 animations: crate::view::animation::AnimationRunner::new(),
1019 previous_cursor_screen_pos: None,
1020 cursor_jump_animation: None,
1021 pending_vb_animations: Vec::new(),
1022 };
1023
1024 t.phase("editor_struct_assembly");
1025 editor.clipboard.apply_config(&editor.config.clipboard);
1027
1028 #[cfg(feature = "plugins")]
1029 {
1030 editor.update_plugin_state_snapshot();
1031 if editor.plugin_manager.is_active() {
1032 editor.plugin_manager.run_hook(
1033 "editor_initialized",
1034 crate::services::plugins::hooks::HookArgs::EditorInitialized {},
1035 );
1036 }
1037 }
1038 t.phase("post_struct_hooks");
1039 t.finish();
1040 Ok(editor)
1041 }
1042
1043 pub fn event_broadcaster(&self) -> &crate::model::control_event::EventBroadcaster {
1045 &self.event_broadcaster
1046 }
1047
1048 pub(super) fn start_background_grammar_build(
1053 &mut self,
1054 additional: Vec<crate::primitives::grammar::GrammarSpec>,
1055 callback_ids: Vec<fresh_core::api::JsCallbackId>,
1056 ) {
1057 let Some(bridge) = &self.async_bridge else {
1058 return;
1059 };
1060 self.grammar_build_in_progress = true;
1061 let sender = bridge.sender();
1062 let config_dir = self.dir_context.config_dir.clone();
1063 tracing::info!(
1064 "Spawning background grammar build thread ({} plugin grammars)...",
1065 additional.len()
1066 );
1067 std::thread::Builder::new()
1068 .name("grammar-build".to_string())
1069 .spawn(move || {
1070 tracing::info!("[grammar-build] Thread started");
1071 let start = std::time::Instant::now();
1072 let registry = if additional.is_empty() {
1073 crate::primitives::grammar::GrammarRegistry::for_editor(config_dir)
1074 } else {
1075 crate::primitives::grammar::GrammarRegistry::for_editor_with_additional(
1076 config_dir,
1077 &additional,
1078 )
1079 };
1080 tracing::info!("[grammar-build] Complete in {:?}", start.elapsed());
1081 drop(sender.send(
1082 crate::services::async_bridge::AsyncMessage::GrammarRegistryBuilt {
1083 registry,
1084 callback_ids,
1085 },
1086 ));
1087 })
1088 .ok();
1089 }
1090
1091 pub fn load_init_script(&mut self, enabled: bool) {
1098 use crate::init_script::{
1099 check, decide_load, describe, record_success, refresh_types_scaffolding, CheckSeverity,
1100 InitOutcome, LoadDecision,
1101 };
1102
1103 let config_dir = self.dir_context.config_dir.clone();
1104
1105 if enabled {
1106 refresh_types_scaffolding(&config_dir);
1110
1111 let report = check(&config_dir);
1116 if !report.ok {
1117 for d in &report.diagnostics {
1118 let level = match d.severity {
1119 CheckSeverity::Error => "error",
1120 CheckSeverity::Warning => "warning",
1121 };
1122 tracing::warn!(
1123 "init.ts pre-load {level} at {}:{}: {}",
1124 d.line,
1125 d.column,
1126 d.message
1127 );
1128 }
1129 }
1130 }
1131
1132 let outcome = match decide_load(&config_dir, enabled) {
1133 LoadDecision::Skip(outcome) => outcome,
1134 LoadDecision::Load { source } => {
1135 if !self.plugin_manager.is_active() {
1136 InitOutcome::Failed {
1137 message: "plugin runtime inactive (--no-plugins); init.ts cannot run"
1138 .into(),
1139 }
1140 } else {
1141 match self.plugin_manager.load_plugin_from_source(
1142 &source,
1143 crate::init_script::INIT_PLUGIN_NAME,
1144 true,
1145 ) {
1146 Ok(()) => {
1147 record_success(&config_dir);
1148 InitOutcome::Loaded
1149 }
1150 Err(e) => InitOutcome::Failed {
1151 message: format!("{e}"),
1152 },
1153 }
1154 }
1155 }
1156 };
1157
1158 let summary = describe(&outcome);
1159 match outcome {
1160 InitOutcome::NotFound | InitOutcome::Disabled => tracing::debug!("{}", summary),
1161 InitOutcome::Loaded => tracing::info!("{}", summary),
1162 InitOutcome::CrashFused { .. } | InitOutcome::Failed { .. } => {
1163 tracing::warn!("{}", summary);
1164 self.set_status_message(summary);
1165 }
1166 }
1167 }
1168
1169 pub fn load_init_script_async(&mut self, enabled: bool) {
1181 use crate::init_script::{
1182 check, decide_load, refresh_types_scaffolding, CheckSeverity, InitOutcome, LoadDecision,
1183 };
1184 use crate::services::async_bridge::PluginInitScriptOutcome;
1185
1186 let config_dir = self.dir_context.config_dir.clone();
1187
1188 if enabled {
1189 refresh_types_scaffolding(&config_dir);
1190 let report = check(&config_dir);
1191 if !report.ok {
1192 for d in &report.diagnostics {
1193 let level = match d.severity {
1194 CheckSeverity::Error => "error",
1195 CheckSeverity::Warning => "warning",
1196 };
1197 tracing::warn!(
1198 "init.ts pre-load {level} at {}:{}: {}",
1199 d.line,
1200 d.column,
1201 d.message
1202 );
1203 }
1204 }
1205 }
1206
1207 let outcome_now: Option<PluginInitScriptOutcome> = match decide_load(&config_dir, enabled) {
1208 LoadDecision::Skip(outcome) => Some(match outcome {
1209 InitOutcome::NotFound => PluginInitScriptOutcome::NotFound,
1210 InitOutcome::Disabled => PluginInitScriptOutcome::Disabled,
1211 InitOutcome::CrashFused { failures } => {
1212 PluginInitScriptOutcome::CrashFused { failures }
1213 }
1214 InitOutcome::Loaded => PluginInitScriptOutcome::Loaded,
1217 InitOutcome::Failed { message } => PluginInitScriptOutcome::Failed { message },
1218 }),
1219 LoadDecision::Load { source } => {
1220 if !self.plugin_manager.is_active() {
1221 Some(PluginInitScriptOutcome::Failed {
1222 message: "plugin runtime inactive (--no-plugins); init.ts cannot run"
1223 .into(),
1224 })
1225 } else {
1226 self.spawn_init_script_forwarder(source);
1227 None
1228 }
1229 }
1230 };
1231
1232 if let Some(outcome) = outcome_now {
1233 if let Some(bridge) = &self.async_bridge {
1237 drop(bridge.sender().send(
1238 crate::services::async_bridge::AsyncMessage::PluginInitScriptLoaded(outcome),
1239 ));
1240 } else {
1241 self.handle_plugin_init_script_loaded(outcome);
1242 }
1243 }
1244 }
1245
1246 #[cfg(feature = "plugins")]
1247 fn spawn_init_script_forwarder(&self, source: String) {
1248 let Some(bridge) = &self.async_bridge else {
1249 return;
1250 };
1251 let Some(rx) = self.plugin_manager.load_plugin_from_source_request(
1252 &source,
1253 crate::init_script::INIT_PLUGIN_NAME,
1254 true,
1255 ) else {
1256 return;
1257 };
1258 let sender = bridge.sender();
1259 std::thread::Builder::new()
1260 .name("plugin-init-forwarder".to_string())
1261 .spawn(move || {
1262 let outcome = match rx.recv() {
1263 Ok(Ok(())) => crate::services::async_bridge::PluginInitScriptOutcome::Loaded,
1264 Ok(Err(e)) => crate::services::async_bridge::PluginInitScriptOutcome::Failed {
1265 message: format!("{e}"),
1266 },
1267 Err(e) => crate::services::async_bridge::PluginInitScriptOutcome::Failed {
1268 message: format!("plugin thread closed: {e}"),
1269 },
1270 };
1271 drop(sender.send(
1272 crate::services::async_bridge::AsyncMessage::PluginInitScriptLoaded(outcome),
1273 ));
1274 })
1275 .ok();
1276 }
1277
1278 #[cfg(not(feature = "plugins"))]
1279 fn spawn_init_script_forwarder(&self, _source: String) {}
1280
1281 pub fn handle_set_setting(&mut self, path: String, value: serde_json::Value) {
1285 let mut json = serde_json::to_value(&*self.config).unwrap_or_default();
1286 set_dot_path(&mut json, &path, value);
1287 match serde_json::from_value::<crate::config::Config>(json) {
1288 Ok(new_config) => {
1289 let old_theme = self.config.theme.clone();
1290 self.config = Arc::new(new_config);
1291 if old_theme != self.config.theme {
1292 if let Some(theme) = self.theme_registry.get_cloned(&self.config.theme) {
1293 self.theme = theme;
1294 }
1295 }
1296 *self.keybindings.write().unwrap() =
1297 crate::input::keybindings::KeybindingResolver::new(&self.config);
1298 self.clipboard.apply_config(&self.config.clipboard);
1299 self.menu_bar_visible = self.config.editor.show_menu_bar;
1300 self.tab_bar_visible = self.config.editor.show_tab_bar;
1301 self.status_bar_visible = self.config.editor.show_status_bar;
1302 self.prompt_line_visible = self.config.editor.show_prompt_line;
1303 #[cfg(feature = "plugins")]
1304 self.update_plugin_state_snapshot();
1305 }
1306 Err(e) => {
1307 self.set_status_message(format!("setSetting({path}): {e}"));
1308 }
1309 }
1310 }
1311
1312 pub(crate) fn handle_plugins_dir_loaded(
1317 &mut self,
1318 dir: std::path::PathBuf,
1319 errors: Vec<String>,
1320 discovered_plugins: std::collections::HashMap<String, fresh_core::config::PluginConfig>,
1321 ) {
1322 if !discovered_plugins.is_empty() {
1323 let cfg = std::sync::Arc::make_mut(&mut self.config);
1324 for (name, plugin_config) in discovered_plugins {
1325 cfg.plugins.insert(name, plugin_config);
1326 }
1327 }
1328 if !errors.is_empty() {
1329 for err in &errors {
1330 tracing::error!("TypeScript plugin load error: {}", err);
1331 }
1332 #[cfg(debug_assertions)]
1333 panic!(
1334 "TypeScript plugin loading failed for {:?} with {} error(s): {}",
1335 dir,
1336 errors.len(),
1337 errors.join("; ")
1338 );
1339 #[cfg(not(debug_assertions))]
1340 {
1341 let _ = dir;
1342 }
1343 }
1344 }
1345
1346 pub(crate) fn handle_plugin_declarations_ready(&self, declarations: Vec<(String, String)>) {
1350 crate::init_script::write_plugin_declarations(&self.dir_context.config_dir, &declarations);
1351 }
1352
1353 pub(crate) fn handle_plugin_init_script_loaded(
1357 &mut self,
1358 outcome: crate::services::async_bridge::PluginInitScriptOutcome,
1359 ) {
1360 use crate::init_script::{describe, record_success, InitOutcome};
1361 use crate::services::async_bridge::PluginInitScriptOutcome as O;
1362 let outcome = match outcome {
1363 O::NotFound => InitOutcome::NotFound,
1364 O::Disabled => InitOutcome::Disabled,
1365 O::CrashFused { failures } => InitOutcome::CrashFused { failures },
1366 O::Loaded => {
1367 record_success(&self.dir_context.config_dir);
1368 InitOutcome::Loaded
1369 }
1370 O::Failed { message } => InitOutcome::Failed { message },
1371 };
1372 let summary = describe(&outcome);
1373 match outcome {
1374 InitOutcome::NotFound | InitOutcome::Disabled => tracing::debug!("{}", summary),
1375 InitOutcome::Loaded => tracing::info!("{}", summary),
1376 InitOutcome::CrashFused { .. } | InitOutcome::Failed { .. } => {
1377 tracing::warn!("{}", summary);
1378 self.set_status_message(summary);
1379 }
1380 }
1381 }
1382
1383 pub fn fire_plugins_loaded_hook(&self) {
1385 #[cfg(feature = "plugins")]
1386 if self.plugin_manager.is_active() {
1387 self.plugin_manager.run_hook(
1388 "plugins_loaded",
1389 crate::services::plugins::hooks::HookArgs::PluginsLoaded {},
1390 );
1391 }
1392 }
1393
1394 pub fn fire_ready_hook(&self) {
1396 #[cfg(feature = "plugins")]
1397 if self.plugin_manager.is_active() {
1398 self.plugin_manager
1399 .run_hook("ready", crate::services::plugins::hooks::HookArgs::Ready {});
1400 }
1401 }
1402
1403 #[doc(hidden)]
1405 pub fn config_for_tests(&self) -> &crate::config::Config {
1406 &self.config
1407 }
1408
1409 #[doc(hidden)]
1411 pub fn dispatch_action_for_tests(&mut self, action: crate::input::keybindings::Action) {
1412 if let Err(e) = self.handle_action(action) {
1413 tracing::warn!("dispatch_action_for_tests: {e}");
1414 }
1415 }
1416
1417 #[doc(hidden)]
1419 pub fn live_grep_last_state_for_tests(
1420 &self,
1421 ) -> Option<&crate::services::live_grep_state::LiveGrepLastState> {
1422 self.live_grep_last_state.as_ref()
1423 }
1424
1425 #[doc(hidden)]
1427 pub fn set_live_grep_last_state_for_tests(
1428 &mut self,
1429 state: Option<crate::services::live_grep_state::LiveGrepLastState>,
1430 ) {
1431 self.live_grep_last_state = state;
1432 }
1433
1434 #[doc(hidden)]
1437 pub fn split_manager_for_tests(&self) -> &crate::view::split::SplitManager {
1438 &self.split_manager
1439 }
1440
1441 #[doc(hidden)]
1446 pub fn split_view_state_for_tests(
1447 &self,
1448 leaf: crate::model::event::LeafId,
1449 ) -> Option<&crate::view::split::SplitViewState> {
1450 self.split_view_states.get(&leaf)
1451 }
1452
1453 #[cfg(feature = "plugins")]
1462 pub(crate) fn refresh_keybinding_labels_snapshot(&self) {
1463 if let Some(snapshot_handle) = self.plugin_manager.state_snapshot_handle() {
1464 if let Ok(mut snapshot) = snapshot_handle.write() {
1465 populate_builtin_keybinding_labels(&mut snapshot, &self.keybindings);
1466 }
1467 }
1468 }
1469}
1470
1471#[cfg(feature = "plugins")]
1487fn populate_builtin_keybinding_labels(
1488 snapshot: &mut crate::services::plugins::api::EditorStateSnapshot,
1489 keybindings: &std::sync::Arc<std::sync::RwLock<crate::input::keybindings::KeybindingResolver>>,
1490) {
1491 use crate::input::keybindings::{Action, KeyContext};
1492 let Ok(resolver) = keybindings.read() else {
1493 return;
1494 };
1495 let contexts = [
1496 KeyContext::Normal,
1497 KeyContext::Prompt,
1498 KeyContext::Popup,
1499 KeyContext::Completion,
1500 KeyContext::FileExplorer,
1501 KeyContext::Menu,
1502 KeyContext::Terminal,
1503 KeyContext::Settings,
1504 KeyContext::CompositeBuffer,
1505 ];
1506 let known_suffixes: Vec<String> = contexts
1513 .iter()
1514 .map(|c| format!("\0{}", c.to_when_clause()))
1515 .collect();
1516 snapshot
1517 .keybinding_labels
1518 .retain(|k, _| !known_suffixes.iter().any(|s| k.ends_with(s)));
1519 for action_name in Action::all_action_names() {
1520 for ctx in &contexts {
1521 if let Some(label) = resolver.find_keybinding_for_action(&action_name, ctx.clone()) {
1522 let key = format!("{}\0{}", action_name, ctx.to_when_clause());
1523 snapshot.keybinding_labels.insert(key, label);
1524 }
1525 }
1526 }
1527}