Skip to main content
This configuration uses Neovim 0.11+‘s native LSP support with vim.lsp.config() and vim.lsp.enable(). Configuration files in after/lsp/*.lua override and extend the base configs from nvim-lspconfig.

Enabling language servers

Language servers are enabled in lua/plugins/lsp/init.lua:
-- From lua/plugins/lsp/init.lua:42
vim.lsp.enable({
  "lua_ls",      -- Lua
  "gopls",       -- Go
  "tsgo",        -- TypeScript/JavaScript (native server)
  "angularls",   -- Angular Language Service
  "biome",       -- Biome (linting/formatting for JS/TS/JSON)
  "bashls",      -- Bash/Shell
  "cssls",       -- CSS/SCSS/Less
  "html",        -- HTML
  "jsonls",      -- JSON
  "yamlls",      -- YAML
  "dockerls",    -- Docker
  "clangd",      -- C/C++
  "tailwindcss", -- Tailwind CSS
  "emmet_language_server", -- Emmet
  "harper_ls",   -- Grammar/spell checking
})
The tsgo server is Microsoft’s native TypeScript server. To use ts_ls instead, see the comment in after/lsp/tsgo.lua:2.

Global configuration

All language servers inherit global settings:
-- From lua/plugins/lsp/init.lua:32
vim.lsp.config("*", {
  capabilities = capabilities,
  root_markers = { ".git" },
})

Capabilities

Capabilities are automatically merged by blink.cmp:
-- From lua/plugins/lsp/init.lua:21
local capabilities = require("blink.cmp").get_lsp_capabilities({
  textDocument = {
    -- Folding capabilities for nvim-ufo
    foldingRange = {
      dynamicRegistration = false,
      lineFoldingOnly = true,
    },
  },
})

Custom configurations

Server-specific configurations are stored in after/lsp/<server_name>.lua. These files return a table that gets merged with the base configuration.

Structure

Each file in after/lsp/ should return a configuration table:
-- Example: after/lsp/myserver.lua
return {
  capabilities = { ... },
  settings = { ... },
  init_options = { ... },
  on_attach = function(client, bufnr) ... end,
}

Merging priority

1

nvim-lspconfig defaults

Base configuration from lsp/<server>.lua in nvim-lspconfig
2

Global defaults

Settings from vim.lsp.config("*", {...})
3

Custom overrides

Your configuration from after/lsp/<server>.lua (highest priority)

LSP attach handler

The LspAttach autocmd sets up keymaps and features when a server attaches:
-- From lua/plugins/lsp/init.lua:137
vim.api.nvim_create_autocmd("LspAttach", {
  group = vim.api.nvim_create_augroup("UserLspConfig", { clear = true }),
  callback = function(args)
    local bufnr = args.buf
    local client = vim.lsp.get_client_by_id(args.data.client_id)

    if not client then
      return
    end

    -- Setup keymaps for this buffer
    setup_keymaps(bufnr)

    -- Enable completion triggered by <c-x><c-o>
    vim.bo[bufnr].omnifunc = "v:lua.vim.lsp.omnifunc"

    -- Inlay hints disabled by default (toggle with <leader>ih)
    if client.server_capabilities.inlayHintProvider and vim.lsp.inlay_hint then
      vim.lsp.inlay_hint.enable(false, { bufnr = bufnr })
    end
  end,
})

Keymaps

LSP keymaps are automatically configured when a language server attaches:
Navigation keymaps (gd, gD, gi, gr, gt) are delegated to Snacks pickers for better UX.

Information

-- From lua/plugins/lsp/init.lua:82
map("n", "K", function()
  vim.lsp.buf.hover({
    border = "rounded",
    max_height = 25,
    max_width = 120,
  })
end, "Hover documentation")

map("n", "<C-k>", function()
  vim.lsp.buf.signature_help({ border = "rounded" })
end, "Signature help")

Code actions and refactoring

-- From lua/plugins/lsp/init.lua:98
map({ "n", "x" }, "ga", vim.lsp.buf.code_action, "Code action")
map("n", "<leader>rn", vim.lsp.buf.rename, "Rename symbol")
<leader>ca is mapped to tiny-code-action for enhanced code action UI.

Diagnostics

-- From lua/plugins/lsp/init.lua:104
map("n", "[d", function()
  vim.diagnostic.jump({ count = -1 })
end, "Previous diagnostic")

map("n", "]d", function()
  vim.diagnostic.jump({ count = 1 })
end, "Next diagnostic")

map("n", "<leader>cd", vim.diagnostic.open_float, "Show diagnostic")

Workspace management

-- From lua/plugins/lsp/init.lua:113
map("n", "<leader>wa", vim.lsp.buf.add_workspace_folder, "Add workspace folder")
map("n", "<leader>wr", vim.lsp.buf.remove_workspace_folder, "Remove workspace folder")
map("n", "<leader>wl", function()
  print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
end, "List workspace folders")

Inlay hints

-- From lua/plugins/lsp/init.lua:121
map("n", "<leader>ih", function()
  vim.lsp.inlay_hint.enable(
    not vim.lsp.inlay_hint.is_enabled({ bufnr = bufnr }),
    { bufnr = bufnr }
  )
  vim.notify(
    "Inlay Hints "
      .. (vim.lsp.inlay_hint.is_enabled({ bufnr = bufnr }) and "Enabled" or "Disabled")
  )
end, "Toggle inlay hints")

Diagnostic configuration

Diagnostics are configured globally:
-- From lua/plugins/lsp/init.lua:163
vim.diagnostic.config({
  severity_sort = true,
  float = { border = "rounded", source = true },
  underline = true,
  update_in_insert = false,
  signs = {
    text = {
      [vim.diagnostic.severity.ERROR] = "󰅚 ",
      [vim.diagnostic.severity.WARN] = "󰀪 ",
      [vim.diagnostic.severity.INFO] = "󰋽 ",
      [vim.diagnostic.severity.HINT] = "󰌶 ",
    },
    numhl = {
      [vim.diagnostic.severity.ERROR] = "ErrorMsg",
      [vim.diagnostic.severity.WARN] = "WarningMsg",
    },
  },
  virtual_text = false, -- Disabled because using tiny-diagnostic plugin
})

LSP detach cleanup

The LspDetach autocmd cleans up when a server detaches:
-- From lua/plugins/lsp/init.lua:186
vim.api.nvim_create_autocmd("LspDetach", {
  group = vim.api.nvim_create_augroup("UserLspDetach", { clear = true }),
  callback = function(args)
    vim.lsp.buf.clear_references()
    pcall(vim.api.nvim_del_augroup_by_name, "LspDocumentHighlight_" .. args.buf)
  end,
})

Debugging LSP

Custom commands for debugging LSP issues are available:

LspInfoCustom

Show comprehensive LSP information:
:LspInfoCustom
This displays:
  • Attached clients
  • Root directories
  • Client status
  • Workspace folders
  • Diagnostic summary
From lua/plugins/lsp/commands.lua:62.

LspCapabilities

Show all capabilities for attached clients:
:LspCapabilities
From lua/plugins/lsp/commands.lua:11.

Extra LSP plugins

Additional plugins enhance the LSP experience:

lazydev.nvim

Enhanced Lua development for Neovim:
-- From lua/plugins/lsp/extras.lua:2
{
  "folke/lazydev.nvim",
  ft = "lua",
  opts = {
    library = {
      { path = "${3rd}/luv/library", words = { "vim%.uv" } },
      { path = "snacks.nvim", words = { "Snacks" } },
    },
  },
}

fidget.nvim

LSP progress notifications:
-- From lua/plugins/lsp/extras.lua:13
{
  "j-hui/fidget.nvim",
  event = "LspAttach",
  opts = {
    notification = {
      window = {
        winblend = 0,
      },
    },
  },
}

tiny-code-action.nvim

Enhanced code action UI with diff preview:
-- From lua/plugins/lsp/extras.lua:24
{
  "rachartier/tiny-code-action.nvim",
  event = "LspAttach",
  opts = {
    backend = "delta",
    picker = "snacks",
  },
}

Next steps

TypeScript setup

Configure TypeScript/JavaScript LSP

Go setup

Configure Go LSP

Lua setup

Configure Lua LSP

Formatting

Set up code formatters

Build docs developers (and LLMs) love