Skip to main content
Plugins extend the editor at three levels:
  1. A language, declaratively. [[contributes.languages]] adds file detection, comments, indentation, language servers and a debugger, with no code. [[contributes.snippets]] adds live templates.
  2. Syntax for a language the IDE has no grammar for. The optional syntax-provider export returns highlighting, structure and folds.
  3. Editor providers for any language, built-in or not: completion, hover, go to definition, gutter markers, semantic tokens, inlay hints, formatting, code lenses and the rest. Declare them under [[contributes.providers]].
Quick fixes and problems are on Diagnostics and inspections; project-wide navigation built from every file is on Indexing.

Declare providers

Only plugins whose declaration matches the document’s language are called, and loading the code is part of the first call, so no activation entry is needed. The host calls only the kinds you declare. Providers need code (wasm) but no extra capability: the document comes with the call. Host functions you call from a provider keep their usual capability checks. Combine exports in one call: register_plugin!(MyPlugin, providers, hover, language_features). Every trait method has a default that returns nothing, so implement only what you declared.

A minimal provider

Every provider gets a Document with the absolute path, the editor language id, a version and the full text, unsaved edits included. Positions are 0-based lines and UTF-16 columns; openidea_plugin::text::offset and text::position convert between them and byte offsets.

Budgets

Editor providers run on the editing path:
  • A result later than about 50 ms is dropped; a call still running at 500 ms is interrupted. Every plugin providing a kind is asked at the same time, so each has the whole budget.
  • Providers share the plugin’s single instance with its commands, events and views. They see the same memory but also wait behind them: a provider call queued behind a long command is dropped at 50 ms like a slow one. Keep commands and event handlers short.
  • 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 let the host interrupt it.
  • formatting and code-action resolution use the 5-second command budget.

Completion, inline completion, navigation and gutter markers

These are methods of providers::Providers (WIT providers):
  • completion(doc, at, trigger) returns CompletionItems that join the language servers’ list. kind (CompletionKind) picks the icon. An item can be a snippet ($1, ${1:name}, $0), replace a replace range, match on filter_text instead of its label, sort by sort_text, carry documentation, and bring additional_edits elsewhere in the document (an import).
  • inline_completion(doc, at) returns ghost text at the caret (InlineSuggestion { insert_text, replace }). AI Completion is a working example.
  • definition(doc, at) returns Locations, merged with the language server’s for Go to Declaration (⌘B / Ctrl+B).
  • line_markers(doc) returns gutter icons with a tooltip and an optional target opened on click.
MyBatis Navigator uses definition, line-markers and completion for Java and XML. Marker icons. icon accepts an icon name (including registered plugin icons), the aliases navigate (default), implemented, implementing, overridden, overriding and run, or a package SVG declared as a marker icon (API 1.3):
Return icon: Some("icons/resource.svg".into()) to use it. Marker SVGs are at most 64 KiB and sanitised like view icons; unknown icons keep the navigation glyph. For a clickable command instead of navigation, use gutter-actions (below).

Semantic tokens

semantic-tokens (API 1.3) colours tokens in documents of the declared languages, including languages the plugin doesn’t contribute, such as php:
A token covers part of one line and carries an LSP token type and modifiers (kebab-case strings in the WIT). Tokens join the language server’s and win where they overlap one, so the colour scheme styles them like the server’s. They are requested again after each edit; at most 65,536 per document.

Hover

hover (API 1.3) adds Markdown to the hover. The host asks the language server and the plugins together and shows the server’s hover first, then each plugin’s below a rule. Hover::new(markdown).range(range) names the text the hover describes (used when the server names none). At most 64 KiB of Markdown per plugin.

Inlay hints

inlay-hints (API 1.3) shows short labels inside the code:
range is the visible part of the document; hints outside it are dropped. kind (Type, Parameter or Other) decides which Inlay Hints setting shows the hint. .padding_left() and .padding_right() add spaces. Labels are cut at 256 bytes, tooltips at 1 KiB, and at most 10,000 hints per call.

More language features (API 1.4)

Declare any of these kinds with engines.openidea = "^1.4", implement language_features::LanguageFeatures and register language_features: A code lens that offers a command, from the Blame Annotations example:
Test Runner shows gutter-actions. Rules that apply to all of them:
  • A missing or unavailable language server lets plugin results through; other server errors stay errors.
  • document-symbol.parent is an index into the returned flat list. A parent must come first and contain its children; invalid parents become roots, depth is capped at 64.
  • Each list holds at most 10,000 items, text at most 64 KiB per string; invalid ranges are dropped. Format edits must be valid and must not overlap.
  • Disabling the selected formatter reports it as unavailable rather than silently using another one.

Embedded languages (injections)

injections (API 1.4) marks ranges of a host document that contain another language, such as SQL inside string literals. Declare the kind for the host language, implement Injections and register injections:
What the host does with the ranges:
  • Highlights each range with the embedded language’s parser and the current colour scheme, refreshed 300 ms after edits.
  • Routes completion, hover, diagnostics and the API 1.4 features to the embedded language’s plugin providers, through a virtual document that keeps the original path and positions and blanks out text outside the ranges.
  • When the embedded language has a configured language server, routes completion, hover, signature help, navigation, references, highlights and code actions to it, each range in its own in-memory sibling document (never written to disk). Formatting and rename stay with the host language’s server.
Limits: at most 1,000 non-overlapping ranges and 32 embedded languages per document; documents over 2 MiB are skipped; injections are not recursive.

Syntax for a plugin language

For a language the plugin contributes and the IDE has no tree-sitter grammar for, export syntax-provider (world plugin-syntax). Offsets here are UTF-8 byte ranges into the text; the IDE converts them.
It runs once per document version (cached) on the editing path, so keep it fast; a hand-written lexer is typical. Add activation = ["on-language:<id>"] so the component loads with the first file. Results are validated: clamped to the text, overlaps dropped. Solidity is the complete example.

Contributing a language

The file uses the IDE’s own language definition format (the same as languages/<id>.toml in the repository):
  • Plugin languages join the registry like built-in ones: detection (extensions, filenames, first-line), comments, indentation, ui-mode, framework-ui-modes, and the language-servers and debugger they declare.
  • Each language server or debugger command becomes an optional process:<program> capability. Refusing it keeps the rest of the plugin working.
  • A plugin can’t 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 go to the server, which starts the servers there.
openidea-plugin new my-lang --template language starts a data-only language plugin.

Snippets

prefix and body take a string or an array of strings (body lines join with newlines). $1, ${1:default} and a final $0 become linked live-template fields; \$ is a literal dollar. Snippets show in completion, expand with Tab, and are listed read-only under Settings › Editor › Live Templates, grouped by plugin. No code or capability is needed. Limits: 1 MiB per file, 1,000 entries, 32 prefixes per entry, 64 KiB per body.

Testing

On your machine the SDK calls its mock host, so call your trait methods directly in cargo test. MockHost::provider_document() returns the current unsaved snapshot of the mock editor. See Testing.