Fixing Clangd “No Locations Found” Error in Neovim for C++ Projects



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:

  1. GCC knows where its standard library headers are located (e.g., /usr/include/c++/14)
  2. Clangd (based on LLVM/Clang) doesn’t automatically know where GCC’s headers are installed
  3. 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

  1. Save the script as enhance_compile_commands.py in your project root
  2. Make it executable: chmod +x enhance_compile_commands.py
  3. 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

  1. Clear clangd cache after enhancing compile_commands.json:
   rm -rf ~/.cache/clangd
  1. Set LSP log level to reduce noise in Neovim:
   vim.lsp.set_log_level("ERROR")
  1. Verify the fix by running clangd manually:
   clangd --check=path/to/your/file.cpp

Why This Works

The script works by:

  1. Querying GCC for its internal include search paths using -E -x c++ -v
  2. Adding these paths to every compilation command in compile_commands.json
  3. Cleaning up non-existent paths that might cause parsing errors
  4. 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++ with clang++ 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