# How to Set Up LSP Configuration in Neovim: A Complete Guide Using jdhao/nvim-config

> Learn to set up LSP configuration in Neovim with this comprehensive guide on jdhao/nvim-config. Master global capabilities, keymaps, and server overrides for efficient coding.

- Repository: [jdhao/nvim-config](https://github.com/jdhao/nvim-config)
- Tags: tutorial
- Published: 2026-03-04

---

**Configure LSP in Neovim by installing nvim-lspconfig via lazy.nvim, defining global capabilities in [`lua/lsp_utils.lua`](https://github.com/jdhao/nvim-config/blob/main/lua/lsp_utils.lua), setting up buffer-local keymaps in [`lua/config/lsp.lua`](https://github.com/jdhao/nvim-config/blob/main/lua/config/lsp.lua), and placing server-specific overrides in the `after/lsp/` directory.**

This guide examines how the jdhao/nvim-config repository implements a production-ready LSP configuration in Neovim. By combining lazy.nvim for plugin management with Neovim's native `vim.lsp` APIs, the setup achieves conditional server loading, custom keymaps, and language-specific optimizations without relying on external LSP installer plugins.

## 1. Installing the LSP Plugin with lazy.nvim

The foundation of the LSP configuration in Neovim begins in [`lua/plugin_specs.lua`](https://github.com/jdhao/nvim-config/blob/main/lua/plugin_specs.lua), where the repository declares `neovim/nvim-lspconfig` as a dependency. The plugin specification includes a config hook that loads the core LSP logic:

```lua
{
  "neovim/nvim-lspconfig",
  config = function()
    require("config.lsp")
  end,
},

```

When you run `:Lazy sync`, lazy.nvim clones the plugin into `~/.local/share/nvim/lazy` and executes the `config` function, which sources [`lua/config/lsp.lua`](https://github.com/jdhao/nvim-config/blob/main/lua/config/lsp.lua) to initialize the LSP stack.

## 2. Defining Shared Client Capabilities

Before any language server attaches, the repository establishes global capabilities in [`lua/lsp_utils.lua`](https://github.com/jdhao/nvim-config/blob/main/lua/lsp_utils.lua). This module exports a `get_default_capabilities` function that enhances Neovim's base protocol with folding support:

```lua
local M = {}
M.get_default_capabilities = function()
  local capabilities = vim.lsp.protocol.make_client_capabilities()
  capabilities.textDocument.foldingRange = {
    dynamicRegistration = false,
    lineFoldingOnly = true,
  }
  return capabilities
end
return M

```

These capabilities are injected globally in [`lua/config/lsp.lua`](https://github.com/jdhao/nvim-config/blob/main/lua/config/lsp.lua) using Neovim 0.10+'s `vim.lsp.config` API:

```lua
local capabilities = require("lsp_utils").get_default_capabilities()
vim.lsp.config("*", { capabilities = capabilities, flags = { debounce_text_changes = 500 } })

```

The `debounce_text_changes` flag reduces unnecessary server notifications by waiting 500ms after text changes.

## 3. Configuring Buffer-Local LSP Behavior

The core LSP logic resides in [`lua/config/lsp.lua`](https://github.com/jdhao/nvim-config/blob/main/lua/config/lsp.lua) within an autocommand callback triggered when any LSP client attaches to a buffer (lines 3-92). This handler:

- Retrieves the active client via `vim.lsp.get_client_by_id`
- Creates buffer-local keymaps using `vim.keymap.set` with `opts.buffer = bufnr`
- Implements a custom `gd` handler that deduplicates definition locations before populating the location list
- Disables Ruff's hover provider when Pyright is active using `client.server_capabilities.hoverProvider = false`

Key buffer-local mappings include `gd` (go to definition), `K` (hover documentation), and `<space>rn` (rename). The `K` mapping customizes the hover UI:

```lua
map("n", "K", function()
  vim.lsp.buf.hover { border = "single", max_height = 20, max_width = 130 }
end)

```

## 4. Enabling Servers Conditionally

Rather than starting all servers unconditionally, [`lua/config/lsp.lua`](https://github.com/jdhao/nvim-config/blob/main/lua/config/lsp.lua) maintains an `enabled_lsp_servers` table mapping server names to their required executables:

```lua
local enabled_lsp_servers = {
  pyright = "delance-langserver",
  ruff = "ruff",
  lua_ls = "lua-language-server",
  vimls = "vim-language-server",
  bashls = "bash-language-server",
  yamlls = "yaml-language-server",
}

```

A validation loop checks for executable availability using a `utils.executable` helper. Only servers with existing binaries are enabled via `vim.lsp.enable(server_name)`:

```lua
for name, exe in pairs(enabled_lsp_servers) do
  if utils.executable(exe) then
    vim.lsp.enable(name)
  else
    vim.notify(
      string.format("Executable '%s' for server '%s' not found!", exe, name),
      vim.log.levels.WARN,
      { title = "Nvim-config" })
  end
end

```

## 5. Server-Specific Configuration Files

The repository leverages nvim-lspconfig's convention of loading files from `after/lsp/{server_name}.lua` for per-server overrides. These files return configuration tables automatically merged when `vim.lsp.enable()` runs.

**Python (Pyright)** – [`after/lsp/pyright.lua`](https://github.com/jdhao/nvim-config/blob/main/after/lsp/pyright.lua) customizes the command, capabilities, and analysis settings:

```lua
return {
  cmd = { "delance-langserver", "--stdio" },
  settings = {
    pyright = { disableOrganizeImports = true },
    python = {
      analysis = {
        typeCheckingMode = "standard",
        inlayHints = {
          callArgumentNames = "partial",
          functionReturnTypes = true,
        },
      },
    },
  },
  capabilities = {
    textDocument = {
      hover = { contentFormat = { "plaintext" } },
    },
  },
}

```

**Ruff** – [`after/lsp/ruff.lua`](https://github.com/jdhao/nvim-config/blob/main/after/lsp/ruff.lua) enables import organization:

```lua
return {
  init_options = {
    settings = {
      organizeImports = true,
    },
  },
}

```

## 6. Putting It All Together

To replicate this LSP configuration in Neovim from scratch, create the following minimal [`init.lua`](https://github.com/jdhao/nvim-config/blob/main/init.lua) that bootstraps lazy.nvim and loads the plugin spec:

```lua
-- Bootstrap lazy.nvim
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.uv.fs_stat(lazypath) then
  vim.fn.system({ "git", "clone", "--filter=blob:none",
    "https://github.com/folke/lazy.nvim.git", "--branch=stable", lazypath })
end
vim.opt.rtp:prepend(lazypath)

-- Load plugins
require("lazy").setup({
  spec = {
    { "neovim/nvim-lspconfig", config = function() require("config.lsp") end },
  },
})

```

Place the [`lsp_utils.lua`](https://github.com/jdhao/nvim-config/blob/main/lsp_utils.lua), [`config/lsp.lua`](https://github.com/jdhao/nvim-config/blob/main/config/lsp.lua), and `after/lsp/` files from the jdhao/nvim-config repository in your `~/.config/nvim/lua/` directory. Run `:Lazy sync`, restart Neovim, and open a file matching one of the enabled servers. The LSP client will attach automatically if the executable exists, activating the buffer-local keymaps and server-specific settings.

## Summary

- **Install nvim-lspconfig** via lazy.nvim in [`lua/plugin_specs.lua`](https://github.com/jdhao/nvim-config/blob/main/lua/plugin_specs.lua) and defer initialization to `config.lsp`.
- **Define global capabilities** in [`lua/lsp_utils.lua`](https://github.com/jdhao/nvim-config/blob/main/lua/lsp_utils.lua) to enable folding and consistent client behavior across all servers.
- **Set up buffer-local keymaps** inside the `LspAttach` autocommand in [`lua/config/lsp.lua`](https://github.com/jdhao/nvim-config/blob/main/lua/config/lsp.lua) to ensure mappings exist only when LSP is active.
- **Enable servers conditionally** by checking executable availability with `utils.executable` before calling `vim.lsp.enable()`.
- **Override per-server settings** by placing `{server_name}.lua` files in the `after/lsp/` directory, following nvim-lspconfig's conventions.

## Frequently Asked Questions

### Why does the configuration disable Ruff's hover provider?

The jdhao/nvim-config repository disables Ruff's hoverProvider by setting `client.server_capabilities.hoverProvider = false` when both Ruff and Pyright are active. This prevents duplicate hover information, ensuring that Pyright—which provides richer type information—handles all hover requests while Ruff focuses on linting and import organization.

### What is the purpose of the after/lsp/ directory in Neovim LSP configuration?

The `after/lsp/` directory follows nvim-lspconfig's loading convention, automatically sourcing any `{server_name}.lua` files found there when `vim.lsp.enable()` activates that server. This separation keeps server-specific logic isolated from the core LSP setup, making it easier to maintain language-specific settings like Pyright's inlay hints or Ruff's import sorting without modifying the global configuration.

### How do I add a new language server to this configuration?

To add a new server, first install the corresponding language server binary on your system. Then add an entry to the `enabled_lsp_servers` table in [`lua/config/lsp.lua`](https://github.com/jdhao/nvim-config/blob/main/lua/config/lsp.lua) with the server name and executable command. If the server requires custom settings, create a new file at `after/lsp/{server_name}.lua` returning a configuration table with your desired `cmd`, `settings`, and `capabilities` overrides.

### What does the debounce_text_changes flag do in vim.lsp.config?

The `debounce_text_changes = 500` setting in [`lua/config/lsp.lua`](https://github.com/jdhao/nvim-config/blob/main/lua/config/lsp.lua) tells the Neovim LSP client to wait 500 milliseconds after you stop typing before sending text synchronization notifications to the server. This reduces CPU load and prevents excessive diagnostic updates while editing, improving performance when working with large files or slower language servers.