Preprocessor Indent Viewby Hazem Al Indari Renders What is committed — the conventional layout, directives at column 0:
What you see — nothing on disk changed:
The repository keeps the conventional layout every formatter and every other contributor expects; the indented reading is local to your editor. Hiding inactive codeTurn on
The Three fade levels are in play, because the fold header is not the same thing as what it hides:
Strictly increasing, deliberately: dead code must never come out brighter than scaffolding around code that really compiles. The header sits between the two because VS Code paints a band across every collapsed fold header — see Known limits — and a band with a ghost inside it reads as a stray selection rather than as folded code. Where nothing in a group survives, the Folded, not deleted — VS Code has no way to give a line zero height, so this is folding, applied for you. By default the folds are sticky: expand one and it goes straight back, immediately — the re-fold is issued on the event itself, with no debounce, so there is no window in which the code is readable. That covers the chevron, the fold commands, and the incidental ways VS Code opens a region for you — moving the cursor into it, Go to Definition, a find result. Inactive code is then reachable only by turning hiding off (the status bar button, or Ctrl+Alt+4), which is the point: "hidden" should not mean "hidden until you click the arrow". A notification says so when it happens, rather than leaving the editor looking broken, and carries a
Show inactive code button that turns hiding off — the escape hatch, where you need it. It is rate
limited to one every few seconds, since a cursor wandering through inactive code triggers the re-fold
repeatedly. Silence it with Set Telling it what is definedMost of the time you do not have to. The file's own What settings are for is the macros that arrive from outside any header, i.e. your build's
These are seeded before the file starts, exactly as The evaluator handles the whole C constant-expression grammar — Following includes
Headers are read once and cached against their modification time, and an unsaved buffer beats the copy on disk — so flipping a flag updates every file that reads it without saving first. Two fade levelsWith the dead branches folded away, what is left on screen is scaffolding, and it comes in two kinds that deserve different treatment:
Presence versus valueFollowing includes raises a question the settings-only version never faced: is a macro that nothing
defines actually undefined? Saying yes is what makes
It is confined to presence. The value of a macro that was never seen stays unknown:
This holds in every mode, Three states, not twoA branch is folded only when it provably cannot compile. Anything the evaluator cannot settle is unresolved and stays completely visible — and the status bar says how many there are, so the view never quietly looks more complete than it is. The button carries an open eye while hiding is off and a closed one while it is on:
Hover the button for what was and was not resolved: how many includes were followed, which could not be found, and whether the presence assumption was in force. The asymmetry is deliberate. Folding too little is a cosmetic disappointment; folding live code away is a trap. Short-circuiting stretches a partial list a long way — How it worksThe transform is purely additive — from canonical form a directive moves
RequirementsSource files must be in canonical form — all conditional directives at column 0. A directive
that is already indented deeper than its target cannot be corrected, because decorations can only
add. Running clang-format with the default InstallSearch for Preprocessor Indent View in the Extensions view, or install a There is no build step and no dependencies — it is plain JavaScript. Running from a clonePoint VS Code at the clone and it loads the extension into a second window, where you can open any folder you like to try it on:
The code is read straight from the working tree, so an edit needs only a reload of that window — nothing to package, nothing to install. Opening the clone in VS Code and pressing F5 does the same thing —
Building a
|
⌸ #if indent: on |
showing the indented view — click to see the file as stored |
⊘ #if indent: off |
showing the raw file — click to indent |
Same thing from the command palette (Preprocessor Indent View: Toggle) or Ctrl+Alt+3.
A second button beside it controls hiding inactive code, and appears only while the file actually has conditionals in it.
Two more commands, both palette-only:
| Command | |
|---|---|
| Re-fold Inactive Code | puts folds back after peeking, with keepFolded off |
| Toggle VS Code's Fold Highlight (Ctrl+Alt+5) | turns the band VS Code paints across collapsed folds off, so dead branches read as folded rather than highlighted — see Troubleshooting |
The button writes the ppIndentView.enabled setting rather than a session flag, so the choice
survives a window reload and the button always shows the real state.
Turn it off before hand-editing indentation. The # line's position is derived from its body's
indent, so the two always move together — with the overlay on you are not seeing the columns you are
actually typing, and it is very easy to "correct" a file that was already right.
Settings
| Setting | Default | Meaning |
|---|---|---|
ppIndentView.enabled |
false |
Render the indented view |
ppIndentView.indentSize |
4 |
Spaces per level |
ppIndentView.languages |
["c","cpp"] |
Language IDs to apply to |
ppIndentView.maxFileLines |
20000 |
Skip larger files to keep redraws cheap |
ppIndentView.showStatusBarItem |
true |
Show the status bar buttons |
ppIndentView.hideInactive |
true |
Fold away branches that cannot compile |
ppIndentView.keepFolded |
true |
Re-fold an inactive branch, immediately, if anything expands it |
ppIndentView.notifyOnRefold |
true |
Say so when that happens, with a button to show the code |
ppIndentView.defines |
[] |
Macros from outside any header, i.e. your -D flags — NAME, NAME=VAL, !NAME |
ppIndentView.includePaths |
["include","inc","src","."] |
Where to look when following #include "..." |
ppIndentView.forcedIncludes |
[] |
Headers read first, the compiler's -include |
ppIndentView.assumeUndefined |
always |
When an undefined-nowhere macro counts as undefined — also whenIncludesResolve / never |
ppIndentView.dimInactive |
true |
Dim dead branches while hiding is on |
ppIndentView.foldHeaderLabel |
true |
Label a dead branch's fold header with how many lines it dropped |
ppIndentView.restoreUserFoldBackground |
true |
Paint the band back on folds this extension did not make |
ppIndentView.userFoldBackground |
theme colour | The colour of that repainted band |
ppIndentView.dimOpacity |
0.05 |
How faint dead code a fold has taken is — lower is fainter |
ppIndentView.foldHeaderOpacity |
0.3 |
How faint the fold header — the #if/#else a dead branch collapses onto — is |
ppIndentView.directiveOpacity |
0.45 |
How faint live #if/#endif scaffolding is — 1 to leave it alone |
Out of the box, hiding is on and the indent overlay is off. Switching hiding on also switches the
overlay off, every time — the two do not read well together, since a folded branch leaves its #if
line as the fold header and shifting that right pushes the collapsed placeholder out of line with the
code around it.
That is a one-shot nudge on the transition, not a lock: turn the overlay back on afterwards and it stays on for as long as hiding remains on. It goes off again the next time you switch hiding off and on.
Known limits
These follow from decorations being a rendering overlay, not a real remap of the text:
- Indent guides are drawn at real columns, so they sit misaligned against shifted text.
Turn them off (
"editor.guides.indentation": false) if it bothers you. - Word wrap computes wrap points on the real text, so continuation lines are not shifted.
- Box/column selection (alt+drag) uses real columns.
- Diff and merge editors do not carry the decorations — code review shows canonical form. Arguably a feature.
- The cursor sits at the real column; the status bar column number reflects the file, not the view.
And these apply to hiding inactive code:
- It is folding. Each dead branch keeps one visible line. Folds are lost on window reload (VS
Code restores its own folds, not ours) and reapplied when the file next becomes active — with
keepFoldedon, also the moment anything expands one. - Nothing is read from
c_cpp_properties.json,compile_commands.json, or your build. Macros come from the file, the quoted headers it includes, andppIndentView.defines— so build-system-Dflags have to be listed by hand. - Angled includes are never followed, by design. Anything only an SDK header defines is unknown.
- A header that depends on what its includer defined first is ambiguous — there may be one right answer per translation unit — so only what the header itself establishes is used.
- Function-like macros are not expanded.
#ifdef IS_ENABLEDis answerable;#if IS_ENABLED(X)is not. - It contributes fold ranges only while hiding is on, and only for
ppIndentView.languages. Those ranges sit alongside whatever your C/C++ language server provides; with no other folding provider installed for C/C++, brace folding is unavailable in a file while hiding is on. "editor.foldingStrategy": "indentation"disables it, because VS Code then ignores every folding provider.- VS Code paints its own band across every collapsed fold header, so a folded
#ifline carries a highlight this extension did not put there, and it cannot be suppressed for one extension's folds — only for all folds, or none. A dead branch is therefore told apart from a function body you folded yourself byfoldHeaderLabelandfoldHeaderOpacity, not by its background. See A folded branch looks highlighted.
Troubleshooting
A folded branch looks highlighted, as if something were selected
Symptom. Every collapsed #if carries a coloured band across the full width of the line. It
reads as a leftover selection, and turning dimOpacity down does not remove it — it makes it worse,
because the text inside the band becomes a ghost and the band is then all you see.
It is not this extension. VS Code paints that band across the header line of every collapsed
fold, whoever folded it. Ten-second check: fold an ordinary function body or a switch case
somewhere with no preprocessor in sight. If it gets the same band, this is what you are looking at.
The colour is editor.foldBackground, which defaults to 30% of your selection colour — hence the
"am I selecting something?" feeling.
The fix is one command. Run Preprocessor Indent View: Toggle VS Code's Fold Highlight from
the palette, or Ctrl+Alt+5. It sets editor.foldBackground to a
transparent colour — which turns the band off for every fold — and
ppIndentView.restoreUserFoldBackground then paints it back on the folds this extension did not
make. Dead branches end up with no band, the folds you made yourself keep one. Press it again to put
the colour back exactly as it was.
The band it paints back is matched to the exact colour VS Code was using: the command reads
editor.foldBackground out of VS Code's own Generate Color Theme From Current Settings dump before
overriding it, so a repainted band and a native one are indistinguishable. Nothing to configure.
If you switch theme afterwards, press the command twice to re-capture — the matched colour is a literal, and once the native colour is overridden the theme's own value can no longer be read.
It writes your global settings, which is why it is a command you press rather than something that happens on its own, and why nothing undoes it behind you — not on deactivate, not on uninstall. The previous value is saved, so the second press restores it, or removes the key if there wasn't one.
To do it by hand instead, put this in settings.json and never press the command:
"workbench.colorCustomizations": {
"editor.foldBackground": "#00000000"
}
restoreUserFoldBackground picks that up on its own, so the repaint still happens. Setting it to a
visible colour instead ("#ffffff08") keeps the native band on every fold, dead branches included.
Two things that reliably catch people out:
editor.foldBackgroundis a theme colour, not a setting. Searching the Settings UI forfoldBackgroundfinds nothing at all. It exists only as a key insideworkbench.colorCustomizations, which is JSON-only — open it with Preferences: Open User Settings (JSON). Once you are inside that object, typing"completes every available colour key.- Leaving the block commented out with
//is valid JSONC, so VS Code parses the file happily and silently ignores the customization. If nothing changed after a reload, check that it is actually uncommented.
Why it takes turning the band off globally. It is one global colour with no per-range or
per-provider hook, and workbench.colorCustomizations scopes by theme only — not by language, and
not by who created the fold. The band is also drawn over extension decorations rather than under
them (VS Code's own description of the colour says it "must not be opaque so as not to hide
underlying decorations"), so it cannot be painted over either. Removing it everywhere and putting it
back selectively is the only order of operations available.
On the extension writing your settings. It does so only when you run the toggle command, never
on its own — that colour is global, and an extension has no natural claim on it. Because the write
is your explicit choice it also persists: nothing restores it on deactivate or uninstall, since a
choice that quietly undoes itself is worse than one that stays. The previous value is saved in
extension state so the same command puts it back exactly as it was, including removing the key if
there wasn't one, and other colorCustomizations keys are merged rather than replaced.
What is available is the layer above it: attachment text and inline styles render on top of the
band. That is the layer foldHeaderLabel and foldHeaderOpacity work in, and it is why a dead
branch reads as #if (CONFIG_TRACE) ⋯ 6 lines dropped while a folded function body stays
unlabelled. Distinguished by what is written on the line, since the background is not available.
Folds stopped being marked at all, and the extension is gone
If you used Toggle VS Code's Fold Highlight and then uninstalled or disabled the extension, the
editor.foldBackground override it wrote is still in your settings, and nothing is left to paint the
band back on. Every fold, in every language, is then unmarked.
That is deliberate — the command changes a global colour only when you ask, and nothing undoes it behind you — but it does mean the tidy order is to press the command again before uninstalling. If it is already gone, remove the override by hand:
"workbench.colorCustomizations": {
"editor.foldBackground": "#00000000" // <- delete this line
}
A leftover "ppIndentView.userFoldBackground" in the same block is harmless but can go too.
Dead code looks dimmed twice over
The C/C++ extension dims inactive preprocessor regions itself: C_Cpp.dimInactiveRegions is on by
default, at C_Cpp.inactiveRegionOpacity 0.55. It computes the same regions this extension does,
so the two stack — two independent fades on one line, and the result is fainter than either setting
says.
It is also redundant, since this extension folds the branch away as well as fading it. Turn it off:
"C_Cpp.dimInactiveRegions": false
microchip.mplab-clangd and other clangd-based extensions have the same feature under
clangd.inactiveRegions.*, including a useBackgroundHighlight option that paints a solid
background rather than fading — worth knowing about if faded lines have a block of colour behind
them that does not track your folds.
Tests
No test framework and no dependencies — the suites are plain Node scripts:
npm test # cases, invariants, dead, include, extension
Without Node installed, VS Code's own runtime works:
$env:ELECTRON_RUN_AS_NODE = 1
& "$env:LOCALAPPDATA\Programs\Microsoft VS Code\Code.exe" test/cases.test.js
cases.test.js is written out by hand on purpose. An earlier version of this work compared a
generated "styled" fixture against a generated "canonical" one — both produced by the same code —
which only proved the two were mutual inverses, not that either was right. It passed while the
output was actually wrong. Case 3 is the regression that exposed it: a nested block whose body sits
deeper than the enclosing block's shallowest line.
invariants.test.js checks properties of the output rather than a golden file, so it can fail
independently of how the layout was computed:
- no line ever moves left (a negative shift is not renderable as a decoration);
- body lines at the same conditional depth all shift equally, preserving relative alignment;
- a block's directives render exactly one level above its body;
- the view is losslessly invertible back to canonical;
- the input really is canonical.
dead.test.js states the expected active / dead / unknown verdict for every line of every
case, not just the folds. That is the point: the interesting failure mode is not folding too little,
it is folding a branch that turns out to be live, so the suite also asserts outright that no fold
range ever covers a line that is not dead.
include.test.js runs the include follower over a virtual filesystem, so the config-header shapes it
has to cope with are declared in the test rather than committed as fixtures. It pins the safety rule
as hard as the feature: a macro set inside a branch that could not be resolved must not be believed,
and an include that could not be found must withdraw the presence assumption for the whole file.
extension.test.js drives the real activate() against a stubbed vscode. The stub deliberately
mimics the awkward parts of the API — its decoration types are frozen and its dispose() really
disposes — because both of those caught bugs that a more permissive stub had waved through.
License
GNU General Public License v3.0 or later — see LICENSE.
Copyright (C) 2026 Hazem Al Indari