дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
Rework the plugin system per docs/plugin_plan.md (P0–P8): - Extension-point registry (ide-extension) so new contribution types register without touching the kernel; languages, run detectors, providers, views and UI contributions go through it. - Per-plugin capability consent with a risk-graded ledger; process capabilities are pinned to resolved binary paths and hashes. - Async, actor-per-plugin Wasm runtime with lazy activation, cancellation, events, long-running tasks and idle reclamation. - Plugin host moved into ide-host, shared by local and remote projects. - Distribution: install/package tooling (openidea-plugin bin) and a static market index. - WIT frozen as openidea:[email protected]; Rust SDK with mock host. - Ten official validation plugins under plugins/official.
622 рядки
36 KiB
Markdown
622 рядки
36 KiB
Markdown
# 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.
|
||
|
||
```
|
||
plugins/
|
||
├─ hello/ # Rust plugin built with the SDK: commands, a tree tool window
|
||
├─ language-toy/ # data only: a language definition
|
||
└─ official/
|
||
├─ 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
|
||
├─ kubernetes/ # tree tool window, terminals, settings
|
||
└─ csv-editor/ # custom editor sharing the text document
|
||
sdk/rust/openidea-plugin/ # the Rust SDK (host API, Plugin trait, mock host)
|
||
```
|
||
|
||
The plugin API is defined in [`wit/`](../wit/) (`openidea:[email protected]`, world
|
||
`plugin`); the design and roadmap are in
|
||
[`docs/plugin_plan.md`](../docs/plugin_plan.md).
|
||
|
||
## 1. Quick start
|
||
|
||
The `openidea-plugin` tool scaffolds, builds, checks and links plugins:
|
||
|
||
```sh
|
||
cargo install --path crates/ide-host --bin openidea-plugin # or: cargo run -p 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
|
||
```
|
||
|
||
`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`:
|
||
|
||
```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 <[email protected]>"]
|
||
homepage = "https://example.com" # optional
|
||
license = "MIT" # optional
|
||
change-notes = "1.2.0 — …" # shown under What's New
|
||
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
|
||
```
|
||
|
||
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`) | yes |
|
||
| `customEditors` | `id`, `title`, `patterns` (file names), `priority` (`default`, `option`) | yes |
|
||
|
||
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` |
|
||
|
||
`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`:
|
||
|
||
```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 = { path = "…/sdk/rust/openidea-plugin" }
|
||
|
||
[profile.release]
|
||
opt-level = "s"
|
||
lto = true
|
||
strip = true
|
||
panic = "abort"
|
||
```
|
||
|
||
`src/lib.rs`:
|
||
|
||
```rust
|
||
use openidea_plugin::{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>, String> {
|
||
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(format!("unknown 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`:
|
||
|
||
```rust
|
||
#[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/`](../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()`. `Err(msg)` shows _"<title> failed: msg"_; the plugin keeps running. |
|
||
| `Plugin::handle_events(events, ctx)` | export `event-handler.handle` | Batches of subscribed events, oldest first, per window. |
|
||
| `Plugin::activate` / `deactivate` | export `lifecycle` | Per instance: after a crash or idle reclaim a new one is activated. |
|
||
| `TextEditor::document()` → `{version, text}` | `editor.text-editor.document` | `selections()` are those when the command ran (0-based lines, UTF-16 columns). |
|
||
| `TextEditor::apply_edits(version, edits)` | `editor.text-editor.apply-edits` | One undoable change, only if the document is still at `version` — else `Error::Stale` and nothing changes. `transform_selections` retries once. |
|
||
| `Project::root()`, `read_file(path)` | `workspace.project` | Absolute or project-relative; `..`/symlinks escaping the root are rejected; UTF-8 only, ≤ 16 MiB; unsaved editor contents win. |
|
||
| `host::projects()` | `workspace.projects` | Every open project (for scheduled work without a window). |
|
||
| `Project::vcs_status()`, `vcs_fetch()` | `vcs` | Fetch runs git, so only in trusted projects; call it inside a task. |
|
||
| `host::subscribe(kind)` | `events.subscribe` | For the session, across idle reclaim (an event activates the plugin). Or declare `on-event:<kind>`. |
|
||
| `host::begin_task(title, cancellable)` → `Task` | `tasks.begin` | Shown with the background tasks; `report`, `is_cancelled`; the call may run 60 s instead of 5 s while it is open; dropping it ends it. |
|
||
| `host::storage::{get, set, delete, keys}` | `storage` | Survives restarts; the instance's memory does not. Deleted on uninstall. |
|
||
| `host::set_status_item(id, item)`, `remove_status_item` | `ui` | `text`, `tooltip`, `command` (one of yours, run on click); at most 8; kept while the plugin is idle. |
|
||
| `host::notify(level, title, body)` | `ui.show-notification` | Balloon in the window the command or events came from (every window otherwise). |
|
||
| `host::log(level, msg)` | `log` | IDE log, tagged with the plugin id. `eprintln!` output is logged too and shown when the plugin crashes. |
|
||
| `host::register_command`, `register_language`, `register_theme` | `contributions` | Run-time contributions; declaring them in `plugin.toml` is better (they show before the code loads). Max 256 commands. |
|
||
| `Project::find_files(glob, limit)` | `workspace.project.find-files` | Project-relative glob (`**/*Mapper.xml`); honours `.gitignore`; at most `limit` paths. |
|
||
| `host::publish_diagnostics(path, items)` | `diagnostics.publish` | Replaces this plugin's problems for the file (editor squiggles and the Problems view); an empty list clears them. |
|
||
| `host::fetch(request)` → `{status, headers, body}` | `net.fetch` | See _Network_. |
|
||
| `Project::run_process(program, args, stdin)` → `{code, stdout, stderr}` | `process.run` | Only the pinned program of a `process:` grant; runs in the project root of a trusted project; 4 MiB of output; killed when the call is cancelled or times out. |
|
||
| `host::refresh_view(view)` | `views.refresh` | The view (or custom editors) render again; call it when the content changed. |
|
||
| `host::post_view_message(view, text)` | `views.post-message` | To the script of the view's `html` body. |
|
||
| `host::open_terminal(title, program, args)` | `views.open-terminal` | A terminal tab in the invoking window running the approved `process:<program>` in the project root: output that keeps coming, interactive sessions. |
|
||
| `host::setting(key)`, `setting_bool`, `setting_string`, `setting_number` | `settings.get` | The user's value (project over global) or the schema's default. |
|
||
|
||
`run-command` receives an optional string argument: `executeAction("plugin.hello.insert-date", "arg")`
|
||
from another feature, or nothing when run from Search Everywhere / a shortcut.
|
||
|
||
### Events
|
||
|
||
| Kind | Capability | Delivered |
|
||
| ------------------ | -------------- | ------------------------------------------------------------------------- |
|
||
| `file-opened` | `events` | a file opens in an editor |
|
||
| `file-saved` | `events` | after the file is written |
|
||
| `editor-focused` | `events` | the focused editor changes (path, or none) |
|
||
| `vcs-changed` | `events` | a repository's state changed (commit, checkout, fetch…; its root) |
|
||
| `action-performed` | `events.input` | an IDE action ran: id, title, source (keyboard, menu, toolbar…), shortcut |
|
||
| `scheduled` | — | the `on-schedule` period elapsed |
|
||
|
||
Events are batched: a running plugin gets them within a fraction of a second,
|
||
an idle one collects them for 2 s before it is woken, so frequent events
|
||
(Key Promoter's actions) don't keep it alive.
|
||
|
||
### Providers
|
||
|
||
Declare what the plugin provides in the editor; only plugins whose
|
||
declaration matches the file's language are called (and loaded):
|
||
|
||
```toml
|
||
[[contributes.providers]]
|
||
kinds = ["definition", "line-markers", "completion"] # also "inline-completion", "code-actions"
|
||
languages = ["java", "xml"] # editor language ids; "*" for any
|
||
```
|
||
|
||
Then `register_plugin!(MyPlugin, providers)` and implement
|
||
`openidea_plugin::providers::Providers` for it: `completion`,
|
||
`inline_completion`, `code_actions`, `definition`, `line_markers`. Each gets
|
||
the document (path, language, version, text). Results join the language
|
||
servers' (completion lists, ⌥⏎ quick fixes, ⌘B, gutter icons; a code action
|
||
carries edits for any files, applied as one change). Editor providers run
|
||
under the edit-path budget: 50 ms, after which the result is dropped, and an
|
||
interruption at 500 ms. Inline completion is exempt from the 50 ms (it may
|
||
need the network) but is cancelled as soon as the user types on: check
|
||
`Task::is_cancelled` or just let the host interrupt the call.
|
||
|
||
### Network and secrets
|
||
|
||
`host::fetch` makes an HTTPS request (plain HTTP only to `localhost`) to a host
|
||
the plugin was granted with `net:<host>`; redirects to another host are
|
||
refused and responses are limited to 8 MiB. Requests are made from the
|
||
machine the project is on.
|
||
|
||
Secrets declared under `[[contributes.secrets]]` are entered by the user in
|
||
Settings › Plugins and kept in the system keychain. The plugin never reads
|
||
them: it writes `{{secret:<name>}}` in a request header and the host fills it
|
||
in on the way out (`secrets` capability). `host::secret_is_set(name)` tells
|
||
whether the user has entered one.
|
||
|
||
### Views and custom editors
|
||
|
||
A view is data: a toolbar, a body — `Tree`, `List`, `Form`, `Table`,
|
||
`Message` or `Html` — and a status line. Declare it, then
|
||
`register_plugin!(MyPlugin, views)` (or `(MyPlugin, providers, views)`) and
|
||
implement `openidea_plugin::views::Views`:
|
||
|
||
```rust
|
||
impl Views for MyPlugin {
|
||
fn render(&mut self, view: &str, viewport: Option<Viewport>, ctx: &Invocation) -> Result<View, Error> {
|
||
Ok(View::new(Body::Tree(vec![TreeItem::new("a", "A").expandable(self.open)]))
|
||
.button(Button::new("refresh", "Refresh").icon("refresh")))
|
||
}
|
||
fn handle(&mut self, view: &str, event: ViewEvent, ctx: &Invocation) -> Result<(), Error> {
|
||
if let ViewEvent::Expand(_) = event {
|
||
self.open = true;
|
||
host::refresh_view(view);
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
```
|
||
|
||
The IDE draws the view with its own components (theme, keyboard, speed
|
||
search) and sends what the user does: buttons, select / activate / expand /
|
||
collapse of items, form fields, table cells, inserted and deleted rows, and
|
||
`html` messages. A tree is a flat list whose items name their `parent`; an
|
||
`expandable` item gets its children when it is expanded. A table gets only
|
||
the rows it shows (`viewport`): report `total_rows` and return those.
|
||
|
||
Showing a view activates the plugin (`on-view:<id>`). `views` become tool
|
||
windows; a `customEditors` file (`priority = "default"`) opens in the
|
||
plugin's editor, or through _Open With_ for `option`. For a custom editor
|
||
`ctx.editor()` is its document: read it with `document()` and change it with
|
||
`apply_edits(version, …)` — the text editor of the same file follows,
|
||
Undo / Redo are one history with it, and Save, the dirty mark and the close
|
||
prompts are the document's. _Open as Text_ opens the text editor.
|
||
|
||
`Html` is the escape hatch for content the declarative bodies can't show. It
|
||
runs in a sandboxed frame — an opaque origin under a CSP that allows only its
|
||
inline scripts and styles and `data:` images, no network, no IDE API — and
|
||
talks to the plugin only through `openidea.postMessage(text)` /
|
||
`openidea.onMessage(fn)` (`ViewEvent::Message`, `host::post_view_message`).
|
||
|
||
### Settings
|
||
|
||
```toml
|
||
[contributes.settings]
|
||
schema = "settings.schema.json"
|
||
```
|
||
|
||
```json
|
||
{
|
||
"properties": {
|
||
"context": { "type": "string", "title": "Context", "default": "" },
|
||
"tailLines": { "type": "integer", "title": "Log lines", "default": 200 },
|
||
"shell": { "type": "string", "enum": ["sh", "bash"], "default": "sh" }
|
||
}
|
||
}
|
||
```
|
||
|
||
Types are `boolean`, `string` (optionally `enum`, `multiline`), `number` and
|
||
`integer`, each with a `default`. The IDE generates the page under
|
||
Settings › Plugins (searchable; projects can override values) and the plugin
|
||
reads values with `host::setting*`; a stored value of the wrong type falls
|
||
back to the default.
|
||
|
||
### Remote projects
|
||
|
||
Plugin code runs where the project is. When a window opens a remote project,
|
||
the desktop sends its enabled code plugins and their grants to
|
||
`openidea-server`, which runs them next to the files: `read_file`,
|
||
`find_files`, `run_process`, `fetch` and diagnostics all act on the remote
|
||
machine, and `process:` programs are resolved on its `PATH`. Changes on the
|
||
desktop (install, disable, grants) follow within a second. Secrets stay in the
|
||
desktop's keychain; the server asks for one when a request header needs it.
|
||
Status items, notifications, tasks, views, custom editors and terminals of
|
||
remote plugins show in that window; a custom editor edits the remote
|
||
document.
|
||
Project plugins (`.openidea/plugins`) don't run in remote projects yet, and a
|
||
plugin's storage on the server starts empty.
|
||
|
||
### Sandbox and limits
|
||
|
||
Each plugin gets its own wasmtime `Store`: no preopened directories, no
|
||
environment, no network, no inherited stdio — only clocks and randomness from
|
||
WASI, plus the host functions above. Every call into the plugin is bounded:
|
||
|
||
- **CPU:** a fuel budget (≈ 5·10⁹ wasm instructions) per call;
|
||
- **time:** 5 s wall clock for commands, activation and deactivation; editor
|
||
providers (syntax, completion, code actions, navigation, line markers) get
|
||
50 ms — a later result is dropped, but the instance keeps running — and are
|
||
interrupted at 500 ms;
|
||
- **memory:** 256 MiB of linear memory.
|
||
|
||
Each active plugin runs on its own thread; calls are queued, never run
|
||
concurrently in one instance. A panic, trap, permission violation or exceeded
|
||
limit drops the instance: the user is told once, and the plugin starts afresh
|
||
the next time it is needed, after a pause that doubles with each crash in a
|
||
row (1 s, 2 s, …). Three crashes in a row mark it _Failed_ for the session
|
||
(with the reason in Settings › Plugins); toggling it off and on, or _Reload_,
|
||
starts it again. A call that returns normally resets the count. Settings ›
|
||
Plugins shows activations, call latency (p95), crashes, late editor results
|
||
and reclaims. The IDE itself is never affected.
|
||
|
||
## 4. Themes
|
||
|
||
```json
|
||
{
|
||
"id": "harbor-night",
|
||
"name": "Harbor Night",
|
||
"kind": "dark",
|
||
"tokens": { "--bg-app": "#14171c", "--accent": "#2b9e93", "--syn-keyword": "#e0a46e" }
|
||
}
|
||
```
|
||
|
||
Tokens are the CSS variables of `src/styles/tokens.css` (`--bg-*`, `--fg-*`,
|
||
`--border*`, `--accent`, `--syn-*`, `--vcs-*`, `--gutter-*`, …). Unlisted tokens
|
||
keep the values of the built-in base named by `kind`. Values must be plain CSS
|
||
values (no `url()`, `;`, braces); `//` and `/* */` comments are allowed. A
|
||
manifest theme entry may add `editor-scheme = "themes/x.scheme.json"` (editor
|
||
colours, merged over the theme). Pick a theme in Settings › Plugins › _Apply_.
|
||
|
||
Start with `openidea-plugin new <dir> --template theme` (or
|
||
[`templates/theme-plugin`](../templates/theme-plugin)); the full guide, token
|
||
reference and IntelliJ / VS Code porting tables are in
|
||
[`docs/plugins/themes.md`](../docs/plugins/themes.md).
|
||
|
||
## 5. File icon themes
|
||
|
||
```toml
|
||
[[contributes.iconThemes]]
|
||
id = "material"
|
||
label = "Material Icons"
|
||
path = "icons/theme.json"
|
||
```
|
||
|
||
`theme.json` maps names to SVGs (paths relative to the package):
|
||
|
||
```json
|
||
{
|
||
"iconDefinitions": { "rust": "icons/rust.svg", "folder-src": "icons/folder-src.svg" },
|
||
"file": "file", "folder": "folder", "folderExpanded": "folder-open",
|
||
"fileExtensions": { "rs": "rust", "d.ts": "typescript-def" },
|
||
"fileNames": { "cargo.toml": "cargo" },
|
||
"folderNames": { "src": "folder-src" }
|
||
}
|
||
```
|
||
|
||
Files match by exact name, then the longest extension (`a.test.ts` tries
|
||
`test.ts`, then `ts`), then `file`. Folders match by name, then
|
||
`folder`/`folderExpanded`; folders with a role (source root, excluded, module)
|
||
keep their built-in icons. Names are case-insensitive. Up to 5000 icons of at
|
||
most 64 KiB each. Every SVG is sanitised on install: allow-listed elements and
|
||
attributes only, no scripts, event handlers, external references, `url()` or
|
||
entities. Apply a theme in Settings › Plugins; see
|
||
[`official/material-icons`](official/material-icons) for a complete one.
|
||
|
||
## 6. Languages
|
||
|
||
`languages/<id>.toml` files use the IDE's own language format; see any file in
|
||
[`languages/`](../languages). `language-toy` is a minimal example,
|
||
[`official/solidity`](official/solidity) a complete one.
|
||
|
||
Plugin languages join the IDE's language registry like built-in ones: file
|
||
detection (`extensions`, `filenames`, `first-line`), comments, indentation,
|
||
`ui-mode`, `framework-ui-modes`, and the `language-servers` and `debugger`
|
||
they declare (each needs its `process:` grant). A plugin cannot replace a
|
||
built-in or user definition: their ids, extensions and file names win (users
|
||
can still map files to a plugin language in Settings › File Types). In a
|
||
remote project the definitions are sent to the server, which starts the
|
||
servers there.
|
||
|
||
`[[framework-ui-modes]]` entries (`ui-mode`, `root-markers`)
|
||
replace `ui-mode` for the files of a framework's projects: the first whose
|
||
root marker is in a directory above the file, up to the project folder, applies
|
||
(see [`languages/html.toml`](../languages/html.toml), Angular templates).
|
||
Markers are matched like a language server's `root-markers`.
|
||
|
||
### 6.1 Syntax for languages without a grammar
|
||
|
||
The IDE parses its built-in languages with tree-sitter. For a plugin language
|
||
it has no grammar for, the plugin's component can export `syntax-provider`
|
||
(world `plugin-syntax`): flat highlight tokens, structure-view symbols and
|
||
folds for a whole document, in UTF-8 byte offsets. With the SDK:
|
||
|
||
```rust
|
||
use openidea_plugin::syntax::{Fold, Highlight, Symbol, SyntaxProvider, Token};
|
||
|
||
impl SyntaxProvider for MyPlugin {
|
||
fn highlights(&mut self, language: &str, text: &str) -> Vec<Token> { … }
|
||
fn outline(&mut self, language: &str, text: &str) -> Vec<Symbol> { … } // optional
|
||
fn folding(&mut self, language: &str, text: &str) -> Vec<Fold> { … } // optional
|
||
}
|
||
|
||
register_plugin!(MyPlugin, syntax);
|
||
```
|
||
|
||
It runs on the editing path (once per document version, cached), so keep it
|
||
fast — a hand-written lexer is typical (`syntax::bracket_folds` helps with
|
||
folding). Add `activation = ["on-language:<id>"]` so the component loads with
|
||
the first file. Results are validated (clamped to the text, overlaps dropped).
|
||
|
||
### 6.2 Run configurations
|
||
|
||
```toml
|
||
[[contributes.runDetectors]]
|
||
id = "forge-test"
|
||
name = "forge test"
|
||
files = ["foundry.toml"] # offered in projects with any of these at the root
|
||
command = "forge" # one program; needs process:forge
|
||
args = ["test"]
|
||
test = true # results go to the test tree
|
||
```
|
||
|
||
The configuration appears among the detected ones while `process:<command>` is
|
||
granted, and starts the approved program.
|
||
|
||
## 7. Installing and developing
|
||
|
||
- **Install:** Settings › Plugins › _Install Plugin from Disk…_ (a package
|
||
folder) or _Install Plugin from Package File…_ (a `.oiplugin`). The install
|
||
page lists every capability and whether the package is signed by a publisher
|
||
the marketplace trusts; nothing is copied until you accept.
|
||
- **Versions:** installed plugins live in `<app-data>/plugins/<id>/<version>/`
|
||
with an `active` pointer that is switched only once the new version is in
|
||
place. An update whose code fails to start the first time is rolled back to
|
||
the version it replaced automatically (you're told why); _Roll Back to …_
|
||
in the plugin's details does it by hand. Data (`storage`) is per plugin, not
|
||
per version.
|
||
- **Linked plugins:** `openidea-plugin link <dir>` (§1). Listed with a _linked_
|
||
tag; capabilities are granted per folder.
|
||
- **Project plugins:** put (or symlink) the package in
|
||
`<project>/.openidea/plugins/<name>/`. They load only in trusted projects and
|
||
only after you approve their capabilities.
|
||
- Commands appear in Search Everywhere › Actions under their category, and can
|
||
be bound in Settings › Keymap like any other action.
|
||
|
||
## 8. Distribution
|
||
|
||
A **package** is a zip named `<id>-<version>.oiplugin`: the manifest, the
|
||
files it references, the component, licence files and, when signed,
|
||
`SIGNATURE.json` (Ed25519 over the plugin id, version and the digest of its
|
||
files).
|
||
|
||
```sh
|
||
openidea-plugin keygen acme.key # once; keep the file secret
|
||
openidea-plugin pack plugins/acme-tools --key acme.key --publisher acme
|
||
openidea-plugin index market/ --publisher acme --name "ACME" --key acme.key
|
||
```
|
||
|
||
A **marketplace** is static: an `index.json` and the packages next to it, on
|
||
any web server or folder. It lists publishers and their public keys (the trust
|
||
root: publisher `acme` owns plugin ids `acme.*`), the plugins with their
|
||
digests, and revocations. Settings › Plugins › Available reads it from the URL
|
||
you set; installing downloads the package, checks it against the index entry
|
||
and the publisher's key, then shows the install page.
|
||
|
||
- A package whose signature doesn't hold (changed after signing, a key that
|
||
isn't the publisher's, another publisher's id prefix, a revoked key) is
|
||
refused. Unsigned packages install from disk with a warning.
|
||
- **Revocation:** the index can revoke plugin versions (or all versions) and
|
||
publisher keys. The IDE reads the index daily; revoked plugins are disabled
|
||
with the reason and the user is notified — also after a restart, before
|
||
their code could run. A publisher rotates keys by adding the new one; older
|
||
keys stay valid until revoked.
|
||
|
||
## 9. Testing the host
|
||
|
||
The host's integration tests (`crates/ide-host/tests/plugins.rs`) load the
|
||
checked-in `hello` and test-fixture components. Rebuild them after changing the
|
||
SDK or the WIT file:
|
||
|
||
```sh
|
||
cargo run -p ide-host --bin openidea-plugin -- build plugins/hello
|
||
cargo run -p ide-host --bin openidea-plugin -- build plugins/official/solidity
|
||
plugins/build.sh crates/ide-plugin/tests/fixtures/misbehave
|
||
cargo test -p ide-host --test plugins
|
||
```
|