vim.keymap.set a fondo: modos, callbacks y opciones
La primitiva por la que pasa todo tu vocabulario. Los cuatro argumentos y la asimetría histórica que corrige, la diferencia real entre los modos v, x, s y o, por qué un callback de Lua es superior a una cadena de teclas, qué altera exactamente cada opción — desc, silent, expr, buffer, unique — y cómo leer la tabla de mapeos como lo que es: una base de datos viva.
Cada atajo que añades a tu configuración pasa por la misma puerta. vim.keymap.set no es azúcar sintáctico sobre nvim_set_keymap: invierte un valor por defecto heredado de Vimscript, acepta funciones de Lua donde la API solo admitía cadenas y rellena solo las opciones que en la capa cruda tienes que deletrear. Conocer sus cuatro argumentos con precisión es la diferencia entre coleccionar atajos sueltos y diseñar un lenguaje — que es, exactamente, el proyecto de este nivel.
- Leer la firma
vim.keymap.set(modo, lhs, rhs, opts)y explicar por quéremapes falso por defecto. - Elegir el modo correcto entre
n,i,x,s,v,o,tyc, evitando la trampa clásica dev. - Sustituir cadenas de teclas por callbacks de Lua y saber cuándo importa su valor de retorno.
- Aplicar
desc,silent,buffer,nowaityuniquesabiendo qué campo alteran en la tabla interna. - Consultar esa tabla en tiempo de ejecución con
nvim_get_keymapy:verbose map.
La firma y la asimetría que corrige
La función toma cuatro argumentos: el modo (una cadena o una lista de cadenas), el lhs o secuencia de teclas que pulsas, el rhs o comportamiento resultante, y una tabla de opciones. Para entender su decisión de diseño más importante hay que mirar atrás: en Vimscript, :map es recursivo por herencia histórica — el resultado vuelve a pasar por la tabla de mapeos, y por eso existe :noremap y por eso todas las guías clásicas lo usan para todo. nvim_set_keymap, la capa de la API que viste en el Nivel 8, arrastra ese defecto y te obliga a escribir noremap = true cada vez. vim.keymap.set invierte la carga de la prueba: nunca remapea salvo que se lo pidas con remap = true.
-- API cruda: heredera de Vimscript, recursiva salvo que digas lo contrariovim.api.nvim_set_keymap('n', '<leader>w', ':write<CR>', { noremap = true, silent = true })-- Envoltorio de Lua: no recursivo por defectovim.keymap.set('n', '<leader>w', '<Cmd>write<CR>', { silent = true, desc = 'Guardar' })-- La excepción legítima: alcanzar un mapeo <Plug> de un plugin exige recursiónvim.keymap.set('n', 'ga', '<Plug>(EasyAlign)', { remap = true, desc = 'Alinear' })Un mapeo <Plug> es, por construcción, inalcanzable desde el teclado: nadie puede teclear <Plug>. Solo existe para ser el destino de otro mapeo, y llegar hasta él requiere recursión. Ese es prácticamente el único caso donde remap = true está justificado. La capa cruda no desaparece del mapa por ello: nvim_set_keymap sigue siendo lo que usa un cliente remoto por RPC y lo que verás en el código de muchos plugins, así que conviene reconocerla; pero en tu configuración la puerta es siempre el envoltorio.
Fíjate también en <Cmd>write<CR> frente a :write<CR>. Con los dos puntos entras de verdad en la línea de comandos: cambias de modo, disparas los eventos CmdlineEnter y CmdlineLeave que estudiaste en el Nivel 9, y en modo visual Neovim te inserta el rango '<,'> automáticamente. Con <Cmd> el comando se ejecuta sin abandonar el modo actual y sin efectos colaterales; a cambio, exige terminar en <CR> o falla con E1255. Salvo que quieras justo ese rango automático del modo visual, <Cmd> es la opción correcta.
Modos: la tabla que casi todos usan mal
| Cadena | Modos que abarca |
|---|---|
n |
Normal |
i |
Insertar |
x |
Visual (solo visual) |
s |
Selección |
v |
Visual y selección |
o |
Operator-pending |
t |
Terminal |
c |
Línea de comandos |
"" |
Normal, visual, selección y operator-pending |
"!" |
Insertar y línea de comandos |
El error más extendido de toda la comunidad es escribir v cuando se quiere decir x. El modo selección existe para que, al saltar a un hueco de un snippet — los de vim.snippet que usas desde la lección 3.2 —, cualquier tecla imprimible sustituya el texto seleccionado, como haría un editor convencional. Si mapeas p en modo v, ese mapeo se instala también en selección, y al escribir sobre el hueco de un snippet la p ya no escribirá una letra: ejecutará tu mapeo. La regla es tajante: mapea en x salvo que sepas explícitamente que quieres tocar el modo selección.
El modo o es el gran desaprovechado. Un mapeo en operator-pending define un complemento gramatical nuevo para la gramática de la lección 0.4: si mapeas la misma secuencia en o y en x, acabas de crear un objeto de texto que funciona con todos los verbos nativos a la vez.
-- Un objeto de texto nuevo: el contenido íntegro del buffervim.keymap.set({ 'o', 'x' }, 'ie', function() vim.cmd('normal! ggVG')end, { desc = 'Objeto de texto: el archivo entero' })Con esas cuatro líneas, die, yie, cie y =ie existen de golpe. Esa es la aritmética de la gramática: los mapeos suman, los complementos y los operadores multiplican. La lección 11.4 explota la otra mitad de esa aritmética — inventar el verbo.
Qué ocurre al pulsar una tecla
Si el lhs que pulsas es además prefijo de otro mapeo, Neovim espera timeoutlen milisegundos antes de decidir cuál ejecutar; esa espera y cómo evitarla son parte del diseño del vocabulario y se tratan en la lección 11.5.
Callbacks de Lua en lugar de cadenas
Cuando el rhs es una función, Neovim no guarda ninguna secuencia de teclas: almacena la referencia en el campo callback de la tabla de mapeos, y nvim_get_keymap devolverá el rhs vacío. Esto va mucho más allá de la comodidad. Puedes cerrar sobre variables locales — las closures del Nivel 7 —, no tienes que escapar termcodes, no atraviesas la frontera Lua–Vimscript–Lua que impone <Cmd>lua ...<CR>, y un error te da una traza legible en lugar de un E5108 desnudo.
vim.keymap.set('n', '<leader>bd', function() local buf = vim.api.nvim_get_current_buf() if vim.bo[buf].modified then vim.notify('El buffer tiene cambios sin guardar', vim.log.levels.WARN) return end vim.api.nvim_buf_delete(buf, {})end, { desc = 'Cerrar el buffer si está guardado' })Dentro del callback tienes el contexto completo de la pulsación a través de vim.v, el espacio de variables del editor que conociste en la lección 8.3: vim.v.count1 es el conteo que el usuario tecleó antes de la tecla (con 1 como valor por defecto; vim.v.count da 0 si no hubo conteo), vim.v.register el registro que precedía con comillas, y vim.fn.mode() el modo exacto.
Textlock: no todo se puede hacer en cualquier momento
Un callback invocado bajo textlock no puede modificar el texto ni cambiar de ventana, y aborta con E565. Ocurre en mapeos expr, dentro de ciertos autocomandos y en callbacks de la interfaz. La salida canónica es vim.schedule(function() ... end), que difiere el efecto al siguiente ciclo del bucle de eventos, cuando el bloqueo ya se ha levantado. El porqué profundo de esa regla — qué es el bucle de eventos y dónde está la frontera — es la lección 12.3.
Las opciones que cambian el comportamiento
desc
Una frase en lenguaje natural. Aparece en :map, la consume which-key y la lee nvim_get_keymap. Convierte tu tabla de mapeos en una base de datos consultable: sin desc tus atajos son opacos incluso para ti.
silent
Equivale a prefijar la ejecución con :silent. Evita el eco del comando en la línea de estado, pero no silencia los errores. Importa en mapeos que ejecutan comandos con dos puntos; con <Cmd> el comando ya no se ecoa, así que allí sobra.
expr
El rhs deja de ser el comportamiento y pasa a ser un generador de teclas: se evalúa en cada pulsación y su resultado se inyecta como si lo hubieras tecleado. Es el tema íntegro de la lección siguiente.
buffer
Restringe el mapeo a un buffer concreto, con 0 o true para el actual. Los mapeos locales tienen prioridad absoluta sobre los globales: ideales para atajos que solo tienen sentido en un lenguaje o en una ventana efímera.
Quedan tres opciones menores pero decisivas. nowait impide que Neovim espere más teclas cuando tu lhs es prefijo de otro mapeo — su caso de uso real está en la 11.5. unique provoca un error E227 si el mapeo ya existía: colocarlo mientras diseñas convierte cualquier colisión silenciosa en un fallo ruidoso al arrancar. Y replace_keycodes solo tiene sentido junto a expr; su valor por defecto es verdadero cuando el rhs es una función y expr está activo, y los detalles viven en la 11.2.
Para retirar mapeos existe vim.keymap.del(modo, lhs, opts), que acepta buffer para los locales. Sirve también para renunciar a los que Neovim instala de fábrica — la lista completa está en :h default-mappings —, aunque en este curso los conservamos todos: la familia gr pertenece al LSP de la lección 3.1 y los corchetes a los saltos, y ese reparto es parte del contrato de teclas que cierra la 11.5.
El patrón que une varias de estas piezas es el mapeo local instalado por un autocomando, la técnica del Nivel 9 aplicada al teclado:
-- Mapeo que solo existe donde hay un cliente LSP enganchadovim.api.nvim_create_autocmd('LspAttach', { group = vim.api.nvim_create_augroup('mis_teclas_lsp', { clear = true }), callback = function(ev) vim.keymap.set('n', '<leader>cf', function() vim.lsp.buf.format({ async = true }) end, { buffer = ev.buf, desc = 'Formatear con el LSP' }) end,})Un mapeo no es un atajo: es una entrada en una base de datos viva
El salto conceptual que separa a quien acumula atajos de quien diseña un editor es entender que la tabla de mapeos es datos consultables en tiempo de ejecución, no una lista muerta de asignaciones. vim.api.nvim_get_keymap('n') te devuelve, ahora mismo, todos los mapeos normales como una tabla de Lua con lhs, rhs, callback, desc, buffer, expr, silent y noremap. Eso significa que tu configuración puede razonar sobre sí misma: generar una chuleta, detectar colisiones antes de que te muerdan, comprobar que ningún mapeo carece de desc, o construir un menú dinámico. Which-key no hace magia; simplemente lee esa tabla y ordena lo que encuentra, y por eso un desc ausente se traduce en un hueco en su ventana. La segunda mitad de la idea es la introspección al revés: :verbose nmap <leader>f te dice el archivo y la línea exactos donde se fijó ese mapeo por última vez, lo que resuelve en dos segundos el misterio de qué plugin te ha robado una tecla. Cuando interiorizas que cada llamada a vim.keymap.set inserta un registro estructurado en una base de datos que puedes leer, auditar y transformar, dejas de configurar Neovim y empiezas a programarlo. La disciplina que sigue es evidente: desc en absolutamente todos los mapeos, unique mientras diseñas, y una función que vuelque la tabla cuando quieras conocer el estado real de tu editor en lugar de recordarlo de memoria — la lección 11.5 la construye contigo.
Mapear en o y en x a la vez es la fórmula completa de un objeto de texto: la versión o sirve a los operadores y la x a la selección visual. Todos los verbos, nativos o tuyos, lo aceptan desde ese momento.
:verbose añade la línea «Last set from …» con el archivo y la línea exactos de la última definición. Es la consulta a la base de datos de mapeos que resuelve el 90 % de los misterios de teclado.
El autocomando entrega el buffer en ev.buf; el mapeo con buffer queda local y con prioridad sobre cualquier global. Es el patrón estándar para todo el teclado del LSP.
La tabla de mapeos es consultable: cada resultado de esta línea es un agujero en tu documentación y en tu ventana de which-key.
Lo esencial
vim.keymap.set(modo, lhs, rhs, opts) no remapea por defecto, al contrario que la API cruda, y remap = true solo se justifica para alcanzar mapeos <Plug>. Usa x en lugar de v salvo que quieras tocar el modo selección, y recuerda que o te permite inventar objetos de texto. Un rhs de función se guarda como callback y su retorno se ignora salvo con expr. desc documenta, silent calla el eco, buffer acota el alcance con prioridad sobre lo global, unique convierte las colisiones en errores visibles y vim.keymap.del retira. Prefiere <Cmd> frente a los dos puntos para no cambiar de modo.
Domina la primitiva
- Escribe un mapeo
<leader>wque guarde el buffer, condesc, y comprueba con:verbose nmap <leader>wque la traza apunta a tu archivo y a tu línea. - Mapea una letra imprimible en modo
xy luego repite el experimento en modov; expande un snippet de la lección 3.2, salta a un hueco y describe qué diferencia observas al teclear sobre él. - Convierte un mapeo tuyo de cadena a callback de Lua y haz que respete
vim.v.count1, por ejemplo bajando esa cantidad de líneas connvim_win_set_cursor. - Instala un mapeo con
unique = trueque colisione a propósito con uno existente y lee elE227que aparece; retíralo después convim.keymap.del. - Recorre
vim.api.nvim_get_keymap('n')y lista por consola todos los mapeos que empiezan por tu tecla líder y no tienendesc. Deja la cuenta a cero.