Comandos de usuario: argumentos, rangos, bang y completado
nvim_create_user_command convierte una función de Lua en un verbo de primera clase de la línea de comandos. La tabla de opciones al declararlo — nargs, range, count, bang, bar, register, addr, complete —, la tabla de contexto que recibe el callback, qué significa de verdad el bang, por qué fargs no respeta comillas, y cómo escribir un completado personalizado que se sienta nativo.
Un mapeo es un atajo: rápido, silencioso, sin parámetros. Un comando de usuario es lo contrario: descubrible, argumentable, componible con rangos y con la historia de la línea de comandos. No compiten, se complementan. Casi toda funcionalidad seria de tu configuración debería existir primero como comando y solo después, si la usas a diario, recibir un atajo que lo invoque.
- Declarar comandos con
nvim_create_user_commandy dominarnargs,range,count,bang,baryaddr. - Leer la tabla de contexto que recibe el callback:
args,fargs,line1,line2,bang,regysmods. - Distinguir un rango de un conteo y elegir el correcto para cada verbo.
- Saber exactamente cómo se parten los argumentos — y por qué las comillas no agrupan.
- Escribir un completado personalizado en Lua sabiendo que el filtrado corre de tu cuenta.
Anatomía de la declaración
La función toma un nombre, un cuerpo — una cadena de Ex o, casi siempre, una función de Lua — y una tabla de opciones. El nombre debe empezar por mayúscula y contener solo letras, dígitos y guiones bajos: la restricción existe para que los comandos de usuario nunca colisionen con los internos, que empiezan por minúscula.
vim.api.nvim_create_user_command('Ordenar', function(opts) local ini, fin = opts.line1, opts.line2 local lineas = vim.api.nvim_buf_get_lines(0, ini - 1, fin, false) table.sort(lineas, function(a, b) if opts.bang then return a > b end return a < b end) vim.api.nvim_buf_set_lines(0, ini - 1, fin, false, lineas)end, { range = '%', bang = true, desc = 'Ordena el rango indicado; con bang, en orden inverso',})Ese comando ya se comporta como uno nativo: :Ordenar sobre todo el archivo — el '%' fija ese rango por defecto —, :10,20Ordenar sobre un rango explícito, :'<,'>Ordenar sobre la selección visual y :Ordenar! invirtiendo el criterio. Nada de eso lo has programado; lo has declarado.
| Opción | Efecto |
|---|---|
nargs |
Cuántos argumentos acepta: 0 (el defecto), 1, ?, * o + |
range |
Acepta rango de líneas; true con la línea actual por defecto, '%' con el archivo entero, un número N como conteo en posición de línea |
count |
Acepta un conteo, en posición de línea o como primer argumento |
bang |
Habilita la variante con admiración |
bar |
Permite encadenar con | otro comando detrás |
register |
Permite anteponer un registro, disponible en reg |
addr |
Qué cuenta el rango: líneas, argumentos, buffers, ventanas, pestañas, entradas de quickfix |
complete |
Cadena predefinida o función de Lua para el completado |
force |
Redefine si ya existía; es true por defecto, y bajarlo a false es el unique de los comandos |
desc |
Descripción visible en la introspección |
range y count son mutuamente excluyentes, y force = true explica por qué redeclarar un comando al recargar tu config nunca protesta: la API sobreescribe en silencio. Mientras diseñas, force = false convierte una colisión de nombres en un error visible — la misma filosofía que unique en los mapeos de la 11.1.
La tabla de contexto del callback
El callback recibe un único argumento: una tabla con todo lo que el usuario tecleó, ya desmenuzado. Conocerla de memoria ahorra un viaje constante a la ayuda.
args y fargs
args es la cadena cruda tal cual se escribió. fargs es esa cadena partida en una lista por espacios sin escapar. Usa fargs salvo que quieras el texto literal.
line1, line2 y range
line1 y line2 delimitan el rango efectivo. El campo range vale 0, 1 o 2 según cuántos extremos escribió el usuario: distingue «sin rango» de «un rango que casualmente coincide con el defecto».
bang, count y reg
bang es un booleano, count el conteo si lo declaraste, y reg el nombre del registro si declaraste register. Los tres son la vía para variantes del mismo verbo.
mods y smods
mods son los modificadores antepuestos como texto, y smods los mismos en forma estructurada, listos para reenviar a nvim_cmd. Es la pieza que hace que tu comando respete un :vertical o un :tab delante.
Sobre fargs hay una creencia extendida que conviene fusilar: las comillas no agrupan. La línea de comandos de Vim no es una shell; el separador son los espacios y tabuladores sin escapar, y el único mecanismo de agrupación es escaparlos con una barra invertida.
El reenvío de smods es lo que separa un comando aficionado de uno indistinguible de los nativos:
vim.api.nvim_create_user_command('Ayuda', function(opts) vim.api.nvim_cmd({ cmd = 'help', args = opts.fargs, mods = opts.smods }, {})end, { nargs = '?', complete = 'help', desc = 'Ayuda que respeta vertical, tab y silent',})Con eso, :vertical Ayuda lua abre la ayuda en una división vertical y :tab Ayuda lua en una pestaña, sin que tú hayas programado ninguno de los dos casos.
Rangos y conteos: dos cosas distintas
Un rango describe una región: dos números que delimitan líneas — o buffers, ventanas, pestañas o entradas de quickfix, según addr. Un conteo describe una cantidad: un solo número antepuesto. :3Cerrar significa cosas muy distintas si declaraste range (la línea 3) o count (tres unidades). Elige según la naturaleza semántica del verbo: si actúa sobre texto, quieres rango; si actúa sobre entidades enumerables o repeticiones, quieres conteo.
vim.api.nvim_create_user_command('Recientes', function(opts) local n = opts.count for _, buf in ipairs(vim.api.nvim_list_bufs()) do if n <= 0 then break end if vim.api.nvim_buf_is_loaded(buf) then print(vim.api.nvim_buf_get_name(buf)) n = n - 1 end endend, { count = 5, desc = 'Lista los N primeros buffers cargados' })Y cuando el rango no habla de líneas, addr cambia el significado de los símbolos especiales: con addr = 'buffers', el % de un rango pasa a significar «todos los buffers» y . el actual. Es el mismo mecanismo con el que :bufdo y compañía interpretan sus rangos.
El bang no significa forzar: significa la otra variante
La convención de Vim es que ! selecciona un comportamiento alternativo, no necesariamente más agresivo. En :write! fuerza, sí, pero en :Ordenar! invierte y en :normal! desactiva los mapeos. Diseña tu bang como el interruptor natural de la única dimensión binaria de tu verbo, y documenta cuál es en desc. Si tu comando tiene dos dimensiones binarias, el bang ya no basta y necesitas argumentos.
Completado personalizado
La opción complete acepta una de las cadenas predefinidas de Vim — file, buffer, dir, help, highlight, filetype y una lista larga en :h :command-complete — o bien una función de Lua. Esa función recibe tres argumentos: la palabra parcial que se está completando, la línea de comandos entera y la posición del cursor en bytes. Y aquí el detalle que sorprende la primera vez: una función de Lua se comporta como customlist, así que Neovim no filtra por ti; devuelves exactamente lo que quieres ofrecer, ya recortado contra la palabra parcial.
local temas = { 'catppuccin', 'gruvbox', 'kanagawa', 'rose-pine', 'tokyonight' }vim.api.nvim_create_user_command('Tema', function(opts) vim.cmd.colorscheme(opts.args)end, { nargs = 1, desc = 'Cambia el tema activo', complete = function(lead) return vim.tbl_filter(function(t) return vim.startswith(t, lead) end, temas) end,})El segundo y el tercer argumento permiten un completado contextual: mirando la línea ya escrita puedes ofrecer subcomandos en la primera posición y opciones distintas en la segunda, que es la técnica con la que los plugins serios construyen interfaces de un solo comando con decenas de subverbos. Para acotar el alcance existen nvim_buf_create_user_command, que instala el comando solo en un buffer — perfecto desde un autocomando FileType, como los mapeos locales de la 11.1 —, y nvim_del_user_command para retirarlo.
La guinda de 0.12: tu comando con vista previa
Los comandos declarados desde Lua pueden además definir un callback preview y engancharse a 'inccommand': mientras el usuario teclea :MiComando…, tu función pinta el resultado provisional en el buffer y Neovim lo descarta si el comando no se ejecuta. Es lo mismo que hace :s/// cuando ves el reemplazo en vivo antes de pulsar Enter. Está documentado en :h :command-preview y es exclusivo de la API de Lua: los comandos declarados con :command no pueden hacerlo.
Un comando es una interfaz pública; un mapeo es solo un acelerador de esa interfaz
Hay una arquitectura implícita en Neovim que casi nadie enuncia y que reorganiza por completo una configuración cuando se acepta: la funcionalidad vive en módulos de Lua — los del Nivel 7 —, se expone como comandos de usuario y solo después se acelera con mapeos. Las tres capas tienen propiedades distintas y complementarias. El módulo es lo que puedes probar, importar desde otro archivo y razonar en aislamiento. El comando es la interfaz descubrible: aparece al pulsar tabulador en la línea de comandos, admite argumentos, acepta rangos, se compone con la barra vertical, entra en la historia recuperable con las flechas, se puede invocar desde un autocomando, desde un script o desde otro plugin, y sobrevive a que olvides el atajo. El mapeo es solo el acelerador del caso de uso más frecuente, y es la capa que menos información transmite: una tecla no admite parámetros, no se puede buscar y desaparece de tu memoria en cuanto pasas dos semanas sin usarla. Invertir ese orden es el error estructural más común en las configuraciones grandes, donde la lógica vive dentro de funciones anónimas atrapadas en la tabla de mapeos, inaccesibles desde cualquier otro sitio y sin más documentación que la tecla que las invoca. Cuando en cambio escribes primero el comando, obtienes gratis tres cosas que nunca podrías añadir después: parametrización, descubribilidad y componibilidad con el resto del ecosistema. Y el mapeo se vuelve trivial, porque solo tiene que ser <Cmd>MiComando<CR>. La prueba de fuego es sencilla: si una funcionalidad tuya no puede invocarse desde la línea de comandos con argumentos, todavía no es parte de tu editor; es un accidente pegado a una tecla.
Al teclear : desde visual, Neovim inserta '<,'> por ti; como el comando declara range, line1 y line2 llegan con los extremos de la selección. Con ! detrás, orden inverso: el bang como «la otra variante».
Reenviar smods es todo el truco: los modificadores viajan estructurados desde el parseo hasta la ejecución. Pruébalo con :vertical Ayuda lua y con :tab Ayuda lua.
Verás tres elementos, dos con comillas incrustadas. args conserva la cadena cruda; fargs parte por espacios sin escapar. Las comillas no significan nada.
La versión local del comando, simétrica al mapeo con buffer de la 11.1. En cualquier otro buffer, :Probar no existe — y el completado de la línea de comandos ni lo ofrece.
Lo esencial
nvim_create_user_command exige un nombre en mayúscula y recibe una tabla donde nargs, range, count, bang, bar, register, addr, complete, force y desc declaran el comportamiento en lugar de programarlo. El callback recibe una única tabla con args, fargs, line1, line2, range, bang, count, reg, mods y smods; reenviar smods a nvim_cmd hace que tu comando respete :vertical y :tab. Un rango delimita una región y un conteo indica una cantidad: no son intercambiables, y addr decide qué cuenta el rango. fargs parte por espacios sin escapar — las comillas no agrupan. En un completado de Lua, el filtrado corre de tu cuenta.
Verbos de primera clase
- Declara
:Ordenarconrangeybang, y verifica sus cuatro invocaciones: sin rango, con:10,20, desde una selección visual y con!. - Ejecuta
:3Ordenary confirma convim.print(opts)queline1,line2yrangevalen lo que predijiste arriba. - Escribe un comando con
nargs = '+'que imprimaargsyfargs, y pásale argumentos con comillas y con espacios escapados para ver los dos repartos. - Añade a un comando tuyo el reenvío de
smodsy comprueba que:verticaly:tabantepuestos cambian dónde se abre la ventana. - Implementa un completado contextual de dos niveles: subcomandos en la primera posición y valores distintos según el subcomando ya escrito.
- Convierte tu mapeo más complejo en un comando de usuario y reduce el mapeo a
<Cmd>NombreDelComando<CR>.