Neovim (nvim) es el mismo vim, solo que mejorado, reescrito y que se ha llevado a su lado a gran parte de la comunidad del editor original. Ahora este proyecto ha superado con creces al vim original en las estadísticas de github (34 vs 75k estrellas en 2024), así que si por alguna razón decidiste empezar a programar en la consola en 2024, neovim es tu elección.
¿Para qué sirve todo esto cuando existen IDEs cómodas y editores modernos como visual studio code? Normalmente se aducen dos argumentos:
Velocidad. En cualquier editor tipo vim, la característica principal son las combinaciones. Y con la práctica adecuada puedes moverte por el código y editarlo mucho más rápido que tomando el ratón cada pocos segundos;
Ligereza. Nvim es una utilidad que se abre en la consola y consume muy pocos recursos. Una IDE es un programa grande y muy pesado que, en un mac del 22, apenas permite abrir 3-4 proyectos a la vez.
Con el primer argumento estoy de acuerdo en parte: leemos más código y pensamos qué escribir de lo que lo tecleamos. Por eso, si crees que dominar nvim te dará ventaja sobre un colega con ratón, no, no te la dará. Pero donde realmente le ganarás al colega con ratón es en la comodidad. Una vez que te acostumbras a las combinaciones de vim, ya no podrás escribir como antes: te parecerá muy incómodo.
Con el segundo argumento estoy completamente de acuerdo: nvim es ligero y rápido, y la posibilidad de lanzar el editor al que estoy acostumbrado en cualquier consola, en cualquier sistema operativo, moderno o no tanto, y en cualquier dispositivo —desde un portátil viejo hasta un teléfono— a mí personalmente me gusta.
Este artículo no tiene el objetivo de venderte este editor: definitivamente no es para todos. Primero tienes que aprender a trabajar con él, y luego necesitas tiempo para configurarlo. Si esta idea no te gusta, lo más probable es que sea más fácil comprar una suscripción a JetBrains o descargar visual studio code, abrirlo y que todo funcione de inmediato. Pero si te gusta linux, te gusta configurar las herramientas con las que trabajas y te gusta la idea del open source, entonces es muy probable que nvim también te guste.
En el marco de este artículo no voy a contar cómo trabajar dentro de vim: hay muchísimos tutoriales y materiales donde se describe eso. Yo, en cambio, contaré cómo configurar nvim desde cero y obtener como resultado un editor muy parecido a una IDE, tanto por fuera como en cuanto a funcionalidad.

Toda la configuración se realizó en un MacBook con MacOS 14, y como terminal se usó WezTerm. WezTerm impone sus particularidades al trabajar con los atajos de teclado, que mencionaré más abajo, pero esa misma configuración se puede ejecutar sin problemas en cualquier sistema tipo unix, lo que incluso será un poco más sencillo. Voy a desplegar todo desde cero en un sistema limpio, para cerrar todas las dudas.
Para empezar, descarguemos e instalemos WezTerm, la mejor terminal en mi opinión, que además se puede personalizar: https://wezfurlong.org/wezterm/installation.html, descargamos e instalamos el paquete para nuestro SO.
De fábrica, por supuesto, no entusiasma:

Vamos a arreglarlo. Creamos la configuración:
poltora ~: mkdir -p ~/.config/wezterm/
poltora ~: touch ~/.config/wezterm/wezterm.lua
poltora ~: vim ~/.config/wezterm/wezterm.luaSe abre vim, donde pegamos los siguientes ajustes:
local wezterm = require 'wezterm'
local config = {}
if wezterm.config_builder then
config = wezterm.config_builder()
end
config.color_scheme = 'Atelierdune (light) (terminal.sexy)'
config.font = wezterm.font('JetBrains Mono', { weight = 'Medium' })
config.font_size = 16
config.hide_tab_bar_if_only_one_tab = true
config.window_background_opacity = 0.95
return configPara wezterm hay disponibles muchísimos temas, puedes encontrar y escribir en la configuración de arriba cualquiera a tu gusto - https://wezfurlong.org/wezterm/config/appearance.html. Lo mismo ocurre con los demás ajustes: gracias a su documentación detallada, se personaliza de forma muy fácil y cómoda.
Pulsamos :wqa, reiniciamos la terminal y esto es lo que obtenemos:

Ahora instalemos neovim. Hay muchísimas formas de instalarlo, según el sistema y el entorno; puedes leer en detalle sobre cada una en el manual oficial - https://github.com/neovim/neovim/blob/master/INSTALL.md
La más sencilla para MacOS es instalarlo como paquete:
poltora ~: brew install neovimTras la instalación, lanzamos nvim:
poltora ~: nvim .Se abre el editor por defecto que, por decirlo suavemente, no recuerda mucho a una IDE:

Cerramos con la combinación :q y empezamos a configurar. Ante todo, creemos el archivo raíz de la configuración:
mkdir ~/.config/nvim
touch ~/.config/nvim/init.lua
nvim ~/.config/nvim/init.luaLa configuración consistirá en su mayor parte en ajustes para diversos plugins. Para instalar plugins necesitamos un gestor especial, que precisamente propongo añadir ahora.
Gestor de plugins
Hay muchos gestores; yo uso uno de los más populares - VimPlug. Lo instalamos con el comando:
sh -c 'curl -fLo "${XDG_DATA_HOME:-$HOME/.local/share}"/nvim/site/autoload/plug.vim --create-dirs \
https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim'Ahora abrimos el archivo de configuración ~/.config/nvim/init.lua, creado antes, y le añadimos la siguiente estructura:
local vim = vim
local Plug = vim.fn['plug#']
vim.call('plug#begin')
vim.call('plug#end')De aquí en adelante, todos los plugins los añadiremos entre las etiquetas vim.call('plug#begin') y vim.call('plug#end'). Guardamos, reiniciamos y, si en la consola no hay errores, el gestor se ha instalado correctamente.
Para no perdernos en nuestras configuraciones, iré añadiendo todos los ajustes y la configuración de cada plugin en un archivo lua separado, que estará junto a init.lua. Creemos el archivo common.lua y escribamos allí una serie de ajustes básicos.
common.lua:
-- show line numbers
vim.wo.number = true
-- enable mouse control
vim.g.mouse = 'a'
vim.opt.encoding="utf-8"
-- disable the swap file
vim.opt.swapfile = false
-- set tab and indentation options
vim.opt.scrolloff = 7
vim.opt.tabstop = 4
vim.opt.softtabstop = 4
vim.opt.shiftwidth = 4
vim.opt.autoindent = true
vim.opt.fileformat = "unix"Ahora volvemos a init.lua y conectamos este archivo:
local vim = vim
local Plug = vim.fn['plug#']
vim.call('plug#begin')
vim.call('plug#end')
home=os.getenv("HOME")
package.path = home .. "/.config/nvim/?.lua;" .. package.path
require"common"Reiniciamos nvim y, si vemos los números de línea, todo funciona:

Temas
Primero hagamos que nvim se parezca visualmente a una IDE. Hay un sitio genial con un catálogo de temas de la comunidad - https://vimcolorschemes.com/, donde puedes elegir un tema a tu gusto. Yo estoy acostumbrado a trabajar con temas claros y, a base de prueba y error, encontré el que en mi opinión es el mejor - kanagawa, que precisamente propongo instalar. Creamos un nuevo archivo theme.lua, donde añadimos:
vim.cmd.colorscheme("kanagawa")En init.lua conectamos el nuevo plugin y el archivo theme.lua:
...
Plug('rebelot/kanagawa.nvim')
vim.call('plug#end')
...
dofile"theme.lua"Reiniciamos nvim, ejecutamos :PlugInstall, reiniciamos una vez más y vemos que el tema se ha aplicado:

