# Neovim LSP Setup: nvim-lspconfig + lazy.nvim Explained

Setting up Language Server Protocol (LSP) support in Neovim used to mean juggling long configuration files, custom autocommands, and a fair amount of trial and error. While `nvim-lspconfig` has always provided the building blocks, managing when and how those pieces load could quickly become messy—especially as your configuration grows.

With the rise of `lazy.nvim`, Neovim plugin management has shifted toward a more declarative, performance-focused approach. Plugins load only when they’re needed, configurations become easier to reason about, and startup time stays fast even as your setup becomes more powerful. When combined with `nvim-lspconfig`, this approach results in an LSP configuration that is both clean and flexible.

In this post, we’ll walk through a modern Neovim LSP setup using `nvim-lspconfig` and `lazy.nvim`. You’ll see how to structure your configuration, load LSP support at the right moment, and avoid common pitfalls—ending up with a maintainable setup that scales as your editor and workflow evolve.

As we saw in the [previous post](https://textmode.dev/lazynvim-installation-and-configuration), we are working with the following folder structure:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768386955966/e67c5694-d6d1-41de-b922-dc4b89ea1fbe.png align="center")

This folder structure reflects a clean and modular approach to configuring Neovim, using **lazy.nvim** as the plugin manager and **nvim-lspconfig** for LSP support. The `init.lua` file is Neovim’s main entry point and typically only contains minimal bootstrap logic, delegating most of the setup to Lua modules. The `lazy-lock.json` file is automatically generated by lazy.nvim and ensures reproducible plugin versions by locking dependencies to specific commits. Inside the `lua/` directory, configuration is split by responsibility: `config/lazy.lua` is dedicated to initializing and configuring lazy.nvim itself (including plugin loading options), while the `plugins/` directory contains individual plugin specifications. In this case, `plugins/lsp.lua` encapsulates all LSP-related configuration, such as server setup.

Let’s start with the configuration for the LSP client in the core config **nvim/lua/config/lazy.lua** file. Depending on the project size and its dependencies, the LSP server can take a noticeable amount of time. In these situations, having visual feedback during initialization is especially helpful, as it lets you know that the LSP is working and gives you an idea of how long it will take to become ready. Neovim’s built-in LSP client supports this by exposing a function that can be integrated into your statusline, allowing you to see the current LSP progress and initialization status at a glance.

```bash
----------------------------------------------------------------------
-- LSP progress
----------------------------------------------------------------------
vim.api.nvim_create_autocmd("LspProgress", {
  group = vim.api.nvim_create_augroup("LspProgressStatusline", { clear = true }),
  callback = function()
    vim.cmd("redrawstatus")
  end,
})

vim.opt.statusline:append("%{v:lua.vim.lsp.status()}")
```

We can now test our configuration by closing Neovim and opening it again with the file lazy.lua and check how the LSP initialization status gets reported on the status line. Once the loading process finishes, the LSP is ready to show diagnostics, if any.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768385469919/ee3a35a2-c09d-45d9-90ba-ea5d27fc5289.gif align="center")

One of the key capabilities of an LSP client is its support for real-time diagnostics, such as warnings, errors, and hints displayed directly in the editor. This effectively acts as a first line of defense when working with compiled languages, surfacing issues as you type rather than waiting for a full build to fail. By highlighting problems inline and providing precise messages, the LSP offers immediate feedback on mistakes, questionable patterns, or potential bugs, allowing you to correct them early and maintain a faster, more confident development flow.

```bash
----------------------------------------------------------------------
-- Diagnostics (INLINE)
----------------------------------------------------------------------
vim.diagnostic.config({
  virtual_text = {
    spacing = 4,
    prefix = "●", -- could be "■", "▎", ""
  },
  signs = true,
  underline = true,
  update_in_insert = false,
  severity_sort = true,
})
```

This configuration customizes how Neovim displays LSP diagnostics, focusing on clear and non-intrusive inline feedback. The `virtual_text` option enables inline diagnostic messages and fine-tunes their appearance: `spacing = 4` adds padding between the code and the message for better readability, while the `prefix = "●"` inserts a small visual marker to quickly draw attention to problematic lines without overwhelming the code. Enabling `signs` places icons in the sign column, providing an at-a-glance indication of errors or warnings, and `underline = true` visually emphasizes the exact range of code associated with the issue. Setting `update_in_insert = false` prevents diagnostics from updating while you are typing, reducing visual noise and distractions during insertion. Finally, `severity_sort = true` ensures that when multiple diagnostics exist on the same line, more severe issues (such as errors) take priority over warnings or hints, helping you focus on what matters most first.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768385667933/1eb7641c-9882-4d56-8d8a-621809a7e6f4.gif align="center")

