Skip to content

Why doesn't clangd LSP find my compiler's system headers?

When using clangd as LSP for C/C++ projects, e.g. in nvim, it enables all the nice convenience features we're used to from heavyweight IDEs like VS Code, Eclipse etc. Jump to definition, jump to references, autocompletion etc.

But sometimes, when using more complex compiler setups like multiple different compilers installed in parallel, cross compilation etc., it can suddenly fail to find standard includes like <fstream>, <string> (aka system headers), making it completely useless. What's happening there?

The problem

clangd must figure out the whole compilation environment - preprocessor symbols (aka #defines), compiler flags, language version, include paths etc. - to interpret the source code correctly outside of the actual build. The standard way to achieve this is to make the build system (e.g. CMake) generate a compilation database, a JSON file called compile_commands.json, which contains the compiler call, including command line parameters, for each source file.

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

When used as LSP in nvim, clangd is smart enough to find compile_commands.json in the build subdirectory on its own. However, by default, CMake will only put the project-specific include paths there, not the standard include paths used implicitly by the compiler. So clangd is forced to hallucinate these paths (a more polite word would be heuristics).

This can work OK if we're lucky, but it can as well go wrong, leave include paths missing or even pointing to the wrong places.

The solution

Luckily, CMake has an option to export the standard directories implicitly used by the compiler. When this option is activated, the system include paths will be exported as -isystem arguments to the compilation database:

set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES})

Now clangd has explicit information about system include paths and won't need to hallucinate them any more. 🎉