wandres.dev
KEYMAPS Y COMANDOS · extender la interfaz

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, la tabla de contexto que recibe el callback, cómo aceptar rangos y conteos, qué significa de verdad el bang, y cómo escribir un completado personalizado que se sienta nativo.

⏱ 18 min

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.

🎯 Al terminar esta lección sabrás
  • Declarar comandos con nvim_create_user_command y dominar nargs, range, count, bang y bar.
  • Leer la tabla de contexto que recibe el callback: args, fargs, line1, line2, bang, reg y smods.
  • Distinguir un rango de un conteo y elegir el correcto para cada verbo.
  • Escribir un completado personalizado en Lua y saber por qué el filtrado corre de tu cuenta.

Anatomía de la declaración

La función toma un nombre, un cuerpo y una tabla de opciones. El nombre debe empezar por mayúscula y contener solo letras, dígitos y guiones bajos: esa restricción existe para que los comandos de usuario nunca colisionen con los comandos 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, :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, 1, ?, * o +
range Acepta rango de líneas; con '%' el rango por defecto es el archivo entero
count Acepta un conteo numérico en lugar de un rango
bang Habilita la variante con admiración
bar Permite encadenar con la barra vertical y comentarios finales
register Permite anteponer un registro, disponible en reg
addr Qué cuenta el rango: líneas, buffers, pestañas, ventanas
complete Cadena predefinida o función de Lua para el completado
desc Descripción visible en la introspección

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 de argumentos tal cual se escribió. fargs es esa misma cadena ya partida en una lista, respetando las comillas. 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, lo que te permite distinguir “sin rango” de “un rango que casualmente coincide con el defecto”.

⌨️

bang, count y reg

bang es un booleano, count el conteo numérico si declaraste count, 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 como texto, y smods los mismos en forma estructurada, listos para pasárselos a nvim_cmd. Es la pieza que permite que tu comando respete un :vertical o un :tab antepuestos.

Ese último punto es el que separa un comando aficionado de uno indistinguible de los nativos. Si reenvías smods, el usuario puede escribir :vertical Ayuda lua o :tab Ayuda lua y tu comando obedecerá sin que tú programes nada al respecto.

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',
})

Rangos y conteos: dos cosas distintas

Un rango describe una región: dos números que delimitan líneas, buffers o pestañas según addr. Un conteo describe una cantidad: un solo número que el usuario antepone. :3Cerrar significa cosas muy distintas si declaraste range (líneas 1 a 3, o la línea 3) o count (tres veces, o el elemento tres). 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
  end
end, { count = 5, desc = 'Lista los N buffers cargados más recientes' })
⚠️
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 con nombre.

flowchart LR
U[El usuario teclea el comando] --> P[Neovim analiza rango bang y argumentos]
P --> T[Construye la tabla de contexto]
T --> C[Invoca tu callback de Lua]
C --> E[Efecto sobre el buffer o la interfaz]
P --> K[Si pulsa Tab llama a tu funcion de completado]
K --> L[Devuelves la lista ya filtrada]
style T fill:#cba6f7,color:#11111b
style C fill:#89b4fa,color:#11111b
style L fill:#a6e3a1,color:#11111b

Completado personalizado

La opción complete acepta una de las cadenas predefinidas de Vim, como file, buffer, dir, help o highlight, 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. Y aquí está el detalle que sorprende a todo el mundo la primera vez: Neovim no filtra por ti. Tu función se comporta como customlist, así que devuelve exactamente lo que quieres ofrecer, ya recortado.

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 sus opciones en la segunda, que es la técnica con la que plugins como el gestor de paquetes construyen interfaces de un solo comando. Para acotar el alcance existen nvim_buf_create_user_command, que instala el comando solo en un buffer, y nvim_del_user_command para retirarlo.

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, 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 para el 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.

📝
Lo esencial

nvim_create_user_command exige un nombre en mayúscula y recibe una tabla de opciones donde nargs, range, count, bang, bar, register, addr, complete 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 es lo que hace que tu comando respete :vertical y :tab. Un rango delimita una región y un conteo indica una cantidad: no son intercambiables. En un completado de Lua el filtrado corre de tu cuenta.

⚔️ Verbos de primera clase
  1. Declara :Ordenar con range y bang, y verifica que funciona sobre una selección visual, sobre un rango explícito y sobre todo el archivo.
  2. Añade a un comando tuyo el reenvío de smods a nvim_cmd y comprueba que :vertical y :tab antepuestos cambian dónde se abre la ventana.
  3. Escribe un comando con nargs = '+' que imprima args y fargs para ver la diferencia exacta entre la cadena cruda y la lista.
  4. Implementa un completado contextual de dos niveles: subcomandos en la primera posición y valores distintos según el subcomando elegido.
  5. Convierte tu mapeo más complejo en un comando de usuario y reduce el mapeo a <Cmd> seguido del nombre del comando.