Frontend: React, TypeScript, Tailwind, CSS/HTML
El stack web completo en Neovim 0.12: TypeScript con vtsls o ts_ls declarados en la carpeta lsp, Tailwind con previews de color, CSS/HTML/Emmet, ESLint, Prettier y parsers de Treesitter con la API viva.
El frontend es donde más servidores conviven: TypeScript, Tailwind, CSS, HTML, Emmet y ESLint, todos a la vez sobre el mismo archivo .tsx. Bien montado, Neovim iguala o supera a VS Code para React — y en 0.12 montarlo son seis ficheros pequeños y una línea que los enciende.
- Elegir servidor de TypeScript:
vtslsots_ls, nunca los dos. - Declarar los seis servidores del stack web en la carpeta
lsp/. - Tailwind con completado de clases en helpers y previews de color.
- Parsers de Treesitter, ESLint y Prettier sin pisar teclas ajenas.
Un fichero por servidor
En Neovim 0.12 no hay que registrar nada en ningún plugin: cada servidor es un fichero lsp/nombre.lua que devuelve una tabla, y una sola llamada decide cuáles se activan. Ese fichero solo se lee cuando el servidor hace falta, así que tener seis declarados no cuesta nada al arrancar.
vim.lsp.enable({
"vtsls", -- o "ts_ls", nunca los dos
"tailwindcss",
"cssls",
"html",
"emmet_language_server",
"eslint",
})
Los binarios los consigues por npm (npm i -g @vtsls/language-server vscode-langservers-extracted @tailwindcss/language-server @olrtg/emmet-language-server) o con Mason, que existe justo para eso: instalar ejecutables y ponerlos en el camino de búsqueda. Ni declara ni activa nada; si prefieres npm, no lo necesitas.
TypeScript: vtsls o ts_ls
ts_ls
typescript-language-server, la envoltura clásica de tsserver. Sólida y estándar. El binario se instala con npm i -g typescript typescript-language-server.
vtsls
Envuelve la extensión de TypeScript de VS Code entera, así que acepta sus mismos ajustes y expone sus comandos y refactors. Soporta monorepos con un solo proceso. La opción “pro”.
Sus propios autores lo dicen: no es recomendable activar vtsls y ts_ls a la vez. Los dos hablan con tsserver, así que tendrías dos procesos analizando el mismo proyecto, diagnósticos duplicados y el doble de memoria. Elige uno y bórralo del vim.lsp.enable.
return {
cmd = { "vtsls", "--stdio" },
filetypes = { "javascript", "javascriptreact", "typescript", "typescriptreact" },
root_markers = { "tsconfig.json", "jsconfig.json", "package.json", ".git" },
init_options = { hostInfo = "neovim" },
settings = {
typescript = {
inlayHints = {
parameterNames = { enabled = "literals" }, -- none, literals o all
variableTypes = { enabled = true },
functionLikeReturnTypes = { enabled = true },
},
},
},
}
Si prefieres ts_ls, la ficha es casi idéntica cambiando el cmd por { "typescript-language-server", "--stdio" }. Los inlay hints los enciende el autocomando LspAttach de la lección 2.1 cuando el servidor los soporta, y los alternas con <leader>th.
Si instalas nvim-lspconfig (lección 2.5), su ficha de ts_ls añade dos comandos de buffer que Neovim no trae: :LspTypescriptSourceAction, para las acciones de archivo completo —organizar imports, borrar código sin usar—, y :LspTypescriptGoToSourceDefinition, que salta a la implementación real en vez de a la declaración de tipos. Eso es lo que aporta hoy ese plugin: fichas mantenidas por gente que ha averiguado estos detalles, no maquinaria.
Tailwind: clases, helpers y colores
return {
cmd = { "tailwindcss-language-server", "--stdio" },
filetypes = {
"html", "css", "scss", "less", "astro", "svelte", "vue",
"javascript", "javascriptreact", "typescript", "typescriptreact",
},
root_markers = {
"tailwind.config.js", "tailwind.config.ts",
"postcss.config.js", "package.json", ".git",
},
settings = {
tailwindCSS = {
-- Funciones en las que también quieres completado de clases
classFunctions = { "tw", "clsx", "cva", "cn" },
-- Atributos que contienen clases
classAttributes = { "class", "className", "class:list", "classList" },
},
},
}
classFunctions es la opción que resuelve el problema clásico de React: las clases escritas dentro de clsx(...), cva(...) o una plantilla etiquetada tw no se completaban porque el servidor no sabía que ahí dentro hay clases. Cada entrada se trata como una expresión regular que casa con el nombre de la función, así que tw\\.[a-z-]+ cubre también cosas como tw.div.
El completado de clases aparece con el autocompletado, y el autocompletado es el dueño de Ctrl-Space y de Tab en todo este track — tanto si usas el nativo de 0.12 (vim.o.autocomplete más vim.opt.completeopt) como blink.cmp de la lección 2.2. Ningún plugin de esta lección ni de las siguientes puede reclamar esas dos teclas. Y s y S son de flash.nvim, con mini.surround en el prefijo gs (lección 2.3).
Previews de color
El servidor de Tailwind ya te enseña el CSS que genera cada clase al hacer hover. Para ver el color real junto a la clase, añade un plugin de resaltado de colores:
return {
"brenoprata10/nvim-highlight-colors",
event = { "BufReadPre", "BufNewFile" },
opts = {
render = "virtual",
enable_tailwind = true,
},
}vim.pack.add({
{ src = "https://github.com/brenoprata10/nvim-highlight-colors" },
})
vim.opt.termguicolors = true
require("nvim-highlight-colors").setup({
render = "virtual",
enable_tailwind = true,
})Con render virtual pinta un cuadradito junto al valor; si prefieres teñir el fondo, usa render background. Necesita termguicolors.
Ahora bg-red-500 o #89b4fa se muestran con su color al lado, y con :HighlightColors Toggle lo apagas cuando estorbe. Además reconoce el textDocument/documentColor que envían servidores como el de Tailwind, así que los colores que solo el servidor conoce también aparecen.
CSS, HTML y Emmet
Tres fichas cortas y muy parecidas. Las tres primeras órdenes vienen del paquete vscode-langservers-extracted:
return {
cmd = { "vscode-css-language-server", "--stdio" },
filetypes = { "css", "scss", "less" },
root_markers = { "package.json", ".git" },
init_options = { provideFormatter = true },
settings = {
css = { validate = true },
scss = { validate = true },
less = { validate = true },
},
}
return {
cmd = { "vscode-html-language-server", "--stdio" },
filetypes = { "html" },
root_markers = { "package.json", ".git" },
init_options = {
provideFormatter = true,
embeddedLanguages = { css = true, javascript = true },
},
}
return {
cmd = { "emmet-language-server", "--stdio" },
filetypes = {
"html", "css", "scss", "less", "astro", "svelte", "vue",
"javascriptreact", "typescriptreact",
},
root_markers = { ".git" },
}
Con Emmet escribes div.card>ul>li*3 y el completado lo expande al HTML entero. Funciona igual dentro de JSX.
Los servidores de CSS y HTML solo ofrecen completado si el cliente declara que soporta snippets, y esa capacidad no viene activada de serie. Se arregla una vez, para todos los servidores, con la entrada comodín:
local caps = vim.lsp.protocol.make_client_capabilities()
caps.textDocument.completion.completionItem.snippetSupport = true
vim.lsp.config("*", { capabilities = caps })Si en la lección 2.1 ya pusiste ahí las capabilities de blink.cmp, añade esta línea a esa misma tabla en lugar de declarar el comodín dos veces: la fusión es superficial y la segunda declaración pisaría la primera.
ESLint y Prettier
Para ESLint tienes dos caminos y no hacen falta los dos:
- El servidor (
vscode-eslint-language-server, del mismo paquete que CSS y HTML): diagnósticos en vivo y arreglo automático por code action. Su ficha es peculiar porque solo se adjunta si encuentra un fichero de configuración de ESLint por encima del archivo. eslint_dpor nvim-lint (lección 2.4): más ligero, corre el linter al guardar y ya lo tienes montado.
Si eliges el servidor y quieres arreglo al guardar, el catálogo de nvim-lspconfig aporta aquí algo concreto: su ficha crea el comando :LspEslintFixAll. Encadenarlo al guardado se hace respetando el on_attach que ya trae:
local base_on_attach = vim.lsp.config.eslint.on_attach
vim.lsp.config("eslint", {
on_attach = function(client, bufnr)
if not base_on_attach then return end
base_on_attach(client, bufnr)
vim.api.nvim_create_autocmd("BufWritePre", {
buffer = bufnr,
command = "LspEslintFixAll",
})
end,
})
Para formatear, ya configuraste Prettier en conform (prettierd con recambio a prettier). Se ejecuta al guardar en JS, TS, CSS, HTML y JSON, y un .prettierrc en la raíz manda sobre el estilo.
Parsers del stack web
require("nvim-treesitter.configs").setup(...) con ensure_installed y highlight es la configuración de la rama master, congelada y solo válida para Neovim 0.11. En la rama main —la única viva— el plugin instala parsers y poco más; el resaltado lo enciende Neovim.
require("nvim-treesitter").install({
"javascript", "typescript", "tsx", "html", "css", "json",
})
-- Sin foldlevelstart = 99, cada archivo se abre ENTERO PLEGADO.
-- Ponlo una vez en tus opciones (leccion 2.3).
vim.o.foldlevelstart = 99
vim.api.nvim_create_autocmd("FileType", {
pattern = { "javascript", "javascriptreact", "typescript", "typescriptreact", "html", "css" },
callback = function()
vim.treesitter.start()
vim.wo[0][0].foldexpr = "v:lua.vim.treesitter.foldexpr()"
vim.wo[0][0].foldmethod = "expr"
end,
})
Ojo a un detalle que despista: el parser de React se llama tsx, no typescriptreact. El tipo de archivo de Neovim y el nombre del parser no tienen por qué coincidir.
El cierre y renombrado automático de etiquetas ya lo instalaste en la lección 2.3 con nvim-ts-autotag: escribes <div> y se cierra solo; renombras la etiqueta de apertura y la de cierre te sigue. En React eso son cientos de pulsaciones al día.
package.json vivo
return {
"vuki656/package-info.nvim",
dependencies = { "MunifTanjim/nui.nvim" },
event = { "BufRead package.json" },
config = function()
require("package-info").setup()
vim.keymap.set("n", "<leader>ns", "<cmd>PackageInfoShow<cr>", { desc = "npm: ver versiones" })
vim.keymap.set("n", "<leader>nu", "<cmd>PackageInfoUpdate<cr>", { desc = "npm: actualizar dependencia" })
vim.keymap.set("n", "<leader>nd", "<cmd>PackageInfoDelete<cr>", { desc = "npm: borrar dependencia" })
end,
}vim.pack.add({
{ src = "https://github.com/MunifTanjim/nui.nvim" },
{ src = "https://github.com/vuki656/package-info.nvim" },
})
require("package-info").setup()
vim.keymap.set("n", "<leader>ns", "<cmd>PackageInfoShow<cr>", { desc = "npm: ver versiones" })
vim.keymap.set("n", "<leader>nu", "<cmd>PackageInfoUpdate<cr>", { desc = "npm: actualizar dependencia" })
vim.keymap.set("n", "<leader>nd", "<cmd>PackageInfoDelete<cr>", { desc = "npm: borrar dependencia" })Necesita nui.nvim y npm en el sistema. Detecta solo si el proyecto usa npm, yarn, pnpm o bun, incluso dentro de un monorepo.
Con <leader>ns ves como texto virtual qué versión de cada dependencia es la última publicada, y actualizas o borras la de la línea sin salir del buffer.
Con TypeScript, Tailwind, autotag, previews de color y Prettier al guardar tienes el bucle completo de React: completado de props y de clases, renombrado seguro de componentes con <leader>rn, imports automáticos, formateo instantáneo y JSX que se cierra solo. Súmale s de flash.nvim para saltar por componentes largos. Pero mira la lección desde arriba: seis servidores conviviendo sobre el mismo buffer y ninguno se estorba, porque cada uno declara a qué tipos de archivo se ofrece y qué raíz busca. Ese es el diseño que hace que el stack web sea manejable en un editor de texto plano — y también el sitio donde mirar cuando algo se comporta raro. Antes de tocar nada, :checkhealth vim.lsp te dice qué clientes están adjuntos a ese buffer. Si ves dos que hacen lo mismo, ya tienes tu bug.
- Escribe las fichas de
vtslsytailwindcssy actívalas junto a las de CSS, HTML y Emmet. - Abre un proyecto React con TypeScript y Tailwind y comprueba con
:checkhealth vim.lspcuántos clientes hay adjuntos al mismo.tsx. - Escribe
className="bg-y mira el completado de Tailwind con los colores al lado. - Mete una clase dentro de
clsx(...)y confirma que también completa gracias aclassFunctions. - Renombra un componente con
<leader>rny observa cómo se actualizan los usos. - Instala los parsers, desordena el formato y guarda: Prettier lo arregla y el resaltado no parpadea.