Guía de uso del cliente LSP de Neovim

Última actualización: 2025-05-23

El título original para este post era "Cómo agregar funcionalidades como las de un IDE a Neovim sin instalar plugins de terceros." Suena más interesante pero tiene demasiadas palabras. Aún así, es básicamente lo que quiero enseñarles.

En esta ocasión quiero explicar cómo configurar un servidor LSP en Neovim v0.11. Y voy a mostrarles cómo funciona porque este nuevo método es simplemente una abstracción sobre funcionalidades que han estado en Neovim durante mucho tiempo. Así podrán construir su propia configuración incluso en versiones anteriores de Neovim.

Requerimientos

  1. Neovim en su versión v0.8 o mayor
  2. Un servidor LSP
  3. Paciencia/Energía para escribir un poco de lua por cada servidor LSP que se quiere configurar

Si no conocen el lenguaje lua aquí les dejo un par de recursos donde pueden aprender lo básico:

Todo comienza con el servidor LSP

Un servidor LSP es un programa externo que sigue el Protocolo de Servidor de Lenguajes (Language Server Protocol). En este se definen los parámetros que puede recibir un servidor LSP, y también cómo debe responder. El propósito de todo esto es que cualquier herramienta que implemente el protocolo pueda comunicarse con un servidor LSP.

Entonces, el servidor LSP es el programa que va a analizar nuestro código y le dice a nuestro editor qué debe hacer.

¿Qué servidores LSP tenemos disponibles?

La página web del protocolo tiene una lista de servidores.

En este caso en particular...

Quiero usar Gleam y LuaLS como ejemplos prácticos.

Gleam en realidad es un conjunto de herramientas. Incluye un compilador, un formateador de código y un servidor LSP. Las instrucciones de instalación se encuentran en la documentación oficial: Getting started.

LuaLS es un servidor LSP para el lenguaje lua. Pueden descargar la versión más reciente desde su repositorio, en la sección github releases. O, pueden compilarlo con el código fuente.

Una vez que tienen un servidor LSP instalado recomiendo verificar si Neovim puede encontrar el ejecutable. Para esto pueden usar la función exepath.

El ejecutable de Gleam es gleam.

:echo exepath('gleam')

Y el de LuaLS es lua-language-server.

:echo exepath('lua-language-server')

Estos comandos deben mostrar la ubicación del ejecutable en su sistema. De no ser así, significa que algo salió mal durante la instalación del servidor LSP.

Uso básico

Antes de empezar a escribir código deberíamos aprender cómo usar el servidor LSP. Esta información debería estar en la documentación oficial del servidor que queremos usar.

Si no podemos encontrar instrucciones de uso básico en la documentación, podemos ir al repositorio de nvim-lspconfig y buscamos el directorio lsp. Ahí podemos encontrar archivos de configuración para un montón de servidores.

Justo ahora estamos interesados en Gleam, entonces deberíamos revisar el contenido de gleam.lua.

return {
    cmd = { 'gleam', 'lsp' },
    filetypes = { 'gleam' },
    root_markers = { 'gleam.toml', '.git' },
}

Aquí tenemos la información esencial que necesitamos para poder integrar un servidor LSP en Neovim.

La propiedad cmd contiene el comando que necesitamos para inicializar el servidor LSP. Recuerden que un servidor LSP es un programa externo. Quiere decir que gleam lsp es un comando válido que podemos ejecutar manualmente en nuestra terminal. Y curiosamente, si hacemos eso nos mostrará este mensaje.

This command is intended to be run by language server clients such
as a text editor rather than being run directly in the console.

El mensaje dice "este comando fue creado para ser ejecutado por un cliente LSP como por ejemplo un editor de texto, en lugar de ser ejecutado directamente por cónsola."

En la propiedad filetypes tenemos la lista de lenguajes que el servidor LSP soporta. Deben tener en cuenta que cada nombre debe ser un "filetype" válido en Neovim.

En root_markers tenemos una lista de archivos y directorios. Esta información se usa para determinar la "raíz" de nuestro proyecto. Es decir el directorio principal del proyecto.

El directorio lsp