Ahora hagamos que el árbol de archivos se muestre igual que estamos acostumbrados en una IDE.
Nvim-Tree - árbol de archivos
Para mostrar el árbol de archivos usaremos el plugin nvim-tree. Creamos el archivo vimtree.lua y añadimos allí los siguientes ajustes:
-- disable netrw at the very start of your init.lua
vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1
-- optionally enable 24-bit colour
vim.opt.termguicolors = true
-- empty setup using defaults
require("nvim-tree").setup()
-- OR setup with some options
require("nvim-tree").setup({
sort = {
sorter = "case_sensitive",
},
view = {
width = 30,
},
renderer = {
group_empty = true,
},
filters = {
dotfiles = true,
},
})Ahora añadimos el plugin nvim-tree y el plugin para el soporte de iconos de archivos y activamos el archivo con los ajustes de nvim-tree en init.lua:
...
Plug('kyazdani42/nvim-tree.lua')
Plug('kyazdani42/nvim-web-devicons')
vim.call('plug#end')
...
require"vimtree"Reiniciamos, instalamos el plugin con el comando :PlugInstall, reiniciamos una vez más y se nos abre el árbol de archivos, parecido a aquel al que estamos acostumbrados:

BarBar - pestañas de archivos
Ahora añadamos al editor las habituales pestañas de archivos. Para ello voy a usar el plugin barbar. Creamos el archivo barbar.lua y añadimos allí los ajustes:
require'barbar'.setup {
animation = true,
clickable = true,
exclude_ft = {'javascript'},
exclude_name = {'package.json'},
focus_on_close = 'left',
highlight_inactive_file_icons = false,
highlight_visible = true,
icons = {
buffer_index = false,
buffer_number = false,
button = '',
diagnostics = {
[vim.diagnostic.severity.ERROR] = {enabled = true, icon = 'ff'},
[vim.diagnostic.severity.WARN] = {enabled = false},
[vim.diagnostic.severity.INFO] = {enabled = false},
[vim.diagnostic.severity.HINT] = {enabled = true},
},
gitsigns = {
added = {enabled = true, icon = '+'},
changed = {enabled = true, icon = '~'},
deleted = {enabled = true, icon = '-'},
},
filetype = {
custom_colors = false,
enabled = true,
},
separator = {left = '▎', right = ''},
separator_at_end = true,
modified = {button = '●'},
pinned = {button = '', filename = true},
preset = 'default',
alternate = {filetype = {enabled = false}},
current = {buffer_index = true},
inactive = {button = '×'},
visible = {modified = {buffer_number = false}},
},
insert_at_end = true,
maximum_padding = 1,
minimum_padding = 1,
maximum_length = 30,
minimum_length = 0,
semantic_letters = true,
sidebar_filetypes = {
NvimTree = true,
undotree = {text = 'undotree'},
['neo-tree'] = {event = 'BufWipeout'},
Outline = {event = 'BufWinLeave', text = 'symbols-outline'},
},
letters = 'asdfjkl;ghnmxcvbziowerutyqpASDFJKLGHNMXCVBZIOWERUTYQP',
no_name_title = nil,
}Ahora en init.lua añadimos el plugin, activamos el archivo con los ajustes:
...
Plug('romgrk/barbar.nvim')
vim.call('plug#end')
...
require"barbar"Y tras reiniciar ejecutamos :PlugInstall, después de lo cual reiniciamos una vez más y vemos que las pestañas han llegado correctamente:

Lualine - barra de estado
A continuación, con ayuda del plugin lualine añadiremos abajo una barra de estado, en la que se mostrará el vim-mode, el archivo actual y el lenguaje de desarrollo. Creamos el archivo lua_line.lua (es importante que el archivo lleve guion bajo, de lo contrario, por conflictos de nombres, nvim no lo verá) y le añadimos los siguientes ajustes:
require('lualine').setup ({
options = {
icons_enabled = true,
theme = 'auto',
component_separators = { left = '', right = ''},
section_separators = { left = '', right = ''},
disabled_filetypes = {
statusline = {},
winbar = {},
},
ignore_focus = {},
always_divide_middle = true,
globalstatus = false,
refresh = {
statusline = 1000,
tabline = 1000,
winbar = 1000,
}
},
sections = {
lualine_a = {'mode'},
lualine_b = {'branch', 'diff', 'diagnostics'},
lualine_c = {'filename'},
lualine_x = {'encoding', 'fileformat', 'filetype'},
lualine_y = {'progress'},
lualine_z = {'location'}
},
inactive_sections = {
lualine_a = {},
lualine_b = {},
lualine_c = {'filename'},
lualine_x = {'location'},
lualine_y = {},
lualine_z = {}
},
tabline = {},
winbar = {},
inactive_winbar = {},
extensions = {}
})init.lua:
...
Plug('nvim-lualine/lualine.nvim')
...
vim.call('plug#end')
...
require"lua_line"
Y la variante con tema oscuro:

El margen para personalizar el aspecto del editor es infinito. Se puede acoplar literalmente de todo, desde animaciones hasta plugins que permiten generar temas con ayuda de la IA.
Ahora que el aspecto básico ya se parece a una IDE, es hora de enseñar a nvim a entender nuestro código, dar sugerencias y resaltar la sintaxis.
TreeSitter - árbol de código
Lo siguiente que quiero hacer es añadir el resaltado del código. TreeSitter es una utilidad que sabe analizar el código abierto en el editor y construir a partir de él un árbol sintáctico. Un árbol sintáctico es una estructura de datos en la que se almacena el código descompuesto en piezas. Los editores que trabajan con ese árbol entienden cómo resaltar tal o cual parte del código, y así obtenemos un resaltado de sintaxis completo.
En el momento de escribir este artículo, TreeSitter es la forma más popular de construir un árbol sintáctico. Para hacer que nvim y TreeSitter se lleven bien necesitamos un driver, cuyo papel jugará el plugin https://github.com/nvim-treesitter/nvim-treesitter. Conectamos el plugin en la configuración:
local vim = vim
local Plug = vim.fn['plug#']
vim.call('plug#begin')
Plug('nvim-treesitter/nvim-treesitter', {['do'] = ':TSUpdate'})
vim.call('plug#end')Creamos el archivo treesitter.lua y le añadimos la configuración del repositorio del plugin:
require'nvim-treesitter.configs'.setup {
ensure_installed = "all",
highlight = { enable = true },
incremental_selection = { enable = true },
textobjects = { enable = true },
}Guardamos, reiniciamos nvim y ejecutamos el comando :PlugInstall, tras cuya ejecución se mostrará la interfaz de VimPlug con un mensaje de que el plugin para TreeSitter se ha instalado correctamente. Ahora hay que reiniciar nvim una vez más para que TreeSitter instale los parsers de los lenguajes. Ahora veamos cómo ha quedado la sintaxis de un proyecto en go:

Se puede comparar con la captura de arriba y apreciar la diferencia. Ha quedado bonito, pero escribir código sigue siendo incómodo: no hay el autocompletado habitual, ni sugerencias, ni resaltado de errores, ni descripción de tipos. Vamos a arreglarlo.
Servidor LSP
TreeSitter enseñó a nvim a presentar el código como estamos acostumbrados. Ahora hay que enseñar a nvim a dar sugerencias, resaltar errores y procesar el código de la manera habitual para nosotros. Para ello vamos a usar LSP.
LSP (language server protocol) es un protocolo mediante el cual el cliente (el editor o la IDE) se comunica con el servidor, que da instrucciones al cliente. Por ejemplo, el cliente puede pasarle al servidor una variable después de la cual pusimos un punto, y el servidor, en respuesta, le devolverá al cliente todas las opciones de autocompletado posibles para esa variable. Así, para enseñar al editor a entender el código, necesitamos levantar un servidor que sepa todo sobre ese código y sobre el lenguaje en el que está escrito, y enseñar al editor a comunicarse con ese servidor.
Para cada lenguaje hay su propio servidor LSP, que primero hay que instalar en el sistema y luego, mediante algún driver, conectar al editor. Como driver actuará el plugin https://github.com/neovim/nvim-lspconfig. El plugin tiene una documentación excelente, donde puedes encontrar cómo configurar lsp para muchísimos lenguajes.
En el marco de este artículo configuraré LSP para golang. Para las manipulaciones posteriores doy por hecho que ya tienes go instalado.
Para golang hay varias opciones de servidores LSP; yo voy a usar el más popular - gopls:
brew install goplsAhora creamos el archivo lsp.lua y añadimos allí los siguientes ajustes:
local nvim_lsp = require'lspconfig'
-- Mappings.
-- See `:help vim.diagnostic.*` for documentation on any of the below functions
local opts = { noremap=true, silent=true }
vim.api.nvim_set_keymap('n', '<space>e', '<cmd>lua vim.diagnostic.open_float()<CR>', opts)
vim.api.nvim_set_keymap('n', '[d', '<cmd>lua vim.diagnostic.goto_prev()<CR>', opts)
vim.api.nvim_set_keymap('n', ']d', '<cmd>lua vim.diagnostic.goto_next()<CR>', opts)
vim.api.nvim_set_keymap('n', '<space>q', '<cmd>lua vim.diagnostic.setloclist()<CR>', opts)
-- Use an on_attach function to only map the following keys
-- after the language server attaches to the current buffer
local on_attach = function(client, bufnr)
-- Enable completion triggered by <c-x><c-o>
vim.api.nvim_buf_set_option(bufnr, 'omnifunc', 'v:lua.vim.lsp.omnifunc')
-- Mappings.
-- See `:help vim.lsp.*` for documentation on any of the below functions
vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gD', '<cmd>lua vim.lsp.buf.declaration()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gd', '<cmd>lua vim.lsp.buf.definition()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', 'K', '<cmd>lua vim.lsp.buf.hover()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gi', '<cmd>lua vim.lsp.buf.implementation()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<C-k>', '<cmd>lua vim.lsp.buf.signature_help()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>wa', '<cmd>lua vim.lsp.buf.add_workspace_folder()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>wr', '<cmd>lua vim.lsp.buf.remove_workspace_folder()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>wl', '<cmd>lua print(vim.inspect(vim.lsp.buf.list_workspace_folders()))<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>D', '<cmd>lua vim.lsp.buf.type_definition()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>rn', '<cmd>lua vim.lsp.buf.rename()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>ca', '<cmd>lua vim.lsp.buf.code_action()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gr', '<cmd>lua vim.lsp.buf.references()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>f', '<cmd>lua vim.lsp.buf.formatting()<CR>', opts)
end
-- golang
nvim_lsp.gopls.setup ({
on_attach = on_attach,
flags = {
-- This will be the default in neovim 0.7+
debounce_text_changes = 150,
},
capabilities = {
workspace = {
didChangeWatchedFiles = {
dynamicRegistration = true,
},
},
}
})
vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(
vim.lsp.diagnostic.on_publish_diagnostics, {
virtual_text = false
}
)La sección con el mapeo y la inicialización del servidor son estándar, tomadas de los ejemplos de configuración del repositorio nvim-lspconfig.
init.lua:
Plug('neovim/nvim-lspconfig')
...
vim.call('plug#end')
...
require"lsp"Reiniciamos, instalamos el plugin con :PlugInstall, reiniciamos una vez más y abrimos un ejemplo de código go. Aquí es importante que gopls, por defecto, está configurado de modo que reconoce el código solo si en la raíz del proyecto hay un go.mod. Otro punto importante: comprueba que go esté añadido al PATH del shell que usas. En mi caso es zsh, y en .zshrc tengo escrito:
export GOPATH=$HOME/golang
export GOROOT=/usr/local/opt/go/libexec
export GOBIN=$GOPATH/bin
export PATH=$PATH:$GOPATH
export PATH=$PATH:$GOROOT/binAhora, habiendo abierto código go y asegurándonos de que no hay errores, ejecutamos :LspInfo:

