Configuring Language Server Protocol (LSP) in Neovim

Language Server Protocol (LSP) is an open standard created by Microsoft that allows different code editors and IDEs to communicate with language servers. These language servers are specific to programming languages and provide advanced language-aware features such as autocompletion, code navigation, and code diagnostics. With LSP installed directly in your neovim, you can enjoy these awesome features.

In this blog, we will explore how to install the Language Server Protocol (LSP) in neovim and configure neovim for autocompletion.

Prerequisites

Before proceeding with the installation and usage of nvim-cmp in Neovim, please make sure you have the following packages installed on your system:

Packer

Packer is a plugin manager for Neovim. You need Packer to install the nvim-cmp plugin. If you haven’t installed Packer yet, consider installing Packer and then follow the rest of the tutorial.

Node.js

Node.js is required to download and install different LSP servers in neovim. You can install Node.js with the following commands:

For macOS:

brew install node

For Debian/Ubuntu:

sudo apt install npm

For Fedora/RHEL:

sudo dnf install npm

For Arch Linux:

sudo pacman -S npm

Installing LSP Server

Before installing LSP servers in Neovim, let’s take a look at the project structure. Please note that you can still follow this guide even if you have a different project structure. Just be careful when editing the configuration files.

~/.config/
└── nvim
    ├── init.lua
    └── lua
        ├── lsp
        │   ├── mason.lua
        │   ├── handlers.lua
        │   └── settings.lua
        ├── plugins.lua
        ├── .....
        └── other configuration files

To install the necessary plugins for configuring LSP servers, open the plugins.lua file using the following command:

nvim ~/.config/nvim/lua/plugins.lua

If your plugins file has a different name or is located in a different directory, open the file accordingly. After opening the plugins file, add the following lines to install the plugins:

-- Managing & installing LSP servers, linters & formatters
use("williamboman/mason.nvim") -- In charge of managing LSP servers, linters & formatters
use("williamboman/mason-lspconfig.nvim") -- Bridges the gap between mason & lspconfig

-- Configuring LSP servers
use("neovim/nvim-lspconfig") -- Easily configure language servers
use("hrsh7th/cmp-nvim-lsp") -- For autocompletion
use({
  "glepnir/lspsaga.nvim",
  branch = "main",
  requires = {
    { "nvim-tree/nvim-web-devicons" },
    { "nvim-treesitter/nvim-treesitter" },
  },
}) -- Enhanced LSP UIs
use("jose-elias-alvarez/typescript.nvim") -- Additional functionality for TypeScript server (e.g., rename file & update imports)
use("onsails/lspkind.nvim") -- VSCode-like icons for autocompletion

Once you’ve added these lines to the plugins.lua file, save it using the :w command to install the plugins. Alternatively, you can use the following command to install the plugins:

:PackerInstall

Setting Up LSP

According to our project structure, we will keep all LSP Lua configuration files under the lsp directory. To create the lsp directory under the lua folder, run the following command in your terminal:

mkdir -p ~/.config/nvim/lua/lsp

Then we need three Lua configuration files for configuring the LSP server. The first and most important configuration is for Mason to install the LSP servers to Neovim. In the previous section, we installed the Mason Neovim plugin, and now it’s time to install your preferred LSP server.

Installed LSP servers

To start configuring the LSP server, run the following command to create/edit the mason.lua configuration file:

nvim ~/.config/nvim/lua/lsp/mason.lua

Then add the following lines to the file:

local servers = {
  "lua_ls",
  "cssls",
  "html",
  "tsserver",
  "pyright",
  "bashls",
  "jsonls",
  "yamlls",
}

local settings = {
  ui = {
    border = "none",
    icons = {
      package_installed = "",
      package_pending = "",
      package_uninstalled = "",
    },
  },
  log_level = vim.log.levels.INFO,
  max_concurrent_installers = 4,
}

require("mason").setup(settings)
require("mason-lspconfig").setup({
  ensure_installed = servers,
  automatic_installation = true,
})

local lspconfig_status_ok, lspconfig = pcall(require, "lspconfig")
if not lspconfig_status_ok then
  return
end

local opts = {}

for _, server in pairs(servers) do
  opts = {
    on_attach = require("lsp.handlers").on_attach,
    capabilities = require("lsp.handlers").capabilities,
  }

  server = vim.split(server, "@")[1]

  local require_ok, conf_opts = pcall(require, "lsp.settings." .. server)
  if require_ok then
    opts = vim.tbl_deep_extend("force", conf_opts, opts)
  end

  lspconfig[server].setup(opts)
end

