clangd — C++ Language Server
"clangd setup for VS Code, Neovim, and CLion: compile_commands.json, .clangd config, cross-compilation, and performance tuning."
TL;DR
clangd is the official C++ language server. It provides completion, diagnostics, go-to-definition, find-references, hover, and code actions in any LSP-compatible editor. It reads compile_commands.json to understand your build.
Installation
# Ubuntu/Debian
sudo apt install clangd # or: clangd-18, clangd-17
# macOS
brew install llvm # includes clangd
export PATH="/opt/homebrew/opt/llvm/bin:$PATH"
# Windows — download LLVM release from llvm.org/releases
# Comes with clang, clangd, clang-tidy, clang-format
# Check version
clangd --version # 17+ recommendedcompile_commands.json — the key
clangd needs compile_commands.json to understand your include paths, defines, and language version. Without it, every third-party include will be a red underline.
# CMake — easiest
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
ln -s build/compile_commands.json compile_commands.json # project root
# Bear — wraps any build system
bear -- make -j$(nproc) # intercepts compile commands
bear -- ninja -C build # same, for Ninja
# Meson
meson setup build
cd build && ninja -n | bear --append -- sh # or: meson compiledbclangd searches for compile_commands.json starting from the source file's directory and walking up to the root. Placing it (or a symlink) in the project root covers all files.
.clangd config file
Place .clangd in the project root for project-wide settings:
# .clangd
CompileFlags:
Add:
- "-Wall"
- "-Wextra"
- "-std=c++20" # override if compile_commands.json has wrong standard
Remove:
- "-W*" # strip all warnings (if they spam clangd output)
- "--target=*" # strip cross-compilation target (for host clangd)
Diagnostics:
ClangTidy:
Add:
- modernize-*
- bugprone-*
- performance-*
Remove:
- modernize-use-trailing-return-type
InlayHints:
Enabled: true
ParameterNames: true
DeducedTypes: true
Designators: true
Hover:
ShowAKA: true # show both "auto" and the deduced typeVS Code setup
Install the clangd extension (llvm-vs-code-extensions.vscode-clangd).
// .vscode/settings.json
{
"clangd.path": "/usr/bin/clangd",
"clangd.arguments": [
"--background-index",
"--clang-tidy",
"--completion-style=detailed",
"--header-insertion=iwyu", // Insert missing #include on completion
"--pch-storage=memory", // PCH in memory — faster on SSDs
"-j=8" // parallel background indexing threads
],
// Disable IntelliSense — conflicts with clangd
"C_Cpp.intelliSenseEngine": "disabled",
"C_Cpp.autocomplete": "disabled"
}Key features:
- Go to definition —
F12orCtrl+Click - Find all references —
Shift+F12 - Rename symbol —
F2 - Code actions —
Ctrl+.(fix includes, apply clang-tidy fixes) - Hover — shows type, documentation, or overloads
Neovim setup (nvim-lspconfig)
-- lua/plugins/lsp.lua (lazy.nvim)
return {
"neovim/nvim-lspconfig",
config = function()
local lspconfig = require("lspconfig")
lspconfig.clangd.setup({
cmd = {
"clangd",
"--background-index",
"--clang-tidy",
"--completion-style=detailed",
"-j=8",
},
on_attach = function(client, bufnr)
-- Key bindings
local opts = { buffer = bufnr }
vim.keymap.set("n", "gd", vim.lsp.buf.definition, opts)
vim.keymap.set("n", "gr", vim.lsp.buf.references, opts)
vim.keymap.set("n", "K", vim.lsp.buf.hover, opts)
vim.keymap.set("n", "<leader>rn", vim.lsp.buf.rename, opts)
vim.keymap.set("n", "<leader>ca", vim.lsp.buf.code_action, opts)
end,
})
end,
}With mason.nvim, MasonInstall clangd handles the binary.
Header / source switching
clangd provides clangd/switchSourceHeader — jump between .h and .cpp:
// VS Code keybinding (keybindings.json)
{
"key": "alt+o",
"command": "clangd.switchSourceHeader"
}-- Neovim
vim.keymap.set("n", "<A-o>", "<cmd>ClangdSwitchSourceHeader<cr>")Cross-compilation
When cross-compiling (e.g., ARM firmware), the host clangd may not understand the target's system headers. Options:
# .clangd — strip target flags, point to sysroot
CompileFlags:
Remove:
- "--target=arm-none-eabi"
- "-mcpu=*"
- "-mfpu=*"
Add:
- "--target=arm-none-eabi"
- "-isysroot=/usr/arm-none-eabi"Or run a cross-clangd that matches the target toolchain:
// .vscode/settings.json
{
"clangd.path": "/usr/lib/llvm-17/bin/clangd",
"clangd.arguments": [
"--query-driver=/usr/bin/arm-none-eabi-g++"
]
}--query-driver tells clangd to query the cross-compiler for its built-in include paths.
Performance tuning
# .clangd — for large projects
CompileFlags:
# Limit PCH to avoid high memory
Add: ["-Wno-everything"] # in PCH context only
# Shell arguments
clangd \
--background-index \
--pch-storage=disk \ # PCH on disk (less memory, slower startup)
-j=4 \ # fewer indexing threads on constrained machines
--limit-references=100 \ # cap reference results for speed
--limit-results=50 # cap completion resultsBackground indexing builds a cache at ~/.cache/clangd/. Subsequent sessions are fast.
Diagnostics inline
clangd runs clang-tidy checks in the editor if --clang-tidy is passed and .clang-tidy exists. Severity mapping:
| clang-tidy severity | LSP severity | Editor display |
|---|---|---|
| warning | Warning | Yellow squiggle |
| error | Error | Red squiggle |
| note | Hint | Blue squiggle (dimmed) |
Disable slow checks for interactive use, run full set in CI:
# .clangd — fast interactive checks only
Diagnostics:
ClangTidy:
Add: [bugprone-*, modernize-use-nullptr, modernize-use-override]
Remove: [bugprone-easily-swappable-parameters]Code Actions and Quick Fixes
clangd's lightbulb menu is a one-keystroke refactoring toolkit. Press Ctrl+. in VS Code (or <leader>ca mapped to vim.lsp.buf.code_action() in Neovim) on any underlined symbol or diagnostic to get a context-sensitive list of fixes.
Common actions clangd offers:
| Trigger | Action |
|---|---|
Missing #include | Add the right header automatically |
| clang-tidy diagnostic | Apply the suggested fix-it in-place |
Missing override | Add override to virtual method declaration |
| Unwanted special member | Insert = delete for copy/move ctor or assignment |
| Expression under cursor | Extract to a named variable |
| Selected lines | Extract to a new function (limited heuristic) |
Type var(args) | Convert to braced initialisation Type var{args} |
-- Neovim: trigger code actions on the current line or visual selection
vim.keymap.set({ "n", "v" }, "<leader>ca", vim.lsp.buf.code_action, { buffer = bufnr })// VS Code: the default binding — shown here for reference
{
"key": "ctrl+.",
"command": "editor.action.quickFix",
"when": "editorHasCodeActionsProvider && editorTextFocus && !editorReadonly"
}The --header-insertion=iwyu flag (set in clangd arguments) controls whether missing-include actions propose the minimal include or the umbrella header. iwyu picks the precise header; never disables automatic insertion entirely.
Hover and Type Deduction
ShowAKA: true makes auto transparent. With this option in .clangd, hovering over auto x = getResult(); shows "auto (= std::expected<Widget, std::error_code>)" instead of just "auto", which saves a round-trip to the definition site.
# .clangd
Hover:
ShowAKA: true # reveal the concrete type behind auto, decltype, typedefType deduction hover works across several patterns:
autovariables — shows the deduced type alongside the keyword- Template instantiations — hovering
std::vector<auto>in a deduced context shows the resolved element type decltype(expr)— shows what the expression's type resolves to without running the compiler separately
Navigation shortcuts that complement hover:
| Editor | Hover | Go to definition | Find references |
|---|---|---|---|
| VS Code | hover over symbol | F12 / Ctrl+Click | Shift+F12 |
| Neovim | K (or Shift+K in older configs) | gd | gr |
| CLion | tooltip on hover | Ctrl+B | Alt+F7 |
-- Neovim hover with border (nicer presentation)
vim.lsp.handlers["textDocument/hover"] = vim.lsp.with(
vim.lsp.handlers.hover,
{ border = "rounded" }
)
vim.keymap.set("n", "K", vim.lsp.buf.hover, { buffer = bufnr })Hover also surfaces documentation comments — clangd parses Doxygen (/** */, @param, @return) and Javadoc-style (///) and renders them in the hover popup. VS Code displays them as Markdown; Neovim renders them via vim.lsp.util.convert_input_to_markdown_lines.
clangd with Emacs (eglot / lsp-mode)
Emacs 29 ships eglot built-in — zero external packages needed for basic clangd support. Add a single hook to your init file and clangd starts automatically for C++ buffers.
;; Emacs 29+ — eglot (built-in)
(add-hook 'c++-mode-hook 'eglot-ensure)
(add-hook 'c-mode-hook 'eglot-ensure)
;; Pass extra clangd flags
(with-eval-after-load 'eglot
(add-to-list 'eglot-server-programs
'((c++-mode c-mode)
. ("clangd"
"--background-index"
"--clang-tidy"
"--completion-style=detailed"
"--header-insertion=iwyu"
"-j=8"))))Key bindings eglot provides out of the box: M-. (go to definition), M-, (go back), M-? (find references), M-' (rename symbol), C-c C-a (code actions).
For a richer experience, lsp-mode with lsp-ui adds a sideline diagnostic panel and breadcrumb navigation:
;; lsp-mode alternative (from MELPA)
(use-package lsp-mode
:hook ((c++-mode . lsp-deferred)
(c-mode . lsp-deferred))
:custom
(lsp-clients-clangd-args
'("--background-index"
"--clang-tidy"
"--completion-style=detailed"
"--header-insertion=iwyu"
"--pch-storage=memory"
"-j=8"))
:bind (:map lsp-mode-map
("M-." . lsp-find-definition)
("M-?" . lsp-find-references)
("<f2>" . lsp-rename)
("C-c C-a" . lsp-execute-code-action)))
(use-package lsp-ui
:hook (lsp-mode . lsp-ui-mode)
:custom
(lsp-ui-sideline-show-diagnostics t)
(lsp-ui-sideline-show-code-actions t)
(lsp-ui-doc-enable t))Both eglot and lsp-mode honour .clangd and compile_commands.json automatically — no extra Emacs configuration is needed for project-specific settings.
Remote Development with clangd
clangd runs where the code lives, not where your editor lives. The LSP protocol is editor-agnostic JSON-RPC, so the language server runs on the remote machine and communicates over the existing SSH or container connection.
VS Code Remote SSH is the most common pattern. VS Code installs a lightweight server process on the remote host, and the clangd extension runs inside that server. The compile_commands.json lives on the remote filesystem; no special path mapping is required.
// Remote .vscode/settings.json (stored on the remote host)
{
"clangd.path": "/usr/bin/clangd-18",
"clangd.arguments": [
"--background-index",
"--clang-tidy",
"-j=4"
]
}The background index cache (~/.cache/clangd/) is written on the remote machine, so re-opening the project later is fast even over a slow connection.
Neovim remote workflows use distant.nvim or nvim-remote-containers:
-- distant.nvim: LSP client talks to clangd running on the remote
-- After :DistantConnect ssh://user@host
require("distant"):setup({
["ssh://my-dev-server"] = {
lsp = {
["clangd"] = {
cmd = { "clangd", "--background-index", "--clang-tidy" },
},
},
},
})For Docker containers, VS Code Dev Containers mounts the workspace and runs clangd inside the container image. The devcontainer.json can install clangd as part of the container build, ensuring the language server matches the exact compiler version used in the container.
// .devcontainer/devcontainer.json
{
"image": "mcr.microsoft.com/devcontainers/cpp:1-ubuntu-24.04",
"features": {
"ghcr.io/devcontainers/features/llvm:1": { "version": "18" }
}
}SSH forwarding of the LSP port is never needed — the editor extension manages the subprocess or SSH tunnel internally.
Debugging clangd Issues
When clangd silently ignores files or shows wrong diagnostics, verbose logging is the first tool to reach for.
# Capture a full verbose log while running clangd standalone
clangd --log=verbose 2>clangd.log
# Check a single file without an editor
clangd --check=src/my_component.cpp--check runs clangd's analysis on one file and prints diagnostics to stdout — useful for CI or for isolating problems without launching an editor. --log=verbose shows which compile_commands.json was loaded, which headers triggered PCH builds, and any parse errors.
Common problems and how to read the log:
| Symptom | What to look for in the log | Fix |
|---|---|---|
| Red underlines on all STL headers | No compile_commands.json found near file path | Symlink compile_commands.json to project root |
| Stale diagnostics after a refactor | PCH mismatch or preamble rebuild messages | Delete ~/.cache/clangd/ and restart |
| Wrong file analysed | Log shows path from a parent directory's compile DB | Add .clangd with CompileFlags to override |
| clangd crashes on template-heavy code | clangd: error: unable to handle compilation | Add -fno-delayed-template-parsing in .clangd |
When multiple compile_commands.json files exist in a tree, clangd picks the closest one walking up from the source file. Use --path-mappings to remap paths if the build directory is on a different mount point or filesystem:
clangd --path-mappings="/remote/build=/local/build"For VS Code, enable the clangd output channel (View → Output → clangd) to see the same log lines without dropping to a terminal.
See Also
- clang-tidy — static analysis and modernisation checks that clangd runs inline
- clang-format — auto-formatting that clangd can trigger on save
- CMake — the canonical way to generate
compile_commands.json - include-what-you-use — IWYU works alongside clangd's
--header-insertion=iwyumode