skip to content
Stephen Van Tran
Table of Contents

TL;DR

  • Start with LazyVim’s :LazyExtras to enable language packs (TS, Python, Go, Rust) instantly.
  • Persist by importing lazyvim.plugins.extras.lang.<lang> in your LazyVim spec.
  • Drop to the minimal manual config below only when you need bespoke keymaps/formatting.

Neovim 0.11 makes the built-in LSP client feel like a first-class part of the editor instead of an add-on. If you’ve been waiting to ditch heavyweight IDEs or legacy coc.nvim, this is the clean, future-proof path: native LSP powered by nvim-lspconfig, servers installed by mason.nvim, and modern completion via nvim-cmp. If you don’t want to hand-wire anything, the pragmatic move is to use LazyVim and install language packs from its Extras—:LazyExtras lets you toggle a full, batteries-included LSP stack per language in seconds.1 Below is a concise setup you can run either way: fast LazyVim Extras for the 80%, or manual wiring for total control.2

Problem

Too many Neovim LSP guides mix plugin stacks, leave edge cases undefined, or assume nightly APIs. The result: broken keymaps, conflicting formatters, and slow completions that feel worse than VS Code.

Stakes

Your editor is a core loop. A minimal, native LSP setup reduces cognitive load and flake. You get predictable hotkeys across languages, consistent diagnostics, and a path to scale from one language to a polyglot monorepo without “works on my machine” drama.3

Contrarian Take

Most days, you don’t need to reinvent LSP. Start with LazyVim’s Extras and enable your language via :LazyExtras; it composes the same primitives (built-in LSP, lspconfig, Mason, nvim-cmp) without the glue code.4 If and when you want fine-grained control, drop to the manual minimal config below and override piece by piece.

Operator Playbook

Quick path (LazyVim Extras) and manual path.

  • Quick path: LazyVim + :LazyExtras
:LazyExtras " open Extras browser
" Pick languages, e.g. Typescript, Python, Go, Rust

Persist your picks by importing Extras in your LazyVim spec:

-- lua/plugins/extras.lua
return {
{ import = "lazyvim.plugins.extras.lang.typescript" },
{ import = "lazyvim.plugins.extras.lang.python" },
{ import = "lazyvim.plugins.extras.lang.go" },
{ import = "lazyvim.plugins.extras.lang.rust" },
-- add more as needed
}

Analytic takeaway: :LazyExtras sets up LSP servers, completion, and sensible keymaps for popular languages with near-zero glue, then you layer small overrides as your needs evolve.1

  • Manual path: native LSP with minimal plugins
  1. Install plugins (lazy.nvim)
-- lua/plugins.lua (or your lazy.nvim spec)
return {
{ "williamboman/mason.nvim", config = true },
{ "williamboman/mason-lspconfig.nvim" },
{ "neovim/nvim-lspconfig" },
{ "hrsh7th/nvim-cmp" },
{ "hrsh7th/cmp-nvim-lsp" },
{ "L3MON4D3/LuaSnip" },
{ "saadparwaiz1/cmp_luasnip" },
-- Optional: visuals & UI
{ "folke/trouble.nvim", opts = {} },
}
  1. Mason: install language servers
-- lua/lsp/mason.lua
require("mason").setup()
require("mason-lspconfig").setup({
ensure_installed = {
"lua_ls", -- Lua
"ts_ls", -- TypeScript/JavaScript (tsserver successor)
"pyright", -- Python
"rust_analyzer", -- Rust
"gopls", -- Go
-- add more as needed
},
})
  1. LSP capabilities and on_attach