Vemos que tenemos un cliente lsp activo, lo que significa que nvim se ha conectado correctamente al servidor lsp. Aunque nvim ya sabe trabajar con el autocompletado, necesitamos instalar una serie de plugins para poder ver ese trabajo. Para visualizarlo voy a usar un conjunto de plugins de https://github.com/hrsh7th. Creamos el archivo cmp.lua e insertamos allí la siguiente configuración:
vim.o.completeopt="menu,menuone,noselect"
-- Setup nvim-cmp.
local cmp = require'cmp'
cmp.setup({
snippet = {
-- REQUIRED - you must specify a snippet engine
expand = function(args)
vim.fn["vsnip#anonymous"](args.body) -- For `vsnip` users.
-- require('luasnip').lsp_expand(args.body) -- For `luasnip` users.
-- require('snippy').expand_snippet(args.body) -- For `snippy` users.
-- vim.fn["UltiSnips#Anon"](args.body) -- For `ultisnips` users.
end,
},
window = {
-- completion = cmp.config.window.bordered(),
-- documentation = cmp.config.window.bordered(),
},
mapping = cmp.mapping.preset.insert({
['<C-b>'] = cmp.mapping.scroll_docs(-4),
['<C-f>'] = cmp.mapping.scroll_docs(4),
['<C-Space>'] = cmp.mapping.complete(),
['<C-e>'] = cmp.mapping.abort(),
['<CR>'] = cmp.mapping.confirm({ select = true }),
['<Tab>'] = cmp.mapping(function(fallback)
local col = vim.fn.col('.') - 1
if cmp.visible() then
cmp.select_next_item(select_opts)
elseif col == 0 or vim.fn.getline('.'):sub(col, col):match('%s') then
fallback()
else
cmp.complete()
end
end, {'i', 's'}),
['<S-Tab>'] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_prev_item(select_opts)
else
fallback()
end
end, {'i', 's'}),
}),
sources = cmp.config.sources({
{ name = 'nvim_lsp' },
{ name = 'vsnip' }, -- For vsnip users.
{ name = 'nvim_lsp_signature_help' },
}, {
{ name = 'buffer' },
})
})
-- Set configuration for specific filetype.
cmp.setup.filetype('gitcommit', {
sources = cmp.config.sources({
{ name = 'cmp_git' }, -- You can specify the `cmp_git` source if you were installed it.
}, {
{ name = 'buffer' },
})
})
-- Use buffer source for `/` (if you enabled `native_menu`, this won't work anymore).
cmp.setup.cmdline('/', {
mapping = cmp.mapping.preset.cmdline(),
sources = {
{ name = 'buffer' }
}
})
-- Use cmdline & path source for ':' (if you enabled `native_menu`, this won't work anymore).
cmp.setup.cmdline(':', {
mapping = cmp.mapping.preset.cmdline(),
sources = cmp.config.sources({
{ name = 'path' }
}, {
{ name = 'cmdline' }
})
})Ahora conectamos los plugins y la configuración en init.lua:
Plug('hrsh7th/cmp-buffer')
Plug('hrsh7th/cmp-path')
Plug('hrsh7th/cmp-cmdline')
Plug('hrsh7th/nvim-cmp')
Plug('hrsh7th/cmp-nvim-lsp' )
Plug('hrsh7th/cmp-nvim-lsp-signature-help')
vim.call('plug#end')
...
require"cmp_config"Instalamos con :PlugInstall y comprobamos:

En esta etapa hemos obtenido un editor que por fuera recuerda al habitual estilo de una IDE y que sabe trabajar con el código en el formato al que estamos acostumbrados. Creo que se puede cerrar lógicamente el primer artículo en este punto, para que no se alargue hasta dimensiones ya del todo indecentes. En las próximas entregas configuraremos la habitual búsqueda por archivos y por coincidencias, configuraremos un mapeo cómodo para la mayoría de las acciones y añadiré otros 5-10 plugins que simplifican mucho el desarrollo del día a día.
El código completo de la configuración se puede encontrar aquí: https://github.com/itxor/go-rust-nvim-config/tree/master
La antigua variante de la configuración en formato nvim (la configuración funciona, pero está desactualizada y tiene muchas diferencias con la versión actual en lua): https://github.com/itxor/go-rust-nvim-config/tree/old_master