Files
vamp/docs/plugins.md
T

6.8 KiB

Ruby plugins

README

Writing trusted Ruby extensions. Load plugins through Ruby configuration; see architecture for module ownership.

Vamp.plugin "greeting", requires: ["text"]

# @param editor [Vamp::Editor]
# @param args [String]
Vamp.command "greet" do |editor, args|
  name = args.empty? ? "world" : args
  editor.insert "Hello, #{name}!\n"
  editor.message "Greeting inserted by Ruby"
end

Run :greet vamp, or press g with the example configuration. examples/plugins/greeting.rb also provides :upper to uppercase the buffer.

Callbacks receive an immutable primary-only context snapshot: editor.text, editor.row, editor.column, editor.mode, editor.path, editor.buffer_id, editor.revision, editor.root, and editor.request_id. Row/column are one-based grapheme positions; mode is NORMAL; path is a string or nil. Arguments are the remaining command text, without surrounding whitespace. Commands run once regardless of target count; successful text effects collapse to the primary before editing once. The plugin API does not expose selections.

The Ruby host and examples use YARD types for Solargraph navigation and hover. Vamp.command declares typed block arguments (Vamp::Editor, String); the example also annotates them explicitly. Context readers and configuration methods are annotated in src/ruby_config.rb; asynchronous callback/effect contracts live in src/ruby_plugin.rb. These comments do not enforce types at runtime; existing boundary validation still does.

editor.insert(text), editor.replace(text), and editor.message(text) queue effects. The context remains the pre-command snapshot even after queuing an effect. All effects are validated before the backend changes anything; buffer changes from one successful invocation form one undo action. Exceptions, process exits, timeouts, malformed replies, and invalid effects disable that plugin and discard its effects. Other plugins and built-ins remain available. Plugin puts, explicit STDOUT.write, and stderr cannot overwrite the TUI; diagnostic output is discarded, while exceptions appear in the status message.

This is crash isolation, not a security sandbox: Ruby configuration and plugins have your user account's filesystem/process permissions. Load only trusted scripts. On Unix, hosts run in their own process groups and their groups are killed on shutdown.

Disabling a plugin removes its commands and bindings until the next launch. There is no hot reload. See development limits for host deadlines and frame bounds.

Capabilities and registration

Built-in Ex commands register as the protected core feature in the same command registry used by plugins. External plugins cannot claim core status, replace existing commands, or disable core. User configuration can override any keybinding without replacing the core implementation. :plugins lists active features and frontend capabilities.

A feature declares all capabilities it needs. Registration is atomic: if any requirement is unavailable or any command conflicts, none of its commands are installed. For example, Vamp.plugin "canvas", requires: ["graphics"] will be refused by this TUI. Capability names are extensible strings, not a terminal-vs-GUI switch. Capability declarations express compatibility; they are not security permissions.

Asynchronous plugins

config.plugin "plugin.rb", options: { ... } passes immutable JSON-compatible options as Vamp.options. Declare Vamp.plugin "name", requires: ["text", "diagnostics"], async: true to opt into asynchronous command results and document notifications. Existing plugins remain synchronous.

Vamp.on_document { |event| ... } receives a deeply frozen hash with "event" ("open", "change", "save", or "close") and "document" ("id", "revision", "path", "root", "text"). Already-open documents are delivered at registration; changes include hidden-buffer edits, undo/redo, and plugin edits. Save-as sends close/open for the changed path. Vamp.on_shutdown { ... } supplies a shutdown hook. Callbacks must enqueue slow work rather than blocking host input.

Worker threads call Vamp.publish(effects, request_id: nil). Background effects are message, revision-tagged diagnostics, or request-correlated navigate/hover; they cannot edit text. Diagnostics carry buffer_id, revision, and items, each with end-exclusive start/end positions, severity (error, warning, information, hint), and message. Positions are zero-based grapheme coordinates, not bytes, terminal cells, or UTF-16. Empty item lists clear the owner's annotations.

Navigation carries the source buffer_id/revision, destination path/row/column, and the invocation's editor.request_id passed to publish. Only the latest still-current source request may move the primary. Closed/stale revisions are ignored; malformed current updates retire their owner. Failed navigation reports an error without discarding buffer edits. Hosts use bounded queues/frames; protocol failure or host exit retires commands, bindings, pending requests, and owned diagnostics.

hover carries source buffer_id/revision and multiline text, with the invocation's editor.request_id passed to publish. Only applicable current requests in Normal mode can show a popup; stale results and replies while another modal list is focused are ignored. Text validates before a batch changes state; empty text clears the popup, and owner retirement dismisses its content.

Messaging

Rust and Ruby exchange UTF-8, newline-delimited JSON objects with version: 1. Startup returns manifest (name, required capabilities, commands, async opt-in) or config (plugin paths/options and bindings). invoke requests carry an ID, command, arguments, and context; result replies carry the matching ID and typed text effects (insert, replace, message). Async hosts also receive document/shutdown and send update frames with optional request IDs and typed presentation/navigation effects. Errors use error frames. Unknown fields, wrong versions, and mismatched replies are rejected. Frames and aggregate effect text are bounded to 4 MiB.

Before loading a script, Rust supplies a bounded startup JSON envelope containing core_actions and options. The action catalog initializes the Ruby interface; only the separate user-options object is exposed through Vamp.options, including when user options contain a key named core_actions.

JSON uses Ruby's standard library. Protocol Buffers is not installed or required; there is no frontend/network wire protocol yet. The frontend currently calls the Rust library directly.