Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
aebf397
Fix #12537: add env object schema to cppdbg and cppvsdbg launch configs
02prashantrai Aug 18, 2026
deb60ac
Fix #12537: add env object schema to cppdbg and cppvsdbg launch configs
8prashant Aug 18, 2026
13f598c
Add support for env object in cppdbg and runWithoutDebugging configur…
02prashantrai Aug 19, 2026
66f6d95
Add support for env object in cppdbg and runWithoutDebugging configur…
8prashant Aug 19, 2026
793ac13
Merge branch 'main' into fix/12537-env-property-schema
8prashant Aug 19, 2026
146e959
Merge branch 'main' into fix/12537-env-property-schema
8prashant Aug 19, 2026
7130385
Update env description to include message and comment structure; enha…
8prashant Sep 1, 2026
84a0467
Update env description to include message and comment structure; enha…
8prashant Sep 1, 2026
e2b17cf
Merge branch 'fix/12537-env-property-schema' of https://github.com/8p…
8prashant Sep 1, 2026
737ecd1
Merge branch 'fix/12537-env-property-schema' of https://github.com/8p…
8prashant Sep 1, 2026
d961d42
Merge branch 'fix/12537-env-property-schema' of https://github.com/8p…
8prashant Sep 1, 2026
034ed40
Merge branch 'main' into fix/12537-env-property-schema
8prashant Sep 2, 2026
30f2f9d
Refactor environment variable handling in DebugConfigurationProvider …
8prashant Sep 2, 2026
c933aee
Merge branch 'fix/12537-env-property-schema' of https://github.com/8p…
8prashant Sep 2, 2026
af30f6c
Merge branch 'main' into fix/12537-env-property-schema
8prashant Sep 4, 2026
875e702
Update environment variable handling to allow null values in DebugCon…
8prashant Sep 4, 2026
51fe13f
Fix environment variable deletion for case insensitivity
8prashant Sep 4, 2026
ce9cbd9
Enhance environment variable handling to support null values in schem…
8prashant Sep 4, 2026
dc91405
Update environment variable schema to allow null values for variable …
8prashant Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions Extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4373,12 +4373,26 @@
"type": "string"
},
"value": {
"type": "string"
"type": [
"string",
"null"
]
}
}
},
"default": []
},
"env": {
"type": "object",
"description": "%c_cpp.debuggers.env.description%",
Comment thread
8prashant marked this conversation as resolved.
"additionalProperties": {
"type": [
"string",
"null"
]
},
"default": {}
},
"envFile": {
"type": "string",
"description": "%c_cpp.debuggers.envFile.description%",
Expand Down Expand Up @@ -6046,12 +6060,26 @@
"type": "string"
},
"value": {
"type": "string"
"type": [
"string",
"null"
]
}
}
},
"default": []
},
"env": {
"type": "object",
"description": "%c_cpp.debuggers.env.description%",
"additionalProperties": {
"type": [
"string",
"null"
]
},
"default": {}
},
"envFile": {
"type": "string",
"description": "%c_cpp.debuggers.envFile.description%",
Expand Down
10 changes: 8 additions & 2 deletions Extension/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -922,9 +922,15 @@
"c_cpp.debuggers.cppvsdbg.visualizerFile.description": ".natvis file to be used when debugging this process.",
"c_cpp.debuggers.showDisplayString.description": "When a visualizerFile is specified, showDisplayString will enable the display string. Turning this option on can cause slower performance during debugging.",
"c_cpp.debuggers.environment.description": {
"message": "Environment variables to add to the environment for the program. Example: [ { \"name\": \"config\", \"value\": \"Debug\" } ], not [ { \"config\": \"Debug\" } ].",
"message": "Environment variables for the program. String values add or replace variables, while null removes inherited variables. Example: [ { \"name\": \"config\", \"value\": \"Debug\" } ], not [ { \"config\": \"Debug\" } ].",
"comment": [
"{Locked=\"[ { \\\"name\\\": \\\"\"} {Locked=\"\\\", \\\"value\\\": \\\"\"} {Locked=\"\\\" } ]\"} {Locked=\"[ { \\\"\"} {Locked=\"\\\": \\\"\"} {Locked=\"\\\" } ]\"}"
"{Locked=\"null\"} {Locked=\"[ { \\\"name\\\": \\\"\"} {Locked=\"\\\", \\\"value\\\": \\\"\"} {Locked=\"\\\" } ]\"} {Locked=\"[ { \\\"\"} {Locked=\"\\\": \\\"\"} {Locked=\"\\\" } ]\"}"
]
},
"c_cpp.debuggers.env.description": {
"message": "Object of environment variables for the program. String values add or replace variables, while null removes inherited variables. Example: { \"MY_VAR\": \"value\" }. Use `environment` for the array-of-objects form.",
"comment": [
"{Locked=\"null\"} {Locked=\"{ \\\"MY_VAR\\\": \\\"value\\\" }\"} {Locked=\"`environment`\"}"
]
},
"c_cpp.debuggers.envFile.description": "Absolute path to a file containing environment variable definitions. This file has key value pairs separated by an equals sign per line. E.g. KEY=VALUE.",
Expand Down
2 changes: 1 addition & 1 deletion Extension/src/Debugger/ParsedEnvironmentFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const localize: nls.LocalizeFunc = nls.loadMessageBundle();

export interface Environment {
name: string;
value: string;
value: string | null;
}

export class ParsedEnvironmentFile {
Expand Down
33 changes: 33 additions & 0 deletions Extension/src/Debugger/configurationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,10 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
// Add environment variables from .env file
this.resolveEnvFile(config, folder);

// Debug adapters consume the legacy `environment` array, not `env`.
// Convert here so both syntaxes work while preserving `env` precedence.
this.resolveEnvObject(config);

await this.expand(config, folder);

this.resolveSourceFileMapVariables(config);
Expand Down Expand Up @@ -700,6 +704,35 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
}
}

