02 — Editor & Debugger

Prereq: 01_toolchain_setup.md complete. clang++ points to brew LLVM. Time budget: One focused evening (~2–3 hours) to get VSCode fully green, plus a debugger dry run.

Your editor is where you’ll live for twelve months. Configure it once, correctly, on the tools that C++ people actually use in 2026 — not the defaults VSCode nudges you toward.


The choice: VSCode + clangd, or CLion

Both are valid. Pick one and commit for at least Phase 0. Switching editors mid-phase burns a weekend you don’t have.

Aspect

VSCode + clangd + CodeLLDB

CLion

Cost

Free

Free for non-commercial use since May 2025; requires JetBrains account.

Setup effort

~30 minutes of config

~5 minutes; opinionated defaults just work

Indexing quality

Excellent (uses real clang frontend)

Excellent (also uses clangd under the hood since 2023)

Debugger UX

CodeLLDB — solid, no frills

Best-in-class visual debugger

Memory footprint

~500 MB with C++ project loaded

~1.5–2 GB

CMake integration

Manual cmake -B build or CMake Tools extension

Built-in and deeply integrated

Refactoring

Basic (rename, extract var/func)

Comprehensive

Terminal integration

Native VSCode terminal

Built-in

Recommendation for Raghul

Default — lighter, closer to CLI reality

Fine alternative if you already use JetBrains at work

Verdict: Start with VSCode + clangd + CodeLLDB. It keeps you close to the command line, which is where study partners and production debug sessions live. Revisit CLion in Phase 2 if you want it. This file walks you through the VSCode path in detail and gives a quick CLion setup at the end.


What most people get wrong

Two failure modes dominate here. First: installing Microsoft’s “C/C++” extension (ms-vscode.cpptools) AND clangd and letting them fight for the same file. Symptoms: red squiggles on valid code, duplicated hover popups, autocompletes that disagree. Pick clangd. Disable cpptools’ IntelliSense (keep only its debugger if you use it — but you’re using CodeLLDB, so uninstall cpptools entirely).

Second, and worse: skipping the debugger and relying on std::cout printf-debugging like it’s 2005. You will fight this instinct all month. When code misbehaves, your reflex must become: set a breakpoint, run, inspect. Not: add a print, recompile, guess.


VSCode: extensions

Install exactly these four. No more.

Extension ID

Purpose

Notes

llvm-vs-code-extensions.vscode-clangd

Language server: completion, diagnostics, go-to-def, hover, format.

Official LLVM extension.

vadimcn.vscode-lldb (CodeLLDB)

Debugger front-end that speaks to lldb.

Do NOT use ms-vscode.cpptools’ debugger.

twxs.cmake

Syntax highlighting for CMakeLists.txt.

Just highlighting, not the ms-vscode CMake Tools.

ms-vscode.cmake-tools (optional)

CMake build target picker, one-click build/run.

Nice-to-have. Skip if you prefer terminal.

Explicitly do NOT install:

  • ms-vscode.cpptools — conflicts with clangd on the same files; its own IntelliSense is inferior to real clang.

  • Any “C++ IntelliSense” third-party extension.

Install from the terminal so you get the exact IDs, not a lookalike:

code --install-extension llvm-vs-code-extensions.vscode-clangd
code --install-extension vadimcn.vscode-lldb
code --install-extension twxs.cmake
# optional:
code --install-extension ms-vscode.cmake-tools

VSCode: user settings.json

Open the command palette (Cmd+Shift+P), run Preferences: Open User Settings (JSON), and merge in:

{
  "clangd.path": "/opt/homebrew/opt/llvm/bin/clangd",
  "clangd.arguments": [
    "--background-index",
    "--clang-tidy",
    "--completion-style=detailed",
    "--header-insertion=iwyu",
    "--pch-storage=memory",
    "--all-scopes-completion",
    "--suggest-missing-includes",
    "-j=4"
  ],
  "clangd.onConfigChanged": "restart",
  "[cpp]": {
    "editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd",
    "editor.formatOnSave": true,
    "editor.tabSize": 4,
    "editor.insertSpaces": true,
    "editor.rulers": [100]
  },
  "editor.inlayHints.enabled": "onUnlessPressed",
  "files.associations": {
    "*.tpp": "cpp",
    "*.ipp": "cpp"
  }
}

