Files
openidea/plugins/README.md
T
l0ng-ai 4b5ef7069c fix(plugins): build new plugins outside the repository
- `openidea-plugin new` makes a Rust plugin depend on the SDK at the tool's
  release tag (git) unless the plugin is inside the source tree the tool was
  built from or `--sdk` is given; the path written before only resolved on
  the machine that built the tool.
- The command template compiles again: `run_command` returns
  `openidea_plugin::Error`. A test now runs `cargo test` on the generated
  plugin against the SDK.
- plugins/README.md: installing the tool with `cargo install --git`, the SDK
  dependency, where plugins can be installed (welcome screen and trusted
  projects), and where to look when a plugin misbehaves.
2026-11-04 20:55:31 +08:00

60 KiB
Raw Permalink Blame History

Writing OpenIDEA plugins

OpenIDEA plugins are sandboxed WebAssembly components (wasmtime, component model, WASI preview 2). A plugin can register commands (they become IDE actions you can search for and bind to keys), read project files, edit the focused editor, react to workbench events, run background tasks, show notifications and status bar items, keep data in its own storage, read and fetch git repositories, provide completion, quick fixes, navigation and gutter markers, call web services and run approved programs, show tool windows and custom editors, and contribute menus, shortcuts, settings, themes, file icon themes and languages. Plugins that only contribute data (themes, icons, languages, menus, settings) need no code at all.

Preview. The plugin system ships as a preview: until it is declared stable the API (openidea:plugin@1.0) may still change, and the official plugins are versioned 0.x. See Evolving the plugin API below.

plugins/
└─ official/
   ├─ harbor-night/           # data only: a colour theme (UI and editor scheme)
   ├─ material-icons/         # data only: a file icon theme
   ├─ solidity/               # language + syntax provider + run configurations
   ├─ string-tools/           # 26 text commands, versioned multi-cursor edits
   ├─ key-promoter/           # action events, storage
   ├─ git-toolbox/            # on-schedule fetch, tasks, VCS, status bar item
   ├─ checkstyle/             # one-shot process, diagnostics, quick fixes
   ├─ ai-completion/          # inline completion over the network, secrets
   ├─ mybatis/                # navigation, gutter markers, completion across files
   ├─ csv-editor/             # custom editor sharing the text document
   ├─ excalidraw/             # web view custom editor, file icons, a template
   ├─ graphviz/               # web view with code: live preview, problems from the page, a language
   ├─ regex-tester/           # html view talking to its code, settings, the editor's selection
   └─ license-header/         # file-opened events, quick fix, project scan in a task
sdk/rust/openidea-plugin/     # the Rust SDK (host API, Plugin trait, mock host)
crates/ide-plugin/tests/fixtures/  # test plugins: hello, jobs, language-toy, misbehave

The plugin API is defined in wit/ (openidea:plugin@1.0.0, world plugin).

1. Quick start

The openidea-plugin tool scaffolds, builds, checks and links plugins:

cargo install --locked --git https://github.com/l0ng-ai/openidea ide-host --bin openidea-plugin
# in a checkout of this repository: cargo install --path crates/ide-host --bin openidea-plugin

openidea-plugin new my-plugin                          # Rust command plugin (SDK)
openidea-plugin new my-theme --template theme          # also: icon-theme, language
cd my-plugin
cargo test                                             # logic, against the SDK's mock host
openidea-plugin build                                  # → dist/plugin.wasm, then `check`
openidea-plugin link                                   # load this folder in the IDE

Each release also attaches the tool for Linux x64 (openidea-plugin-linux-x86_64.gz). A plugin is a project of its own: keep it in its own repository, and list it in the community marketplace when it is ready (§8.1).

A Rust plugin made by new depends on the SDK of the tool's release, from this repository's tag (openidea-plugin = { git = "…", tag = "vX.Y.Z" }); change the tag to build against a later API. Inside a checkout of this repository, or with --sdk <path to sdk/rust/openidea-plugin>, it depends on that SDK by path instead.

build runs cargo build --release --target wasm32-wasip2 (installing the target through rustup if needed), copies the component to the manifest's wasm path and runs check. check validates the package exactly as Install Plugin from Disk… would — manifest, every contribution, the SVG sanitiser, the component — and that the component imports nothing but the plugin API and WASI. It prints what the user will be asked to grant.