private resolveEnvObject(config: CppDebugConfiguration): void {
if ((config.type !== DebuggerType.cppdbg && config.type !== DebuggerType.cppvsdbg) || config.request !== 'launch') {
return;
}

const envObject = config.env;
if (!util.isObject(envObject)) {
return;
}

const environment: Environment[] = util.isArray(config.environment) ? config.environment : [];
const mergedEnvironment = new Map<string, string | null>();

for (const entry of environment) {
if (util.isString(entry?.name) && (util.isString(entry?.value) || entry?.value === null)) {
mergedEnvironment.set(entry.name, entry.value);
}
}

for (const [name, value] of Object.entries(envObject)) {
if (util.isString(value) || value === null) {
mergedEnvironment.set(name, value);
}
Comment on lines +726 to +729

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨Copilot (agent117): Confirmed on dc91405e: this commit now permits nullable environment[].value, but it does not change the normal adapter. MIEngine passes EnvironmentEntry.Value to GetSetEnvironmentVariableCommand(string name, string value); GDB formats set env NAME VALUE, and LLDB calls EscapeQuotes(value). A null therefore still cannot express deletion in normal cppdbg. Please keep this thread active until that path is implemented or nullable syntax is limited to supported paths, with a normal-debug test.

}

config.environment = Array.from(mergedEnvironment.entries()).map(([name, value]) => ({ name, value }));
delete config.env;
}