A partir de la versión v0.11 el directorio lsp forma parte de lo que llamamos runtimepath. Quiere decir que tenemos la posibilidad de crear nuestro propio directorio lsp. Ahí podemos colocar la configuración de los servidores LSP que tenemos instalados, sin necesidad de instalar nvim-lspconfig como un plugin.

Imaginen por un momento que nuestra configuración personal de Neovim tiene esta estructura:

nvim
├── init.vim
├── .nvim.lua
└── lsp
    ├── gleam.lua
    └── luals-nvim.lua

La configuración dentro de nvim/lsp/gleam.lua puede ser exactamente igual a la que tiene nvim-lspconfig.

-- nvim/lsp/gleam.lua

return {
  cmd = {'gleam', 'lsp'},
  filetypes = {'gleam'},
  root_markers = {'gleam.toml', '.git'},
}

Ahora bien, cuando configuramos un servidor LSP tenemos ciertas libertades. Entonces, en luals-nvim voy a mostrar una configuración de LuaLS específicamente creada para Neovim donde hay un script llamado .nvim.lua en el directorio raíz.

-- nvim/lsp/luals-nvim.lua

return {
  cmd = {'lua-language-server'},
  filetypes = {'lua'},
  root_markers = {'.nvim.lua'},
  settings = {
    Lua = {
      runtime = {
        version = 'LuaJIT',
        path = {'lua/?.lua', 'lua/?/init.lua'},
      },
      diagnostics = {
        globals = {'vim'},
      },
      telemetry = {
        enable = false,
      },
      workspace = {
        checkThirdParty = false,
        library = {
          vim.env.VIMRUNTIME,
        },
      },
    },
  },
}

Dentro del directorio lsp los archivos que creamos pueden tener cualquier nombre. Por eso elegí luals-nvim en lugar de algo genérico como lua o luals. Y la configuración que "retornamos" en el script debe contener los mismos parámetros que vim.lsp.config() espera.

Noten que en este ejemplo tenemos una propiedad llamada settings. Esa propiedad está reservada para opciones específicas del servidor LSP. Quiere decir que en Neovim no disponemos de una documentación que nos diga cuáles son las opciones que podemos colocar. Debemos ir a la documentación del servidor LSP y ver qué opciones tenemos disponibles.

Deben saber que los archivos dentro del directorio lsp no son ejecutados automáticamente. Debemos decirle a Neovim qué servidores LSP queremos usar. Para usar un servidor LSP debemos ejecutar la función vim.lsp.enable().

En este ejemplo el archivo init.vim puede tener este código:

" nvim/init.vim

set exrc

lua vim.lsp.enable('gleam')

¿Por qué usar init.vim?

Simplemente quería una excusa para mostrar que se puede ejecutar una función de lua en vimscript. He notado que algunos usuarios de Vim creen que para usar lua deben eliminar toda la configuración que tienen en vimscript. No es así. En vimscript podemos usar el comando lua para ejecutar una línea de código escrita en lua.

En fin, al iniciar Neovim este buscará un archivo en el runtimepath que coincida con el patrón lsp/gleam.lua. Luego creará un autocomando usando la lista de lenguajes que suministramos en filetypes. Entonces si abrimos un archivo de tipo gleam Neovim intentará inicializar el servidor LSP.

¿Y qué pasa con LuaLS?

La configuración que tenemos para LuaLS sólo es útil para Neovim, lo que nos da una oportunidad para usar una configuración local. En Vim logramos esto usando la opción exrc. Cuando habilitamos los archivos exrc Vim/Neovim ejecutará un script que esté en directorio de trabajo actual. En nuestro ejemplo sería .nvim.lua. Esto es una espada de doble filo. Por eso en Neovim la opción exrc tiene un comportamiento diferente. Cuando se detecta una configuración local Neovim nos preguntará si "confiamos" en el archivo, y si aceptamos se ejecutará. La próxima vez que se detecte la misma configuración local se ejecutará automáticamente si el contenido del archivo no ha cambiado.

Yo recomendaría crear un alias para habilitar la opción exrc desde la terminal, en lugar de colocarlo directo en su configuración personal. Algo como esto.

alias code='nvim --cmd "set exrc"'