Rationale for each clangd argument:

  • --background-index — build a project-wide symbol index in the background. Enables fast global “go to symbol.”

  • --clang-tidy — run clang-tidy diagnostics inline. Free code review.

  • --completion-style=detailed — richer completion items (parameter names, return types).

  • --header-insertion=iwyu — add #includes only when clangd thinks the file directly needs them. Cuts include bloat.

  • --pch-storage=memory — keep precompiled headers in RAM instead of disk. Faster on Apple Silicon.

  • --all-scopes-completion — offer completions from all namespaces, not just currently-included ones.

  • --suggest-missing-includes — clangd will point you at the header you forgot to include.

  • -j=4 — four background threads. Adjust to your CPU core count; 4 is fine on M1/M2/M3.


Project-level .clangd config

Drop a .clangd file at the root of any project. It tells clangd how to interpret files it can’t find in compile_commands.json (e.g., new files you just added).

# .clangd
CompileFlags:
  Add:
    - -std=c++20
    - -Wall
    - -Wextra
    - -Wpedantic
  Compiler: /opt/homebrew/opt/llvm/bin/clang++

Diagnostics:
  ClangTidy:
    Add:
      - modernize-*
      - performance-*
      - readability-*
      - bugprone-*
    Remove:
      - modernize-use-trailing-return-type
      - readability-magic-numbers
      - readability-identifier-length
  UnusedIncludes: Strict
  MissingIncludes: Strict

InlayHints:
  Enabled: Yes
  ParameterNames: Yes
  DeducedTypes: Yes

Commit this file alongside your source. Every project in Phase 0 onward gets one.


compile_commands.json — the file that makes clangd real

clangd is only as smart as the flags it thinks you’re using. It reads compile_commands.json, a JSON array where each entry says “for this source file, use these exact compiler flags.” Without it, clangd falls back to guessing and you get spurious errors on perfectly valid code.

Generate it two ways:

With CMake (preferred):

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)   # in CMakeLists.txt

Then configure:

cmake -B build -G Ninja
ln -sf build/compile_commands.json .    # symlink to project root so clangd finds it

The symlink is important. clangd searches upward from each source file for compile_commands.json, and it looks in the project root first, not in build/.

Without CMake (for single-file drills): use bear:

brew install bear
bear -- make          # or: bear -- clang++ ...

bear intercepts your build commands and writes compile_commands.json in the current directory.


launch.json for CodeLLDB

In your project, create .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "lldb",
      "request": "launch",
      "name": "Debug (current target)",
      "program": "${workspaceFolder}/build/${input:targetName}",
      "args": [],
      "cwd": "${workspaceFolder}",
      "preLaunchTask": "cmake-build",
      "stopOnEntry": false
    },
    {
      "type": "lldb",
      "request": "launch",
      "name": "Debug tests",
      "program": "${workspaceFolder}/build/tests",
      "args": [],
      "cwd": "${workspaceFolder}",
      "preLaunchTask": "cmake-build"
    }
  ],
  "inputs": [
    {
      "id": "targetName",
      "type": "promptString",
      "description": "Target binary name (e.g., word_counter)",
      "default": "main"
    }
  ]
}

And .vscode/tasks.json for the pre-launch build:

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "cmake-build",
      "type": "shell",
      "command": "cmake --build build",
      "group": { "kind": "build", "isDefault": true },
      "problemMatcher": ["$gcc"]
    },
    {
      "label": "cmake-configure",
      "type": "shell",
      "command": "cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug"
    }
  ]
}

The debugger workflow — the seven moves

