Файли
openidea/crates/ide-run/src/lib.rs
T
1c5a92f952 feat(plugins): plugin system with extension points, consent, Wasm runtime and plugin API 1.0 (#971)
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]>
2026-11-04 02:54:31 +08:00

298 рядки
11 KiB
Rust
Неформатований Анотація Історія

Цей файл містить неоднозначні символи Юнікоду
Цей файл містить символи Юнікоду, які можна сплутати з іншими символами. Якщо ви вважаєте, що це зроблено навмисно, можете сміливо ігнорувати це попередження. Щоб показати їх, скористайтеся кнопкою Escape.
//! Run configurations and process management.
mod config;
mod detect;
pub mod dotnet;
pub mod envfile;
mod external;
pub mod go;
pub mod gradle_jvm;
pub mod import;
pub mod java_cmdline;
pub mod junit;
mod process;
pub mod python_path;
pub mod tomcat;
mod utf8;
pub mod which;
pub mod wrap;
pub use process::{ProcessManager, StartOptions};
use ide_core::{IdeResult, RunId};
use serde::{Deserialize, Serialize};
use specta::Type;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct RunConfig {
/// Stable id, e.g. `npm:dev`, `cargo:run:app`, or a uuid for user-defined.
pub id: String,
pub name: String,
/// `npm` | `cargo` | `gradle` | `maven` | `go` | `python` | `pytest` | `make` | `shell` …
pub kind: String,
pub command: String,
pub args: Vec<String>,
pub cwd: Option<ide_core::IpcPath>,
pub env: BTreeMap<String, String>,
/// Detected configs are regenerated; user configs persist in `.openidea/run.toml`.
pub detected: bool,
/// Test runner configs render results in the test tree.
pub is_test: bool,
/// DAP adapter that can debug this config, if any.
pub debug_adapter: Option<String>,
/// "Allow multiple instances": Run on a running config starts another one instead of restarting it.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
#[specta(optional)]
pub allow_parallel: bool,
/// Options of user-defined configurations (all optional; old `run.toml` files load unchanged).
#[serde(flatten)]
pub extras: RunExtras,
}
/// A step run before a configuration launches; a failing step aborts the launch.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct BeforeLaunchTask {
/// `run` (another configuration, `target` = its id) | `build` | `tool` (external tool id) |
/// `command` (a shell command line) | `toolWindow` (tool window id).
pub kind: String,
#[serde(default)]
pub target: String,
}
fn yes() -> bool {
true
}
fn is_true(b: &bool) -> bool {
*b
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct RunExtras {
/// Module supplying a JVM application's classpath; empty means the project root.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub module: Option<String>,
/// Values of the configuration type's own form fields (the command line derives from them).
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[specta(optional)]
pub params: BTreeMap<String, String>,
/// Folder in the Run/Debug Configurations tree.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub folder: Option<String>,
/// Stored in the workspace (`.openidea/run.local.toml`, not shared) instead of `run.toml`.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
#[specta(optional)]
pub local: bool,
/// Off: the process gets only the configured variables.
#[serde(default = "yes", skip_serializing_if = "is_true")]
#[specta(optional)]
pub include_system_env: bool,
/// `.env` files (relative to the project root) loaded before `env`, in order.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[specta(optional)]
pub env_files: Vec<String>,
/// "Redirect input from": the process reads this file as stdin.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub stdin_file: Option<String>,
/// "Save console output to file" (overwritten on each run).
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub output_file: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[specta(optional)]
pub before_launch: Vec<BeforeLaunchTask>,
/// Compound configurations: ids of the configurations started together.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[specta(optional)]
pub compound: Vec<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
#[specta(optional)]
pub show_on_stdout: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
#[specta(optional)]
pub show_on_stderr: bool,
/// Overrides Settings › Run › "Show the Run tool window when a process starts".
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub open_tool_window: Option<bool>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
#[specta(optional)]
pub focus_tool_window: bool,
/// "Emulate terminal in output console": the process runs on a pseudo-terminal
/// (colours, line editing, full-screen programs); the console renders it as a terminal.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
#[specta(optional)]
pub emulate_terminal: bool,
/// Shell command line run on Stop before the process is signalled (e.g. `docker stop <name>`),
/// with the run's working directory and environment.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub stop_command: Option<String>,
/// Settings › Run Targets id the process runs on (over SSH); `None` = this machine.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub target: Option<String>,
}
impl Default for RunExtras {
fn default() -> Self {
Self {
module: None,
params: BTreeMap::new(),
folder: None,
local: false,
include_system_env: true,
env_files: Vec::new(),
stdin_file: None,
output_file: None,
before_launch: Vec::new(),
compound: Vec::new(),
show_on_stdout: false,
show_on_stderr: false,
open_tool_window: None,
focus_tool_window: false,
emulate_terminal: false,
stop_command: None,
target: None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum OutputChunk {
Stdout {
text: String,
},
Stderr {
text: String,
},
/// System message from the IDE (command line echo, exit code).
System {
text: String,
},
}
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct RunInfo {
pub id: RunId,
pub config_id: String,
pub name: String,
pub pid: Option<u32>,
pub running: bool,
/// Runs on a pseudo-terminal: output is raw terminal data, input goes in keystroke by keystroke.
pub pty: bool,
}
pub use external::{EXTERNAL_PREFIX, ExternalDetection, set_external_detection};
/// Detects configs from package.json scripts, Cargo.toml targets, build.gradle(.kts),
/// pom.xml, go.mod, pyproject/pytest, Makefile.
pub fn detect(root: &Path) -> Vec<RunConfig> {
detect::detect(root)
}
/// Detects configurations excluding the supplied project-relative Maven POM paths.
pub fn detect_excluding(root: &Path, excluded_poms: &[PathBuf]) -> Vec<RunConfig> {
detect::detect_excluding(root, excluded_poms)
}
/// Detects configurations with explicit CMake profiles, or legacy discovery when absent.
pub fn detect_with_profiles(
root: &Path,
excluded_poms: &[PathBuf],
profiles: Option<&[ide_core::settings::CMakeProfile]>,
) -> Vec<RunConfig> {
detect::detect_with_profiles(root, excluded_poms, profiles)
}
/// Reads `<root>/.openidea/run.toml`; a missing file yields no configs.
pub fn load_user_configs(root: &Path) -> IdeResult<Vec<RunConfig>> {
config::load(root)
}
/// Writes `<root>/.openidea/run.toml` (and the workspace-local `run.local.toml` for
/// configurations not stored as project files), preserving unrelated content.
pub fn save_user_configs(root: &Path, configs: &[RunConfig]) -> IdeResult<()> {
config::save(root, configs, &[])
}
/// Detected ids explicitly removed in this workspace.
pub fn load_suppressed_config_ids(root: &Path) -> IdeResult<Vec<String>> {
config::load_suppressed(root)
}
/// Saves user configurations and suppresses explicitly removed detections.
/// Saving a configuration with a suppressed id restores that id.
pub fn save_user_configs_with_removals(
root: &Path,
configs: &[RunConfig],
removed_detected_ids: &[String],
) -> IdeResult<()> {
config::save(root, configs, removed_detected_ids)
}
/// Configuration templates (per-type defaults for new configurations) from `run.toml`.
pub fn load_templates(root: &Path) -> IdeResult<Vec<RunConfig>> {
config::load_templates(root)
}
pub fn save_templates(root: &Path, templates: &[RunConfig]) -> IdeResult<()> {
config::save_templates(root, templates)
}
/// The environment runs inherit (unless "Include system environment variables" is off).
pub fn system_env() -> BTreeMap<String, String> {
ide_core::process::host_vars().filter_map(|(k, v)| Some((k.into_string().ok()?, v.into_string().ok()?))).collect()
}
/// Complete environment for a preparatory tool belonging to this run configuration.
pub fn resolved_env(root: &Path, config: &RunConfig) -> IdeResult<BTreeMap<String, String>> {
process::resolved_env(root, config)
}
/// Run configurations from another IDE's project file (`.vscode/launch.json`), not yet saved:
/// names already in `existing` are skipped, so importing again adds nothing twice.
pub fn import_configs(
root: &Path,
source: import::RunImportSource,
existing: &[RunConfig],
) -> IdeResult<import::RunImport> {
// `${env:NAME}` reads what a run inherits, the login shell's variables included.
import::import(root, source, existing, &process::inherited_env())
}
/// Path of the user run configuration file for `root`.
pub fn user_configs_path(root: &Path) -> PathBuf {
config::config_path(root)
}
#[cfg(test)]
mod linkage_tests {
#[test]
fn detection_skips_unlinked_maven_roots() {
let tmp = tempfile::tempdir().unwrap();
for name in ["a", "b"] {
std::fs::create_dir(tmp.path().join(name)).unwrap();
std::fs::write(tmp.path().join(name).join("pom.xml"), "<project/>").unwrap();
}
let configs = super::detect_excluding(tmp.path(), &["a/pom.xml".into()]);
assert!(configs.iter().any(|c| c.kind == "maven"));
assert!(
configs
.iter()
.filter(|c| c.kind == "maven")
.all(|c| c.cwd.as_ref().is_some_and(|d| d.as_ref() == std::path::Path::new("b")))
);
}
}