Add two contribution-only theme plugins, each with a light and a dark theme that sets every UI token plus an editor colour scheme with syntax, editor and terminal ANSI colours: - plugins/theme-solarized: Solarized Light, Solarized Dark - plugins/theme-one: One Light, One Dark (Atom One, original HSL values) They replace the partial theme-solarized-dark sample. Each package ships the MIT notice of its palette in LICENSE, which is now copied on install with the files the manifest references. scripts/theme checks every theme plugin in plugins/ against the token list of tokens.css. Theme handling that any theme benefits from: - Start-up: boot.js restores the selected plugin theme's stylesheet and the last resolved frame, text and accent colours before any CSS loads, so the splash and workbench no longer show the built-in theme until the plugin host answers. The UI keeps the boot theme until the first plugin list, and the list now waits for the initial plugin load. - Native windows: the UI reports the plugin theme's resolved frame colour, which the plugin host persists and new project windows open in. - hsl(), color-mix() and var() tokens are resolved to RGB for Tauri's window background, the native Settings window (hex) and Mermaid diagrams. - Mermaid, the terminal and the editor colour scheme follow switches between plugin themes of the same light / dark base; an explicit editor scheme now replaces a plugin theme's editor colours. Supersedes #85. Co-authored-by: bghtyu <3931591+bghtyu@users.noreply.github.com> Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Writing OpenIDEA plugins
OpenIDEA plugins are sandboxed WebAssembly components (wasmtime, component model, WASI preview 2), in the style of Zed extensions. A plugin can register commands (they become IDE actions you can search for and bind to keys), read project files, insert text into the editor, show notifications and contribute themes and languages. Plugins that only contribute themes or languages need no code at all.
plugins/
├─ build.sh # builds Rust plugins into components
├─ hello/ # Rust plugin: two commands
├─ theme-solarized/ # manifest-only: Solarized Light / Dark themes
├─ theme-one/ # manifest-only: One Light / Dark themes
└─ language-toy/ # manifest-only: a language definition
The API is defined in wit/plugin.wit (openidea:plugin/plugin@0.1.0).
1. The package
A plugin is a directory with a plugin.toml:
id = "hello" # [a-z0-9][a-z0-9_-]*, unique
name = "Hello"
version = "0.1.0" # semver
description = "Inserts today's date and counts words."
authors = ["You <you@example.com>"]
homepage = "https://example.com" # optional
openidea-api = "0.1" # API version this plugin targets
wasm = "dist/plugin.wasm" # omit for contribution-only plugins
permissions = ["project-read", "editor-read", "editor-write"]
# Optional static contributions (active even before / without code):
[[contributes.commands]]
id = "insert-date" # [a-z0-9][a-z0-9._-]*
title = "Hello: Insert Date"
category = "Hello" # group in Search Everywhere / Keymap
[[contributes.themes]]
path = "themes/my-theme.json"
[[contributes.languages]]
path = "languages/my-lang.toml"
All paths are relative to the package directory and may not leave it.
Permissions
| Permission | Unlocks |
|---|---|
project-read |
workspace-root, read-file (project root only) |
editor-read |
active-file |
editor-write |
insert-text, replace-selection |
log, register-command, show-notification, register-language and
register-theme are always available. Calling a gated function without its
permission traps: the plugin is stopped and the user is told which
permission is missing.
2. Writing code (Rust)
wasm32-wasip2 produces a component directly, so all you need is rustup:
rustup target add wasm32-wasip2
Cargo.toml:
[package]
name = "my-plugin"
version = "0.1.0"
edition = "2024"
[workspace] # keep it out of any parent workspace
[lib]
crate-type = ["cdylib"]
[dependencies]
wit-bindgen = "0.63"
[profile.release]
opt-level = "s"
lto = true
strip = true
panic = "abort"
src/lib.rs:
wit_bindgen::generate!({ world: "plugin", path: "path/to/openidea/wit" });
use openidea::plugin::host::{self, LogLevel, NotificationLevel};
struct MyPlugin;
impl Guest for MyPlugin {
fn activate(_ctx: Context) -> Result<(), String> {
host::register_command("shout", "My Plugin: Shout Selection", Some("My Plugin"));
Ok(())
}
fn deactivate() {}
fn run_command(id: String, arg: Option<String>) -> Result<Option<String>, String> {
match id.as_str() {
"shout" => {
host::replace_selection(&arg.unwrap_or_default().to_uppercase());
Ok(None)
}
_ => Err(format!("unknown command {id}")),
}
}
}
export!(MyPlugin);
Build (copy build.sh or run it with your plugin's path):
plugins/build.sh hello # → plugins/hello/dist/plugin.wasm
plugins/build.sh /path/to/my-plugin
Any language with component-model tooling works (e.g. componentize-py,
jco componentize, TinyGo): the only contract is the plugin world.
Host functions
| Function | Notes |
|---|---|
log(level, msg) |
Goes to the IDE log, tagged with the plugin id. eprintln! output is logged too and shown when the plugin crashes. |
register-command(id, title, category?) |
Becomes action plugin.<plugin-id>.<id>; call again to rename. Max 256 commands. |
show-notification(level, title, body) |
Balloon in the invoking window. |
workspace-root() |
Root of the project the command runs in. |
active-file() |
Absolute path of the focused editor's file. |
read-file(path) |
Absolute or project-relative; ../symlinks escaping the root are rejected; UTF-8 only, ≤ 16 MiB. Unsaved editor contents win over disk. |
insert-text(text) |
Inserts at every cursor of the active editor (cursor ends after the text). |
replace-selection(text) |
Replaces every selection and selects the new text. |
register-language(toml) |
Same format as languages/<id>.toml. |
register-theme(json) |
See Themes below. |
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.
Returning Err(msg) shows "