-- lua/lsp/common.lua
local M = {}
-- Advertise completion capabilities to servers
local cmp_caps = require("cmp_nvim_lsp").default_capabilities()
M.capabilities = cmp_caps
-- Buffer-local LSP keymaps
function M.on_attach(client, bufnr)
local map = function(mode, lhs, rhs, desc)
vim.keymap.set(mode, lhs, rhs, { buffer = bufnr, desc = desc })
end
map("n", "gd", vim.lsp.buf.definition, "Go to definition")
map("n", "gD", vim.lsp.buf.declaration, "Go to declaration")
map("n", "gr", vim.lsp.buf.references, "References")
map("n", "gi", vim.lsp.buf.implementation, "Implementation")
map("n", "K", vim.lsp.buf.hover, "Hover")
map("n", "<leader>rn", vim.lsp.buf.rename, "Rename symbol")
map("n", "<leader>ca", vim.lsp.buf.code_action, "Code action")
map("n", "[d", vim.diagnostic.goto_prev, "Prev diagnostic")
map("n", "]d", vim.diagnostic.goto_next, "Next diagnostic")
map("n", "<leader>e", vim.diagnostic.open_float, "Line diagnostics")
-- Inlay hints (Neovim 0.10+)
if vim.lsp.inlay_hint then
vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })
end
-- Format on save (opt-in per server)
if client.server_capabilities.documentFormattingProvider then
vim.api.nvim_create_autocmd("BufWritePre", {
buffer = bufnr,
callback = function()
vim.lsp.buf.format({ async = false })
end,
})
end
end
return M
  1. Configure servers with lspconfig
-- lua/lsp/servers.lua
local lsp = require("lspconfig")
local common = require("lsp.common")
local function setup(server, opts)
opts = opts or {}
opts.capabilities = common.capabilities
opts.on_attach = common.on_attach
lsp[server].setup(opts)
end
-- Lua (Neovim config awareness)
setup("lua_ls", {
settings = {
Lua = {
workspace = { checkThirdParty = false },
diagnostics = { globals = { "vim" } },
telemetry = { enable = false },
},
},
})
-- TypeScript/JavaScript
setup("ts_ls", {
-- example: prefer local project tsserver
single_file_support = false,
})
-- Python
setup("pyright")
-- Rust
setup("rust_analyzer", {
settings = {
["rust-analyzer"] = { cargo = { allFeatures = true } },
},
})
-- Go
setup("gopls")
  1. Completion with nvim-cmp
-- lua/cmp.lua
local cmp = require("cmp")
local luasnip = require("luasnip")
cmp.setup({
snippet = {
expand = function(args) luasnip.lsp_expand(args.body) end,
},
mapping = cmp.mapping.preset.insert({
["<C-Space>"] = cmp.mapping.complete(),
["<CR>"] = cmp.mapping.confirm({ select = true }),
["<Tab>"] = cmp.mapping(function(fallback)
if cmp.visible() then cmp.select_next_item()
elseif luasnip.expand_or_jumpable() then luasnip.expand_or_jump()
else fallback() end
end, { "i", "s" }),
["<S-Tab>"] = cmp.mapping(function(fallback)
if cmp.visible() then cmp.select_prev_item()
elseif luasnip.jumpable(-1) then luasnip.jump(-1)
else fallback() end
end, { "i", "s" }),
}),
sources = cmp.config.sources({
{ name = "nvim_lsp" },
{ name = "luasnip" },
}),
})
  1. Diagnostics UX
-- lua/lsp/diagnostics.lua
vim.diagnostic.config({
virtual_text = false,
severity_sort = true,
float = { border = "rounded" },
})
-- Show diagnostics on CursorHold
vim.api.nvim_create_autocmd("CursorHold", {
callback = function()
vim.diagnostic.open_float(nil, { focus = false })
end,
})
  1. Optional: formatters and linters via none-ls
-- lua/lsp/none-ls.lua
local null = require("null-ls")
null.setup({
sources = {
-- JS/TS
null.builtins.formatting.prettier,
-- Python
null.builtins.formatting.black,
null.builtins.diagnostics.ruff,
-- Lua
null.builtins.formatting.stylua,
},
})
  1. Wire it together in init.lua
-- init.lua (minimal glue)
require("lsp.mason")
require("lsp.diagnostics")
require("lsp.servers")
require("cmp")
-- optional: require("lsp.none-ls")
-- Make keymaps buffer-local only when LSP attaches
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(args)
local client = vim.lsp.get_client_by_id(args.data.client_id)
local bufnr = args.buf
if client and bufnr then
require("lsp.common").on_attach(client, bufnr)
end
end,
})

Comparison: four ways to do “LSP”

ApproachWhere it runsConfig surfaceExtensibilityTypical tradeoff
LazyVim + ExtrasInside NeovimToggle via :LazyExtras or import linesLarge (curated modules)Fastest path; conventions over customization
Neovim built-in LSP + lspconfigInside NeovimLua files you controlHuge (Lua ecosystem)You own the setup, fewer black boxes
VS Code (built-in client)External GUIJSON settings + UIMassive marketplaceHeavier runtime; less terminal-native
coc.nvim (Node client in Vim/Neovim)Embedded NodeJSON + extensionsRich but divergentExtra layer vs native client

Analytic takeaway: Start with LazyVim Extras for instant ergonomics; drop to native minimal when you need bespoke behavior. Use coc.nvim only if a specific extension lacks a native equivalent.15

What Could Break This Thesis?

  • API shifts: Minor LSP client API changes may alter function names or signatures between 0.10 and 0.11. Pin plugin versions and skim release notes before upgrading.6
  • Server churn: TypeScript’s tsserver wrapper and Python’s ecosystem evolve quickly; occasionally, defaults change (e.g., formatting responsibility moving from server to tool). Keep per-project settings in lua/lsp/local.lua and conditionally load.
  • Formatter conflicts: Don’t format via both server and none-ls. Decide per-language which owns it and disable the other provider in on_attach.
  • Performance: Giant monorepos may benefit from turning off semanticTokens or disabling diagnostics for generated folders. Use vim.lsp.stop_client for runaway processes.

Outlook

The win with Neovim 0.11 isn’t “new features,” it’s crisp defaults and smaller glue code. If you value time-to-productive, LazyVim’s Extras give you a pre-wired stack that rides those defaults and stays close to upstream; if you value total control, the manual minimal config is a straightforward, stable base. Add Treesitter for syntax, trouble.nvim for quicklist ergonomics, and you’re production-ready either way.7

Operator Checklist

  • Run :LazyExtras and enable language packs you use (TS, Python, Go, Rust). Persist imports in your LazyVim spec.
  • Or add mason.nvim, mason-lspconfig, nvim-lspconfig, nvim-cmp, and LuaSnip for a manual minimal.
  • Run :Mason to install servers (lua_ls, ts_ls, pyright, rust_analyzer, gopls).
  • Drop in on_attach, capabilities, diagnostics config, and per-server overrides.
  • Choose a single formatter owner per language (server vs none-ls).
  • Bind gd, gr, K, ]d/[d, <leader>rn, <leader>ca.
  • Test on one project per language and iterate.

Footnotes

  1. LazyVim Extras docs: the easiest way to install extras is with the :LazyExtras command. https://www.lazyvim.org/extras 2 3

  2. Neovim’s built-in LSP client implements the Language Server Protocol for features like hover, go-to-definition, diagnostics, and formatting. See docs: https://neovim.io/doc/user/lsp.html

  3. nvim-lspconfig provides quickstart configs and sensible defaults for many servers: https://github.com/neovim/nvim-lspconfig

  4. LazyVim Typescript extra: enable via :LazyExtras or import = "lazyvim.plugins.extras.lang.typescript". https://www.lazyvim.org/extras/lang/typescript

  5. Language Server Protocol overview from Microsoft: https://microsoft.github.io/language-server-protocol/

  6. Track Neovim releases and breaking changes here: https://github.com/neovim/neovim/releases

  7. Treesitter for fast, incremental parsing improves highlighting and structural selection: https://github.com/nvim-treesitter/nvim-treesitter