LSP Setup
NeoVim 0.11+ introduced a declarative LSP configuration system. You define server configs, enable them, and NeoVim handles starting/stopping clients as you open files.
The New Way (0.11+)
Three steps: configure, enable, customize on attach.
-- 1. Configure a language server
vim.lsp.config["lua_ls"] = {
cmd = { "lua-language-server" },
filetypes = { "lua" },
root_markers = { ".luarc.json", ".luarc.jsonc", ".git" },
settings = {
Lua = {
runtime = { version = "LuaJIT" },
workspace = { library = vim.api.nvim_get_runtime_file("", true) },
},
},
}
-- 2. Enable it
vim.lsp.enable("lua_ls")
-- 3. Customize behavior on attach (optional)
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(args)
local client = vim.lsp.get_client_by_id(args.data.client_id)
local buf = args.buf
-- Keymaps for LSP-attached buffers
vim.keymap.set("n", "gd", vim.lsp.buf.definition, { buffer = buf })
vim.keymap.set("n", "K", vim.lsp.buf.hover, { buffer = buf })
vim.keymap.set("n", "<leader>ca", vim.lsp.buf.code_action, { buffer = buf })
vim.keymap.set("n", "<leader>rn", vim.lsp.buf.rename, { buffer = buf })
-- Format on save
if client and client:supports_method("textDocument/formatting") then
vim.api.nvim_create_autocmd("BufWritePre", {
buffer = buf,
callback = function()
vim.lsp.buf.format({ bufnr = buf })
end,
})
end
end,
})
Config Files on Runtimepath
Instead of inline config, you can put server configs in files:
~/.config/nvim/
lsp/
lua_ls.lua -- return { cmd = ..., filetypes = ... }
gopls.lua
ts_ls.lua
after/lsp/
lua_ls.lua -- Overrides/extends the base config
-- lsp/gopls.lua
return {
cmd = { "gopls" },
filetypes = { "go", "gomod", "gowork", "gotmpl" },
root_markers = { "go.mod", ".git" },
settings = {
gopls = {
analyses = { unusedparams = true },
staticcheck = true,
},
},
}
-- init.lua - just enable, config is found automatically
vim.lsp.enable({ "lua_ls", "gopls", "ts_ls" })
Config resolution merges in order:
vim.lsp.config["*"](wildcard, applies to all servers)lsp/<name>.luafiles on runtimepathafter/lsp/<name>.luafiles- Inline
vim.lsp.config["name"] = {...}calls
Common Server Configs
-- TypeScript
vim.lsp.config["ts_ls"] = {
cmd = { "typescript-language-server", "--stdio" },
filetypes = { "typescript", "typescriptreact", "javascript", "javascriptreact" },
root_markers = { "tsconfig.json", "jsconfig.json", "package.json", ".git" },
}
-- Python
vim.lsp.config["pyright"] = {
cmd = { "pyright-langserver", "--stdio" },
filetypes = { "python" },
root_markers = { "pyproject.toml", "setup.py", ".git" },
settings = {
python = {
analysis = { typeCheckingMode = "basic" },
},
},
}
-- Rust
vim.lsp.config["rust_analyzer"] = {
cmd = { "rust-analyzer" },
filetypes = { "rust" },
root_markers = { "Cargo.toml" },
settings = {
["rust-analyzer"] = {
checkOnSave = { command = "clippy" },
},
},
}
-- JSON with schemas
vim.lsp.config["jsonls"] = {
cmd = { "vscode-json-language-server", "--stdio" },
filetypes = { "json", "jsonc" },
root_markers = { ".git" },
settings = {
json = {
validate = { enable = true },
},
},
}
Built-in Completion
NeoVim has a built-in LSP completion source. Enable it per-client:
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(args)
local client = vim.lsp.get_client_by_id(args.data.client_id)
if client and client:supports_method("textDocument/completion") then
vim.lsp.completion.enable(true, client.id, args.buf, {
autotrigger = true, -- Trigger on typing (not just manual)
})
end
end,
})
With autotrigger enabled, completions appear as you type. Use <C-y> to confirm, <C-e> to dismiss.
Tip: The built-in completion is functional but minimal. For a richer experience (snippet expansion, multiple sources, fuzzy matching), use nvim-cmp or blink.cmp.
Default Behaviors (Free Without Config)
NeoVim sets these automatically when an LSP client attaches:
| Feature | What happens |
|---|---|
omnifunc | Set to vim.lsp.omnifunc (trigger with <C-x><C-o>) |
tagfunc | Set to vim.lsp.tagfunc (<C-]> uses LSP) |
formatexpr | Set to vim.lsp.formatexpr() (gq uses LSP) |
K | Mapped to vim.lsp.buf.hover() |
| Document colors | Highlighted automatically if server supports it |
Default LSP Keymaps (0.11+)
These ship with NeoVim, no config needed:
| Keymap | Action |
|---|---|
grn | vim.lsp.buf.rename() |
gra | vim.lsp.buf.code_action() |
grr | vim.lsp.buf.references() |
gri | vim.lsp.buf.implementation() |
grt | vim.lsp.buf.type_definition() |
grx | vim.lsp.codelens.run() |
gO | vim.lsp.buf.document_symbol() |
<C-S> | vim.lsp.buf.signature_help() (insert mode) |
Gotcha: These default keymaps only activate when an LSP client is attached. If
grndoesn’t work, check:LspInfo(or:checkhealth lsp) to verify a client is running.
Diagnostics
Diagnostics come from LSP and display as virtual text, signs, and underlines. Configure them globally:
vim.diagnostic.config({
virtual_text = { spacing = 2 },
signs = {
text = {
[vim.diagnostic.severity.ERROR] = "E",
[vim.diagnostic.severity.WARN] = "W",
[vim.diagnostic.severity.INFO] = "I",
[vim.diagnostic.severity.HINT] = "H",
},
},
float = { border = "rounded", source = true },
severity_sort = true,
})
Navigate diagnostics with built-in keymaps:
| Keymap | Action |
|---|---|
]d | Next diagnostic |
[d | Previous diagnostic |
]D | Last diagnostic |
[D | First diagnostic |
<C-W>d | Open diagnostic float |
The Old Way (nvim-lspconfig)
Before 0.11, the community plugin nvim-lspconfig was required. It still works and has configs for 100+ servers:
-- Old style (still works, not needed for 0.11+)
require("lspconfig").lua_ls.setup({
settings = {
Lua = {
runtime = { version = "LuaJIT" },
},
},
})
For 0.11+, vim.lsp.config + vim.lsp.enable replaces nvim-lspconfig for most use cases. The plugin is still useful if you want its pre-built configs without writing them yourself.