link adds the folder to links.json in the IDE's plugins directory ($OPENIDEA_PLUGINS_DIR, default <app-data>/ai.l0ng.openidea/plugins). Linked plugins are read from the folder itself, so the edit loop is build then Settings › Plugins › Reload. unlink and links undo and list.

2. The package

A plugin is a directory with a plugin.toml:

id = "acme.word-tools"            # [a-z0-9][a-z0-9_-]*, optionally with dotted namespaces
name = "Word Tools"
version = "1.2.0"                 # semver
description = "Counts and transforms words."
authors = ["You <you@example.com>"]
homepage = "https://example.com"  # optional
license = "MIT"                   # optional
change-notes = "1.2.0 — …"        # shown under What's New
categories = ["tools"]            # marketplace categories, see below
engines.openidea = "^1.0"         # the plugin API this package needs
wasm = "dist/plugin.wasm"         # omit for data-only plugins
activation = ["on-language:markdown"]   # optional, see below

[capabilities]
required = ["project.read", "editor.write"]
optional = ["net:api.example.com"]

[[contributes.commands]]
id = "count"                      # [a-z0-9][a-z0-9._-]*
title = "Word Tools: Count Words"
category = "Word Tools"           # group in Search Everywhere / Keymap
when = "editorFocus"              # optional: same expressions as built-in actions

categories (up to 3) file the plugin in the marketplace: languages, themes, icon-themes, linters (linters and formatters), vcs, ai, frameworks, tools, other. Without them, a plugin contributing themes, icon themes or languages is filed under those, any other under other. Categories an IDE doesn't know, and any after the first three it knows, are ignored (check warns about both).

All paths are relative to the package directory and may not leave it. A manifest is validated as a whole: if one contribution is invalid, nothing is installed or loaded. Contribution types the IDE does not know (a newer API) are skipped with a warning.

Manifests in the 0.1 format (openidea-api = "0.1", permissions = [...]) still load (their permissions become required capabilities; contributions work), but 0.1 code no longer runs: rebuild it against the 1.0 SDK.

Contribution points

