# Better Debugging With LLDB (in VS Code + CodeLLDB) ## The Problem When debugging Unreal with VS Code + CodeLLDB, the **Variables** pane and the **Watch** pane use two completely different evaluation paths: - **Variables pane** walks a tree built by lldb's *synthetic children providers* (including Unreal's Python formatters for TArray, TMap, FName, FString, TSharedRef, etc.). Values are looked up by offset/type — no compilation. Base classes appear as named children ("SCompoundWidget", "UWidget"), smart-pointer inners get unwrapped, container elements get indexed. - **Watch pane** (and `p`/`expression`) runs your text through Clang to compile a real C++ expression against the program's full type system, then applies formatters to the result. For Unreal, this is slow and often fails outright — synthetic children don't exist as C++ members. The practical consequence: a path you see in the Variables pane — like `Widget.Object.SCompoundWidget.SWidget.bCanSupportFocus` — cannot be typed into the Watch pane, because the intermediate labels (`SCompoundWidget`, `SWidget`) aren't real C++ members. Even "Copy as Expression" in the Variables pane gives you that broken synthetic path. Hand-writing the equivalent real C++ expression (with explicit `->` for pointers, `static_cast`s through base classes, formatter-internal array indexing math, etc.) is impractical. ## The Fix: A Python Helper That Walks Synthetic Children We added a function `fv(path)` to `tools/UEDataFormatter.py`. It walks a dotted path through the synthetic tree using the same APIs the Variables pane uses (`GetChildMemberWithName`, then fallback to iterating children by name so base classes match). Pointers are auto-dereferenced. CodeLLDB's `/py` expression mode evaluates Python in the Watch pane and renders returned `SBValue`s through the full formatter pipeline — same expandable tree as Variables. So: /py fv("Widget.Object.SCompoundWidget.SWidget") works in Watch the way you'd originally expect `Widget.Object.SCompoundWidget.SWidget` to work. `fv` is also injected into Python builtins, so from the Debug Console you can also just: script fv("Widget.Object.SCompoundWidget.SWidget") ### How `fv` is loaded The launch configurations in `Integration.code-workspace.tpl.json` run: command script import /home/jyelon/integration/tools/UEDataFormatter.py This both (a) loads all the Unreal data formatters and (b) defines `fv` and injects it into builtins. ### Reloading without restarting the debug session If you edit `tools/UEDataFormatter.py`, reload in the Debug Console: script import importlib; importlib.reload(UEDataFormatter) ## The Remaining Gap: "Add to Watch" VS Code's "Add to Watch" context menu copies the variable's `evaluateName` as reported by the debug adapter. CodeLLDB sends the synthetic path — so "Add to Watch" produces exactly the unusable expression that started this whole problem. The clean solution is a small VS Code extension that registers a new command **Add to Watch (fv)** on the Variables pane context menu. When invoked, it reads the variable's evaluateName and adds `/py fv("")` to the watch expressions. ### Scaffolding the extension Create a folder, e.g. `tools/vscode-fv-watch/`: ``` tools/vscode-fv-watch/ package.json src/extension.ts tsconfig.json ``` **`package.json`** declares the contribution points — the command, and its appearance in the Variables pane context menu: ```json { "name": "fv-watch", "version": "0.0.1", "engines": { "vscode": "^1.80.0" }, "activationEvents": ["onDebug"], "main": "./out/extension.js", "contributes": { "commands": [{ "command": "fv.addToWatch", "title": "Add to Watch (fv)" }], "menus": { "debug/variables/context": [{ "command": "fv.addToWatch", "group": "3_modification@1" }] } }, "devDependencies": { "@types/vscode": "^1.80.0", "typescript": "^5.0.0" } } ``` **`src/extension.ts`** implements the command. The variable reference is passed as the command argument; we pull its `evaluateName` and call the built-in `debug.addToWatchExpressions` with the wrapped form: ```ts import * as vscode from 'vscode'; export function activate(ctx: vscode.ExtensionContext) { ctx.subscriptions.push(vscode.commands.registerCommand( 'fv.addToWatch', async (variable: any) => { const name = variable?.evaluateName ?? variable?.name; if (!name) return; const expr = `/py fv(${JSON.stringify(name)})`; await vscode.commands.executeCommand( 'debug.addToWatchExpressions', { variable: { evaluateName: expr } } ); })); } export function deactivate() {} ``` **`tsconfig.json`** is standard: ```json { "compilerOptions": { "module": "commonjs", "target": "ES2020", "outDir": "out", "strict": true, "esModuleInterop": true }, "include": ["src"] } ``` ### Building and installing ``` cd tools/vscode-fv-watch npm install npx tsc ln -s "$PWD" ~/.vscode/extensions/fv-watch ``` Restart VS Code. Right-click any entry in the Variables pane and **Add to Watch (fv)** appears alongside the built-in **Add to Watch**. ## Summary of Workflow | Action | Where | How | |---|---|---| | Explore a value | Variables pane | Click disclosure triangles | | Track a value across steps | Watch pane | "Add to Watch (fv)" from Variables context menu, or type `/py fv("...")` manually | | One-shot inspection | Debug Console | `script fv("path")` | | Reload formatter edits | Debug Console | `script import importlib; importlib.reload(UEDataFormatter)` | ## Why Not Make It Automatic? The cleanest solution would be to change CodeLLDB's default Watch evaluator to route through `frame variable` / synthetic children instead of Clang. That's not exposed as a setting — CodeLLDB's `"expressions"` option only chooses between its own parser, lldb's `expr`, or raw Python. None of those hit the synthetic-children walk. A feature request against CodeLLDB to add a "native frame-variable" expression mode would address this at the source. In the meantime, the `fv` helper + extension combo reproduces the missing behavior.