Language servers (LSP)¶
The Language Server Protocol is an editor-agnostic protocol for code intelligence (diagnostics, hover, go-to-definition, completion, rename, formatting, code actions). An editor (the client) launches a per-language server over stdio/TCP and asks it questions about the buffer; the server speaks one language well, the editor stays language-agnostic. Coding agents that read source under the hood (Claude Code, Cursor, OpenCode, Codex) increasingly piggyback on the same servers to get type-aware context instead of grep-only context.
This repo provisions LSP from three independent surfaces. Mixing surfaces is normal — each has different reach.
The three LSP surfaces in this repo¶
| Surface | Who manages it | Where state lives | When to use |
|---|---|---|---|
| NeoVim + Mason | mason.nvim auto-downloads on demand |
~/.local/share/nvim/mason/bin/ |
Editor-only intelligence inside nvim. Default for most languages. |
| Claude Code plugin marketplace | Claude Code, via the enabledPlugins map in ~/.claude/settings.json |
~/.claude/plugins/ |
Adds LSP context to the agent's buffer reads. Independent of the nvim install. |
| System packages (ansible / brew) | dot_ansible/roles/devtools/, Brewfiles |
$PATH (e.g. /opt/homebrew/bin/, ~/.local/bin/) |
Only when the LSP doubles as a useful CLI (linter, formatter, CI tool). |
You can have the same server installed via two surfaces simultaneously — they run as separate processes from separate binaries, no conflict.
Currently installed¶
Via NeoVim (LazyVim extras)¶
Active extras live in dot_config/nvim/lazyvim.json. Each lang.* extra pulls in the relevant LSP, treesitter parser, formatter, and linter via Mason on first use:
| Extra | Languages covered | LSPs (via Mason) |
|---|---|---|
lazyvim.plugins.extras.lang.python |
Python | basedpyright, ruff |
lazyvim.plugins.extras.lang.json |
JSON / JSON5 / JSONC | jsonls (+ SchemaStore.nvim) |
lazyvim.plugins.extras.lang.markdown |
Markdown | marksman (+ markdownlint-cli2) |
lazyvim.plugins.extras.lang.toml |
TOML | taplo |
lazyvim.plugins.extras.lang.docker |
Dockerfile, docker-compose | dockerls, docker-compose-language-service |
lazyvim.plugins.extras.formatting.black |
Python (formatter only) | not an LSP — black via conform.nvim |
LazyVim's core spec also wires up lua_ls (Lua language server) automatically through lazydev.nvim — no extras file needed. SchemaStore.nvim is loaded once and shared by JSON / YAML LSPs.
LSP infra plugins from create_lazy-lock.json: nvim-lspconfig, mason.nvim, mason-lspconfig.nvim, nvim-cmp + cmp-nvim-lsp, conform.nvim (formatting), nvim-lint (linting), nvim-treesitter, lazydev.nvim, SchemaStore.nvim.
The file
dot_config/nvim/lua/exact_plugins/example.luais inert — line 3 isif true then return {} end. It's a reference template showing how to override LSP server settings, but it isn't loaded. Don't read it as the active config; the source of truth islazyvim.jsonplus any non-example file underlua/exact_plugins/.
Via Claude Code plugins¶
dot_claude/modify_settings.json overlays enabledPlugins into ~/.claude/settings.json:
pyright-lsp@claude-plugins-official— Python LSP for Claude Code, sourced from the official marketplace.claude-hud@claude-hud— the statusline plugin that shows the "LSP Plugin Recommendation" prompt when you open a file whose extension matches a marketplace LSP that isn't installed yet (e.g. opening a.gofile triggers a popup forgopls-lsp@claude-plugins-official).
The overlay deep-merges via a hook-aware jq script (details) so Claude / CodeIsland runtime additions to hooks.* arrays are preserved instead of replaced.
Via ansible / system packages¶
taplo— installed bydot_ansible/roles/devtools/tasks/main.yml(system-wide CLI:taplo fmt,taplo lint). Note: also auto-installed bylazyvim.plugins.extras.lang.tomlinto Mason. Two binaries, both fine.
Runtime prerequisites (not LSPs themselves, but most LSPs need them):
node(LTS) andtree-sitter-clifromdot_ansible/roles/lazyvim_deps/tasks/main.yml— needed by Mason to install Node-based LSPs (jsonls, dockerls, vtsls, yamlls, bashls, …).- mise toolchains in
dot_config/mise/config.toml.tmpl:node lts,rust latest,dotnet latest,ruby 3. Adding alang.go/lang.rustextra later does not require new ansible work — Mason picks up the toolchain.
Recommended additions¶
These are unenabled LSPs that would have noticeable value given the languages this repo itself contains and the languages the user edits frequently. Each item lists where to flip the switch — do it when you want it, not preemptively.
| Language | NeoVim path | Claude Code path | Notes |
|---|---|---|---|
| Go | lazyvim.plugins.extras.lang.go (gopls + goimports via Mason) |
gopls-lsp@claude-plugins-official |
This is the popup in the screenshot. Toolchain (Go) is not preinstalled by mise — the extra installs gopls but you need go in $PATH separately. |
| YAML | lazyvim.plugins.extras.lang.yaml (yamlls + SchemaStore) |
n/a | High value for this repo: dot_ansible/, GitHub Actions, MkDocs. SchemaStore already loaded for JSON. |
| Bash | lazyvim.plugins.extras.lang.bash (bashls + shellcheck + shfmt) |
n/a | High value for dot_*scripts/ and .chezmoiscripts/. shellcheck is already used by pre-commit. |
| Rust | lazyvim.plugins.extras.lang.rust (rust-analyzer via Mason) |
n/a | Rust toolchain already provisioned via mise. |
| TypeScript | lazyvim.plugins.extras.lang.typescript (vtsls) |
n/a | Node already installed; the inert example.lua references the older tsserver setup, ignore it and use the modern extra. |
| Lua | already on (LazyVim default via lazydev) | n/a | No action. |
For Claude Code, the official marketplace ships an *-lsp@claude-plugins-official plugin per language; inspect available ones with /plugin list inside Claude or watch for claude-hud popups when opening unfamiliar file types. Enable only the ones for languages you actually edit in Claude — each adds startup cost.
Adding a new LSP¶
To NeoVim¶
- Inside
nvim, run:LazyExtras. Toggle thelang.<language>row with<CR>. LazyVim writes the change into~/.config/nvim/lazyvim.json. lazyvim.jsonis a normal tracked file in this repo (nocreate_/modify_prefix), so propagate to other machines by committing the diff:chezmoi re-add ~/.config/nvim/lazyvim.jsonthengit -C "$(chezmoi source-path)" add+ commit.- Restart
nvim; on first use of the language Mason auto-installs the binary.:LspInfoconfirms the server attached.
create_lazy-lock.json is seed-once (why) — chezmoi apply won't update it. To refresh the lock baseline after adding plugins:
To Claude Code¶
Edit dot_claude/modify_settings.json → add to the enabledPlugins map inside the overlay heredoc:
"enabledPlugins": {
"pyright-lsp@claude-plugins-official": true,
"claude-hud@claude-hud": true,
"gopls-lsp@claude-plugins-official": true
}
Then chezmoi apply ~/.claude/settings.json. The hook-aware merger preserves any runtime entries Claude / CodeIsland have added to hooks.*.
To ansible (system-wide CLI)¶
Only when the LSP also runs as a CLI tool you want in $PATH (e.g. linting in pre-commit, CI). Add to dot_ansible/roles/devtools/tasks/main.yml following the existing taplo pattern. For nvim-only intelligence, prefer Mason — keeps the binary out of $PATH and lets nvim manage versions independent of the system package.
Cross-tool notes¶
- Mason
pyright-language-servervs Claudepyright-lsp@claude-plugins-official— separate processes, separate binaries (Mason →~/.local/share/nvim/mason/bin/, Claude plugin →~/.claude/plugins/). The two clients (nvim, Claude) each spawn their own server; no shared state. taploansible install vs Mason install — same situation. The ansible binary is in$PATHfor shell use (taplo fmt some.toml), the Mason binary is what nvim'slspconfigspawns. Keeping both is intentional.- Toolchain ≠ LSP —
miseprovides language runtimes, not LSPs.node ltslets Mason install JS-based LSPs;rust latestmakeslazyvim.plugins.extras.lang.rustwork without further setup.
Pitfalls (LSP-adjacent)¶
pitfalls/tree-sitter-cli-empty-native-binary.md— npm postinstall sometimes downloads an emptytree-sitterbinary; affects LSP-adjacent syntax tooling. Aggregate health check covers it.pitfalls/nvim-fs-find-enoent-stale-cwd.md— Neovim 0.12 startup crash from plugins (avante, lualine) callingvim.fs.findwith a stale cwd. Not LSP-specific but representative of the plugin-ecosystem fragility you'll hit when adding new servers.
Verification¶
# In nvim
:Mason # list installed servers and their state
:LspInfo # show the active server attached to the current buffer
:checkhealth lsp
# Claude Code
ls ~/.claude/plugins/ # installed plugin trees
jq .enabledPlugins ~/.claude/settings.json
# System CLIs
taplo --version
shellcheck --version
# Docs (after editing this page)
uv run mkdocs build --strict
See also¶
- agent-overlays — how the Claude Code overlay merges, including
enabledPluginsand the@claude-plugins-officialmarketplace. - chezmoi-prefixes — why
lazy-lock.jsonusescreate_and how to refresh the baseline. - agent-skills — sibling extension surface for Claude Code (skills, not LSPs).