From 3ef247c55fcb8252e177ed85d66d1ffc9f0eaaf6 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:04:11 +0800 Subject: [PATCH] test(plugins): freeze plugin API 1.0.0 v0.1.36 shipped API 1.0.0, so its WIT is now the snapshot api_frozen checks every later version against. World items are compared without their @version, since a later minor renames them and wasmtime resolves the release's names to it. --- .../tests/api/1.0.0/events.wit | 54 ++++++ .../tests/api/1.0.0/exports.wit | 75 +++++++++ .../openidea-plugin/tests/api/1.0.0/host.wit | 38 +++++ .../openidea-plugin/tests/api/1.0.0/io.wit | 96 +++++++++++ .../tests/api/1.0.0/plugin.wit | 67 ++++++++ .../tests/api/1.0.0/providers.wit | 81 +++++++++ .../openidea-plugin/tests/api/1.0.0/types.wit | 73 +++++++++ .../openidea-plugin/tests/api/1.0.0/views.wit | 154 ++++++++++++++++++ .../tests/api/1.0.0/workspace.wit | 90 ++++++++++ sdk/rust/openidea-plugin/tests/api_frozen.rs | 10 +- 10 files changed, 736 insertions(+), 2 deletions(-) create mode 100644 sdk/rust/openidea-plugin/tests/api/1.0.0/events.wit create mode 100644 sdk/rust/openidea-plugin/tests/api/1.0.0/exports.wit create mode 100644 sdk/rust/openidea-plugin/tests/api/1.0.0/host.wit create mode 100644 sdk/rust/openidea-plugin/tests/api/1.0.0/io.wit create mode 100644 sdk/rust/openidea-plugin/tests/api/1.0.0/plugin.wit create mode 100644 sdk/rust/openidea-plugin/tests/api/1.0.0/providers.wit create mode 100644 sdk/rust/openidea-plugin/tests/api/1.0.0/types.wit create mode 100644 sdk/rust/openidea-plugin/tests/api/1.0.0/views.wit create mode 100644 sdk/rust/openidea-plugin/tests/api/1.0.0/workspace.wit diff --git a/sdk/rust/openidea-plugin/tests/api/1.0.0/events.wit b/sdk/rust/openidea-plugin/tests/api/1.0.0/events.wit new file mode 100644 index 00000000..2d118d81 --- /dev/null +++ b/sdk/rust/openidea-plugin/tests/api/1.0.0/events.wit @@ -0,0 +1,54 @@ +package openidea:plugin@1.0.0; + +/// Workbench events, delivered in batches to `event-handler.handle`. +interface events { + use types.{error, extension}; + + record action-performed { + /// Action id (`editor.duplicate-line`). + action: string, + title: string, + /// How the user started it: `keyboard`, `menu`, `toolbar`, + /// `context-menu`, `search` or `other` (more may be added). + source: string, + /// Its keyboard shortcut, when it has one (`Ctrl+D`). + shortcut: option, + } + + variant event { + file-opened(string), + file-saved(string), + editor-focused(option), + action-performed(action-performed), + vcs-changed(string), + scheduled, + /// A kind added after 1.0 (subscribed to by its name). + other(extension), + } + + /// Delivers `kind` events from now on, for the rest of the session (also + /// across idle reclaim: an event activates the plugin again). Declare + /// `activation = ["on-event:"]` to get them before first use. + /// Kinds: `file-opened`, `file-saved`, `editor-focused` (the focused editor + /// changed), `vcs-changed` (a repository's state changed: commit, + /// checkout, fetch…), `action-performed` (an IDE action ran) and + /// `scheduled` (the `on-schedule` period elapsed); later IDEs add more, + /// and an IDE that doesn't know one fails with `invalid`. + /// Requires `events` (`events.input` for `action-performed`). + subscribe: func(kind: string) -> result<_, error>; + unsubscribe: func(kind: string); +} + +/// Long-running work with a progress indicator. While a task is open, the +/// call that began it may run for the background budget (60 s) instead of 5 s. +interface tasks { + resource task { + /// `done` of `total` units (`none`: indeterminate), with a short status. + report: func(done: u32, total: option, message: option); + /// The user cancelled it: stop soon and return. + is-cancelled: func() -> bool; + } + + /// Starts a task shown in the status bar; dropping it ends it. + begin: func(title: string, cancellable: bool) -> task; +} diff --git a/sdk/rust/openidea-plugin/tests/api/1.0.0/exports.wit b/sdk/rust/openidea-plugin/tests/api/1.0.0/exports.wit new file mode 100644 index 00000000..065ef9a5 --- /dev/null +++ b/sdk/rust/openidea-plugin/tests/api/1.0.0/exports.wit @@ -0,0 +1,75 @@ +package openidea:plugin@1.0.0; + +interface lifecycle { + use types.{context, error}; + + /// Called once after the instance is created: subscribe to events and + /// restore state from `storage`. Contributions (commands, languages, + /// themes, …) are declared in plugin.toml, not registered here. + /// An error fails the activation (shown on the plugin's page). + activate: func(ctx: context) -> result<_, error>; + /// Called before the instance is dropped (disable, uninstall, idle reclaim). + deactivate: func(); +} + +interface commands { + use types.{error}; + use invocation.{invocation}; + + /// Runs one of the plugin's commands. Returns an optional message. + /// `cancelled` (the user gave up) and `denied` (a capability is missing) + /// are not counted as the plugin failing. + run: func(id: string, ctx: borrow, arg: option) -> result, error>; +} + +interface event-handler { + use events.{event}; + use invocation.{invocation}; + + /// A batch of subscribed events, oldest first, from one window (`ctx`). + handle: func(events: list, ctx: borrow); +} + +/// Optional export (world `plugin-syntax`): syntax of the plugin's own +/// languages, for languages the IDE has no grammar for. The IDE calls it +/// with the whole document; results are flat. Offsets are UTF-8 byte offsets +/// into `text` (the IDE converts them); lines are 0-based. +interface syntax-provider { + /// One highlighted range. `kind` is a highlight category: keyword, string, + /// string-special, escape, regexp, atom, number, constant, comment, + /// doc-comment, function, function-call, builtin, type, namespace, + /// variable, self-variable, property, attribute, macro, label, tag, + /// operator, punctuation. Unknown kinds are ignored; ranges must be + /// sorted and not overlap. + record token { + start: u32, + end: u32, + kind: string, + } + + /// A symbol of the structure view. `parent` is the index of the enclosing + /// symbol in the same list (which must come first). + record symbol { + name: string, + detail: option, + /// class, interface, struct, enum, function, method, constructor, + /// field, property, variable, constant, module, namespace, event, … + kind: string, + start: u32, + end: u32, + name-start: u32, + name-end: u32, + parent: option, + } + + /// A foldable line range. `kind`: region, comment or imports. + record fold { + start-line: u32, + end-line: u32, + kind: string, + } + + highlights: func(language: string, text: string) -> list; + outline: func(language: string, text: string) -> list; + folding: func(language: string, text: string) -> list; +} diff --git a/sdk/rust/openidea-plugin/tests/api/1.0.0/host.wit b/sdk/rust/openidea-plugin/tests/api/1.0.0/host.wit new file mode 100644 index 00000000..b6325cb7 --- /dev/null +++ b/sdk/rust/openidea-plugin/tests/api/1.0.0/host.wit @@ -0,0 +1,38 @@ +package openidea:plugin@1.0.0; + +/// Writes to the IDE log, tagged with the plugin id. +interface log { + use types.{log-level}; + + log: func(level: log-level, message: string); +} + +interface ui { + use types.{notification-level}; + + record status-item { + text: string, + tooltip: option, + /// One of the plugin's commands, run when the item is clicked. + command: option, + } + + /// Shows a notification balloon in the window the current command or + /// event came from (every window when there is none). + show-notification: func(level: notification-level, title: string, body: string); + /// Shows or updates the plugin's status bar item `id` in every window. + set-status-item: func(id: string, item: status-item); + remove-status-item: func(id: string); +} + +/// Persistent key-value storage of the plugin (survives restarts and idle +/// reclaim; instance memory does not). Requires `storage`. At most 1 MiB. +interface storage { + use types.{error}; + + get: func(key: string) -> result, error>; + set: func(key: string, value: string) -> result<_, error>; + delete: func(key: string) -> result<_, error>; + /// Keys starting with `prefix`, sorted. + keys: func(prefix: string) -> result, error>; +} diff --git a/sdk/rust/openidea-plugin/tests/api/1.0.0/io.wit b/sdk/rust/openidea-plugin/tests/api/1.0.0/io.wit new file mode 100644 index 00000000..f1fe634a --- /dev/null +++ b/sdk/rust/openidea-plugin/tests/api/1.0.0/io.wit @@ -0,0 +1,96 @@ +package openidea:plugin@1.0.0; + +/// Problems in files, shown in the editor and the Problems view. +interface diagnostics { + use types.{error, range, location}; + + enum severity { + error, + warning, + info, + hint, + } + + /// Another place that explains a diagnostic (the earlier declaration, say). + record related-information { + location: location, + message: string, + } + + record diagnostic { + range: range, + severity: severity, + message: string, + /// Rule or check id (`LineLength`). + code: option, + /// The tool that found it, when not the plugin itself (`checkstyle`). + source: option, + /// `unnecessary` (shown faded) and `deprecated` (struck through); unknown ones are ignored. + tags: list, + related: list, + } + + /// Replaces the plugin's diagnostics for the file at `path` (absolute, in + /// an open project); an empty list clears them. Requires `project.read`. + publish: func(path: string, diagnostics: list) -> result<_, error>; +} + +/// HTTP(S) to the hosts granted with `net:`. +interface net { + use types.{error}; + + record header { + name: string, + value: string, + } + + record request { + method: string, + /// `https://…`; plain `http://` only to `localhost` / `127.0.0.1`. + url: string, + /// `{{secret:}}` in a value is replaced by the plugin's secret + /// `name` on the way out (requires `secrets`); the plugin never sees it. + headers: list
, + body: option>, + /// Default 30 s; the call's own budget applies too. + timeout-ms: option, + } + + record response { + status: u16, + headers: list
, + /// At most 8 MiB. + body: list, + } + + /// Fails with `denied` for a host the plugin may not reach, `cancelled` + /// when a newer request replaced the call or the user cancelled. + fetch: func(req: request) -> result; +} + +/// Secrets the user stored for the plugin (Settings › Plugins, kept in the +/// OS keychain). Plugins only use them through `{{secret:}}` in +/// `net` requests. Requires `secrets`. +interface secrets { + is-set: func(name: string) -> bool; +} + +/// One-off runs of programs granted with `process:` (the file the +/// user approved, checked before every run). +interface process { + use types.{error}; + use workspace.{project}; + + record output { + /// `none` when killed by a signal. + status: option, + /// At most 4 MiB each (the rest is dropped). + stdout: string, + stderr: string, + } + + /// Runs `program` with `args` in `cwd`'s root (else the plugin's data + /// directory) and waits; cancelled with the task or call. Call it inside + /// a task for anything slow. + run: func(program: string, args: list, cwd: option>, stdin: option) -> result; +} diff --git a/sdk/rust/openidea-plugin/tests/api/1.0.0/plugin.wit b/sdk/rust/openidea-plugin/tests/api/1.0.0/plugin.wit new file mode 100644 index 00000000..938ee084 --- /dev/null +++ b/sdk/rust/openidea-plugin/tests/api/1.0.0/plugin.wit @@ -0,0 +1,67 @@ +/// OpenIDEA plugin API, version 1.0. Once released, 1.x only adds (new +/// interfaces, new optional exports; plugins/README.md, "Evolving the plugin +/// API"); a 2.0 keeps 1.0 running through an adapter in the host. +/// +/// A plugin is a WebAssembly component targeting the `plugin` world (or +/// `plugin-syntax`). It runs sandboxed: no filesystem, network, environment +/// or processes beyond WASI's clocks and randomness; everything else goes +/// through the imports below, gated by the capabilities granted in +/// `plugin.toml` (a missing one returns `error::denied`). +package openidea:plugin@1.0.0; + +world plugin { + import log; + import ui; + import storage; + import workspace; + import editor; + import invocation; + import vcs; + import events; + import tasks; + import diagnostics; + import net; + import secrets; + import process; + import views; + import settings; + + export lifecycle; + export commands; + export event-handler; +} + +/// A plugin that also provides syntax for its languages. +world plugin-syntax { + include plugin; + export syntax-provider; +} + +/// A plugin with editor providers (`[[contributes.providers]]`). +world plugin-providers { + include plugin; + export providers; +} + +/// A plugin with views or custom editors (`[[contributes.views]]`, +/// `[[contributes.custom-editors]]`). +world plugin-views { + include plugin; + export view-provider; +} + +/// Only the `syntax-provider` export: guests generate it next to `plugin` +/// (the component tools merge both into one component). +world syntax { + export syntax-provider; +} + +/// Only the `providers` export, generated next to `plugin` like `syntax`. +world editor-providers { + export providers; +} + +/// Only the `view-provider` export, generated next to `plugin` like `syntax`. +world views-provider { + export view-provider; +} diff --git a/sdk/rust/openidea-plugin/tests/api/1.0.0/providers.wit b/sdk/rust/openidea-plugin/tests/api/1.0.0/providers.wit new file mode 100644 index 00000000..97dd4c7a --- /dev/null +++ b/sdk/rust/openidea-plugin/tests/api/1.0.0/providers.wit @@ -0,0 +1,81 @@ +package openidea:plugin@1.0.0; + +/// Editor features a plugin provides for the languages it declares under +/// `[[contributes.providers]]` (the host calls only declared kinds). They run +/// on the editing path: a result later than ~50 ms is dropped, a call still +/// running at 500 ms is interrupted. +interface providers { + use types.{error, position, range, text-edit, location}; + + /// The document a request is about. + record document { + path: string, + language: string, + version: u64, + text: string, + } + + record completion-item { + label: string, + detail: option, + /// Icon and sort group: text, method, function, constructor, field, + /// variable, class, interface, module, property, unit, value, enum, + /// keyword, snippet, color, file, reference, folder, enum-member, + /// constant, struct, event, operator or type-parameter. Unknown kinds + /// show no icon. + kind: option, + /// With `snippet`, tab stops and placeholders (`$1`, `${1:name}`, `$0`). + insert-text: string, + snippet: bool, + /// Replaced on accept; `none`: the word at the caret. + replace: option, + sort-text: option, + /// Matched against what was typed instead of `label`. + filter-text: option, + documentation: option, + /// Applied with the item elsewhere in the document (an import). + additional-edits: list, + } + + record inline-suggestion { + insert-text: string, + /// Replaced range; `none` inserts at the caret. + replace: option, + } + + record file-edit { + path: string, + edits: list, + } + + record code-action { + title: string, + /// `quickfix`, `refactor`, … + kind: option, + preferred: bool, + edits: list, + /// The plugin's own reference: with it and no `edits`, the edits are + /// computed by `resolve-code-action` once the user picks the action. + data: option, + } + + record line-marker { + line: u32, + /// Gutter icon: `navigate` (default), `implemented`, `implementing`, `run`. + icon: option, + tooltip: string, + /// Opened on click. + target: option, + } + + completion: func(doc: document, at: position, trigger: option) -> list; + /// Ghost text at the caret (typing replaces an older request: it is cancelled). + inline-completion: func(doc: document, at: position) -> option; + /// Actions for `selection` (quick fixes for the plugin's own diagnostics). + code-actions: func(doc: document, selection: range) -> list; + /// The edits of an action `code-actions` returned with `data`, when the + /// user picks it (the command budget applies, not the editor's 50 ms). + resolve-code-action: func(doc: document, action: code-action) -> result; + definition: func(doc: document, at: position) -> list; + line-markers: func(doc: document) -> list; +} diff --git a/sdk/rust/openidea-plugin/tests/api/1.0.0/types.wit b/sdk/rust/openidea-plugin/tests/api/1.0.0/types.wit new file mode 100644 index 00000000..2e2b48f7 --- /dev/null +++ b/sdk/rust/openidea-plugin/tests/api/1.0.0/types.wit @@ -0,0 +1,73 @@ +package openidea:plugin@1.0.0; + +/// Shared types. Positions are 0-based lines and UTF-16 code unit columns, +/// as in the editor and LSP. +interface types { + /// Why a host call failed. A capability that is not granted is reported + /// as `denied`, so the plugin can degrade (0.1 trapped instead). + variant error { + /// Not granted; the payload names the capability (`editor.write`). + denied(string), + /// The document changed since the version the edit was made for. + stale, + not-found(string), + invalid(string), + /// Cancelled by the user, or the plugin is being stopped. + cancelled, + failed(string), + } + + enum log-level { + trace, + debug, + info, + warn, + error, + } + + enum notification-level { + info, + warning, + error, + } + + record position { + line: u32, + character: u32, + } + + record range { + start: position, + end: position, + } + + record text-edit { + range: range, + text: string, + } + + /// A range in a file (absolute path). + record location { + path: string, + range: range, + } + + /// A case added after 1.0, carried by the `other` case of a variant that + /// grows (events, view bodies, view events, form fields): its kind and its + /// data as JSON. Whoever doesn't know the kind ignores it or shows a + /// fallback; the SDKs decode the kinds they know. + record extension { + kind: string, + data: string, + } + + /// Passed to `lifecycle.activate`. + record context { + plugin-id: string, + plugin-version: string, + /// Version of this API the host implements, e.g. "1.0". + api-version: string, + /// Capabilities granted to the plugin (`editor.write`, `process:git`, …). + capabilities: list, + } +} diff --git a/sdk/rust/openidea-plugin/tests/api/1.0.0/views.wit b/sdk/rust/openidea-plugin/tests/api/1.0.0/views.wit new file mode 100644 index 00000000..dcc806dc --- /dev/null +++ b/sdk/rust/openidea-plugin/tests/api/1.0.0/views.wit @@ -0,0 +1,154 @@ +package openidea:plugin@1.0.0; + +/// Declarative views: tool windows declared +/// under `[[contributes.views]]` and custom editors under +/// `[[contributes.customEditors]]`. The plugin describes what to show; the +/// IDE renders it with its own components (theme, keyboard, accessibility) +/// and sends the user's actions back as `view-event`s. Nothing of the plugin +/// runs in the WebView, except a `html` body, which is shown in a sandboxed +/// frame that can only exchange messages with the plugin. +interface view-types { + use types.{extension}; + + record button { + id: string, + label: string, + /// An IDE icon name (`refresh`, `add`, `remove`, `run`, …). + icon: option, + enabled: bool, + } + + /// A tree node; `parent` is the key of an earlier item (none: a root). + record tree-item { + key: string, + parent: option, + label: string, + icon: option, + /// Grey text after the label. + detail: option, + /// Shows an expander even before its children are known + /// (`expand` asks for them). + expandable: bool, + expanded: bool, + } + + record list-item { + key: string, + label: string, + icon: option, + detail: option, + } + + variant field-value { + text(string), + multiline(string), + checkbox(bool), + /// The options and the chosen one. + choice(tuple, string>), + /// A field type added after 1.0 (an IDE that doesn't know it leaves it out). + other(extension), + } + + record field { + key: string, + label: string, + value: field-value, + } + + /// Rows `first-row`..`first-row + len(rows)` of `total-rows`: the IDE + /// asks for the rows it shows (`viewport`), so large tables stay cheap. + record table { + columns: list, + total-rows: u32, + first-row: u32, + rows: list>, + editable: bool, + } + + variant body { + /// A centred message (empty state, errors). + message(string), + tree(list), + %list(list), + form(list), + table(table), + /// HTML shown in a sandboxed frame: no scripts from elsewhere, no + /// network, no IDE API. Its script talks to the plugin with + /// `openidea.postMessage(text)` / `openidea.onMessage(fn)`. + html(string), + /// A body added after 1.0; an IDE that doesn't know it says it needs updating. + other(extension), + } + + record view { + toolbar: list