This content originally appeared on DEV Community and was authored by Arun Pal
The Problem: When Your LSP Can’t Navigate Your Code
If you’re a C++ developer using Neovim with clangd as your Language Server Protocol (LSP), you might have encountered this frustrating scenario:
- You try to go to a definition with
gd
or<leader>gd
- You get “No locations found” error
- Autocompletion doesn’t work properly
- Your LSP seems broken despite having a valid
compile_commands.json
This issue is particularly common when:
- Your project is built with GCC but you’re using clangd (LLVM-based) as your LSP
- You’re on Ubuntu or other Linux distributions with GCC as the system compiler
- Your CMake-generated
compile_commands.json
looks correct but clangd still fails
Understanding the Root Cause
The issue stems from a fundamental difference between how GCC and Clang handle system headers:
-
GCC knows where its standard library headers are located (e.g.,
/usr/include/c++/14
) - Clangd (based on LLVM/Clang) doesn’t automatically know where GCC’s headers are installed
-
CMake doesn’t include system header paths in
compile_commands.json
because it assumes the compiler knows where they are
This creates a perfect storm where clangd can’t parse your code properly because it can’t find basic headers like <string>
, <vector>
, or <map>
.
The Symptoms in Your LSP Log
When you check your LSP log (~/.local/state/nvim/lsp.log
), you might see errors like:
[pp_file_not_found] Line 1: in included file: 'bits/c++config.h' file not found
[no_member_template] Line 26: no template named 'map' in namespace 'std'
[no_member_suggest] Line 26: no member named 'string' in namespace 'std'
These errors cascade, preventing clangd from building a proper Abstract Syntax Tree (AST) of your code, which breaks navigation, completion, and other LSP features.
The Solution: Enhancing compile_commands.json
Here’s a Python script that automatically fixes this issue by enhancing your compile_commands.json
with the necessary system include paths:
#!/usr/bin/env python3
"""
enhance_compile_commands.py
Enhances compile_commands.json with proper system includes for clangd
"""
import json
import subprocess
import os
import sys
def get_system_includes():
"""Get system include paths from the compiler"""
try:
# Get C++ system includes from GCC
result = subprocess.run(
['g++', '-E', '-x', 'c++', '-', '-v'],
input='',
capture_output=True,
text=True
)
lines = result.stderr.split('\n')
includes = []
in_include_section = False
for line in lines:
if '#include <...> search starts here:' in line:
in_include_section = True
continue
elif 'End of search list.' in line:
in_include_section = False
continue
elif in_include_section and line.strip():
include_path = line.strip()
if os.path.exists(include_path):
includes.append(f'-I{include_path}')
return includes
except Exception as e:
print(f"Error getting system includes: {e}")
return []
def clean_include_paths(parts):
"""Remove non-existent include paths"""
cleaned_parts = []
skip_next = False
for i, part in enumerate(parts):
if skip_next:
skip_next = False
continue
if part == '-I' and i + 1 < len(parts):
# Check if the next part is the include path
include_path = parts[i + 1]
if os.path.exists(include_path):
cleaned_parts.append(part)
cleaned_parts.append(include_path)
else:
print(f"Removing non-existent include path: {include_path}")
skip_next = True
elif part.startswith('-I'):
# Check if the include path exists
include_path = part[2:]
if os.path.exists(include_path):
cleaned_parts.append(part)
else:
print(f"Removing non-existent include path: {include_path}")
else:
cleaned_parts.append(part)
return cleaned_parts
def enhance_compile_commands(input_file, output_file=None):
"""Enhance compile_commands.json with system includes"""
if output_file is None:
output_file = input_file
# Read the original compile_commands.json
with open(input_file, 'r') as f:
commands = json.load(f)
# Get system includes
system_includes = get_system_includes()
print(f"Found {len(system_includes)} system include paths")
# Add system includes to each command
for entry in commands:
if 'command' in entry:
# Split the command into parts
parts = entry['command'].split()
# Clean up non-existent paths first
parts = clean_include_paths(parts)
# Find where to insert the includes (after the compiler command)
insert_pos = 1
for i, part in enumerate(parts):
if part.startswith('-'):
insert_pos = i
break
# Insert system includes
for include in reversed(system_includes):
if include not in parts:
parts.insert(insert_pos, include)
# Rebuild the command
entry['command'] = ' '.join(parts)
# Write the enhanced compile_commands.json
with open(output_file, 'w') as f:
json.dump(commands, f, indent=2)
print(f"Enhanced compile_commands.json written to {output_file}")
if __name__ == "__main__":
compile_commands_path = "build/compile_commands.json"
if len(sys.argv) > 1:
compile_commands_path = sys.argv[1]
if not os.path.exists(compile_commands_path):
print(f"Error: {compile_commands_path} not found")
sys.exit(1)
enhance_compile_commands(compile_commands_path)
How to Use the Script
- Save the script as
enhance_compile_commands.py
in your project root - Make it executable:
chmod +x enhance_compile_commands.py
- Run it after generating your
compile_commands.json
:
# Generate compile_commands.json with CMake
cd build
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ..
cd ..
# Enhance it with system includes
python3 enhance_compile_commands.py build/compile_commands.json
Setting Up Neovim for Success
Here’s a minimal .clangd
configuration file for your project root:
CompileFlags:
CompilationDatabase: build/
Index:
Background: Build
StandardLibrary: Yes
Diagnostics:
UnusedIncludes: Strict
MissingIncludes: Strict
Completion:
AllScopes: Yes
And ensure your Neovim LSP configuration includes clangd. Here’s an example using Mason and nvim-lspconfig:
require("mason-lspconfig").setup({
ensure_installed = {
"clangd"
},
})
require("lspconfig").clangd.setup {
cmd = {
"clangd",
"--background-index",
"--clang-tidy",
"--header-insertion=iwyu",
"--completion-style=detailed",
"--function-arg-placeholders=1",
"--fallback-style=llvm",
"--log=error",
},
}
Additional Tips
- Clear clangd cache after enhancing compile_commands.json:
rm -rf ~/.cache/clangd
- Set LSP log level to reduce noise in Neovim:
vim.lsp.set_log_level("ERROR")
- Verify the fix by running clangd manually:
clangd --check=path/to/your/file.cpp
Why This Works
The script works by:
-
Querying GCC for its internal include search paths using
-E -x c++ -v
-
Adding these paths to every compilation command in
compile_commands.json
- Cleaning up non-existent paths that might cause parsing errors
- Creating a complete compilation database that clangd can use effectively
This bridges the gap between GCC (your build compiler) and clangd (your LSP), enabling proper code parsing and all LSP features.
Common Variations
Depending on your system, you might need to adjust the script:
- Replace
g++
withclang++
if using Clang as your build compiler - Add support for C files by also querying
gcc
for C includes - Handle cross-compilation by specifying the target compiler
Conclusion
This issue affects many C++ developers using modern development tools, but the solution is straightforward once you understand the problem. By enhancing your compile_commands.json
with system include paths, you can enjoy all the powerful features of clangd in Neovim, regardless of which compiler you use for building.
The beauty of this solution is that it’s:
- Automatic: Run once after CMake configuration
- Portable: Works across different GCC versions
- Non-invasive: Doesn’t modify your build system
- Reusable: Can be integrated into your build workflow
Happy coding, and may your LSP always find your definitions!
Have you encountered similar issues with clangd? What other LSP challenges have you faced? Share your experiences in the comments below!
This content originally appeared on DEV Community and was authored by Arun Pal