De vuelta a LuaLS. Luego de Neovim termine de ejecutar el archivo init.vim (o init.lua) buscará el script .nvim.lua en el directorio actual. El que está en nuestra configuración tendrá este código.

-- nvim/.nvim.lua

vim.lsp.enable('luals-nvim')

De esta manera Neovim no intentará usar lsp/luals-nvim.lua si no lo abrimos en el directorio donde está nuestra configuración personal. La desventaja de este método es que debemos abrir Neovim exactamente donde está .nvim.lua.

¿Se puede configurar todo en un archivo?

Si ustedes son la clase de persona que prefiere una configuración simple donde todo está en un sólo archivo, les tengo buenas noticias. No están obligados a usar el directorio lsp.

La función vim.lsp.config() puede usarse para extender las configuraciones que se encuentran en el directorio lsp. Pero también la podemos usar para crear configuraciones nuevas, sin necesidad de crear un archivo aparte.

Por ejemplo:

-- nvim/init.lua

vim.lsp.config('gleam', {
  cmd = {'gleam', 'lsp'},
  filetypes = {'gleam'},
  root_markers = {'gleam.toml', '.git'},
})

vim.lsp.enable('gleam')

¿Y si algo sale mal?

Si Neovim no fue capaz de iniciar el proceso del servidor LSP pueden revisar el log. Ejecuten este comando en Neovim:

:lua vim.cmd.edit(vim.lsp.get_log_path())

Cuando algo sale mal habrá unas líneas que comienzan con [ERROR]. Tal vez ahí pueden encontrar alguna información útil.

Si quieren un log con más detalles pueden ejecutar esta función.

vim.lsp.set_log_level('debug')

Y desde Neovim v0.10 pueden hacer un "chequeo de salud" usando este comando:

:checkhealth lsp

Filetypes

En versiones anteriores de Neovim no tenemos disponible vim.lsp.enable(), pero podemos replicar su funcionalidad. Ya sabemos lo que necesitamos sólo nos falta juntar las piezas.

Si el servidor LSP que queremos usar sólo soporta un lenguaje de programación entonces podemos crear un "filetype plugin."

Para crear un filetype plugin debemos ir a nuestra configuración de Neovim, y ahí debemos crear un directorio llamado ftplugin. Podemos ejecutar este comando en Neovim.

:call mkdir('./ftplugin', 'p')

Luego debemos crear un script con el mismo nombre de un tipo de archivo.

Si queremos crear un filetype plugin para Gleam debemos crear un archivo llamado gleam.lua.

:edit ftplugin/gleam.lua | write

En este script vamos a ejecutar la función que "conectará" el servidor LSP con Neovim.

Por otro lado, si el servidor LSP soporta varios tipos de archivos tenemos la opción de crear un autocomando en el evento FileType. Algo como esto.

vim.api.nvim_create_autocmd('FileType', {
  pattern = {'css', 'less', 'sass'},
  callback = function()
    ---
    -- Aquí podemos habilitar el servidor LSP
    ---
  end,
})

Aquí la propiedad pattern es donde debemos colocar los tipos de archivos que soporta el servidor LSP. Esta es la misma información que colocamos en la propiedad filetypes cuando configuramos un servidor LSP en Neovim v0.11.

Dentro del filetype plugin o el autocomando debemos ejecutar la función que va a inicializar el servidor LSP. Pero primero, debemos conocer la lógica detrás de root_markers.

Directorio raíz

Un servidor LSP necesita saber la ruta de nuestro proyecto. La ubicación exacta dentro de nuestro sistema. Este es un problema que debemos resolver en Neovim.

En Neovim v0.10 podemos usar una función llamada vim.fs.root(). A esta función le vamos a dar una lista de nombres de archivos y nos devolverá una ruta que podemos usar.

¿Pero qué vamos a buscar? Archivos de configuración, los que usualmente colocamos en la raíz de un proyecto. En proyectos de Gleam siempre hay un archivo gleam.toml. En proyectos de javascript usualmente hay un package.json. En rust está el cargo.toml. Este es el tipo de información que vamos a darle a vim.fs.root() para que nos devuelva la ruta correcta.

