docs: annotate Ruby configuration and plugin APIs with YARD types
This commit is contained in:
@@ -9,6 +9,7 @@ By default, vamp loads `$XDG_CONFIG_HOME/vamp/config.rb`, or `~/.config/vamp/con
|
|||||||
Rejected configuration opens a centered floating **Configuration error** window, sized like the floating file explorer. It shows the selected config path, full failure reason, and explains that default settings and bindings are in use. The diagnostic remains visible even if background messages arrive; editing keys and paste are ignored until **Enter** or **Escape** dismisses it. Use arrows (or Ctrl+h/j/k/l) to scroll long paths/reasons horizontally or vertically, and **Home** to reset the view. Correct the config and restart vamp to apply it. Invalid individual preferences/bindings that do not reject the whole configuration still report ordinary notices.
|
Rejected configuration opens a centered floating **Configuration error** window, sized like the floating file explorer. It shows the selected config path, full failure reason, and explains that default settings and bindings are in use. The diagnostic remains visible even if background messages arrive; editing keys and paste are ignored until **Enter** or **Escape** dismisses it. Use arrows (or Ctrl+h/j/k/l) to scroll long paths/reasons horizontally or vertically, and **Home** to reset the view. Correct the config and restart vamp to apply it. Invalid individual preferences/bindings that do not reject the whole configuration still report ordinary notices.
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
|
# @param config [Vamp::Config]
|
||||||
Vamp.configure do |config|
|
Vamp.configure do |config|
|
||||||
config.leader = "space"
|
config.leader = "space"
|
||||||
config.clipboard_mode = "system"
|
config.clipboard_mode = "system"
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ cargo fmt --check
|
|||||||
ruby -c src/ruby_host.rb
|
ruby -c src/ruby_host.rb
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Ruby types
|
||||||
|
|
||||||
|
Use YARD `@param`, `@return`, and `@yieldparam` annotations for Ruby APIs, and `@type` for locals where inference needs help. Examples annotate DSL block parameters before the call, without adding runtime assignments or dependencies. `Vamp::Action` / `Vamp::Mode` field types and the dynamically defined mode methods use Solargraph-supported directives.
|
||||||
|
|
||||||
|
Core action methods are generated from Rust's authoritative catalog at host startup. Their namespace is annotated, but static Solargraph analysis cannot enumerate those generated members. Do not maintain a second action list in Ruby comments; use the [configuration action reference](configuration.md#action-reference).
|
||||||
|
|
||||||
## Test organization
|
## Test organization
|
||||||
|
|
||||||
Rust tests live alongside their modules; native LSP integration checks are in [`tests/lsp.rs`](../tests/lsp.rs).
|
Rust tests live alongside their modules; native LSP integration checks are in [`tests/lsp.rs`](../tests/lsp.rs).
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ Writing trusted Ruby extensions. Load plugins through [Ruby configuration](confi
|
|||||||
```ruby
|
```ruby
|
||||||
Vamp.plugin "greeting", requires: ["text"]
|
Vamp.plugin "greeting", requires: ["text"]
|
||||||
|
|
||||||
|
# @param editor [Vamp::Editor]
|
||||||
|
# @param args [String]
|
||||||
Vamp.command "greet" do |editor, args|
|
Vamp.command "greet" do |editor, args|
|
||||||
name = args.empty? ? "world" : args
|
name = args.empty? ? "world" : args
|
||||||
editor.insert "Hello, #{name}!\n"
|
editor.insert "Hello, #{name}!\n"
|
||||||
@@ -18,6 +20,8 @@ Run `:greet vamp`, or press `g` with the example configuration. [`examples/plugi
|
|||||||
|
|
||||||
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.
|
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](https://solargraph.org/guides/yard) for Solargraph navigation and hover. `Vamp.command` declares typed block arguments (`Vamp::Editor`, `String`); the example also annotates them explicitly. Context readers, configuration methods, and asynchronous callback/effect contracts are annotated in [`src/ruby_host.rb`](../src/ruby_host.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.
|
`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.
|
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.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# @param config [Vamp::Config]
|
||||||
Vamp.configure do |config|
|
Vamp.configure do |config|
|
||||||
config.leader = "space"
|
config.leader = "space"
|
||||||
config.clipboard_mode = "system" # "separate" keeps ordinary y/d/c/p/P internal
|
config.clipboard_mode = "system" # "separate" keeps ordinary y/d/c/p/P internal
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
Vamp.plugin "greeting", requires: ["text"]
|
Vamp.plugin "greeting", requires: ["text"]
|
||||||
|
|
||||||
|
# @param editor [Vamp::Editor]
|
||||||
|
# @param args [String]
|
||||||
Vamp.command "greet" do |editor, args|
|
Vamp.command "greet" do |editor, args|
|
||||||
|
# @type [String]
|
||||||
name = args.empty? ? "world" : args
|
name = args.empty? ? "world" : args
|
||||||
editor.insert "Hello, #{name}!\n"
|
editor.insert "Hello, #{name}!\n"
|
||||||
editor.message "Greeting inserted by Ruby"
|
editor.message "Greeting inserted by Ruby"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param editor [Vamp::Editor]
|
||||||
|
# @param _args [String]
|
||||||
Vamp.command "upper" do |editor, _args|
|
Vamp.command "upper" do |editor, _args|
|
||||||
editor.replace editor.text.upcase
|
editor.replace editor.text.upcase
|
||||||
editor.message "Buffer uppercased by Ruby"
|
editor.message "Buffer uppercased by Ruby"
|
||||||
|
|||||||
+116
-1
@@ -1,9 +1,11 @@
|
|||||||
require "json"
|
require "json"
|
||||||
|
|
||||||
# Keep protocol IO separate even from explicit STDOUT.write and ordinary puts.
|
# Keep protocol IO separate even from explicit STDOUT.write and ordinary puts.
|
||||||
|
# @type [IO]
|
||||||
protocol_out = STDOUT.dup
|
protocol_out = STDOUT.dup
|
||||||
protocol_out.binmode
|
protocol_out.binmode
|
||||||
protocol_out.sync = true
|
protocol_out.sync = true
|
||||||
|
# @type [IO]
|
||||||
protocol_in = STDIN.dup
|
protocol_in = STDIN.dup
|
||||||
protocol_in.binmode
|
protocol_in.binmode
|
||||||
STDOUT.reopen(STDERR)
|
STDOUT.reopen(STDERR)
|
||||||
@@ -13,12 +15,19 @@ Thread.abort_on_exception = true
|
|||||||
module Vamp
|
module Vamp
|
||||||
MAX_FRAME = 4 * 1024 * 1024
|
MAX_FRAME = 4 * 1024 * 1024
|
||||||
|
|
||||||
|
# Validates a boundary value and returns a UTF-8 copy.
|
||||||
|
# @param value [Object]
|
||||||
|
# @param label [String]
|
||||||
|
# @return [String]
|
||||||
def self.string!(value, label)
|
def self.string!(value, label)
|
||||||
raise TypeError, "#{label} must be a String" unless value.is_a?(String)
|
raise TypeError, "#{label} must be a String" unless value.is_a?(String)
|
||||||
raise ArgumentError, "#{label} must be valid UTF-8" unless value.dup.force_encoding(Encoding::UTF_8).valid_encoding?
|
raise ArgumentError, "#{label} must be valid UTF-8" unless value.dup.force_encoding(Encoding::UTF_8).valid_encoding?
|
||||||
value.dup.force_encoding(Encoding::UTF_8)
|
value.dup.force_encoding(Encoding::UTF_8)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Recursively validates JSON values and normalizes hash keys to strings.
|
||||||
|
# @param value [Object]
|
||||||
|
# @return [Hash{String => Object}, Array<Object>, String, Integer, Float, Boolean, nil]
|
||||||
def self.json_value!(value)
|
def self.json_value!(value)
|
||||||
case value
|
case value
|
||||||
when Hash
|
when Hash
|
||||||
@@ -40,6 +49,9 @@ module Vamp
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Freezes the supplied value and every nested hash/array member in place.
|
||||||
|
# @param value [Object]
|
||||||
|
# @return [Object] the same value
|
||||||
def self.deep_freeze(value)
|
def self.deep_freeze(value)
|
||||||
case value
|
case value
|
||||||
when Hash
|
when Hash
|
||||||
@@ -50,11 +62,19 @@ module Vamp
|
|||||||
value.freeze
|
value.freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @!override Vamp::Action#name
|
||||||
|
# @return [String] the catalog's dotted action name
|
||||||
Action = Struct.new(:name)
|
Action = Struct.new(:name)
|
||||||
|
# @!override Vamp::Mode#name
|
||||||
|
# @return [String] the lowercase binding mode
|
||||||
Mode = Struct.new(:name)
|
Mode = Struct.new(:name)
|
||||||
|
|
||||||
|
# Action methods and nested modules come exclusively from the Rust catalog.
|
||||||
|
# @type [Module<Vamp::Core>]
|
||||||
Core = Module.new
|
Core = Module.new
|
||||||
|
|
||||||
|
# @param names [Array<String>] dotted core action names
|
||||||
|
# @return [Array<Module>] frozen action namespaces
|
||||||
def self.install_actions(names)
|
def self.install_actions(names)
|
||||||
raise TypeError, "core action catalog must be an Array" unless names.is_a?(Array)
|
raise TypeError, "core action catalog must be an Array" unless names.is_a?(Array)
|
||||||
namespaces = [Core]
|
namespaces = [Core]
|
||||||
@@ -85,6 +105,20 @@ module Vamp
|
|||||||
end
|
end
|
||||||
|
|
||||||
module Modes
|
module Modes
|
||||||
|
# @!method self.normal
|
||||||
|
# @return [Vamp::Mode]
|
||||||
|
# @!method self.insert
|
||||||
|
# @return [Vamp::Mode]
|
||||||
|
# @!method self.command
|
||||||
|
# @return [Vamp::Mode]
|
||||||
|
# @!method self.search
|
||||||
|
# @return [Vamp::Mode]
|
||||||
|
# @!method self.visual
|
||||||
|
# @return [Vamp::Mode]
|
||||||
|
# @!method self.selection
|
||||||
|
# @return [Vamp::Mode]
|
||||||
|
# @!method self.tree
|
||||||
|
# @return [Vamp::Mode]
|
||||||
%w[normal insert command search visual selection tree].each do |name|
|
%w[normal insert command search visual selection tree].each do |name|
|
||||||
mode = Mode.new(name.freeze).freeze
|
mode = Mode.new(name.freeze).freeze
|
||||||
define_singleton_method(name) { mode }
|
define_singleton_method(name) { mode }
|
||||||
@@ -95,8 +129,10 @@ module Vamp
|
|||||||
Mode.private_class_method :new
|
Mode.private_class_method :new
|
||||||
|
|
||||||
class Config
|
class Config
|
||||||
|
# @return [Hash{String => Boolean, Hash{String, Symbol => Object}}] language presets/options
|
||||||
attr_reader :lsp
|
attr_reader :lsp
|
||||||
|
|
||||||
|
# @return [void]
|
||||||
def initialize
|
def initialize
|
||||||
@plugins = []
|
@plugins = []
|
||||||
@lsp = {}
|
@lsp = {}
|
||||||
@@ -108,6 +144,9 @@ module Vamp
|
|||||||
@clipboard_mode = "system"
|
@clipboard_mode = "system"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param path [String] relative to the configuration file
|
||||||
|
# @param options [Hash{String, Symbol => Object}] JSON-compatible plugin options
|
||||||
|
# @return [nil]
|
||||||
def plugin(path, options: {})
|
def plugin(path, options: {})
|
||||||
path = Vamp.string!(path, "plugin path")
|
path = Vamp.string!(path, "plugin path")
|
||||||
raise ArgumentError, "plugin path must not be empty" if path.empty? || path.include?("\0")
|
raise ArgumentError, "plugin path must not be empty" if path.empty? || path.include?("\0")
|
||||||
@@ -116,44 +155,60 @@ module Vamp
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param key [String]
|
||||||
|
# @return [String]
|
||||||
def leader=(key)
|
def leader=(key)
|
||||||
key = Vamp.string!(key, "leader key")
|
key = Vamp.string!(key, "leader key")
|
||||||
raise ArgumentError, "leader key must not be empty" if key.empty?
|
raise ArgumentError, "leader key must not be empty" if key.empty?
|
||||||
@leader = key
|
@leader = key
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param side [String]
|
||||||
|
# @return [String]
|
||||||
def file_explorer_side=(side)
|
def file_explorer_side=(side)
|
||||||
side = Vamp.string!(side, "file explorer side")
|
side = Vamp.string!(side, "file explorer side")
|
||||||
raise ArgumentError, "file explorer side must not be empty" if side.empty?
|
raise ArgumentError, "file explorer side must not be empty" if side.empty?
|
||||||
@file_explorer_side = side
|
@file_explorer_side = side
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param view [String]
|
||||||
|
# @return [String]
|
||||||
def file_picker_view=(view)
|
def file_picker_view=(view)
|
||||||
view = Vamp.string!(view, "file picker view")
|
view = Vamp.string!(view, "file picker view")
|
||||||
raise ArgumentError, "file picker view must not be empty" if view.empty?
|
raise ArgumentError, "file picker view must not be empty" if view.empty?
|
||||||
@file_picker_view = view
|
@file_picker_view = view
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param view [String]
|
||||||
|
# @return [String]
|
||||||
def file_explorer_view=(view)
|
def file_explorer_view=(view)
|
||||||
view = Vamp.string!(view, "file explorer view")
|
view = Vamp.string!(view, "file explorer view")
|
||||||
raise ArgumentError, "file explorer view must not be empty" if view.empty?
|
raise ArgumentError, "file explorer view must not be empty" if view.empty?
|
||||||
@file_explorer_view = view
|
@file_explorer_view = view
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param mode [String]
|
||||||
|
# @return [String]
|
||||||
def clipboard_mode=(mode)
|
def clipboard_mode=(mode)
|
||||||
mode = Vamp.string!(mode, "clipboard mode")
|
mode = Vamp.string!(mode, "clipboard mode")
|
||||||
raise ArgumentError, "clipboard mode must not be empty" if mode.empty?
|
raise ArgumentError, "clipboard mode must not be empty" if mode.empty?
|
||||||
@clipboard_mode = mode
|
@clipboard_mode = mode
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [Module<Vamp::Core>] frozen namespace with catalog-generated Action methods
|
||||||
def core
|
def core
|
||||||
Core
|
Core
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [Module<Vamp::Modes>] frozen namespace whose methods return Mode values
|
||||||
def modes
|
def modes
|
||||||
Modes
|
Modes
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param key [String] one key or a leader-prefixed sequence
|
||||||
|
# @param command [Vamp::Action, String, nil] core action, external command, or disabled route
|
||||||
|
# @param mode [Vamp::Mode]
|
||||||
|
# @return [nil]
|
||||||
def bind(key, command, mode: Modes.normal)
|
def bind(key, command, mode: Modes.normal)
|
||||||
key = Vamp.string!(key, "binding key")
|
key = Vamp.string!(key, "binding key")
|
||||||
raise TypeError, "binding mode must be a value from config.modes" unless mode.is_a?(Mode)
|
raise TypeError, "binding mode must be a value from config.modes" unless mode.is_a?(Mode)
|
||||||
@@ -175,6 +230,7 @@ module Vamp
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [Hash{String => Object}] validated startup configuration frame
|
||||||
def response
|
def response
|
||||||
lsp = @lsp.to_h do |language, value|
|
lsp = @lsp.to_h do |language, value|
|
||||||
language = Vamp.string!(language, "LSP language key")
|
language = Vamp.string!(language, "LSP language key")
|
||||||
@@ -201,8 +257,20 @@ module Vamp
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Editor
|
class Editor
|
||||||
attr_reader :text, :row, :column, :mode, :path, :effects, :buffer_id, :revision, :root, :request_id
|
# @return [String] immutable context text, uppercase mode, or workspace path
|
||||||
|
attr_reader :text, :mode, :root
|
||||||
|
# @return [Integer] one-based grapheme row/column or nonnegative document ID/revision
|
||||||
|
attr_reader :row, :column, :buffer_id, :revision
|
||||||
|
# @return [String, nil] document path; nil for an unnamed buffer
|
||||||
|
attr_reader :path
|
||||||
|
# @return [Integer] positive invocation ID
|
||||||
|
attr_reader :request_id
|
||||||
|
# @return [Array<Hash{String => String}>] queued insert/replace/message effects
|
||||||
|
attr_reader :effects
|
||||||
|
|
||||||
|
# @param context [Hash{String => String, Integer, nil}] primary caret/document snapshot
|
||||||
|
# @param request_id [Integer] positive invocation ID
|
||||||
|
# @return [void]
|
||||||
def initialize(context, request_id)
|
def initialize(context, request_id)
|
||||||
unless context.is_a?(Hash) && context.keys.sort == %w[buffer_id column mode path revision root row text]
|
unless context.is_a?(Hash) && context.keys.sort == %w[buffer_id column mode path revision root row text]
|
||||||
raise ArgumentError, "invalid editor context"
|
raise ArgumentError, "invalid editor context"
|
||||||
@@ -226,20 +294,29 @@ module Vamp
|
|||||||
@effect_bytes = 0
|
@effect_bytes = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param text [String]
|
||||||
|
# @return [nil]
|
||||||
def insert(text)
|
def insert(text)
|
||||||
queue("insert", text)
|
queue("insert", text)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param text [String]
|
||||||
|
# @return [nil]
|
||||||
def replace(text)
|
def replace(text)
|
||||||
queue("replace", text)
|
queue("replace", text)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param text [String]
|
||||||
|
# @return [nil]
|
||||||
def message(text)
|
def message(text)
|
||||||
queue("message", text)
|
queue("message", text)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# @param type [String] insert, replace, or message
|
||||||
|
# @param text [String]
|
||||||
|
# @return [nil]
|
||||||
def queue(type, text)
|
def queue(type, text)
|
||||||
text = Vamp.string!(text, "effect text")
|
text = Vamp.string!(text, "effect text")
|
||||||
@effect_bytes += text.bytesize
|
@effect_bytes += text.bytesize
|
||||||
@@ -249,6 +326,10 @@ module Vamp
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param mode [String] config or plugin
|
||||||
|
# @param startup [Hash{String => Object}] action catalog and plugin options
|
||||||
|
# @param sender [Proc] writes a Hash{String => Object} protocol frame
|
||||||
|
# @return [Proc] the installed sender
|
||||||
def self.setup(mode, startup, sender)
|
def self.setup(mode, startup, sender)
|
||||||
unless startup.is_a?(Hash) && startup.keys.sort == %w[core_actions options] && startup["options"].is_a?(Hash)
|
unless startup.is_a?(Hash) && startup.keys.sort == %w[core_actions options] && startup["options"].is_a?(Hash)
|
||||||
raise TypeError, "invalid Ruby startup catalog/options"
|
raise TypeError, "invalid Ruby startup catalog/options"
|
||||||
@@ -267,16 +348,24 @@ module Vamp
|
|||||||
@sender = sender
|
@sender = sender
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [Hash{String => Object}] deeply frozen plugin options
|
||||||
def self.options
|
def self.options
|
||||||
@options
|
@options
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @yieldparam config [Vamp::Config]
|
||||||
|
# @yieldreturn [Object]
|
||||||
|
# @return [Object] the block result
|
||||||
def self.configure
|
def self.configure
|
||||||
raise ArgumentError, "configure is only available in config files" unless @mode == "config"
|
raise ArgumentError, "configure is only available in config files" unless @mode == "config"
|
||||||
raise ArgumentError, "configure requires a block" unless block_given?
|
raise ArgumentError, "configure requires a block" unless block_given?
|
||||||
yield @config
|
yield @config
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param name [String]
|
||||||
|
# @param requires [Array<String>] frontend capabilities
|
||||||
|
# @param async [Boolean]
|
||||||
|
# @return [nil]
|
||||||
def self.plugin(name, requires: [], async: false)
|
def self.plugin(name, requires: [], async: false)
|
||||||
raise ArgumentError, "plugin is only available in plugin files" unless @mode == "plugin"
|
raise ArgumentError, "plugin is only available in plugin files" unless @mode == "plugin"
|
||||||
raise ArgumentError, "plugin already declared" unless @name.nil?
|
raise ArgumentError, "plugin already declared" unless @name.nil?
|
||||||
@@ -291,6 +380,12 @@ module Vamp
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param name [String] external command name
|
||||||
|
# @param callback [Proc]
|
||||||
|
# @yieldparam editor [Vamp::Editor] immutable context and queued effects
|
||||||
|
# @yieldparam args [String] remaining command text
|
||||||
|
# @yieldreturn [Object] ignored
|
||||||
|
# @return [nil]
|
||||||
def self.command(name, &callback)
|
def self.command(name, &callback)
|
||||||
raise ArgumentError, "declare a plugin before its commands" unless @mode == "plugin" && @name
|
raise ArgumentError, "declare a plugin before its commands" unless @mode == "plugin" && @name
|
||||||
name = string!(name, "command name")
|
name = string!(name, "command name")
|
||||||
@@ -301,6 +396,10 @@ module Vamp
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param callback [Proc]
|
||||||
|
# @yieldparam event [Hash{String => String, Hash{String => String, Integer, nil}}] frozen event/document
|
||||||
|
# @yieldreturn [Object] ignored
|
||||||
|
# @return [nil]
|
||||||
def self.on_document(&callback)
|
def self.on_document(&callback)
|
||||||
raise ArgumentError, "document callbacks require an asynchronous plugin" unless @mode == "plugin" && @name && @async
|
raise ArgumentError, "document callbacks require an asynchronous plugin" unless @mode == "plugin" && @name && @async
|
||||||
raise ArgumentError, "on_document requires a block" unless callback
|
raise ArgumentError, "on_document requires a block" unless callback
|
||||||
@@ -309,6 +408,9 @@ module Vamp
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param callback [Proc]
|
||||||
|
# @yieldreturn [Object] ignored
|
||||||
|
# @return [nil]
|
||||||
def self.on_shutdown(&callback)
|
def self.on_shutdown(&callback)
|
||||||
raise ArgumentError, "shutdown callbacks require an asynchronous plugin" unless @mode == "plugin" && @name && @async
|
raise ArgumentError, "shutdown callbacks require an asynchronous plugin" unless @mode == "plugin" && @name && @async
|
||||||
raise ArgumentError, "on_shutdown requires a block" unless callback
|
raise ArgumentError, "on_shutdown requires a block" unless callback
|
||||||
@@ -317,11 +419,15 @@ module Vamp
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [Object, nil] callback result, or nil when no hook is registered
|
||||||
def self.shutdown
|
def self.shutdown
|
||||||
callback, @shutdown_callback = @shutdown_callback, nil
|
callback, @shutdown_callback = @shutdown_callback, nil
|
||||||
callback&.call
|
callback&.call
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param effects [Array<Hash{String, Symbol => Object}>] JSON-compatible async update effects
|
||||||
|
# @param request_id [Integer, nil] positive unsigned 64-bit invocation ID, or background update
|
||||||
|
# @return [nil]
|
||||||
def self.publish(effects, request_id: nil)
|
def self.publish(effects, request_id: nil)
|
||||||
raise ArgumentError, "publish requires an asynchronous plugin" unless @mode == "plugin" && @name && @async
|
raise ArgumentError, "publish requires an asynchronous plugin" unless @mode == "plugin" && @name && @async
|
||||||
raise TypeError, "published effects must be an Array of Hashes" unless effects.is_a?(Array) && effects.all? { |effect| effect.is_a?(Hash) }
|
raise TypeError, "published effects must be an Array of Hashes" unless effects.is_a?(Array) && effects.all? { |effect| effect.is_a?(Hash) }
|
||||||
@@ -333,6 +439,8 @@ module Vamp
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param request [Hash{String => Object}] incoming document event frame
|
||||||
|
# @return [nil]
|
||||||
def self.document(request)
|
def self.document(request)
|
||||||
unless @async && request.is_a?(Hash) && request.keys.sort == %w[document event type version] &&
|
unless @async && request.is_a?(Hash) && request.keys.sort == %w[document event type version] &&
|
||||||
request["version"].is_a?(Integer) && request["version"] == 1 && request["type"] == "document" &&
|
request["version"].is_a?(Integer) && request["version"] == 1 && request["type"] == "document" &&
|
||||||
@@ -352,6 +460,7 @@ module Vamp
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [Hash{String => Object}] configuration or plugin manifest frame
|
||||||
def self.startup
|
def self.startup
|
||||||
return @config.response if @mode == "config"
|
return @config.response if @mode == "config"
|
||||||
raise ArgumentError, "plugin file must declare Vamp.plugin" unless @name
|
raise ArgumentError, "plugin file must declare Vamp.plugin" unless @name
|
||||||
@@ -359,6 +468,8 @@ module Vamp
|
|||||||
"requires" => @requires, "commands" => @commands.keys, "async" => @async }
|
"requires" => @requires, "commands" => @commands.keys, "async" => @async }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param request [Hash{String => Object}] invocation frame
|
||||||
|
# @return [Hash{String => Object}] result frame with queued effects
|
||||||
def self.invoke(request)
|
def self.invoke(request)
|
||||||
unless request.is_a?(Hash) && request.keys.sort == %w[args command context id type version] &&
|
unless request.is_a?(Hash) && request.keys.sort == %w[args command context id type version] &&
|
||||||
request["version"].is_a?(Integer) && request["version"] == 1 && request["type"] == "invoke" &&
|
request["version"].is_a?(Integer) && request["version"] == 1 && request["type"] == "invoke" &&
|
||||||
@@ -376,6 +487,9 @@ module Vamp
|
|||||||
end
|
end
|
||||||
|
|
||||||
protocol_mutex = Mutex.new
|
protocol_mutex = Mutex.new
|
||||||
|
# @type [Proc]
|
||||||
|
# @param frame [Hash{String => Object}]
|
||||||
|
# @return [Integer] bytes written by the terminating newline
|
||||||
send_frame = lambda do |frame|
|
send_frame = lambda do |frame|
|
||||||
protocol_mutex.synchronize do
|
protocol_mutex.synchronize do
|
||||||
payload = JSON.generate(frame)
|
payload = JSON.generate(frame)
|
||||||
@@ -385,6 +499,7 @@ send_frame = lambda do |frame|
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @type [Integer, nil]
|
||||||
request_id = nil
|
request_id = nil
|
||||||
begin
|
begin
|
||||||
mode, path, encoded_startup = ARGV
|
mode, path, encoded_startup = ARGV
|
||||||
|
|||||||
Reference in New Issue
Block a user