Autocompletado: primero el nativo, luego blink.cmp
En Neovim 0.12 el menú de completado en Insert es una opción del editor: autocomplete y completeopt. Qué te da sin instalar nada, qué añade blink.cmp encima, y por qué Ctrl-Espacio y Tab no se tocan.
Durante años, montar un IDE en Neovim empezaba por instalar un plugin de completado. En 0.12 ya no hace falta: el menú automático en modo Insert es una opción del editor, y el LSP se enchufa a él con dos líneas. Esta lección empieza por lo que ya tienes gratis y solo después mira qué justifica añadir un plugin encima.
- Activar el completado nativo con
autocomplete,completeoptycomplete. - Enchufar el LSP como fuente del menú nativo.
- La regla de teclas que rompe más configs:
<C-Space>y<Tab>son del completado. - Qué añade
blink.cmpencima y cuándo no merece la pena instalarlo.
El completado ya está dentro
Neovim 0.12 trae la opción autocomplete: cuando está activa, el menú aparece solo mientras escribes, sin pulsar nada y sin plugins. Va acompañada de dos opciones que ya existían y ahora importan más: complete decide de dónde salen los candidatos y en qué orden, y completeopt decide cómo se comportan el menú y la inserción.
-- El menú aparece solo mientras escribes.
vim.o.autocomplete = true
-- Retardo antes de abrirlo (0 por defecto). Súbelo un poco si te molesta
-- que salte a media palabra.
vim.o.autocompletedelay = 50
-- Cómo se comporta el menú.
vim.opt.completeopt = {
"menuone", -- muestra el menú aunque solo haya un candidato
"popup", -- ventana flotante con la info del candidato seleccionado
"fuzzy", -- coincidencia difusa: perdona letras saltadas
}
-- De dónde salen los candidatos, en orden. El "^5" limita cada fuente
-- a 5 resultados para que el menú no se convierta en una lista infinita.
vim.opt.complete = { ".^5", "w^5", "b^5", "u^5" }
Las banderas de complete que vas a usar: . es el buffer actual, w los buffers de otras ventanas, b los buffers cargados de la lista, u los descargados, y o el omnifunc (aquí es donde entrará el LSP). Neovim reparte un presupuesto de tiempo decreciente entre ellas: las primeras de la lista tienen prioridad, pero todas llegan a responder.
Antes, sin plugin, no tenías menú automático: tenías <C-n> y <C-p> a mano. Ahora el comportamiento por defecto de un editor moderno es una opción booleana. Para muchas configuraciones —sobre todo si escribes Lua, shell, Markdown o textos— esto ya es suficiente y el plugin de completado sobra.
Enchufar el LSP al menú nativo
Cuando un servidor se adjunta, Neovim ya pone omnifunc a vim.lsp.omnifunc, así que <C-x><C-o> funciona sin configurar nada. Para que los candidatos del LSP entren en el menú automático, hay dos piezas: activar vim.lsp.completion en el buffer y añadir la fuente o a complete.
vim.api.nvim_create_autocmd("LspAttach", {
group = vim.api.nvim_create_augroup("completado_lsp", { clear = true }),
callback = function(ev)
-- autotrigger usa los triggerCharacters que declara el servidor
-- (el punto, la flecha, los dos puntos dobles...).
vim.lsp.completion.enable(true, ev.data.client_id, ev.buf, { autotrigger = true })
-- La fuente "o" es el omnifunc, es decir, el LSP.
vim.opt_local.complete:append("o")
end,
})
Aceptar un candidato del LSP con <C-y> no solo inserta texto: aplica los efectos secundarios que trae el servidor —expandir el snippet, añadir el import que falta, ejecutar el comando asociado—. Eso funciona igual si el menú vino del autotrigger, del omnifunc o de una llamada manual.
La pregunta la responde el propio manual: comprueba :verbose set omnifunc? en ese buffer. Si no vale v:lua.vim.lsp.omnifunc, algún plugin te lo ha pisado. Y con :lsp tienes los subcomandos para ver y manejar los clientes: :lsp restart, :lsp stop.
Las teclas del completado
| Tecla | Acción |
|---|---|
| Ctrl-Space | pedir el completado a mano |
| Ctrl-y | aceptar el candidato seleccionado |
| Ctrl-e | cerrar el menú sin aceptar |
| Ctrl-n / Ctrl-p | siguiente / anterior candidato |
| Tab / S-Tab | moverse por el menú |
| Ctrl-x Ctrl-o | completado del LSP por omnifunc |
<C-y>, <C-e>, <C-n> y <C-p> los tienes ya. Las otras dos las pones tú, y son exactamente las que propone la documentación de Neovim:
-- Tab y Shift-Tab navegan el menú; si no hay menú, hacen lo de siempre.
vim.keymap.set("i", "<Tab>", function()
return vim.fn.pumvisible() == 1 and "<C-n>" or "<Tab>"
end, { expr = true, desc = "Completado: siguiente" })
vim.keymap.set("i", "<S-Tab>", function()
return vim.fn.pumvisible() == 1 and "<C-p>" or "<S-Tab>"
end, { expr = true, desc = "Completado: anterior" })
-- Ctrl-Espacio pide el completado del LSP sin esperar al autotrigger.
vim.keymap.set("i", "<C-Space>", function()
vim.lsp.completion.get()
end, { desc = "Completado: invocar LSP" })
Regla de oro: Ctrl-Espacio y Tab no son tuyos
<C-Space> dispara el completado y <Tab> navega el menú. Ningún otro plugin puede quedárselas.
No es una preferencia estética: es la convención que asume todo el ecosistema. La propia ayuda de Neovim propone <C-Space> para vim.lsp.completion.get(), y los tres presets de blink.cmp —default, super-tab y enter— la mapean a show. Con <Tab> pasa lo mismo: el ejemplo oficial de ins-autocompletion la usa para moverse por el menú, blink.cmp la usa para saltar entre huecos de snippet, y los plugins de IA la encadenan encima.
Lo que las hace peligrosas es cómo fallan. Si otro plugin las reclama, no hay error, ni aviso, ni nada en :messages. El completado simplemente deja de responder. Buscas en la config del completado, que está bien; reinstalas el plugin, que está bien; y la tarde se te va antes de sospechar del plugin que ni siquiera tiene que ver con completar.
Este es el choque que más se cuenta por ahí, y conviene ser preciso porque hoy no existe: media internet mapea la selección incremental a <C-Space>, pero eso es herencia de cuando había que configurarla a mano con el módulo incremental_selection de nvim-treesitter. Ese módulo desapareció con la reescritura, y Neovim 0.12 trae la selección incremental de serie con teclas propias en modo Visual: an sube al nodo padre, in baja al hijo, ]n y [n van al siguiente y al anterior. Ninguna toca <C-Space>. Si sigues esta guía no tienes que arbitrar nada: las dos cosas conviven porque no comparten tecla. Lo tienes en la lección 2.3.
Sigue importando si vienes de fuera: una configuración vieja, un tutorial anterior a 2026 o una distro. LazyVim, por ejemplo, sí cede <C-Space> —junto con s, S, r y R— a flash.nvim, y ahí el conflicto es real. Por eso el diagnóstico de abajo vale igual.
Quien de verdad se pelea hoy por estas teclas son los plugins de saltos y los de IA, casi siempre por <Tab>. Ese caso está tratado en la lección 3.4: el <Tab> de la IA se encadena dentro de la capa de completado en vez de robarlo.
Cuando algo “deja de funcionar”, el primer reflejo debe ser preguntar quién tiene la tecla:
:map <C-Space>
:imap <Tab>
:verbose imap <Tab>
:map te dice si alguien la ha tomado en Normal o Visual; :imap es la que importa aquí, porque el completado vive en Insert; y :verbose añade el archivo y la línea exactos donde se definió el mapeo. Tres comandos, quince segundos, en vez de una tarde.
Después de instalar cualquier plugin que hable de “saltar”, “seleccionar”, “aceptar” o “sugerir”, ejecuta :imap <Tab> y :map <C-Space>. Si aparece alguien nuevo, decide tú quién gana antes de que lo decida el orden de carga.
inlineCompletion: la IA también habla LSP
Neovim 0.12 añadió al cliente el soporte de textDocument/inlineCompletion. Es la parte del protocolo pensada para sugerencias largas —una función entera, no una palabra— que se muestran como texto superpuesto en vez de como menú.
Importa aquí por una razón: el “ghost text” de las herramientas de IA ya no necesita un plugin propio si el proveedor publica un servidor de lenguaje. El módulo se maneja como cualquier otra función del cliente:
vim.lsp.inline_completion.enable()
-- Aceptar la sugerencia superpuesta. get() devuelve false si no hay
-- ninguna, así que el fallback a Tab es limpio.
vim.keymap.set("i", "<Tab>", function()
if not vim.lsp.inline_completion.get() then
return "<Tab>"
end
end, { expr = true, desc = "Aceptar sugerencia inline" })
Fíjate en que esto vuelve a caer sobre <Tab>: es la misma tecla, encadenada. Eso es lo correcto — el completado y las sugerencias inline son la misma capa y comparten teclado. Lo que no puedes es tener dos plugins distintos reclamando <Tab> por su cuenta. La lección 3.4 monta esta parte a fondo.
Qué añade blink.cmp encima
Con lo anterior tienes un completado decente. blink.cmp no lo sustituye: lo mejora en cosas concretas y medibles.
| Nativo (0.12) | blink.cmp | |
|---|---|---|
| Menú automático | sí, con autocomplete |
sí |
| Coincidencia difusa | sí, con completeopt=fuzzy |
tolerante a typos, con bonus de proximidad y frecuencia |
| Fuentes | las banderas de complete |
proveedores enchufables, con orden y prioridad propios |
| Snippets | vim.snippet a mano |
fuente integrada: friendly-snippets, LuaSnip o mini.snippets |
| Sugerencia inline (la de la IA) | sí, vim.lsp.inline_completion |
no es su capa: la sirve el LSP |
| Vista previa del ítem del menú | ventana flotante con completeopt=popup |
superpuesta en línea, con ghost_text |
| Ayuda de firma | a mano con <C-s> en Insert, que es mapeo global de serie |
automática mientras tecleas los argumentos (opcional, marcada como experimental) |
| Cmdline y terminal | completado propio de Vim | mismo motor y misma UI |
Si buscas “ghost text en Neovim” vas a encontrar respuestas contradictorias, y las dos tienen razón porque hablan de cosas diferentes:
- La sugerencia inline, esa línea o función entera en gris que propone Copilot y compañía. En 0.12 es nativa: viene del protocolo, con
textDocument/inlineCompletion, y se enciende convim.lsp.inline_completion.enable(). No necesitas ningún plugin si tu proveedor publica un servidor de lenguaje. Es lo que acabas de ver en la sección anterior. - La vista previa del ítem seleccionado del menú, que te muestra en línea lo que se insertaría si aceptas el candidato sobre el que estás. Eso sí lo aporta
blink.cmpcon su opciónghost_text. El nativo resuelve la misma necesidad de otra forma: concompleteopt=popupte enseña la documentación del ítem en una ventana flotante, no superpuesta al texto.
Cuando alguien te diga que Neovim “no tiene ghost text”, pregunta a cuál de las dos se refiere. Casi siempre está hablando de la segunda y generalizando de más.
La diferencia práctica no es la velocidad —el nativo es rápido—, es la gestión de fuentes. Si quieres que las rutas de archivo, los snippets, el buffer y el LSP convivan en un mismo menú con un orden que tú decides, y que buscar fuction encuentre function, eso el nativo no lo hace.
return {
"saghen/blink.cmp",
version = "1.*", -- las etiquetas traen el binario Rust precompilado
dependencies = { "rafamadriz/friendly-snippets" },
event = "InsertEnter",
opts = {
-- default: <C-Space> abre, <C-y> acepta, <Tab> salta huecos de snippet.
-- 'super-tab' acepta con <Tab>; 'enter' acepta con <CR>.
keymap = { preset = "default" },
appearance = { nerd_font_variant = "mono" },
-- El orden importa: es el orden en que se agrupan en el menú.
sources = {
default = { "lsp", "path", "snippets", "buffer" },
},
completion = {
documentation = { auto_show = true, auto_show_delay_ms = 200 },
ghost_text = { enabled = true },
menu = { border = "rounded" },
},
signature = { enabled = true, window = { border = "rounded" } },
fuzzy = { implementation = "prefer_rust_with_warning" },
},
opts_extend = { "sources.default" },
}vim.pack.add({
{ src = "https://github.com/rafamadriz/friendly-snippets" },
{
src = "https://github.com/saghen/blink.cmp",
version = vim.version.range("1.x"), -- última etiqueta de la serie 1
},
})
require("blink.cmp").setup({
keymap = { preset = "default" },
appearance = { nerd_font_variant = "mono" },
sources = { default = { "lsp", "path", "snippets", "buffer" } },
completion = {
documentation = { auto_show = true, auto_show_delay_ms = 200 },
ghost_text = { enabled = true },
menu = { border = "rounded" },
},
signature = { enabled = true, window = { border = "rounded" } },
fuzzy = { implementation = "prefer_rust_with_warning" },
})La v2 está en desarrollo con cambios rompedores; el proyecto recomienda quedarse en la serie 1 salvo que quieras seguir el desarrollo.
Si instalas blink.cmp, apaga el nativo: dos menús a la vez es exactamente el tipo de conflicto que esta lección intenta evitar.
vim.o.autocomplete = false
Un cliente anuncia al servidor qué sabe hacer, y blink.cmp sabe más que el cliente por defecto. La forma clásica es pasar require("blink.cmp").get_lsp_capabilities() a la config del LSP, como en la lección 2.1. La propia documentación de blink.cmp avisa de que, con vim.lsp.config integrado desde 0.11, este paso puede omitirse en muchos setups. Si lo tienes puesto no molesta; si no lo tienes y el completado se siente pobre (sin auto-import, sin snippets del servidor), ahí está la primera sospecha.
Cuándo NO instalar blink.cmp
Vale la pena decirlo claro, porque va contra la costumbre:
Config mínima
Si tu objetivo es un editor que arranca en 20 ms y hace pocas cosas muy bien, el nativo te sobra. Un plugin menos es una dependencia menos que actualizar.
Escribes prosa, no código
Markdown, notas, commits, configuración. Ahí las fuentes de buffer y diccionario del nativo son justo lo que quieres, y el fuzzy de completeopt cubre el resto.
Un solo lenguaje con LSP fuerte
Si vives dentro de un único servidor bien configurado, la mayor parte de lo que aporta blink.cmp es gestión de fuentes múltiples, que no vas a usar.
Sin curl, git o Rust
El matcher rápido llega como binario precompilado y necesita descargarlo. En una máquina cerrada, esa fricción no compensa.
El valor real del completado no es teclear menos, es descubrir la API: al escribir objeto. ves todos los métodos disponibles con su documentación. Combinado con la ayuda de firma, editas lenguajes que apenas conoces sin salir del editor a buscar la doc.
Y la lección de fondo, la que se repite en todo el nivel 2: cuando una capacidad entra en el núcleo, la pregunta deja de ser “qué plugin instalo” y pasa a ser “qué me sigue faltando después de activar lo que ya viene”. Esa pregunta, hecha una vez al año sobre cada plugin de tu config, es lo que separa una config viva de una acumulación de dependencias heredadas.
- Activa
vim.o.autocompletey el bloque decompleteoptycomplete. Reinicia y escribe: el menú debe aparecer solo. - Añade el
LspAttachconvim.lsp.completion.enabley la fuenteo. Abre un archivo con LSP y comprueba que ahora completa símbolos, no solo palabras del buffer. - Mapea
<Tab>y<C-Space>como arriba. Confirma con:imap <Tab>que el mapeo es tuyo y de nadie más. - Prueba a mapear a propósito
<C-Space>a otra cosa, reinicia, y observa el síntoma: el completado calla, sin error. Luego encuéntralo con:verbose imap <C-Space>y deshaz el estropicio. - Solo ahora instala
blink.cmp, apagaautocomplete, y escribe mal a propósito (fuction) para ver qué te estabas perdiendo.
Con el LSP y el completado ya tienes la mitad de un IDE. Ahora afinamos la edición con Treesitter.