lib.nvim · Foundation · vimdoc
:help lib.nvim-composer
Subcommand user commands with completion and docgen
doc/lib.nvim-composer.txt — rendered from the plugin's own vimdoc
*lib.nvim-composer.txt* Subcommand user commands with completion and docgen lib.nvim.bindings.usercmd.composer *lib.nvim-composer* Compose a declarative route spec into ONE Neovim user command with subcommands,<Tab>completion, and Markdown docs — all read from the same route tree, so behavior and docs can never drift. Turns the:VerbFeatureA/:VerbFeatureBanti-pattern into:Verb feature-a/:Verb feature-b, where{Verb}is the central action (:Replace,:File).
CONTENTS
1. Usage ..................................... |lib.nvim-composer-usage| 2. Context ................................... |lib.nvim-composer-ctx| 3. Argument types ............................ |lib.nvim-composer-argtypes| 4. Flags ...................................... |lib.nvim-composer-flags| 5. Bare key=value ............................. |lib.nvim-composer-kv| 6. Buffer-local commands ...................... |lib.nvim-composer-buffer| 7. Count prefix ............................... |lib.nvim-composer-count| 8. Documentation generation .................. |lib.nvim-composer-docgen| 9. Functions ................................. |lib.nvim-composer-functions|
1. USAGE
local composer = require("lib.nvim.bindings.usercmd.composer")
composer.verb("Replace", {
desc = "Text replacement operations",
default = function(ctx) require("myplugin").prompt() end, -- bare :Replace
routes = {
{ path = { "buffer" }, desc = "Replace in the current buffer",
run = function(ctx) require("myplugin").buffer() end },
{ path = { "surround" },
args = {
{ name = "kind", type = "STRING", enum = { "quote", "paren" } },
{ name = "target", type = "STRING" },
},
desc = "Wrap TARGET with KIND surroundings",
run = function(ctx)
require("myplugin").surround(ctx.args.kind, ctx.args.target)
end },
},
})
CDX: replacer.nvim's real :Replace/:Surround registration (command.lua, surround.lua) does use this module, but only singlepath = {}root routes that forward ctx.raw into its pre-existing parser -- it never declares abuffer/surroundsubcommand tree. Judgment call whether the USAGE example should instead be trimmed straight from that real registration (less illustrative of subcommand routing) or stay arequire("myplugin")stand-in (current fix: no longer implies a replacer.nvim API that doesn't exist).:Replace <Tab>completesbuffer | surround;:Replace surround <Tab>completesquote | paren; bad input is reported with the route's usage. A fluent form is also available (:build()registers):
composer.verb("Replace")
:desc("…")
:route("surround", { args = {...}, run = function(ctx) end })
:build()
2. CONTEXT
Each run receives a context table:
ctx.args coerced positional args, keyed by ArgSpec.name
ctx.pos coerced positional args, in order
ctx.flags coerced --flag/-x values, keyed by FlagSpec.name
ctx.kv coerced bare key=value pairs, keyed by KvSpec.key
ctx.rest leftover tokens beyond the declared schema
ctx.path the literal path that matched, e.g. { "surround" }
ctx.bang true when invoked as :Verb!
ctx.range { line1, line2, count, range }
ctx.raw the untouched nvim callback args
3. ARGUMENT TYPES
Each type carries validation and completion. Built-ins: STRING, INT, FLOAT, BOOL, PATH, DIR, FILE, BUFFER, WINDOW, plusenum = {...}on any ArgSpec (closed set, case-insensitive, completed from the members). Coercion reuseslib.nvim.normalizevalidators; PATH/DIR/FILE completion uses Neovim's own file completion. Register a custom type once:
composer.register_type("HIGHLIGHT_GROUP", {
validate = function(raw) return true, raw, nil end,
complete = function(lead) return vim.fn.getcompletion(lead, "highlight") end,
})
run may also be a module-path string, required lazily on first dispatch.
4. FLAGS
A route may declareflags, parsed out of its token tail before positional binding — modeled on replacer.nvim's BOOL_FLAGS/VALUE_FLAGS split. Strictly opt-in: a route with noflagsbehaves exactly as before a leading "--" in a positional value is never special-cased unless the route declares flags.
composer.verb("Replace", {
routes = {
-- path = {} is the verb's ROOT route: it matches even with no
-- literal subcommand, reproducing replacer.nvim's flat grammar
-- :Replace {old} {new} [scope] [--flags] verbatim.
{ path = {},
args = { { name = "old", type = "STRING" }, { name = "new", type = "STRING" } },
flags = {
{ name = "dry", bool = true },
{ name = "type", type = "STRING", repeatable = true },
{ name = "engine", type = "STRING", enum = { "fzf", "telescope" } },
},
run = function(ctx) require("replacer").run(ctx.args, ctx.flags) end },
},
})
Flags may appear anywhere in the tail — before, after, or between positionals (:Replace --dry foo barand:Replace foo bar --dryare equivalent) — and a literal "--" stops flag parsing (matches replacer.nvim'sflags_donesentinel). A value flag accepts--name=valueor--name value. An undeclared--nameis a hard error, not silently treated as positional.<Tab>completes flag names and, after--name=, the flag's own value completer — derived from the same FlagSpec used for dispatch. Known limitation: completion is ambiguous for a verb mixing apath = {}root route with sibling subcommand routes (an unusual combination). Dispatch is unaffected; only the<Tab>suggestion in that mixed case can be unhelpful.
Short-flag aliases (-x)
A flag may declare a single-char short alias:
flags = {
{ name = "replace", short = "r", bool = true }, -- --replace or -r
{ name = "output", short = "o", type = "STRING" },-- --output=<v> or -o <v>
}
-x and --name are interchangeable and may be mixed in one call (:Recommend query -r --output=out.txt). Short flags only take their value from the NEXT token (-o file.txt), never -o=file.txt. An unrecognized -x (no short matches) is left as an ordinary positional rather than an error -- unlike --name, a bare "-" collides too easily with real values (a negative number, a passthrough CLI arg). <Tab> after a bare "-" completes every declared short flag.
5. BARE KEY=VALUE
A route may separately declare kv, for grammars like
":Diff target=file.lua view=vsplit" (no -- / - prefix at all):
composer.verb("Diff", {
routes = {
{ path = {},
kv = {
{ key = "target", type = "STRING" },
{ key = "view", type = "STRING", enum = { "vsplit", "split" }, default = "vsplit" },
},
run = function(ctx)
-- ctx.kv.target, ctx.kv.view (defaults applied when omitted)
end },
},
})
Unlike flags, an UNDECLARED key=value-shaped token is left as an ordinary positional rather than an error -- "=" shows up in too many legitimate positional values (URLs, passthrough env assignments, ...) to treat every match as intentional; only a token whose key matches a declared KvSpec.key is ever consumed. <Tab> offers "key=" for every declared key, and value completion after "key=" -- kv tokens have no marker prefix, so these candidates are merged alongside whatever else is valid at that slot (a subcommand name, a positional arg's own completions, ...), not used exclusively.flagsandkvcompose freely on the same route (ctx.flags and ctx.kv are both populated; parsing runs flags first, then kv, then whatever's left binds to args).
6. BUFFER-LOCAL COMMANDS
spec.buffer = true (current buffer) or an explicit bufnr registers via nvim_buf_create_user_command instead of the global nvim_create_user_command -- for per-buffer commands like a markdown preview's :TableView, typically called from a FileType autocmd:
vim.api.nvim_create_autocmd("FileType", {
pattern = "markdown",
callback = function()
composer.verb("TableView", { buffer = true, routes = { ... } })
end,
})
Re-registering (e.g. the autocmd firing again for the same buffer) is safe -- nvim_buf_create_user_command overwrites like the global form does. The fluent builder has the matching :buffer(v) method.
7. COUNT PREFIX
spec.count = 0 (or any route's route.count) accepts a ":N Verb" count
prefix, the same shape as nvim_create_user_command's own count option --
the number becomes the default ctx.range.count when the prefix is
omitted:
composer.verb("File", {
count = 0,
routes = {
{ path = { "next" }, run = function(ctx)
cycle.navigate("next", ctx.range.count > 0 and ctx.range.count or 1)
end },
},
})
-- :File next -> ctx.range.count == 0
-- :3File next -> ctx.range.count == 3
Like bang/range, nvim_create_user_command has one count slot per command,
not one per route -- an explicit spec.count wins; otherwise the first route
that declares count sets it. The fluent builder's :count(v) defaults to 0
when called with no argument.
8. DOCUMENTATION GENERATION
The route tree drives docs too:
handle:document() -- one verb -> docs/BINDINGS/Usercmds.md
composer.document() -- every verb -> default path
composer.document("docs/CMDS.md") -- explicit path
composer.setup({ docs = { path = "...", mode = "replace"|"section" } })sets the default path and mode. "section" updates a delimited<!-- lib.nvim:composer --> … <!-- /lib.nvim:composer -->block so hand-written prose survives regeneration; "replace" overwrites the file.
9. FUNCTIONS
composer.verb({name}, {spec?}) *lib.nvim-composer-verb*
Register a verb. With {spec} it registers immediately and returns a
handle; without it, returns a fluent builder (call :build()).
composer.document({path?}) *lib.nvim-composer-document*
Write Markdown docs for every registered verb. Returns ok, err.
composer.setup({opts?}) *lib.nvim-composer-setup*
Configure docs defaults: { docs = { path, mode } }.
composer.register_type({name}, {def}) *lib.nvim-composer-register_type*
Register a custom argument type { validate, complete? }.
composer.registry() *lib.nvim-composer-registry*
The name->handle map of every verb built in this process.
composer.check_all() *lib.nvim-composer-check_all*
Pre-flight check for every verb registered in this process, keyed by
name.
composer.checkhealth({name_or_handle}) *lib.nvim-composer-checkhealth*
Report one verb's route checks through vim.health. Call this from
your plugin's own health.lua -- composer cannot register a
discoverable :checkhealth target on your behalf.
composer.notify_check_all() *lib.nvim-composer-notify_check_all*
Notification-based alternative to checkhealth, for plugins with no
health.lua to hook into. Returns ok_overall.
Access: require("lib.nvim.bindings.usercmd.composer") (direct),
require("lib").composer, or require("lib").usercmd.composer.