Entonces en el filetype plugin de Gleam podemos hacer una prueba con vim.fs.root().

-- nvim/ftplugin/gleam.lua
-- NOTA: vim.fs.root() sólo está disponble en Neovim v0.10 o mayor

local root_markers = {'gleam.toml'}
local root_dir = vim.fs.root(0, root_markers)

print(root_dir)

En Neovim v0.9 o menor debemos recrear el comportamiento de vim.fs.root(). En este caso podemos usar vim.fs.find().

-- nvim/ftplugin/gleam.lua
-- NOTA: Este código es para Neovim v0.9.5 o menor

local root_markers = {'gleam.toml'}
local buffer = vim.api.nvim_buf_get_name(0)
local paths = vim.fs.find(root_markers, {
    upward = true,
    path = vim.fn.fnamemodify(buffer, ':p:h'),
})

local root_dir = vim.fs.dirname(paths[1])

print(root_dir)

Creando el cliente

Estamos listos para conectar Neovim con el servidor LSP. Ahora debemos usar la función vim.lsp.start() para inicializar el servidor LSP. Esta es la misma función que vim.lsp.enable() ejecuta por nosotros.

Esto es lo que deben saber, la primera vez que ejecutamos vim.lsp.start() Neovim iniciará nuestro servidor LSP como un proceso externo. Cuando se ejecuta nuevamente Neovim simplemente le enviará información al proceso del servidor LSP.

Entonces, el filetype plugin para Gleam debería tener este código:

-- nvim/ftplugin/gleam.lua
-- NOTA: vim.fs.root() sólo está disponble en Neovim v0.10 o mayor

local root_markers = {'gleam.toml'}
local root_dir = vim.fs.root(0, root_markers)

if root_dir then
  vim.lsp.start({
    cmd = {'gleam', 'lsp'},
    root_dir = root_dir,
  })
end

Con esta configuración ya podremos utilizar algunas funcionalidades. Por ejemplo, si editamos un archivo Gleam y este tiene un error, Neovim nos dirá en qué línea se encuentra el error.

Diagnostics

"Diagnostic" es el término que se usa en Neovim para los errores que se encuentran en nuestro código fuente.

Un diagnóstico puede tener un signo al inicio de la línea para indicarnos que hay un error. Este es el diagnostic sign. El espacio que necesita este signo está escondido por defecto, y cuando Neovim va a mostrarlo toda la pantalla se mueve a la derecha. Este comportamiento puede configurarse.

Si configuramos la opción signcolumn con la cadena de texto yes Neovim reserva el espacio para el signo. Tendremos un espacio en blanco al inicio de la línea en todo momento.

En init.vim pueden configurarlo de esta manera.

set signcolumn=yes

Si usan init.lua.

vim.o.signcolumn = 'yes'

Por otro lado si asignamos la cadena de texto no, Neovim no mostrará ningún signo. No recomiendo esta opción, especialmente si no están al tanto de las consecuencias. Si quieren esconder los signos de diagnósticos hay una alternativa mejor.

vim.diagnostic

En lua tenemos acceso al módulo vim.diagnostic, y dentro de este módulo está una función llamada .config(). Si con esta función podremos configurar el comportamiento de los diagnósticos.

Esta es la forma segura de deshabilitar los signos de los diagnósticos.

vim.diagnostic.config({
  signs = false,
})

Tenemos la opción de mostrar un fragmento del mensaje de error al final de la línea. A esto se le conoce como "virtual text." En Neovim v0.11 está deshabilitado por defecto. Así lo podemos habilitar.

vim.diagnostic.config({
  virtual_text = false,
})

Si tienen Neovim v0.10 o mayor pueden leer el diagnóstico completo usando <C-w>d (control + w luego d). Este atajo ejecuta la función vim.diagnostic.open_float().

Si están usando Neovim v0.9.5 o menor tendrán que crear un atajo de teclado.

vim.keymap.set('n', '<C-w>d', '<cmd>lua vim.diagnostic.open_float()<cr>')

Ahora bien, me gustaría poder explicarles con más detalle cada opción en vim.diagnostic.config() pero no tenemos tiempo. Si quieren explorar las demás opciones pueden leer la documentación.