private resolveSourceFileMapVariables(config: CppDebugConfiguration): void {
const messages: string[] = [];
if (config.sourceFileMap) {
Expand Down
89 changes: 74 additions & 15 deletions Extension/src/Debugger/runWithoutDebuggingAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ import { isWindows } from '../constants';

nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize = nls.loadMessageBundle();
type TerminalEnvironment = NonNullable<vscode.TerminalOptions['env']>;
const managedTerminals = new Map<string, vscode.Terminal>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨Copilot (agent117): [Minor] Closed terminals remain strongly referenced here until the same executable is launched again. Since each distinct program adds an entry and the per-session close listener is disposed when the session terminates, manually closing retained terminals can accumulate stale Terminal handles for the extension-host lifetime. Please install module-lifetime onDidCloseTerminal cleanup that deletes an entry only when its value is the closed terminal.

const terminalEnvironments = new WeakMap<vscode.Terminal, TerminalEnvironment>();

type LaunchEnvironmentEntry = { name: string; value: string | null; };

type LaunchConfiguration = {
program?: string;
args?: string[];
cwd?: string;
environment?: LaunchEnvironmentEntry[];
env?: Record<string, string | null>;
console?: string;
externalConsole?: boolean;
};

/**
* A minimal inline Debug Adapter that runs the target program directly without a debug adapter
Expand Down Expand Up @@ -59,31 +74,30 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
}

private async launch(request: { command: string; seq: number; arguments?: any; }): Promise<void> {
const config = request.arguments as {
program?: string;
args?: string[];
cwd?: string;
environment?: { name: string; value: string; }[];
console?: string;
externalConsole?: boolean;
};
const config = request.arguments as LaunchConfiguration;

const program: string = config.program ?? '';
const args: string[] = config.args ?? [];
const cwd: string | undefined = config.cwd;
const environment: { name: string; value: string; }[] = config.environment ?? [];
const environment: LaunchEnvironmentEntry[] = config.environment ?? [];
const envObject: Record<string, string | null> = config.env ?? {};
const consoleMode: string = config.console ?? (config.externalConsole ? 'externalTerminal' : 'integratedTerminal');

// Merge the launch config's environment variables on top of the inherited process environment.
// Merge environment values in this order: inherited process environment, legacy
// `environment` entries, then shorthand `env` values (higher precedence).
const env: NodeJS.ProcessEnv = { ...process.env };
const terminalEnv: TerminalEnvironment = {};
for (const e of environment) {
env[e.name] = e.value;
this.applyEnvironmentValue(env, terminalEnv, e.name, e.value);
}
for (const [key, value] of Object.entries(envObject)) {
this.applyEnvironmentValue(env, terminalEnv, key, value);
}

this.sendResponse(request, {});

if (consoleMode === 'integratedTerminal' || consoleMode === 'internalConsole') {
await this.launchIntegratedTerminal(program, args, cwd, env);
await this.launchIntegratedTerminal(program, args, cwd, terminalEnv);
} else if (consoleMode === 'externalTerminal') {
this.launchExternalTerminal(program, args, cwd, env);
}
Expand All @@ -93,14 +107,25 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
* Launch the program in a VS Code integrated terminal.
* The terminal will remain open after the program exits and be reused for the next session, if applicable.
*/
private async launchIntegratedTerminal(program: string, args: string[], cwd: string | undefined, env: NodeJS.ProcessEnv): Promise<void> {
private async launchIntegratedTerminal(program: string, args: string[], cwd: string | undefined, env: TerminalEnvironment): Promise<void> {
const terminalName = path.normalize(program);
const existingTerminal = vscode.window.terminals.find(t => t.name === terminalName);
const managedTerminal = managedTerminals.get(terminalName);
let existingTerminal = managedTerminal && vscode.window.terminals.includes(managedTerminal) ? managedTerminal : undefined;
if (!existingTerminal) {
managedTerminals.delete(terminalName);
}
if (existingTerminal && !this.environmentsEqual(terminalEnvironments.get(existingTerminal), env)) {
existingTerminal.dispose();
Comment thread
8prashant marked this conversation as resolved.
existingTerminal = undefined;
managedTerminals.delete(terminalName);
}
this.terminal = existingTerminal ?? vscode.window.createTerminal({
name: terminalName,
cwd,
env: env as Record<string, string>
env
});
managedTerminals.set(terminalName, this.terminal);
terminalEnvironments.set(this.terminal, env);
this.terminal.show(true);

const shellIntegration: vscode.TerminalShellIntegration | undefined =
Expand Down Expand Up @@ -212,6 +237,40 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
return arg.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}

private applyEnvironmentValue(processEnv: NodeJS.ProcessEnv, terminalEnv: TerminalEnvironment, name: string, value: string | null): void {
const matchingKeys = isWindows
? new Set([...Object.keys(processEnv), ...Object.keys(terminalEnv)].filter(key => key.toLowerCase() === name.toLowerCase()))
: new Set([name]);

for (const key of matchingKeys) {
delete processEnv[key];
if (key !== name || value === null) {
terminalEnv[key] = null;
}
}

if (value === null) {
terminalEnv[name] = null;
} else {
processEnv[name] = value;
terminalEnv[name] = value;
}
}

private environmentsEqual(first: TerminalEnvironment | undefined, second: TerminalEnvironment): boolean {
if (!first) {
return false;
}

const firstKeys = Object.keys(first);
const secondKeys = Object.keys(second);
if (firstKeys.length !== secondKeys.length) {
return false;
}

return firstKeys.every(key => first[key] === second[key]);
}

private waitForShellIntegration(terminal: vscode.Terminal, timeoutMs: number): Promise<vscode.TerminalShellIntegration | undefined> {
return new Promise(resolve => {
let resolved: boolean = false;
Expand Down
21 changes: 21 additions & 0 deletions Extension/test/scenarios/RunWithoutDebugging/assets/envTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#include <cstdlib>
#include <fstream>

int main(int argc, char *argv[]) {
if (argc < 3) {
return 1;
}

const char *value = std::getenv(argv[1]);

std::ofstream resultFile(argv[2]);
if (!resultFile) {
return 2;
}

if (value) {
resultFile << value;
}

return 0;
}
Loading
Loading