Select a variable (or just put your cursor in/on it), press Ctrl+Shift+L (Cmd+Shift+L on Mac), and get a debug line inserted right below — in whatever language you're editing.
int count = 0;
→
int count = 0;
printf("count = %d\n", count);
count = 0
→
count = 0
print(f"{count=}")
let count = 0;
→
let count = 0;
console.log('count =', count);
Supported languages
C / C++ get the full treatment: real type resolution (scans the file
for the variable's declaration), correct printf format specifiers,
indexed access (arr[2]), struct/pointer member access (obj.field,
ptr->field), out-of-bounds warnings, and a for-loop mode for whole
arrays. See the details below.
Everything else — Python, JavaScript/TypeScript (+ JSX/TSX), Java, Go,
Rust, C#, PHP, Ruby, Swift, Kotlin, shell scripts, Lua — gets the correct
idiomatic debug-print line for that language. No type resolution is
needed there because these languages already auto-format any value
(f-strings, %v, {:?}, string interpolation, etc.), so the extension
just picks the right print/log call:
| Language | Generated line |
|---|---|
| Python | print(f"{count=}") |
| JS/TS | console.log('count =', count); |
| Java | System.out.println("count = " + count); |
| Go | fmt.Printf("count = %v\n", count) |
| Rust | println!("count = {:?}", count); |
| C# | Console.WriteLine($"count = {count}"); |
| PHP | error_log("count = " . print_r(count, true)); |
| Ruby | puts "count = #{count}" |
| Swift | print("count = \(count)") |
| Kotlin | println("count = $count") |
| Shell | echo "count = $count" |
| Lua | print("count = " .. tostring(count)) |
Language is detected from the file's VS Code language mode
(editor.document.languageId), not the file extension, so it works in
any file VS Code recognizes. If your language isn't in the table, it
inserts a // TODO print debug: ... placeholder and shows a warning —
open an issue / edit GENERIC_LANGUAGES in extension.js to add it
(it's a one-line template function).
C/C++ details
It guesses the correct format specifier by scanning the file for the
variable's declaration: %c/%d/%u for char variants, %hd/%hu for
short, %ld/%lu for long, %lld/%llu for long long, %f for
float/double, %zu/%zd for size_t/ssize_t, correct widths for the
int8_t...uint64_t family, %s for char *, and %p for any other
pointer. Falls back to %d if no declaration is found.
Indexed access (arr[2]) and struct member access (obj.field,
ptr->field) are handled — the extension resolves the real element/field
type rather than defaulting to %d. If a constant index is out of bounds
for a known array size, you'll get a warning popup instead of a silently
broken printf.
Whole-array printing: select just the array name (e.g. arr, no
brackets) and it generates a for loop that prints every element instead
of a single line:
int arr[5] = {1, 2, 3, 4, 5};
for (size_t __i_arr = 0; __i_arr < 5; __i_arr++)
printf("arr[%zu] = %d\n", __i_arr, arr[__i_arr]);
Struct dump: select a bare variable whose type is a struct/aggregate
and it prints every field instead of refusing. Scalar fields get a direct
printf line; a pointer field that points to another known struct type
and has a sibling size/count/len/capacity field is treated as a
dynamic array and expanded into a for loop over its elements' fields;
nested structs held by value are flattened recursively. Given:
typedef struct s_heap_node { long long key; int id; } t_heap_node;
typedef struct s_heap { t_heap_node *data; int size; int capacity; } t_heap;
t_heap *heap = ...;
selecting heap generates:
for (size_t __i0_data = 0; __i0_data < (size_t)heap->size; __i0_data++)
{
printf("heap->data[%zu].key = %lld\n", __i0_data, heap->data[__i0_data].key);
printf("heap->data[%zu].id = %d\n", __i0_data, heap->data[__i0_data].id);
}
printf("heap->size = %d\n", heap->size);
printf("heap->capacity = %d\n", heap->capacity);
It picks size over capacity as the loop bound when both exist, since
that's usually "elements in use" rather than "slots allocated" — rename
your fields to match if you want a different one picked, or just edit the
generated loop condition afterward.
How to install (no publishing needed)
- Install the packaging tool once:
npm install -g @vscode/vsce - From this folder, package it:
This producesvsce packageprintf-debug-0.0.1.vsix. - In VS Code: Extensions view →
...menu (top right) → "Install from VSIX..." → select that file.
Reload VS Code and the shortcut is live in any file.
Changing the shortcut
Edit the keybindings section in package.json, e.g. change
"key": "ctrl+Shift+l" to whatever you prefer, then repackage.
Notes / limitations
- Type detection is a lightweight regex scan, not a real C parser.
- Struct field resolution handles
struct Tag { ... };andtypedef struct { ... } Alias;forms, one level deep — e.g.p.xorptr->x. Chains likea.b.cora->b->conly resolve the last field againsta's type and will fall back to%dif that lookup fails. A warning popup tells you when this happens. - Whole-array loop mode only triggers when you select the bare array name with no index or brackets.
- Struct dump: array-of-struct detection only fires when the pointer
field's type is a struct known from this file and a sibling
size/count/len/capacity field exists; otherwise it just prints the
pointer address. Self-referential types (e.g. a linked-list
nextpointer to the same struct) are detected and skipped with a comment to avoid infinite recursion; nesting deeper than 4 levels is also cut off. - All
%pargs get an explicit(void *)cast, and all%lld/%lluargs get an explicit(long long)/(unsigned long long)cast — this keeps output warning-free under-Wformateven forint64_t/uint64_t, whose underlying type differs by platform (longon Linux/macOS x86-64 and ARM64,long longelsewhere). - Works for both
.cand.cppfiles (no language restriction).