Now that the basic LSP setup is in place, we can move on to configuring a language server for a specific language and see the LSP client in action. We’ll do this in the `nvim/lua/plugins/lsp.lua` file, which contains the specification for the `nvim-lspconfig` plugin. Each plugin spec is made up of several configurable parts; here, we’ll focus on defining when the plugin should be loaded and setting its core configuration options. We are going to configure the Lua language server. That way, it can help you out from now on to spot any issues on the configuration files for other plugins. And you can explore more easily the APIs provided by Neovim to implement extra functionality.

```bash
return {
  "neovim/nvim-lspconfig",
  event = { "BufReadPre", "BufNewFile" },
  config = function()

    ----------------------------------------------------------------------
    -- Lua LSP configuration
    ----------------------------------------------------------------------
    vim.lsp.config("lua_ls", {
      settings = {
        Lua = {
          runtime = { version = "LuaJIT" },
          diagnostics = {
            globals = { "vim" },
          },
          workspace = {
            checkThirdParty = false,
            library = vim.api.nvim_get_runtime_file("", true),
          },
          telemetry = { enable = false },
        },
      },
    })

    -- Enable Lua LSP
    vim.lsp.enable("lua_ls")
```

This plugin specification defines how **nvim-lspconfig** is loaded and how the Lua language server is configured in a lazy.nvim setup. The plugin is lazy-loaded on the `BufReadPre` and `BufNewFile` events, meaning it is only initialized when you open an existing file or create a new one, which helps keep Neovim’s startup time fast. Inside the `config` function, the Lua language server (`lua_ls`) is configured using `vim.lsp.config`, with settings tailored specifically for Neovim development. The runtime is set to `LuaJIT`, matching the Lua version embedded in Neovim, and the diagnostics are instructed to recognize `vim` as a global variable to avoid false warnings. The workspace configuration disables third-party checks and adds Neovim’s runtime files to the server’s library, enabling better completion and navigation for Neovim APIs. Telemetry is explicitly disabled to avoid sending usage data. Finally, `vim.lsp.enable("lua_ls")` activates the server, ensuring the configuration is applied and the Lua LSP starts automatically when editing Lua files. You can test its functionality with the default keybinding ‘K’ that shows the hover information of a symbol.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768386562115/b1d7efb8-a3fd-4ac5-b6db-f90e1b33a23f.gif align="center")

To ease navigation over autocompletion options, we will define a couple of keybindings. These keybindings enhance the LSP auto-completion workflow by making `<Tab>` and `<S-Tab>` context-aware in insert mode. When the completion pop-up menu is visible (`pumvisible()`), `<Tab>` and `<S-Tab>` are repurposed to navigate forward and backward through the list of completion candidates using `<C-n>` and `<C-p>`, allowing you to quickly browse suggestions without leaving the home row. When no completion menu is shown, pressing `<Tab>` explicitly triggers omni-completion (`<C-x><C-o>`), which asks the active LSP server for context-aware suggestions such as symbols, methods, or types. This dual behavior keeps completion both discoverable and fluid: you can trigger LSP completion on demand and seamlessly navigate results with familiar keys, reducing friction and making auto-completion feel like a natural extension of typing rather than a separate action.

```bash
-- Omni complete
vim.keymap.set("i", "<Tab>", function()
  if vim.fn.pumvisible() == 1 then
    return "<C-n>"
  end
  return "<C-x><C-o>"
end, { expr = true })

vim.keymap.set("i", "<S-Tab>", function()
  if vim.fn.pumvisible() == 1 then
    return "<C-p>"
  end
  return "<S-Tab>"
end, { expr = true })
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768386732231/44c232d4-0488-44e7-aed9-684e14dd6c57.gif align="center")

Wrapping things up, configuring **nvim-lspconfig** with **lazy.nvim** gives you a clean, modular, and performant way to manage LSP support in Neovim. By letting lazy.nvim handle loading and dependencies, you keep your configuration declarative and easy to reason about, while nvim-lspconfig focuses on what it does best: connecting language servers to your editor. This setup scales naturally as you add more servers, tools, or customizations, and it encourages a workflow where your editor grows with your needs instead of fighting them. With this foundation in place, you’re well-equipped to fine-tune diagnostics, keymaps, and capabilities—and make Neovim feel truly tailored to your development style.