These are the only interactions you need for 95% of debug sessions. Practice each one on a broken drill.

  1. Set a breakpoint. Click the gutter next to a line number. Red dot appears. In terminal lldb: b file.cpp:42.

  2. Run. F5 in VSCode. In lldb: run (or r).

  3. Step over. F10. Executes the current line without descending into function calls. lldb: n (next).

  4. Step into. F11. Descends into the function call on the current line. lldb: s (step).

  5. Step out. Shift+F11. Runs to the end of the current function and stops in the caller. lldb: finish.

  6. Inspect a variable. Hover, or open the Variables pane. In lldb: p varname. To print a std::vector’s contents: p vec. To print through a pointer: p *ptr.

  7. Print backtrace. Debug Console: bt. Shows the call stack up to the current frame. Essential when you hit a crash.

Reading pointers in lldb — the trick returning devs forget

(lldb) p ptr                                # prints the address
(lldb) p *ptr                               # prints the value at that address
(lldb) memory read --size 4 --format d --count 8 ptr
                                            # 8 ints of 4 bytes each in decimal, starting at ptr
(lldb) p (int[8])*ptr                       # cast + dereference — shows an array of 8 ints

For a std::string, std::vector, std::map, std::unique_ptr: LLDB has data formatters built in. Just p obj. If output is unreadable, run type summary list to see what formatters are loaded.


Verifying the whole stack — the 5-minute smoke test

Do this before you close this document. If any step fails, fix it now.

mkdir -p ~/cpp-scratch/smoke && cd ~/cpp-scratch/smoke
cat > main.cpp <<'EOF'
#include <print>
#include <vector>
#include <string>

int main() {
    std::vector<std::string> names = {"alice", "bob", "carol"};
    for (auto& n : names) {
        std::println("hi, {}", n);
    }
    return 0;
}
EOF

cat > CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.28)
project(smoke CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_compile_options(-Wall -Wextra -Wpedantic -Werror)
add_executable(smoke main.cpp)
EOF

cmake -B build -G Ninja
cmake --build build
ln -sf build/compile_commands.json .
code .

Inside VSCode:

  1. Open main.cpp.

  2. Hover over std::vector — you should see the definition popup within 2–3 seconds. If not, clangd isn’t finding compile_commands.json.

  3. Type std::pr inside main — completions should include println, print. If completions don’t appear, clangd path is wrong.

  4. Set a breakpoint on the for line.

  5. F5, select the LLDB launch config, verify you stop at the breakpoint.

  6. F10 to step over each iteration. Watch n change in the Variables pane.

  7. Type bt in the debug console. You should see main.

If all seven steps pass, you are done with editor setup for the year.


CLion — the quick alternative

If you’d rather use CLion (JetBrains, free for non-commercial use since May 2025):

  1. Install: brew install --cask clion or download from jetbrains.com/clion.

  2. Sign in with a JetBrains account and pick the free non-commercial license during first launch.

  3. Settings → Build, Execution, Deployment → Toolchains: add a toolchain pointing to /opt/homebrew/opt/llvm/bin/clang and /opt/homebrew/opt/llvm/bin/clang++.

  4. Settings → CMake: add -DCMAKE_EXPORT_COMPILE_COMMANDS=ON to CMake options.

  5. Open your project folder. CLion configures, indexes, and gives you a working debugger in one shot.

CLion’s debugger is a genuine upgrade over CodeLLDB — the memory view, watches, and evaluate-expression panels are excellent. Its cost is RAM (~1.5–2 GB) and a small annual account renewal step for the free tier. For Raghul’s laptop and workflow, VSCode is lighter and closer to what you’ll ship in a container later; recommend it as default and revisit CLion if you find yourself hitting VSCode’s debugger limits.


Exit checklist for this file

  • clangd extension installed; MS cpptools extension not installed.

  • ~/Library/Application Support/Code/User/settings.json has the clangd config from above.

  • Smoke-test project builds and hovers/completions work in VSCode.

  • .clangd file understood; you can name three checks it enables.

  • You can name the seven debugger moves and their keyboard shortcuts.

  • You can print a pointer’s target and a std::vector’s contents inside lldb.


Return to Phase 0 README · Next: 03_syntax_refresh_drills.md