Funcionalidades gratis

Hablemos de las funcionalidades que Neovim nos ofrece con la configuración que tenemos hasta ahora, que es básicamente lo mínimo.

Cuando un servidor LSP está activo en un archivo Neovim habilita ciertas funcionalidades.

Desde Neovim v0.8

Desde Neovim v0.9

Desde Neovim v0.10

Desde Neovim v0.11

Atajos

En Neovim v0.11 ya tenemos atajos asignados para las funcionalidades que ofrece el editor. Pero en versiones anteriores debemos crear estos atajos nosotros mismos.

Este es el código que necesitamos en lua.

-- Pueden agregar esto en su archivo `init.lua`

-- Estos atajos ya forman parte de Neovim v0.10
vim.keymap.set('n', '[d', '<cmd>lua vim.diagnostic.goto_prev()<cr>')
vim.keymap.set('n', ']d', '<cmd>lua vim.diagnostic.goto_next()<cr>')
vim.keymap.set('n', '<C-w>d', '<cmd>lua vim.diagnostic.open_float()<cr>')
vim.keymap.set('n', '<C-w><C-d>', '<cmd>lua vim.diagnostic.open_float()<cr>')

vim.api.nvim_create_autocmd('LspAttach', {
  callback = function(event)
    local bufmap = function(mode, rhs, lhs)
      vim.keymap.set(mode, rhs, lhs, {buffer = event.buf})
    end

    -- Estos atajos ya forman parte de Neovim v0.11
    bufmap('n', 'K', '<cmd>lua vim.lsp.buf.hover()<cr>')
    bufmap('n', 'grr', '<cmd>lua vim.lsp.buf.references()<cr>')
    bufmap('n', 'gri', '<cmd>lua vim.lsp.buf.implementation()<cr>')
    bufmap('n', 'grn', '<cmd>lua vim.lsp.buf.rename()<cr>')
    bufmap('n', 'gra', '<cmd>lua vim.lsp.buf.code_action()<cr>')
    bufmap('n', 'gO', '<cmd>lua vim.lsp.buf.document_symbol()<cr>')
    bufmap({'i', 's'}, '<C-s>', '<cmd>lua vim.lsp.buf.signature_help()<cr>')

    -- Estos son atajos extras
    bufmap('n', 'gd', '<cmd>lua vim.lsp.buf.definition()<cr>')
    bufmap('n', 'grt', '<cmd>lua vim.lsp.buf.type_definition()<cr>')
    bufmap('n', 'grd', '<cmd>lua vim.lsp.buf.declaration()<cr>')
    bufmap({'n', 'x'}, 'gq', '<cmd>lua vim.lsp.buf.format({async = true})<cr>')
  end,
})

Y este sería el equivalente en vimscript.

" Pueden agregar esto en su archivo `init.vim`

" Estos atajos ya forman parte de Neovim v0.10
nnoremap [d <cmd>lua vim.diagnostic.goto_prev()<cr>
nnoremap ]d <cmd>lua vim.diagnostic.goto_next()<cr>
nnoremap <C-w>d <cmd>lua vim.diagnostic.open_float()<cr>
nnoremap <C-w><C-d> <cmd>lua vim.diagnostic.open_float()<cr>

function! LspAttached() abort
  " Estos atajos ya forman parte de Neovim v0.11
  nnoremap <buffer> K <cmd>lua vim.lsp.buf.hover()<cr>
  nnoremap <buffer> grr <cmd>lua vim.lsp.buf.references()<cr>
  nnoremap <buffer> gri <cmd>lua vim.lsp.buf.implementation()<cr>
  nnoremap <buffer> grn <cmd>lua vim.lsp.buf.rename()<cr>
  nnoremap <buffer> gra <cmd>lua vim.lsp.buf.code_action()<cr>
  nnoremap <buffer> gO <cmd>lua vim.lsp.buf.document_symbol()<cr>
  inoremap <buffer> <C-s> <cmd>lua vim.lsp.buf.signature_help()<cr>
  snoremap <buffer> <C-s> <cmd>lua vim.lsp.buf.signature_help()<cr>

  " Estos son atajos extras
  nnoremap <buffer> gd <cmd>lua vim.lsp.buf.definition()<cr>
  nnoremap <buffer> gq <cmd>lua vim.lsp.buf.format({async = true})<cr>
  xnoremap <buffer> gq <cmd>lua vim.lsp.buf.format({async = true})<cr>
  nnoremap <buffer> grd <cmd>lua vim.lsp.buf.declaration()<cr>
  nnoremap <buffer> grt <cmd>lua vim.lsp.buf.type_definition()<cr>
endfunction

autocmd LspAttach * call LspAttached()

Advertencia

No todos los servidores implementan el protocolo LSP en su totalidad. Y no todas las funcionalidades son consistentes entre servidores.

Por ejemplo, Gleam puede mostrar errores en nuestro código en tiempo real, sin necesidad de guardar los cambios en el archivo. Pero rust-analyzer, el servidor LSP para rust, sólo puede mostrar errores una vez que se guardan cambios en el archivo.

Otro ejemplo. ruff-lsp, un servidor LSP para python, en su descripción menciona que su especialidad es reportar errores y formatear código. Hasta donde pude ver este servidor no ofrece completado de código o resaltado de código semántico.

Lo que intento decir es esto: lean la documentación del servidor LSP para que estén informados de las funcionalidades que ofrece.

Contenido extra

A estas alturas me atrevería a decir que ya tienen el conocimento esencial para usar el cliente LSP de Neovim y ser productivos. Lo que sigue son consejos, ajustes y funcionalidades que pueden implementar agregando un bloque de código a su configuración personal.

Formatear al guardar

Lo que haremos aquí es ejecutar la función vim.lsp.buf.format() antes de que Neovim guarde los cambios a un archivo. Y por supuesto, esto sólo lo haremos cuando hay un servidor LSP activo.

Importante: los servidores LSP con capacidades para formatear código tienen su propia configuración. Por ejemplo, puede que nosotros tengamos la indentación configurada a 2 espacios pero el servidor LSP puede formatear el código con 4 espacios. Entonces, siempre es buena idea indagar en la documentación del servidor LSP para saber cómo configurar el estilo del formateo.

-- Pueden agregar esto en su archivo `init.lua`

local fmt_group = vim.api.nvim_create_augroup('autoformat_cmds', {clear = true})

local function setup_autoformat(event)
  local id = vim.tbl_get(event, 'data', 'client_id')
  local client = id and vim.lsp.get_client_by_id(id)
  if client == nil then
    return
  end

  vim.api.nvim_clear_autocmds({group = fmt_group, buffer = event.buf})

  local buf_format = function(e)
    vim.lsp.buf.format({
      bufnr = e.buf,
      async = false,
      timeout_ms = 10000,
    })
  end

  vim.api.nvim_create_autocmd('BufWritePre', {
    buffer = event.buf,
    group = fmt_group,
    desc = 'Formatear archivo',
    callback = buf_format,
  })
end

vim.api.nvim_create_autocmd('LspAttach', {
  desc = 'Configurar formatear al guardar',
  callback = setup_autoformat,
})

Cambiar texto de signos de diagnósticos

Si estamos usando Neovim en su versión v0.9.5 o menor, debemos usar la función vim.fn.sign_define().

-- Pueden agregar esto en su archivo `init.lua`

local function sign_define(args)
  vim.fn.sign_define(args.name, {
    texthl = args.name,
    text = args.text,
    numhl = ''
  })
end

sign_define({name = 'DiagnosticSignError', text = ''})
sign_define({name = 'DiagnosticSignWarn', text = ''})
sign_define({name = 'DiagnosticSignHint', text = ''})
sign_define({name = 'DiagnosticSignInfo', text = '»'})

Si estamos usando Neovim en su versión v0.10 o mayor, debemos usar la función vim.diagnostic.config().

-- Pueden agregar esto en su archivo `init.lua`

vim.diagnostic.config({
  signs = {
    text = {
      [vim.diagnostic.severity.ERROR] = '',
      [vim.diagnostic.severity.WARN] = '',
      [vim.diagnostic.severity.HINT] = '',
      [vim.diagnostic.severity.INFO] = '»',
    },
  },
})

