use crate::state::AppState; use ide_core::settings::Settings; use ide_core::{EventSink, IdeError, IdeResult, RequestId, TaskId}; use ide_language::LanguageInfo; use ide_workspace::settings::SettingsScope; use ide_workspace::{RecentProject, RecentProjectGroup}; use serde::{Deserialize, Serialize}; use specta::Type; use std::path::PathBuf; use tauri::{Manager, State, Window}; #[derive(Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct AppInfo { pub version: String, pub platform: String, pub config_dir: ide_core::IpcPath, /// Linux on a Wayland display: GTK draws the frameless windows' shadow and resize edges. pub wayland: bool, } pub use ide_host::ops::project::ProjectInfo; #[tauri::command] #[specta::specta] pub fn app_info(state: State<'_, AppState>) -> AppInfo { state.note_ipc(); AppInfo { version: env!("CARGO_PKG_VERSION").into(), platform: std::env::consts::OS.into(), config_dir: state.config_dir.clone().into(), #[cfg(target_os = "linux")] wayland: crate::linux_window_state::is_wayland(), #[cfg(not(target_os = "linux"))] wayland: false, } } #[tauri::command] #[specta::specta] pub fn recent_projects(state: State<'_, AppState>) -> Vec { state.note_ipc(); state.recent.list() } #[tauri::command] #[specta::specta] pub fn remove_recent_project(state: State<'_, AppState>, path: PathBuf) -> IdeResult<()> { state.recent.remove(&path)?; crate::jump_list::refresh(&state.app, state.recent.list()); Ok(()) } /// The welcome screen's project groups. #[tauri::command] #[specta::specta] pub fn recent_project_groups(state: State<'_, AppState>) -> Vec { state.recent.groups() } /// Replaces the project groups; returns them as saved (names trimmed and unique, each /// project in one group, only recent projects). #[tauri::command] #[specta::specta] pub fn set_recent_project_groups( state: State<'_, AppState>, groups: Vec, ) -> IdeResult> { state.recent.set_groups(groups) } #[tauri::command] #[specta::specta] pub async fn open_project(window: Window, path: PathBuf) -> IdeResult { // Opening touches the disk (index cache, watcher, git): keep it off the async runtime threads. let app = window.app_handle().clone(); let label = window.label().to_owned(); // Replacing this window's project (the UI skips reopening the same one). if app.state::().project(&label).is_ok() { crate::windows::close_auxiliary_windows(&app, &label); } let project = tauri::async_runtime::spawn_blocking(move || app.state::().open_project(&label, &path)) .await .map_err(|e| IdeError::internal(e.to_string()))??; let info = project_info(&project); let _ = window.set_title(&info.name); window.state::().sink.emit(ide_core::IdeEvent::WindowGroupsChanged); // Every project window is part of the session reopened at the next start. crate::window_groups::persist(window.app_handle()).await?; Ok(info) } /// Edits the loose file `path` in this window without a project (LightEdit); /// a window already in LightEdit keeps its files. Returns the effective (global) settings. #[tauri::command] #[specta::specta] pub async fn light_edit_open(window: Window, path: PathBuf) -> IdeResult { let app = window.app_handle().clone(); let label = window.label().to_owned(); if app.state::().project(&label).is_ok_and(|p| !p.light) { crate::windows::close_auxiliary_windows(&app, &label); crate::window_groups::detach(&app, &label)?; crate::window_groups::persist(&app).await?; } let project = tauri::async_runtime::spawn_blocking(move || app.state::().open_light(&label, &path)) .await .map_err(|e| IdeError::internal(e.to_string()))??; Ok(project.settings.effective()) } fn project_info(project: &crate::state::Project) -> ProjectInfo { ide_host::ops::project::info(project) } /// File › Rename Project…: the project's display name (title bar, recent /// projects); an empty `name` restores the folder name. Stored in the project's settings. #[tauri::command] #[specta::specta] pub fn project_rename(window: Window, state: State<'_, AppState>, name: String) -> IdeResult { let project = state.project(window.label())?; let name = name.trim(); if let Some(workspace) = ide_workspace::workspace::Workspace::load(&project.root)? { return ide_host::ops::project::save_workspace(&state.host, window.label(), name, workspace.projects); } let value = if name.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(name.to_owned()) }; project.settings.set(SettingsScope::Project, "general.projectName", value)?; state.recent.rename(&project.root, (!name.is_empty()).then_some(name))?; crate::jump_list::refresh(&state.app, state.recent.list()); Ok(project_info(&project)) } #[tauri::command] #[specta::specta] pub fn close_project(window: Window, state: State<'_, AppState>) { crate::windows::close_auxiliary_windows(window.app_handle(), window.label()); let _ = crate::window_groups::detach(window.app_handle(), window.label()); state.close_project(window.label()); let app = window.app_handle().clone(); tauri::async_runtime::spawn(async move { let _ = crate::window_groups::persist(&app).await; }); } #[tauri::command] #[specta::specta] pub fn languages(state: State<'_, AppState>) -> Vec { state.note_ipc(); // Includes plugin languages (built-in definitions win on id clashes). state.languages.all().iter().map(LanguageInfo::from).collect() } /// Effective settings for the window's project, or global settings without one. #[tauri::command] #[specta::specta] pub fn settings_get(window: Window, state: State<'_, AppState>) -> Settings { state.note_ipc(); ide_host::ops::project::settings(&state.host, window.label()) } /// Defaults plus the global layer, without any project's values: what new /// projects (and projects without their own value) get. #[tauri::command] #[specta::specta] pub fn settings_get_global(state: State<'_, AppState>) -> Settings { state.host.with_proxy_password(state.global_settings.effective()) } /// `value` is JSON text, e.g. `14` or `"dark"`. Every window (for a project /// value: that project's windows) is told (`IdeEvent::SettingsChanged`) so it /// re-reads its effective settings. #[tauri::command] #[specta::specta] pub fn settings_set( window: Window, state: State<'_, AppState>, scope: SettingsScope, key: String, value: String, ) -> IdeResult { let applied = ide_host::ops::project::set_settings(&state.host, window.label(), scope, &key, &value)?; // A project value concerns only that project's window (and its settings window). let event = ide_core::IdeEvent::SettingsChanged { key }; if applied.project_scope { let owner = AppState::owner_window(window.label()); for label in [owner.to_owned(), crate::windows::settings_label(owner)] { let sink = crate::events::TauriSink { app: state.app.clone(), window: Some(label) }; sink.emit(event.clone()); if let Some(root) = &applied.roots_changed { sink.emit(ide_core::IdeEvent::ProjectRootsChanged { root: root.clone() }); } } } else { state.sink.emit(event); } Ok(applied.settings) } /// File › Manage IDE Settings › Export Settings…: the global settings plus the /// frontend's IDE-level `storage` (live / file templates, macros) to `path`. #[tauri::command] #[specta::specta] pub async fn settings_export( state: State<'_, AppState>, path: PathBuf, storage: std::collections::BTreeMap, ) -> IdeResult<()> { state.global_settings.export_bundle(&path, storage) } #[derive(Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct SettingsImport { /// Effective settings of the calling window after the import. pub settings: Settings, /// IDE state the frontend keeps (written back to its storage). pub storage: std::collections::BTreeMap, } /// Import Settings…: replaces the global settings with the file's; every window re-reads them. #[tauri::command] #[specta::specta] pub async fn settings_import(window: Window, state: State<'_, AppState>, path: PathBuf) -> IdeResult { let (_, storage) = state.global_settings.import_bundle(&path)?; for project in state.projects() { let effective = project.settings.reload(); project .documents .set_file_defaults(ide_workspace::FileDefaults::from_settings(&effective, Some(&project.root))); let lsp = project.lsp.clone(); tauri::async_runtime::spawn(async move { lsp.reconfigure().await }); } crate::commands::ssh::apply_ssh_settings(&state); state.host.apply_proxy(); state.sink.emit(ide_core::IdeEvent::SettingsChanged { key: String::new() }); let settings = ide_host::ops::project::settings(&state.host, window.label()); Ok(SettingsImport { settings, storage }) } /// Effective source / test / resource / excluded roots of the window's project: /// auto-detected from build files, overridden by Settings › Project Structure. #[tauri::command] #[specta::specta] pub fn project_roots(window: Window, state: State<'_, AppState>) -> IdeResult { ide_host::ops::project::roots(&state.host, window.label()) } /// The project open in this window (a settings window: its parent's), if any. #[tauri::command] #[specta::specta] pub fn project_current(window: Window, state: State<'_, AppState>) -> Option { state.project(window.label()).ok().filter(|p| !p.light).map(|p| project_info(&p)) } #[tauri::command] #[specta::specta] pub fn cancel_request(state: State<'_, AppState>, id: RequestId) { state.cancel_request(id); } /// Stops a background task the backend reported with `cancellable` set; false when it /// has ended or can't be cancelled. #[tauri::command] #[specta::specta] pub fn cancel_task(id: TaskId) -> bool { ide_core::task::cancel(id) } #[derive(Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub enum LogLevel { Debug, Info, Warn, Error, } /// Routes frontend logs into the backend `tracing` pipeline. #[tauri::command] #[specta::specta] pub fn log_frontend(level: LogLevel, message: String) { match level { LogLevel::Debug => tracing::debug!(target: "frontend", "{message}"), LogLevel::Info => tracing::info!(target: "frontend", "{message}"), LogLevel::Warn => tracing::warn!(target: "frontend", "{message}"), LogLevel::Error => tracing::error!(target: "frontend", "{message}"), } } #[tauri::command] #[specta::specta] pub fn project_trust_status(state: State<'_, AppState>, path: PathBuf) -> IdeResult { state.trust.status(&path) } #[tauri::command] #[specta::specta] pub fn project_trust_list(state: State<'_, AppState>) -> IdeResult { state.trust.decisions() } #[tauri::command] #[specta::specta] pub async fn project_trust_set( state: State<'_, AppState>, path: PathBuf, trusted: bool, folder: bool, ) -> IdeResult<()> { state.host.set_project_trust(&path, trusted, folder).await?; if !trusted { state.plugins.restrict_project(&dunce::canonicalize(&path).unwrap_or(path)); } Ok(()) } #[tauri::command] #[specta::specta] pub async fn project_trust_remove_location(state: State<'_, AppState>, path: PathBuf) -> IdeResult<()> { state.trust.remove_location(&path)?; state.host.refresh_project_trust().await?; for project in state.host.projects().into_iter().filter(|p| !p.is_trusted()) { state.plugins.restrict_project(&project.root); } Ok(()) }