Point Declaration Code needed
commands id, title, category?, when? to run them
themes path (theme JSON), editor-scheme? no
iconThemes id, label, path (icon theme JSON, see §5) no
languages path (languages/<id>.toml format, see §6) no
runDetectors id, name, files, command, args?, test? (§6.2) no
providers kinds, languages (editor providers, see Providers) yes
secrets name, label, description? (see Network) to use them
menus menu (editor.context, projectView.context, editorTab.context, tools), command, when? no
keymaps command, key, mac? (Ctrl+Alt+C; a chord: Meta+K Meta+C) no
statusBar id, text, tooltip?, command?, align?, priority? no
settings schema (JSON file), title? — one table, see Settings no
views id, title, icon?, location (left, right, bottom), web? (see Web views) without web
customEditors id, title, patterns (file names), priority (default, option), layout?, web? without web
fileIcons id, patterns (file names), icon (SVG), iconDark? — whatever icon theme is in use no
fileTemplates id, name, extension, path (the new file's content; template variables work) — New menu no

Declared contributions are live as soon as the plugin is enabled — commands show in Find Action, menus and the status bar, shortcuts work, themes and icon themes can be applied, settings pages are there — before any plugin code is loaded. Menus, shortcuts and status bar items refer to the plugin's declared commands. A plugin shortcut never takes a keystroke the IDE (or the user) already uses; the user can rebind it in the keymap editor.

Activation

Code is instantiated lazily: when one of the plugin's commands runs, or when an event in activation fires:

Event Fires when
on-command:<id> the command runs (implied for every command)
on-language:<id> a file of that language is opened (*: any)
on-file:<glob> a project file matching the glob is opened (**)
on-view:<id> one of the plugin's views is shown
on-schedule:<secs> every secs seconds (at least 60): Event::Scheduled
on-event:<kind> a workbench event of that kind (see Events)
on-startup-finished shortly after the workbench is ready (use sparingly)

Until then the plugin is listed as Inactive and costs nothing at start-up. An instance unused for 5 minutes is reclaimed (its deactivate runs) and the plugin is Inactive again until its next event; on-schedule activates it when due but does not keep it alive. Keep nothing in memory you can't rebuild in activate.

Capabilities

Code can only do what its capabilities allow, and the user approves them when installing (or when a new version asks for more). Low-risk capabilities are listed; high-risk ones are called out as equivalent to running code on the machine. Users can revoke optional capabilities in Settings › Plugins.

Capability Risk Unlocks today
project.read low Project::root, read_file, find_files, host::projects, publish_diagnostics
editor.read low TextEditor::document, selections
editor.write medium TextEditor::apply_edits
vcs.read low Project::vcs_status (branch, upstream, ahead/behind, changes)
vcs.write medium Project::vcs_fetch (trusted projects only)
storage low host::storage (1 MiB per plugin)
events low file opened/saved, editor focused, repository changed events
events.input medium action-performed events (which actions you run, and how)
process:<program> high Project::run_process, language servers, debug adapters, runs
net:<host> medium host::fetch to that host (*.example.com: its subdomains)
secrets medium {{secret:<name>}} in request headers, host::secret_is_set
clipboard medium the clipboard API in the plugin's web views (keyboard copy / paste work without it)

process: capabilities are also derived from what the contributions start: a language's language-servers and debugger commands and a run detector's command. Those are optional — refusing one keeps the rest of the plugin working. On grant the program is resolved to an absolute path and its content hash is recorded; it is checked again right before every start, and a replaced file is refused until the user approves it again. In a remote project the server resolves and pins the program on its own machine (see Remote projects). fs.read:<path> can be declared and granted; it unlocks nothing yet.

Logging, notifications, status bar items, tasks and run-time contributions are always available. Calling a gated function without its capability returns Error::Denied("<capability>") — the plugin keeps running and can degrade (Git Toolbox still shows the branch without vcs.write).

3. Writing code (Rust SDK)

openidea-plugin new writes this for you. Cargo.toml:

[package]
name = "openidea-plugin-word-tools"
version = "0.1.0"
edition = "2024"

[workspace]                       # keep it out of any parent workspace

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
openidea-plugin = { git = "https://github.com/l0ng-ai/openidea", tag = "vX.Y.Z" }   # or { path = "…/sdk/rust/openidea-plugin" }

[profile.release]
opt-level = "s"
lto = true
strip = true
panic = "abort"

src/lib.rs:

use openidea_plugin::{Error, Invocation, NotificationLevel, Plugin, host, register_plugin};

#[derive(Default)]
struct WordTools;

impl Plugin for WordTools {
    fn run_command(&mut self, id: &str, ctx: &Invocation, _arg: Option<&str>) -> Result<Option<String>, Error> {
        let editor = ctx.editor().ok_or("no file is open")?;
        match id {
            "count" => {
                let words = editor.document()?.text.split_whitespace().count();
                host::notify(NotificationLevel::Info, "Word count", &format!("{words} words"));
                Ok(None)
            }
            // Every selection (a cursor: empty) in one undoable change.
            "shout" => {
                editor.transform_selections(|s| s.to_uppercase())?;
                Ok(None)
            }
            _ => Err(Error::NotFound(format!("command {id}"))),
        }
    }
}

register_plugin!(WordTools);

On wasm32-wasip2 the SDK calls the IDE; on your machine it calls the SDK's mock host, so the logic is unit-tested with cargo test:

#[test]
fn counts_and_shouts() {
    let host = openidea_plugin::mock::MockHost::install();
    host.open_editor("/project/a.md", "markdown", "one two three", &[(0, 4, 0, 7)]);
    WordTools.run_command("count", &host.invocation(), None).unwrap();
    assert_eq!(host.notifications()[0].body, "3 words");
    WordTools.run_command("shout", &host.invocation(), None).unwrap();
    assert_eq!(host.editor_text(), "one TWO three");
}

The mock enforces capabilities like the IDE (MockHost::install().with_capabilities(&[...])), keeps a versioned editor document (type_in_editor makes edits stale), and records notifications, status items, storage, subscriptions, tasks, fetches, commands, languages, themes and log lines.

Any language with component-model tooling works (componentize-py, jco componentize, TinyGo): the only contract is the plugin world in wit/ (plugin-syntax adds the syntax provider). Build it yourself, then run openidea-plugin check.

The API

Rust SDK WIT Notes
Plugin::run_command(id, ctx, arg) export commands.run ctx is the window: ctx.project(), ctx.editor(). An error shows "