Table of Contents
TL;DR
- Start with LazyVim’s
:LazyExtrasto 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, RustPersist your picks by importing Extras in your LazyVim spec:
-- lua/plugins/extras.luareturn { { 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
- 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 = {} },}- Mason: install language servers
-- lua/lsp/mason.luarequire("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 },})- LSP capabilities and on_attach
-- lua/lsp/common.lualocal M = {}
-- Advertise completion capabilities to serverslocal cmp_caps = require("cmp_nvim_lsp").default_capabilities()M.capabilities = cmp_caps
-- Buffer-local LSP keymapsfunction 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, }) endend
return M- Configure servers with lspconfig
-- lua/lsp/servers.lualocal 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/JavaScriptsetup("ts_ls", { -- example: prefer local project tsserver single_file_support = false,})
-- Pythonsetup("pyright")
-- Rustsetup("rust_analyzer", { settings = { ["rust-analyzer"] = { cargo = { allFeatures = true } }, },})
-- Gosetup("gopls")- Completion with nvim-cmp
-- lua/cmp.lualocal 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" }, }),})- Diagnostics UX
-- lua/lsp/diagnostics.luavim.diagnostic.config({ virtual_text = false, severity_sort = true, float = { border = "rounded" },})
-- Show diagnostics on CursorHoldvim.api.nvim_create_autocmd("CursorHold", { callback = function() vim.diagnostic.open_float(nil, { focus = false }) end,})- Optional: formatters and linters via none-ls
-- lua/lsp/none-ls.lualocal 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, },})- 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 attachesvim.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”
| Approach | Where it runs | Config surface | Extensibility | Typical tradeoff |
|---|---|---|---|---|
| LazyVim + Extras | Inside Neovim | Toggle via :LazyExtras or import lines | Large (curated modules) | Fastest path; conventions over customization |
| Neovim built-in LSP + lspconfig | Inside Neovim | Lua files you control | Huge (Lua ecosystem) | You own the setup, fewer black boxes |
| VS Code (built-in client) | External GUI | JSON settings + UI | Massive marketplace | Heavier runtime; less terminal-native |
| coc.nvim (Node client in Vim/Neovim) | Embedded Node | JSON + extensions | Rich but divergent | Extra 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
tsserverwrapper and Python’s ecosystem evolve quickly; occasionally, defaults change (e.g., formatting responsibility moving from server to tool). Keep per-project settings inlua/lsp/local.luaand conditionally load. - Formatter conflicts: Don’t format via both server and
none-ls. Decide per-language which owns it and disable the other provider inon_attach. - Performance: Giant monorepos may benefit from turning off
semanticTokensor disabling diagnostics for generated folders. Usevim.lsp.stop_clientfor 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
:LazyExtrasand 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, andLuaSnipfor a manual minimal. - Run
:Masonto 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
-
LazyVim Extras docs: the easiest way to install extras is with the
:LazyExtrascommand. https://www.lazyvim.org/extras ↩ ↩2 ↩3 -
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 ↩
-
nvim-lspconfigprovides quickstart configs and sensible defaults for many servers: https://github.com/neovim/nvim-lspconfig ↩ -
LazyVim Typescript extra: enable via
:LazyExtrasorimport = "lazyvim.plugins.extras.lang.typescript". https://www.lazyvim.org/extras/lang/typescript ↩ -
Language Server Protocol overview from Microsoft: https://microsoft.github.io/language-server-protocol/ ↩
-
Track Neovim releases and breaking changes here: https://github.com/neovim/neovim/releases ↩
-
Treesitter for fast, incremental parsing improves highlighting and structural selection: https://github.com/nvim-treesitter/nvim-treesitter ↩