1use super::*;
13
14impl Editor {
15 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 #[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 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 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 #[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 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 editor.needs_full_grammar_build = false;
121 Ok(editor)
122 }
123
124 #[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 let time_source = time_source.unwrap_or_else(RealTimeSource::shared);
142 tracing::info!("Editor::new called with width={}, height={}", width, height);
143
144 let working_dir = working_dir
146 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
147
148 let working_dir = working_dir.canonicalize().unwrap_or(working_dir);
151
152 tracing::info!("Loading themes...");
154 let theme_loader = crate::view::theme::ThemeLoader::new(dir_context.themes_dir());
155 let scan_result =
159 crate::services::packages::scan_installed_packages(&dir_context.config_dir);
160
161 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 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 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 theme.set_terminal_cursor_color();
195
196 let keybindings = Arc::new(RwLock::new(KeybindingResolver::new(&config)));
197
198 let mut buffers = HashMap::new();
200 let mut event_logs = HashMap::new();
201
202 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 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 tracing::info!("EditorState created for buffer {:?}", buffer_id);
221 buffers.insert(buffer_id, state);
222 event_logs.insert(buffer_id, EventLog::new());
223
224 let mut buffer_metadata = HashMap::new();
226 buffer_metadata.insert(buffer_id, BufferMetadata::new());
227
228 let root_uri = types::file_path_to_lsp_uri(&working_dir);
230
231 let tokio_runtime = tokio::runtime::Builder::new_multi_thread()
233 .worker_threads(2) .thread_name("editor-async")
235 .enable_all()
236 .build()
237 .ok();
238
239 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 let mut lsp = LspManager::new(root_uri);
248
249 if let Some(ref runtime) = tokio_runtime {
251 lsp.set_runtime(runtime.handle().clone(), async_bridge.clone());
252 }
253
254 for (language, lsp_configs) in &config.lsp {
256 lsp.set_language_configs(language.clone(), lsp_configs.as_slice().to_vec());
257 }
258
259 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 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 let split_manager = SplitManager::new(buffer_id);
287
288 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 let fs_manager = Arc::new(FsManager::new(Arc::clone(&filesystem)));
304
305 let command_registry = Arc::new(RwLock::new(CommandRegistry::new()));
307
308 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 let theme_cache = Arc::new(RwLock::new(theme_registry.to_json_map()));
327
328 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 #[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 if plugin_manager.is_active() {
351 let mut plugin_dirs: Vec<std::path::PathBuf> = vec![];
352
353 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 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 #[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 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 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 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 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 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 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 #[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 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 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 let user_config_raw = Config::read_user_config_raw(&working_dir);
472
473 let mut editor = Editor {
474 buffers,
475 event_logs,
476 next_buffer_id: 2,
477 config,
478 user_config_raw,
479 dir_context: dir_context.clone(),
480 grammar_registry,
481 pending_grammars: scan_result
482 .additional_grammars
483 .iter()
484 .map(|g| PendingGrammar {
485 language: g.language.clone(),
486 grammar_path: g.path.to_string_lossy().to_string(),
487 extensions: g.extensions.clone(),
488 })
489 .collect(),
490 grammar_reload_pending: false,
491 grammar_build_in_progress: false,
492 needs_full_grammar_build: true,
493 streaming_grep_cancellation: None,
494 pending_grammar_callbacks: Vec::new(),
495 theme,
496 theme_registry,
497 theme_cache,
498 ansi_background: None,
499 ansi_background_path: None,
500 background_fade: crate::primitives::ansi_background::DEFAULT_BACKGROUND_FADE,
501 keybindings,
502 clipboard: crate::services::clipboard::Clipboard::new(),
503 should_quit: false,
504 should_detach: false,
505 session_mode: false,
506 software_cursor_only: false,
507 session_name: None,
508 pending_escape_sequences: Vec::new(),
509 restart_with_dir: None,
510 status_message: None,
511 plugin_status_message: None,
512 plugin_errors: Vec::new(),
513 prompt: None,
514 terminal_width: width,
515 terminal_height: height,
516 lsp: Some(lsp),
517 buffer_metadata,
518 mode_registry: ModeRegistry::new(),
519 tokio_runtime,
520 async_bridge: Some(async_bridge),
521 split_manager,
522 split_view_states,
523 previous_viewports: HashMap::new(),
524 scroll_sync_manager: ScrollSyncManager::new(),
525 file_explorer: None,
526 preview: None,
527 suppress_position_history_once: false,
528 fs_manager,
529 filesystem,
530 local_filesystem: Arc::new(crate::model::filesystem::StdFileSystem),
531 process_spawner,
532 file_explorer_visible: false,
533 file_explorer_sync_in_progress: false,
534 file_explorer_width_percent: file_explorer_width,
535 pending_file_explorer_show_hidden: None,
536 pending_file_explorer_show_gitignored: None,
537 menu_bar_visible: show_menu_bar,
538 file_explorer_decorations: HashMap::new(),
539 file_explorer_decoration_cache:
540 crate::view::file_tree::FileExplorerDecorationCache::default(),
541 menu_bar_auto_shown: false,
542 tab_bar_visible: show_tab_bar,
543 status_bar_visible: show_status_bar,
544 prompt_line_visible: show_prompt_line,
545 mouse_enabled: true,
546 same_buffer_scroll_sync: false,
547 mouse_cursor_position: None,
548 gpm_active: false,
549 key_context: KeyContext::Normal,
550 menu_state: crate::view::ui::MenuState::new(dir_context.themes_dir()),
551 menus: crate::config::MenuConfig::translated(),
552 working_dir,
553 position_history: PositionHistory::new(),
554 in_navigation: false,
555 next_lsp_request_id: 0,
556 pending_completion_requests: HashSet::new(),
557 completion_items: None,
558 scheduled_completion_trigger: None,
559 completion_service: crate::services::completion::CompletionService::new(),
560 dabbrev_state: None,
561 pending_goto_definition_request: None,
562 hover: hover::HoverState::default(),
563 pending_references_request: None,
564 pending_references_symbol: String::new(),
565 pending_signature_help_request: None,
566 pending_code_actions_requests: HashSet::new(),
567 pending_code_actions_server_names: HashMap::new(),
568 pending_code_actions: None,
569 pending_inlay_hints_requests: HashMap::new(),
570 pending_folding_range_requests: HashMap::new(),
571 folding_ranges_in_flight: HashMap::new(),
572 folding_ranges_debounce: HashMap::new(),
573 pending_semantic_token_requests: HashMap::new(),
574 semantic_tokens_in_flight: HashMap::new(),
575 pending_semantic_token_range_requests: HashMap::new(),
576 semantic_tokens_range_in_flight: HashMap::new(),
577 semantic_tokens_range_last_request: HashMap::new(),
578 semantic_tokens_range_applied: HashMap::new(),
579 semantic_tokens_full_debounce: HashMap::new(),
580 search_state: None,
581 search_namespace: crate::view::overlay::OverlayNamespace::from_string(
582 "search".to_string(),
583 ),
584 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace::from_string(
585 "lsp-diagnostic".to_string(),
586 ),
587 pending_search_range: None,
588 interactive_replace_state: None,
589 mouse_state: MouseState::default(),
590 tab_context_menu: None,
591 theme_info_popup: None,
592 cached_layout: CachedLayout::default(),
593 command_registry,
594 quick_open_registry,
595 plugin_manager,
596 plugin_dev_workspaces: HashMap::new(),
597 seen_byte_ranges: HashMap::new(),
598 panel_ids: HashMap::new(),
599 buffer_groups: HashMap::new(),
600 buffer_to_group: HashMap::new(),
601 next_buffer_group_id: 0,
602 grouped_subtrees: HashMap::new(),
603 background_process_handles: HashMap::new(),
604 prompt_histories: {
605 let mut histories = HashMap::new();
607 for history_name in ["search", "replace", "goto_line"] {
608 let path = dir_context.prompt_history_path(history_name);
609 let history = crate::input::input_history::InputHistory::load_from_file(&path)
610 .unwrap_or_else(|e| {
611 tracing::warn!("Failed to load {} history: {}", history_name, e);
612 crate::input::input_history::InputHistory::new()
613 });
614 histories.insert(history_name.to_string(), history);
615 }
616 histories
617 },
618 pending_async_prompt_callback: None,
619 lsp_progress: std::collections::HashMap::new(),
620 lsp_server_statuses: std::collections::HashMap::new(),
621 lsp_window_messages: Vec::new(),
622 lsp_log_messages: Vec::new(),
623 diagnostic_result_ids: HashMap::new(),
624 scheduled_diagnostic_pull: None,
625 scheduled_inlay_hints_request: None,
626 stored_push_diagnostics: HashMap::new(),
627 stored_pull_diagnostics: HashMap::new(),
628 stored_diagnostics: HashMap::new(),
629 stored_folding_ranges: HashMap::new(),
630 event_broadcaster: crate::model::control_event::EventBroadcaster::default(),
631 bookmarks: bookmarks::BookmarkState::default(),
632 search_case_sensitive: true,
633 search_whole_word: false,
634 search_use_regex: false,
635 search_confirm_each: false,
636 macros: macros::MacroState::default(),
637 #[cfg(feature = "plugins")]
638 pending_plugin_actions: Vec::new(),
639 #[cfg(feature = "plugins")]
640 plugin_render_requested: false,
641 chord_state: Vec::new(),
642 pending_lsp_confirmation: None,
643 pending_lsp_status_popup: None,
644 user_dismissed_lsp_languages: std::collections::HashSet::new(),
645 pending_close_buffer: None,
646 auto_revert_enabled: true,
647 last_auto_revert_poll: time_source.now(),
648 last_file_tree_poll: time_source.now(),
649 git_index_resolved: false,
650 file_mod_times: HashMap::new(),
651 dir_mod_times: HashMap::new(),
652 pending_file_poll_rx: None,
653 pending_dir_poll_rx: None,
654 file_rapid_change_counts: HashMap::new(),
655 file_open_state: None,
656 file_browser_layout: None,
657 recovery_service: {
658 let recovery_config = RecoveryConfig {
659 enabled: recovery_enabled,
660 ..RecoveryConfig::default()
661 };
662 RecoveryService::with_config_and_dir(recovery_config, dir_context.recovery_dir())
663 },
664 full_redraw_requested: false,
665 time_source: time_source.clone(),
666 last_auto_recovery_save: time_source.now(),
667 last_persistent_auto_save: time_source.now(),
668 active_custom_contexts: HashSet::new(),
669 plugin_global_state: HashMap::new(),
670 editor_mode: None,
671 warning_log: None,
672 status_log_path: None,
673 warning_domains: WarningDomainRegistry::new(),
674 update_checker,
675 terminal_manager: crate::services::terminal::TerminalManager::new(),
676 terminal_buffers: HashMap::new(),
677 terminal_backing_files: HashMap::new(),
678 terminal_log_files: HashMap::new(),
679 terminal_mode: false,
680 keyboard_capture: false,
681 terminal_mode_resume: std::collections::HashSet::new(),
682 previous_click_time: None,
683 previous_click_position: None,
684 click_count: 0,
685 settings_state: None,
686 calibration_wizard: None,
687 event_debug: None,
688 keybinding_editor: None,
689 key_translator: crate::input::key_translator::KeyTranslator::load_from_config_dir(
690 &dir_context.config_dir,
691 )
692 .unwrap_or_default(),
693 color_capability,
694 pending_file_opens: Vec::new(),
695 pending_hot_exit_recovery: false,
696 wait_tracking: HashMap::new(),
697 completed_waits: Vec::new(),
698 stdin_stream: stdin_stream::StdinStream::default(),
699 line_scan: line_scan::LineScan::default(),
700 search_scan: search_scan::SearchScan::default(),
701 search_overlay_top_byte: None,
702 review_hunks: Vec::new(),
703 active_action_popup: None,
704 composite_buffers: HashMap::new(),
705 composite_view_states: HashMap::new(),
706 };
707
708 editor.clipboard.apply_config(&editor.config.clipboard);
710
711 #[cfg(feature = "plugins")]
712 {
713 editor.update_plugin_state_snapshot();
714 if editor.plugin_manager.is_active() {
715 editor.plugin_manager.run_hook(
716 "editor_initialized",
717 crate::services::plugins::hooks::HookArgs::EditorInitialized,
718 );
719 }
720 }
721
722 Ok(editor)
723 }
724
725 pub fn event_broadcaster(&self) -> &crate::model::control_event::EventBroadcaster {
727 &self.event_broadcaster
728 }
729
730 pub(super) fn start_background_grammar_build(
735 &mut self,
736 additional: Vec<crate::primitives::grammar::GrammarSpec>,
737 callback_ids: Vec<fresh_core::api::JsCallbackId>,
738 ) {
739 let Some(bridge) = &self.async_bridge else {
740 return;
741 };
742 self.grammar_build_in_progress = true;
743 let sender = bridge.sender();
744 let config_dir = self.dir_context.config_dir.clone();
745 tracing::info!(
746 "Spawning background grammar build thread ({} plugin grammars)...",
747 additional.len()
748 );
749 std::thread::Builder::new()
750 .name("grammar-build".to_string())
751 .spawn(move || {
752 tracing::info!("[grammar-build] Thread started");
753 let start = std::time::Instant::now();
754 let registry = if additional.is_empty() {
755 crate::primitives::grammar::GrammarRegistry::for_editor(config_dir)
756 } else {
757 crate::primitives::grammar::GrammarRegistry::for_editor_with_additional(
758 config_dir,
759 &additional,
760 )
761 };
762 tracing::info!("[grammar-build] Complete in {:?}", start.elapsed());
763 drop(sender.send(
764 crate::services::async_bridge::AsyncMessage::GrammarRegistryBuilt {
765 registry,
766 callback_ids,
767 },
768 ));
769 })
770 .ok();
771 }
772}