дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
A plugin system built on WebAssembly components (wasmtime, WASI p2), with a typed WIT API (`openidea:[email protected]`) and a Rust SDK, shipped as a preview. - Extension points declared in `plugin.toml` (commands, languages, themes, icon themes, menus, key bindings, status items, settings, views, custom editors, file icons and templates, run detectors, editor providers), validated before anything takes effect and projected onto the workbench's own registries: plugin themes and file icon themes are chosen in Settings › Appearance and Quick Switch, plugin shortcuts are edited in Settings › Keymap, plugin problems are grouped in Settings › Inspections. - Capabilities with risk levels and consent at install, pinned `process:` programs, secrets in the OS keychain. - Runtime: one thread per active plugin, fuel/time/memory budgets, soft and hard deadlines on the editing path, cancellation, crash backoff, idle reclaim, a precompiled-component cache; remote projects run code plugins on `openidea-server`. - Web views and custom editors with a sandboxed page, a document bridge joined to the text editor's undo history, and split or preview layouts. - Distribution: signed `.oiplugin` packages, a static marketplace index with revocation, versioned installs with rollback, install from a `.zip` or a folder. The release workflow signs and publishes the official plugins and their index, which released builds use as the default marketplace. - Official plugins: String Tools, Key Promoter, Git Toolbox, AI Completion, Checkstyle, MyBatis Navigator, CSV Editor, Solidity, Material Icons, Harbor Night, Excalidraw, Graphviz, Regex Tester, License Header. Closes #732 Co-authored-by: Deng Liu <[email protected]> Co-authored-by: florentvdry <[email protected]>
197 рядки
8.1 KiB
Rust
197 рядки
8.1 KiB
Rust
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<Host>,
|
||
pub startup: Startup,
|
||
pub recent: RecentProjects,
|
||
/// Plugins (`ide_host::plugins`), shared by all windows.
|
||
pub plugins: Arc<ide_host::plugins::PluginHost>,
|
||
/// 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<ide_ssh::askpass::AskpassServer>,
|
||
/// 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<Duration>,
|
||
/// Process start → `AppState::new` (Tauri / WebView boot before our code).
|
||
pub process_to_setup: Option<Duration>,
|
||
/// Time spent in `AppState::new`.
|
||
pub setup: Duration,
|
||
/// Process start → first IPC call that reached [`AppState::note_ipc`].
|
||
pub first_ipc: OnceLock<Duration>,
|
||
}
|
||
|
||
impl AppState {
|
||
pub fn new(app: AppHandle) -> Result<Self, Box<dyn std::error::Error>> {
|
||
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<OnceLock<Arc<Syntax>>> = 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<Duration>| 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::settings_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<Arc<Project>> {
|
||
// 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()))?;
|
||
}
|
||
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<Arc<Project>> {
|
||
self.remote_dev.disconnect(window);
|
||
self.host.open_light(window, file)
|
||
}
|
||
|
||
pub fn close_project(&self, window: &str) {
|
||
self.remote_dev.disconnect(window);
|
||
self.host.close_project(window);
|
||
}
|
||
}
|
||
|
||
/// Serves askpass prompts to the focused window and routes `git`'s prompts there.
|
||
fn start_askpass(app: &AppHandle) -> Option<ide_ssh::askpass::AskpassServer> {
|
||
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
|
||
}
|
||
}
|
||
}
|