Skip to content

Instantly share code, notes, and snippets.

@Strus
Last active January 17, 2026 18:29
Show Gist options
  • Select an option

  • Save Strus/042a92a00070a943053006bf46912ae9 to your computer and use it in GitHub Desktop.

Select an option

Save Strus/042a92a00070a943053006bf46912ae9 to your computer and use it in GitHub Desktop.
How to use clangd C/C++ LSP in any project

How to use clangd C/C++ LSP in any project

tl;dr: If you want to just know the method, skip to How to section

Clangd is a state-of-the-art C/C++ LSP that can be used in every popular text editors like Neovim, Emacs or VS Code. Even CLion uses clangd under the hood. Unfortunately, clangd requires compile_commands.json to work, and the easiest way to painlessly generate it is to use CMake.

For simple projects you can try to use Bear - it will capture compile commands and generate compile_commands.json. Although I could never make it work in big projects with custom or complicated build systems.

But what if I tell you you can quickly hack your way around that, and generate compile_commands.json for any project, no matter how compilcated? I have used that way at work for years, originaly because I used CLion which supported only CMake projects - but now I use that method succesfully with clangd and Neovim.

Method summary

Basically what we need to achieve is to create a CMake file that will generate a compile_commands.json file with information about:

  1. All source files
  2. All include directories
  3. External libraries
  4. Precompiler definitions

We can do that easily without really caring about if the CMake-generate result will compile at all - we don't need to rewrite our existing build system, just hack a CMake file that will generate enough information for Clangd to work.

Prerequisities

  1. CMake
  2. clangd

How to

First, create a CMakeLists.txt file in the root folder of your projects, with content similar to this:

cmake_minimum_required(VERSION 3.8)
project(my_project)

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# Change path from /src if needed, or add more directories
file(GLOB_RECURSE sources
        "${CMAKE_SOURCE_DIR}/src/*.c"
        "${CMAKE_SOURCE_DIR}/src/*.cpp"
        )
# Add precompiler definitions like that:
add_definitions(-DSOME_DEFINITION)

add_executable(my_app ${sources})

# Add more include directories if needed
target_include_directories(my_app PUBLIC "${CMAKE_SOURCE_DIR}/include")

# If you have precompiled headers you can add them like this
target_precompiled_headers(my_app PRIVATE "${CMAKE_SOURCE_DIR}/src/pch.h")

(If your project already uses CMake, then you just need to add set(CMAKE_EXPORT_COMPILE_COMMANDS ON) to your main CMakeLists.txt file.)

Modify hacky CMakeLists.txt according to your project structure, and run:

cmake -S . -G "Unix Makefiles" -B cmake

which will generate the CMake output inside cmake directory. Check if compile_commands.json is there.

NOTE: You need to run that command every time you add/remove a source file in your project.

If you need more (ex. include external libraries like Boost), check out CMake documentation

Now you have two options:

  1. Symlink compile_commands.json to your root project folder:
ln -s cmake/compile_commands.json .

OR

  1. Create .clangd file in your root project folder, with the following contents:
CompileFlags:
  CompilationDatabase: "cmake"

Now open the project in you editor and everything should work (assuming clangd LSP is started).

@xomiachuna
Copy link

Thx, saved me a lot of trouble

@activedecay
Copy link

this worked for me. could probably be improved.

#!/usr/bin/env bash
set -euo pipefail

# Usage:
#   ./gen_compile_commands.sh gcc example.c -o example -I./include -L./lib -lthirdparty

if [[ $# -lt 2 ]]; then
  echo "usage: $0 <compiler> <compiler-args...>" >&2
  exit 1
fi

COMPILER="$1"
shift

BUILD_DIR="$(pwd)"
COMPILER_PATH="$(command -v "$COMPILER")"

SOURCE_FILE=""
INCLUDE_FLAGS=()

# Parse gcc-like arguments
while [[ $# -gt 0 ]]; do
  case "$1" in
    -I*)
      INCLUDE_FLAGS+=("$1")
      shift
      ;;
    -I)
      INCLUDE_FLAGS+=("-I$2")
      shift 2
      ;;
    *.c)
      SOURCE_FILE="$(realpath "$1")"
      shift
      ;;
    *)
      # Ignore link-time flags (-L, -l, -o, etc.)
      shift
      ;;
  esac
done

if [[ -z "$SOURCE_FILE" ]]; then
  echo "error: no source file (.c) provided" >&2
  exit 1
fi

OBJECT_FILE="./$(basename "${SOURCE_FILE%.c}.o")"

cat > compile_commands.json <<EOF
[
  {
    "directory": "$BUILD_DIR",
    "command": "$COMPILER_PATH ${INCLUDE_FLAGS[*]} -o $OBJECT_FILE -c $SOURCE_FILE",
    "file": "$SOURCE_FILE",
    "output": "$OBJECT_FILE"
  }
]
EOF

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment