Skip to content

feat: generate clangd header configuration for the default platformio.ini env so go-to-definition works #533

@zackees

Description

@zackees

Summary

Add a fbuild command (e.g. fbuild clangd-config or a flag like fbuild build --emit-clangd-config) that, given a project directory with a platformio.ini, resolves the default environment and emits a working clangd/VS Code configuration into the project so that "Go to Definition", header hover, and include resolution work in VS Code without any extra user setup.

Concretely, after running the command the user should get (at the project root):

  • compile_commands.json — already produced by fbuild build -t compiledb today
  • .clangd — a YAML config that pins CompileFlags.CompilationDatabase: . and exposes the cross-compiler via --query-driver
  • .vscode/settings.json — disables the built-in MS C/C++ IntelliSense provider for this folder, points clangd.arguments at the correct --compile-commands-dir and --query-driver glob, and recommends the llvm-vs-code-extensions.vscode-clangd extension

The goal: a fresh clone of a FastLED/PlatformIO sketch, plus fbuild clangd-config, plus the clangd VS Code extension, gives the user working symbol navigation into the env's framework headers (Arduino core, ESP-IDF, AVR libc, Teensy core, etc.) on the first open.

Sibling issue for the WASM path: zackees/fastled-wasm#177.

Current state

fbuild already has most of the moving parts but does not stitch them into an "IDE-ready" configuration:

  • fbuild build -t compiledb exists (crates/fbuild-cli/src/cli/args.rs lines ~220–222, parser restricted to "compiledb"), and it is used internally by run_iwyu and run_clang_tool in crates/fbuild-cli/src/cli/clang_tools.rs to materialize compile_commands.json.
  • compile_commands.json is written to both the build dir and the project root by crates/fbuild-build/src/compile_database/database.rs, which is exactly what clangd auto-discovers.
  • crates/fbuild-build/src/compile_database/generate.rs::generate_entries already records the real GCC/G++ binary paths (not cache wrappers) as arguments[0] — these are the paths clangd needs for --query-driver.
  • The default environment is already resolved in crates/fbuild-config/src/ini_parser/mod.rs::get_default_environment (priority: PLATFORMIO_DEFAULT_ENVS env override → [platformio] default_envs → first env in file order).
  • Toolchain include dirs are computed via fbuild_packages::toolchain::clang::find_gcc_builtin_include_dirs() (used in run_iwyu).

What is missing: a thin "emit IDE config" step on top of compiledb that writes .clangd and .vscode/settings.json so that:

  1. clangd does not need any manual setup,
  2. VS Code's MS C/C++ extension does not race against clangd, and
  3. clangd extracts the cross-compiler's builtin headers via --query-driver so symbols defined in toolchain headers (e.g. <Arduino.h>, ESP-IDF <esp_log.h>) resolve.

Today, even after fbuild build -t compiledb succeeds, a fresh checkout opened in VS Code still shows red squiggles on framework includes for most users because clangd is either not enabled, not pointed at a query driver, or fighting the MS extension.

Proposed implementation

New CLI surface

Two options, both fine:

  • Preferred: new subcommand fbuild clangd-config [project_dir] [-e <env>] in crates/fbuild-cli/src/cli/args.rs and dispatched from crates/fbuild-cli/src/cli/dispatch.rs. Implementation lives in a new module crates/fbuild-cli/src/cli/clangd_config.rs.
  • Alternative: new flag --emit-clangd-config on fbuild build that runs after a successful build (or after -t compiledb).

Mechanism

  1. Resolve the default env name:

    • If -e <env> is given, use it.
    • Else call IniParser::get_default_environment() from fbuild-config.
  2. Ensure compile_commands.json exists at the project root: if absent, internally invoke the same code path as fbuild build -t compiledb -e <env> (run_build(..., target = Some("compiledb"), ...), same call used by run_iwyu).

  3. Read the generated compile_commands.json, take the first entry, and pull arguments[0] — that is the absolute path to the env's real cross-compiler (e.g. xtensa-esp32-elf-gcc, avr-g++, arm-none-eabi-g++). Compute its directory (the toolchain bin/) for the --query-driver glob. Also collect any -isysroot / --sysroot / --target= flags present in arguments.

  4. Write <project>/.clangd:

    # Generated by `fbuild clangd-config` — safe to edit, regenerate to refresh.
    CompileFlags:
      CompilationDatabase: .
      # Trust the build's compiler. Avoid clangd's default driver guess.
      Compiler: <abs path to env's g++>
    Diagnostics:
      # Many embedded toolchains emit warnings clangd cannot parse cleanly.
      Suppress: [drv_unknown_argument, unknown-warning-option]
  5. Write <project>/.vscode/settings.json (merge if it exists; only update the clangd-related keys and a couple of intellisense disable keys):

    {
      "C_Cpp.intelliSenseEngine": "disabled",
      "C_Cpp.autoAddFileAssociations": false,
      "clangd.arguments": [
        "--compile-commands-dir=${workspaceFolder}",
        "--query-driver=<toolchain bin glob, e.g. C:/Users/<u>/.platformio/packages/toolchain-xtensa-esp32/bin/*>",
        "--background-index",
        "--clang-tidy",
        "--header-insertion=never",
        "--completion-style=detailed"
      ]
    }

    On Windows the --query-driver glob must use forward slashes and end with * (or *.exe); document this in the generator.

  6. Write <project>/.vscode/extensions.json recommending llvm-vs-code-extensions.vscode-clangd if no extensions.json exists. Do not overwrite an existing one.

  7. Print a short summary: env name, compiler used, files written, and a one-line hint to run clangd: Restart language server in VS Code.

Where to hook

  • New module: crates/fbuild-cli/src/cli/clangd_config.rs (mirrors the small, self-contained shape of clang_tools.rs).
  • Reuse super::build::{normalize_path, run_build} and fbuild_config::ini_parser for env resolution.
  • Reuse fbuild_packages::toolchain::clang::find_gcc_builtin_include_dirs() as a fallback when compile_commands.json arguments do not contain a usable compiler path (defensive).

Alternatives considered

  • .clangd only, no settings.json. Works for users who have clangd already configured, but the most common failure mode is that the MS C/C++ extension is active and clangd is silent. Emitting settings.json solves the dominant complaint.
  • Skip --query-driver, rely on -isystem in compile_commands.json. Works in some envs but fails in others (ESP-IDF, AVR libc) because clangd still chooses its own -resource-dir. --query-driver is the recommended path for cross-toolchains and matches what run_iwyu effectively does today.
  • Push this into the VS Code extension (vscode-fbuild/). Reasonable, but the CLI command works for users on Cursor, Neovim+clangd, Helix, Zed, and CI lint jobs too. The extension can later expose a button that just calls fbuild clangd-config.
  • Generate c_cpp_properties.json for the MS extension. Explicitly avoided — the MS extension cannot follow the cross-toolchain reliably and conflicts with clangd. The whole point is to make clangd the source of truth.

Acceptance criteria

  • fbuild clangd-config (or equivalent flag) exists, is documented in --help, and accepts an optional [project_dir] and -e/--environment.
  • When no env is provided, the command resolves the default env using the same priority rules as IniParser::get_default_environment (PLATFORMIO_DEFAULT_ENVS → [platformio].default_envs → first env).
  • On success, the following files exist at the project root: compile_commands.json, .clangd, .vscode/settings.json. .vscode/extensions.json is created only if absent.
  • .clangd references the project root as the compilation database directory.
  • .vscode/settings.json disables the MS C/C++ IntelliSense engine for this folder and sets clangd.arguments with --compile-commands-dir and --query-driver pointing at the env's toolchain bin/ directory.
  • Re-running the command is idempotent for the generated content and preserves unrelated keys in pre-existing .vscode/settings.json.
  • Works on Linux, macOS, and Windows. Windows paths in .vscode/settings.json use forward slashes (clangd-friendly); the --query-driver glob ends with * and matches the toolchain executables.
  • Manual smoke test on at least one ESP32 and one AVR sketch: opening any src/*.cpp in VS Code (with the clangd extension installed and the MS extension also installed) shows zero red squiggles on framework includes and "Go to Definition" jumps into the toolchain headers.

Test plan

  • Unit tests in crates/fbuild-cli (or a small helper crate) for the file-merge logic: given a pre-existing .vscode/settings.json with unrelated keys, the generator preserves them and only updates the clangd/MS-extension keys.
  • Unit test that extraction of the compiler/toolchain-bin directory from a real compile_commands.json entry (one ESP32, one AVR, one Teensy) yields a sensible --query-driver glob.
  • Integration test under tests/ that runs fbuild clangd-config against a fixture project with [platformio] default_envs = uno and asserts the three files exist and that .clangd mentions the compiler from the compile DB.
  • Cross-platform CI: extend the existing check-windows.yml / check-macos.yml / check-ubuntu.yml jobs to run the new command against one fixture and assert exit 0 plus file presence.
  • Manual: smoke test on a real sketch in VS Code on Windows (path separators are the historical foot-gun).

Notes

  • This is a tooling/IDE-integration feature and does not touch the build pipeline; it sits cleanly on top of the existing compile_database machinery.
  • The same generator can later be reused by the vscode-fbuild extension as a one-click "Configure clangd" action.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions