use crate::events::TauriSink; use ide_core::{EventSink, IdeEvent, IdeResult, SharedSink}; use ide_language::{LanguageRegistry, Syntax}; use ide_workspace::RecentProjects; use std::path::Path; use std::sync::{Arc, OnceLock}; use std::time::Duration; use tauri::{AppHandle, Manager}; pub use ide_host::{Host, Project}; /// Process-wide state shared by all windows: the backend [`Host`] (reachable /// through `Deref`) plus what only the desktop shell has. pub struct AppState { pub app: AppHandle, pub host: Arc, pub startup: Startup, pub recent: RecentProjects, /// Plugins (`ide_host::plugins`), shared by all windows. pub plugins: Arc, /// The built-in web server (Open In › Browser, the editor's browser bar). pub web: ide_web::WebServer, /// Credential prompts of `ssh` / `git` child processes (`None` if it failed to start). pub askpass: Option, /// Remote development connections, by window label. pub remote_dev: crate::remote_dev::Connections, /// Broadcasts to every window. pub sink: SharedSink, } impl std::ops::Deref for AppState { type Target = Host; fn deref(&self) -> &Host { &self.host } } /// Start-up timings (`app_metrics`). #[derive(Debug)] pub struct Startup { /// Process start → `main` (exec, code-signature scan, dynamic loading). pub process_to_main: Option, /// Process start → `AppState::new` (Tauri / WebView boot before our code). pub process_to_setup: Option, /// Time spent in `AppState::new`. pub setup: Duration, /// Process start → first IPC call that reached [`AppState::note_ipc`]. pub first_ipc: OnceLock, } impl AppState { pub fn new(app: AppHandle) -> Result> { ide_workspace::process::mark_start(); let process_to_main = ide_workspace::process::before_main(); let process_to_setup = ide_workspace::process::process_age(); let started = std::time::Instant::now(); // Grammar set-up is not needed for the first paint: build it off the main thread. let syntax: Arc>> = Arc::default(); { let syntax = syntax.clone(); std::thread::Builder::new().name("openidea-syntax-init".into()).spawn(move || { syntax.get_or_init(|| Arc::new(Syntax::new())); })?; } let config_dir = app.path().app_config_dir()?; let data_dir = app.path().app_data_dir()?; let local_data_dir = app.path().app_local_data_dir()?; std::fs::create_dir_all(&config_dir)?; std::fs::create_dir_all(&data_dir)?; if let Err(error) = std::fs::create_dir_all(&local_data_dir) { tracing::warn!(?local_data_dir, %error, "cannot create local data directory"); } tracing::info!(?data_dir, ?local_data_dir, "application data directories"); // Saved git HTTPS credentials (askpass answers them silently). ide_ssh::git_creds::init(data_dir.join("git-credentials.json")); // File › Invalidate Caches… requested at the last run. let removed = ide_workspace::caches::apply_pending(&data_dir, &local_data_dir); if !removed.is_empty() { tracing::info!("invalidated caches: {removed:?}"); } ide_host::data_dirs::migrate(&data_dir, &local_data_dir); let global_sink: SharedSink = Arc::new(TauriSink { app: app.clone(), window: None }); let languages = Arc::new(LanguageRegistry::load(Some(&config_dir.join("languages")))?); let sink_app = app.clone(); let host = Arc::new(Host::new(ide_host::HostConfig { config_dir: config_dir.clone(), data_dir: data_dir.clone(), local_data_dir, languages: languages.clone(), syntax, sink_for: Box::new(move |window| { Arc::new(TauriSink { app: sink_app.clone(), window: Some(window.to_owned()) }) }), owner: Self::owner_window, runtime: tauri::async_runtime::handle().inner().clone(), project_environment: false, })); Ok(Self { recent: RecentProjects::load(&config_dir), askpass: start_askpass(&app), sink: global_sink, plugins: crate::commands::plugin::create_host(&app, &data_dir, &host)?, web: ide_web::WebServer::new(), remote_dev: crate::remote_dev::Connections::default(), host, startup: Startup { process_to_main, process_to_setup, setup: started.elapsed(), first_ipc: OnceLock::new(), }, app, }) } /// Records the first IPC call for the start-up metric. pub fn note_ipc(&self) { self.startup.first_ipc.get_or_init(|| { let age = ide_workspace::process::process_age().unwrap_or_else(ide_workspace::process::uptime); let ms = |d: Option| d.map_or(f64::NAN, |d| d.as_secs_f64() * 1000.0); tracing::info!( "startup: first IPC {:.0} ms after process start (main {:.0} ms, setup at {:.0} ms, took {:.1} ms)", age.as_secs_f64() * 1000.0, ms(self.startup.process_to_main), ms(self.startup.process_to_setup), self.startup.setup.as_secs_f64() * 1000.0 ); age }); } /// The window that owns what `window` starts: a settings window's parent, else `window`. pub fn owner_window(window: &str) -> &str { crate::windows::auxiliary_parent(window).unwrap_or(window) } /// Opens `root` in `window` and records it in the recent projects. pub fn open_project(&self, window: &str, root: &Path) -> IdeResult> { // A local project replaces a remote one in this window. self.remote_dev.disconnect(window); let project = self.host.open_project(window, root)?; let branch = project.repos.read().primary().and_then(|r| r.branch_info().ok()).and_then(|b| b.head); self.recent.touch(&project.root, branch)?; let display_name = project.settings.effective().general.project_name; if !display_name.trim().is_empty() { self.recent.rename(&project.root, Some(display_name.trim()))?; } crate::jump_list::refresh(&self.app, self.recent.list()); Ok(project) } /// Opens `window` for editing the loose file `file` (LightEdit); not a recent project. pub fn open_light(&self, window: &str, file: &Path) -> IdeResult> { self.remote_dev.disconnect(window); self.host.open_light(window, file) } /// Called on the event-loop thread (window destruction, the `close_project` /// command), so it must not wait for the project's workers to shut down. pub fn close_project(&self, window: &str) { self.remote_dev.disconnect(window); self.host.close_project_detached(window); } } /// Serves askpass prompts to the focused window and routes `git`'s prompts there. fn start_askpass(app: &AppHandle) -> Option { let exe = std::env::current_exe().ok()?; let handle = app.clone(); let on_prompt = Arc::new(move |p: ide_ssh::askpass::AskpassPrompt| { // A focused Settings window hands the prompt to its project window. let window = crate::windows::target_window(&handle).map(|w| AppState::owner_window(w.label()).to_owned()); let remember = ide_ssh::askpass::remembers(&p.prompt); TauriSink { app: handle.clone(), window }.emit(IdeEvent::Askpass { id: p.id, prompt: p.prompt, kind: p.kind, remember, }); }); match ide_ssh::askpass::AskpassServer::start(exe, on_prompt) { Ok(server) => { ide_vcs::set_git_command_observer( ide_ssh::askpass::COMMAND_ENV, Arc::new(|id, success, stderr| { if let Some(store) = ide_ssh::git_creds::get() { store.command_finished(id, success, stderr); } }), ); ide_vcs::set_askpass_env(server.env()); Some(server) } Err(e) => { tracing::warn!("askpass unavailable: {}", e.message); None } } }