Skip to content
C++
IDE / Editor
Updated 2025-01-01T00:00:00.000Z

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

bash
# 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+ recommended

compile_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.

bash
# 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 compiledb

clangd 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:

yaml
# .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 type

VS Code setup

Install the clangd extension (llvm-vs-code-extensions.vscode-clangd).

json
// .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 — F12 or Ctrl+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
-- 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:

json
// VS Code keybinding (keybindings.json)
{
  "key": "alt+o",
  "command": "clangd.switchSourceHeader"
}
lua
-- 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:

yaml
# .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:

json
// .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

yaml
# .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 results

Background 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 severityLSP severityEditor display
warningWarningYellow squiggle
errorErrorRed squiggle
noteHintBlue squiggle (dimmed)

Disable slow checks for interactive use, run full set in CI:

yaml
# .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:

TriggerAction
Missing #includeAdd the right header automatically
clang-tidy diagnosticApply the suggested fix-it in-place
Missing overrideAdd override to virtual method declaration
Unwanted special memberInsert = delete for copy/move ctor or assignment
Expression under cursorExtract to a named variable
Selected linesExtract to a new function (limited heuristic)
Type var(args)Convert to braced initialisation Type var{args}
lua
-- 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 })
json
// 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.

yaml
# .clangd
Hover:
  ShowAKA: true   # reveal the concrete type behind auto, decltype, typedef

Type deduction hover works across several patterns:

  • auto variables — 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:

EditorHoverGo to definitionFind references
VS Codehover over symbolF12 / Ctrl+ClickShift+F12
NeovimK (or Shift+K in older configs)gdgr
CLiontooltip on hoverCtrl+BAlt+F7
lua
-- 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.

elisp
;; 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:

elisp
;; 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.

json
// 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:

lua
-- 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.

json
// .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.

bash
# 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:

SymptomWhat to look for in the logFix
Red underlines on all STL headersNo compile_commands.json found near file pathSymlink compile_commands.json to project root
Stale diagnostics after a refactorPCH mismatch or preamble rebuild messagesDelete ~/.cache/clangd/ and restart
Wrong file analysedLog shows path from a parent directory's compile DBAdd .clangd with CompileFlags to override
clangd crashes on template-heavy codeclangd: error: unable to handle compilationAdd -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:

bash
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=iwyu mode
Edit on GitHubUpdated 2025-01-01T00:00:00.000Z