Deshabilitar resaltado semántico

La documentación de Neovim sugiere modificar la instancia del cliente LSP. Debemos "eliminar" la propiedad que activa el resaltado semántico.

-- Pueden agregar esto en su archivo `init.lua`

vim.api.nvim_create_autocmd('LspAttach', {
  desc = 'Deshabilitar resaltado semántico',
  callback = function(event)
    local id = vim.tbl_get(event, 'data', 'client_id')
    local client = id and vim.lsp.get_client_by_id(id)
    if client == nil then
      return
    end

    client.server_capabilities.semanticTokensProvider = nil
  end,
})

Resaltar símbolo debajo del cursor

Para esto vamos a usar la función vim.lsp.buf.document_highlight(). Cuando el cursor pase una cantidad de tiempo sobre un símbolo vamos a ejecutar esa función. Y luego vamos desactivar el resaltado cuando el cursor se mueva de lugar.

Para que esto funcione de manera apropiada el tema del editor debe soportar los siguientes grupos:

Si el tema que tenemos no define ningún color para esos grupos podemos hacer un "vinculo" con un grupo que ya existe. Este es un ejemplo que muestra cómo vincular al grupo Search.

vim.api.nvim_set_hl(0, 'LspReferenceRead', {link = 'Search'})
vim.api.nvim_set_hl(0, 'LspReferenceText', {link = 'Search'})
vim.api.nvim_set_hl(0, 'LspReferenceWrite', {link = 'Search'})
-- Pueden agregar esto en su archivo `init.lua`

-- tiempo en milisegundos para emitir el event `CursorHold`
vim.opt.updatetime = 400

local function highlight_symbol(event)
  local id = vim.tbl_get(event, 'data', 'client_id')
  local client = id and vim.lsp.get_client_by_id(id)
  if client == nil or not client.supports_method('textDocument/documentHighlight') then
    return
  end

  local group = vim.api.nvim_create_augroup('highlight_symbol', {clear = false})

  vim.api.nvim_clear_autocmds({buffer = event.buf, group = group})

  vim.api.nvim_create_autocmd({'CursorHold', 'CursorHoldI'}, {
    group = group,
    buffer = event.buf,
    callback = vim.lsp.buf.document_highlight,
  })

  vim.api.nvim_create_autocmd({'CursorMoved', 'CursorMovedI'}, {
    group = group,
    buffer = event.buf,
    callback = vim.lsp.buf.clear_references,
  })
end

vim.api.nvim_create_autocmd('LspAttach', {
  desc = 'Resaltar símbolo debajo del cursor',
  callback = highlight_symbol,
})

Tab para completar

Para mantener una implementación simple vamos a hacer lo siguiente: Si el menú de sugerencias es visible, usaremos Tab y Shift + Tab para navegar entre las sugerencias. Si el cursor está sobre un espacio en blanco, insertaremos el caracter <Tab>. Si no, mostraremos el menú de sugerencias.

Ahora bien, si tenemos un servidor LSP activo que puede proveer sugerencias usamos eso. De lo contrario le pediremos a Neovim que sugiera palabras que se encuentran en el archivo actual.

Nota: recuerden que pueden confirmar una sugerencia usando Enter o <C-y> (Control + y).

-- Pueden agregar esto en su archivo `init.lua`

vim.opt.completeopt = {'menu', 'menuone', 'noselect', 'noinsert'}
vim.opt.shortmess:append('c')

local function tab_complete()
  if vim.fn.pumvisible() == 1 then
    -- navega al siguiente elemento en la lista
    return '<Down>'
  end

  local c = vim.fn.col('.') - 1
  local is_whitespace = c == 0 or vim.fn.getline('.'):sub(c, c):match('%s')

  if is_whitespace then
    -- inserta el caracter tab
    return '<Tab>'
  end

  local lsp_completion = vim.bo.omnifunc == 'v:lua.vim.lsp.omnifunc'

  if lsp_completion then
    -- activa el completado con el servidor LSP
    return '<C-x><C-o>'
  end

  -- sugiere palabras en el archivo actual
  return '<C-x><C-n>'