In this configuration file, we have added the following servers:

  • bashls (A language server for Bash)
  • cssls (Language Server Protocol implementation for CSS, SCSS & LESS)
  • html (Language Server Protocol implementation for HTML)
  • jsonls (Language Server Protocol implementation for JSON)
  • lua_ls (A language server that offers Lua language support – programmed in Lua)
  • pyright (Static type checker for Python)
  • tsserver (TypeScript & JavaScript Language Server)

With Mason, you can install a wide range of supported LSP servers. To view all the available LSP servers, check the list of available lsp server.

As always, to load the configuration file each time Neovim starts, you need to add the configuration file to the init.lua file. Open the init.lua file:

nvim ~/.config/nvim/init.lua

Then add the following line to activate Mason:

require "lsp.mason"

Save the file and restart Neovim; then you will see the LSP servers automatically installed and activated when they detect specific file types.

Using Mason to Manage LSP Servers

Mason provides several commands to manage your LSP servers. To see the installed servers and all available servers that can be installed with Mason, enter the following command in Neovim:

:Mason
Mason-graphical-status

From this graphical status window, you can see which servers are installed in Neovim as well as available servers that can be used in Neovim.

You can also install any LSP server with the :MasonInstall command. For example, if you want to install the LSP server for Python, you can install pyright (Python LSP implementation for Neovim) with the following command:

:MasonInstall pyright

You can also uninstall LSP server packages by defining the LSP server name in the mason.lua configuration file.

If you don’t need an LSP server anymore, you can uninstall the package with the :MasonUninstall command. For example, if you don’t need the Python LSP server anymore, you can remove it with the following command:

:MasonUninstall pyright

Mason also supports uninstalling all the packages with a single command. To remove all the installed LSP servers, you can use the following command:

:MasonUninstallAll

If you occasionally encounter any errors in Mason or the LSP server, you can see the details about the error in the Mason log. To access the details log, use the following command:

:MasonLog

Configuring Language Server Protocol (LSP)

Now that you have installed your preferred Language Server Protocol (LSP) servers with Mason, it’s time to configure the LSP servers to get the most out of the Language Server Protocol. For example, you may want the LSP server to activate automatically based on the file extension when you open a file. For instance, if you open an HTML file, the HTML language server protocol should be automatically installed. To configure the LSP server, open your terminal and create/open the handlers.lua file:

nvim ~/.config/nvim/lua/lsp/handlers.lua

Then add the following configuration to the file:

local M = {}

local status_cmp_ok, cmp_nvim_lsp = pcall(require, "cmp_nvim_lsp")
if not status_cmp_ok then
	return
end

M.capabilities = vim.lsp.protocol.make_client_capabilities()
M.capabilities.textDocument.completion.completionItem.snippetSupport = true
M.capabilities = cmp_nvim_lsp.default_capabilities(M.capabilities)

M.setup = function()
	local signs = {

		{ name = "DiagnosticSignError", text = "" },
		{ name = "DiagnosticSignWarn", text = "" },
		{ name = "DiagnosticSignHint", text = "" },
		{ name = "DiagnosticSignInfo", text = "" },
	}

	for _, sign in ipairs(signs) do
		vim.fn.sign_define(sign.name, { texthl = sign.name, text = sign.text, numhl = "" })
	end

	local config = {
		virtual_text = false, -- disable virtual text
		signs = {
			active = signs, -- show signs
		},
		update_in_insert = true,
		underline = true,
		severity_sort = true,
		float = {
			focusable = true,
			style = "minimal",
			border = "rounded",
			source = "always",
			header = "",
			prefix = "",
		},
	}

	vim.diagnostic.config(config)

	vim.lsp.handlers["textDocument/hover"] = vim.lsp.with(vim.lsp.handlers.hover, {
		border = "rounded",
	})

	vim.lsp.handlers["textDocument/signatureHelp"] = vim.lsp.with(vim.lsp.handlers.signature_help, {
		border = "rounded",
	})
end

local function lsp_keymaps(bufnr)
	local opts = { noremap = true, silent = true }
	local keymap = vim.api.nvim_buf_set_keymap
	keymap(bufnr, "n", "gD", "<cmd>lua vim.lsp.buf.declaration()<CR>", opts)
	keymap(bufnr, "n", "gd", "<cmd>lua vim.lsp.buf.definition()<CR>", opts)
	keymap(bufnr, "n", "K", "<cmd>lua vim.lsp.buf.hover()<CR>", opts)
	keymap(bufnr, "n", "gI", "<cmd>lua vim.lsp.buf.implementation()<CR>", opts)
	keymap(bufnr, "n", "gr", "<cmd>lua vim.lsp.buf.references()<CR>", opts)
	keymap(bufnr, "n", "gl", "<cmd>lua vim.diagnostic.open_float()<CR>", opts)
	keymap(bufnr, "n", "<leader>lf", "<cmd>lua vim.lsp.buf.format{ async = true }<cr>", opts)
	keymap(bufnr, "n", "<leader>li", "<cmd>LspInfo<cr>", opts)
	keymap(bufnr, "n", "<leader>lI", "<cmd>LspInstallInfo<cr>", opts)
	keymap(bufnr, "n", "<leader>la", "<cmd>lua vim.lsp.buf.code_action()<cr>", opts)
	keymap(bufnr, "n", "<leader>lj", "<cmd>lua vim.diagnostic.goto_next({buffer=0})<cr>", opts)
	keymap(bufnr, "n", "<leader>lk", "<cmd>lua vim.diagnostic.goto_prev({buffer=0})<cr>", opts)
	keymap(bufnr, "n", "<leader>lr", "<cmd>lua vim.lsp.buf.rename()<cr>", opts)
	keymap(bufnr, "n", "<leader>ls", "<cmd>lua vim.lsp.buf.signature_help()<CR>", opts)
	keymap(bufnr, "n", "<leader>lq", "<cmd>lua vim.diagnostic.setloclist()<CR>", opts)
end

M.on_attach = function(client, bufnr)
	if client.name == "tsserver" then
		client.server_capabilities.documentFormattingProvider = false
	end

	if client.name == "sumneko_lua" then
		client.server_capabilities.documentFormattingProvider = false
	end

	lsp_keymaps(bufnr)
	local status_ok, illuminate = pcall(require, "illuminate")
	if not status_ok then
		return
	end
	illuminate.on_attach(client)
end

return M

In this configuration file, necessary keyword mappings are provided, as well as useful icons for code diagnostics.

You need to add another configuration file called null-ls.lua configuration file. Use the following command to create/open the file:

nvim ~/.config/nvim/lua/lsp/null-ls.lua

And add the following line to the configuration file:

local null_ls_status_ok, null_ls = pcall(require, "null-ls")
if not null_ls_status_ok then
  return
end

-- <https://github.com/jose-elias-alvarez/null-ls.nvim/tree/main/lua/null-ls/builtins/formatting>
local formatting = null_ls.builtins.formatting
-- <https://github.com/jose-elias-alvarez/null-ls.nvim/tree/main/lua/null-ls/builtins/diagnostics>
local diagnostics = null_ls.builtins.diagnostics

null_ls.setup({
  debug = false,
  sources = {
    formatting.prettier.with({ extra_args = { "--no-semi", "--single-quote", "--jsx-single-quote" } }),
    formatting.black.with({ extra_args = { "--fast" } }),
    formatting.stylua,
    -- diagnostics.flake8
  },
})

After adding the configuration, you need to define them in the init.lua file so that they are executed when Neovim starts. To add these lines, open the init.lua file:

nvim ~/.config/nvim/init.lua

and add the following at the end of the file:

require("lsp.handlers").setup()
require("lsp.null-ls")

Save the file and restart Neovim so that the changes could take effect.

Configure Autocomplete for LSP

In our other tutorial, we have discussed basic auto-completion with the cmp plugin. If you haven’t installed cmp for autocompletion, consider setting up basic cmp autocompletion before configuring autocompletion for LSP.

If you have already configured cmp for autocompletion, proceed to the following steps. Open the nvim-cmp.lua file:

nvim ~/.config/nvim/lua/nvim-cmp.lua

and add the following line to the configuration file below the line calling nvim-cmp:

-- Import lspkind plugin safely
local lspkind_status, lspkind = pcall(require, "lspkind")
if not lspkind_status then
  return
end

Your configuration file should look like this:

configuration-autocompletion-for-lsp

Then, add LSP as a source at the bottom:

{ name = "nvim_lsp" }, -- LSP
add-source-for-lsp

Save the file and restart Neovim.

Conclusion

In this blog post, we’ve explored the Language Server Protocol (LSP) and its integration with Neovim and how to to unlock a wealth of powerful features for developers. By setting up LSP in Neovim, we’ve learned how to configure LSP servers for different programming languages, enabling intelligent code autocompletion, diagnostics, and error highlighting. We’ve also seen how to manage LSP servers with Mason and how to configure autocomplete for LSP with the cmp plugin.

References
  1. Neovim-from-scratch
  2. dev-environment-files
Share your love

Newsletter Updates

Stay updated with our latest guides and tutorials about Linux.

Leave a Reply

Your email address will not be published. Required fields are marked *