end

local function tab_prev()
  if vim.fn.pumvisible() == 1 then
    -- navega al elemento anterior en la lista
    return '<Up>'
  end

  -- inserta el caracter tab
  return '<Tab>'
end

vim.keymap.set('i', '<Tab>', tab_complete, {expr = true})
vim.keymap.set('i', '<S-Tab>', tab_prev, {expr = true})

Expandir snippets

En Neovim v0.11 tenemos acceso a un módulo llamado vim.lsp.completion, con esto podremos extender las funcionalidades del mecanismo de completado de código. Para ser más específico, Neovim podrá responder a más "comandos de edición" que retorne un servidor LSP. Por ejemplo, podremos expandir snippets de código.

Por el momento las funcionalidades de vim.lsp.completion son opcionales, debemos usar la función vim.lsp.completion.enable() para habilitarlas.

-- Pueden agregar esto en su archivo `init.lua`

vim.opt.completeopt = {'menu', 'menuone', 'noinsert', 'noselect'}

vim.api.nvim_create_autocmd('LspAttach', {
  desc = 'Habilitar vim.lsp.completion',
  callback = function(event)
    local client_id = vim.tbl_get(event, 'data', 'client_id')
    if client_id == nil then
      return
    end

    vim.lsp.completion.enable(true, client_id, event.buf, {autotrigger = false})

    -- Activar el menú de sugerencias manualmente
    vim.keymap.set('i', '<C-Space>', '<cmd>lua vim.lsp.completion.trigger()<cr>')
  end
})

Noten que el último argumento en .enable() tiene la propiedad autotrigger. false es el valor por defecto entonces yo lo dejo así. Si cambian ese valor a true Neovim podrá activar el completado de código cuando encuentre un "trigger character". Por ejemplo, con el servidor LSP de lua Neovim mostrará sugerencias cuando se encuentre con el caracter . o :.

Habilitar "inlay hints"

Inlay hints es parecido al texto virtual. Estos son fragmento de texto que Neovim agrega a lo que estamos editando, pero que en realidad no es parte del archivo. En este caso se usa para mostrar el tipo/clase de una variable o argumento de una función.

En este caso debemos usar la función vim.lsp.inlay_hint.enable() en el archivo donde queremos habilitar esta funcionalidad. Como siempre, podemos usar un autocomando con el evento LspAttach.

Nota: la mayoría de los servidores que tienen soporte para esta funcionalidad la deshabilitan por defecto. Esto quiere decir que activar los inlay hints en Neovim no es suficiente, también debemos buscar las opciones específicas del servidor LSP y configurarlas en la función vim.lsp.start().

-- Pueden agregar esto en su archivo `init.lua`

vim.api.nvim_create_autocmd('LspAttach', {
  desc = 'Habilitar inlay hints',
  callback = function(event)
    local id = vim.tbl_get(event, 'data', 'client_id')
    local client = id and vim.lsp.get_client_by_id(id)
    if client == nil or not client.supports_method('textDocument/inlayHint') then
      return
    end

    vim.lsp.inlay_hint.enable(true, {bufnr = event.buf})
  end,
})

Conclusión

Espero haber demostrado que no es tan difícil usar un servidor LSP en Neovim. La parte que realmente requiere esfuerzo es aprender todo el contexto necesario. ¿Qué significa LSP? ¿Qué es un servidor LSP? ¿Filetype plugin? Pero una vez que tienen conocimiento de todas las piezas involucradas todo se hace más fácil.

Una cosa más... por favor no ignoren el "conocimento básico." Dediquen un poco de tiempo para aprender la sintaxis de lua. Aprendan sobre el comando :help en Neovim. Investiguen un poco sobre la API de lua que ofrece Neovim. Les aconsejo leer la guía oficial de lua que está en la documentación de Neovim, es buen punto de partida.


Si tienen alguna pregunta pueden contactarme por las redes sociales:

Gracias por su tiempo. Si este artículo les pareció útil y quieren apoyar mis esfuerzos para crear más contenido, pueden dejar una propina en buy me a coffee ☕.

Buy Me A Coffee