Skip to content

config: support variable substitution in settings values (#2809)#319156

Open
DisturbedSage5840C wants to merge 3 commits into
microsoft:mainfrom
DisturbedSage5840C:feat/settings-variable-substitution
Open

config: support variable substitution in settings values (#2809)#319156
DisturbedSage5840C wants to merge 3 commits into
microsoft:mainfrom
DisturbedSage5840C:feat/settings-variable-substitution

Conversation

@DisturbedSage5840C
Copy link
Copy Markdown

Summary

Fixes #2809 — Support variables when resolving values in settings.

This is a 927-reaction, 9-year-old feature request from the community. It enables ${env:NAME}, ${userHome}, ${workspaceFolder}, ${pathSeparator}, and ${config:section} in settings.json string values — the same variable syntax already used in launch.json and tasks.json. Teams can finally share a .vscode/settings.json that contains machine-specific paths without everyone needing to override them locally.


What changed (5 files, 70 lines added)

1. IConfigurationPropertySchema.supportsVariableSubstitutionconfigurationRegistry.ts

A new opt-in boolean field on every setting's schema declaration.

"typescript.tsdk": {
  "type": "string",
  "supportsVariableSubstitution": true,
  "description": "..."
}

Only settings that declare this flag will have variables substituted. All existing settings are unaffected — no behaviour changes without explicit opt-in.

2. IConfigurationResolverService.resolveSettingValue()configurationResolver.ts

New method on the resolver service interface:

resolveSettingValue(folder: IWorkspaceFolderData | undefined, value: string): Promise<string>;

Resolves only static, non-interactive variables — the same set that works in launch.json without user prompts: ${env:X}, ${userHome}, ${workspaceFolder}, ${workspaceFolderBasename}, ${pathSeparator}, ${config:section}. Command and input variables are intentionally excluded — settings must never trigger UI interaction.

3. AbstractVariableResolverService.resolveSettingValue()variableResolver.ts

Base implementation: delegates to the existing resolveAsync() and swallows all resolution errors, returning the original value instead of throwing. A typo or missing variable degrades gracefully.

4. IWorkbenchConfigurationService.getResolvedValue<T>()configuration.ts

New API method that mirrors getValue<T>() with an optional workspace folder context:

// Before
const tsdk = configService.getValue<string>('typescript.tsdk');
// After — variables resolved, e.g. "${env:HOME}/node_modules/typescript/lib"
const tsdk = await configService.getResolvedValue<string>('typescript.tsdk', folder);

5. WorkspaceService.getResolvedValue<T>()configurationService.ts

Concrete implementation:

  • Reads raw value with getValue()
  • Checks supportsVariableSubstitution on the schema
  • Resolves via IConfigurationResolverService fetched lazily through instantiationService (avoids circular dependency at construction time — the resolver depends on IConfigurationService)
  • Returns raw value unchanged for non-string types, non-annotated settings, and calls made before DI is ready

Architecture decisions

Decision Rationale
Opt-in supportsVariableSubstitution flag Prevents surprises in settings that happen to contain ${...} for other reasons (e.g. regex, template strings in language settings)
Static variables only, no command/input Settings are read programmatically, often during extension activation; UI interaction is not acceptable
Swallow resolution errors A missing env var or no open folder should not crash extension activation
Lazy DI for resolver IConfigurationResolverServiceIConfigurationService creates a cycle; lazy fetch via instantiationService.invokeFunction breaks it
New getResolvedValue() not modifying getValue() Zero breaking changes; existing callers unaffected; opt-in call site

Test plan

  • Add supportsVariableSubstitution: true to a test setting; set value ${env:HOME}/test; call getResolvedValue() → resolves to home dir path
  • Same setting without the flag; call getResolvedValue() → returns raw ${env:HOME}/test unchanged
  • Set value ${userHome}/projects → resolves to OS home dir
  • Set value ${workspaceFolder}/lib in a workspace setting → resolves to workspace root
  • Set value ${env:UNSET_VARIABLE_XYZ}/path → returns "/path" (empty string for unset var, no throw)
  • Non-string setting (boolean, number) → raw value returned unchanged
  • resolveSettingValue() called with ${command:someCommand} → pattern left unreplaced (command vars excluded)
  • Call getResolvedValue() during extension host activation (early DI phase) → returns raw value without crashing

)

Adds infrastructure for `${env:NAME}`, `${userHome}`, `${workspaceFolder}`,
`${workspaceFolderBasename}`, `${pathSeparator}`, and `${config:section}`
variables inside settings.json string values. This has been the most
consistently requested developer-experience improvement for team-shared
settings that contain machine-specific paths (issue microsoft#2809, 927 reactions).

Implementation is spread across five layers and keeps every existing API
backward-compatible:

**1. `IConfigurationPropertySchema.supportsVariableSubstitution` (configurationRegistry.ts)**
A new opt-in boolean field on the per-setting schema. Setting contributors
(built-in and extensions) annotate path-like settings with
`supportsVariableSubstitution: true` to declare that their string values
may contain variable patterns. Settings without the flag are unaffected.

**2. `IConfigurationResolverService.resolveSettingValue()` (configurationResolver.ts)**
New method on the resolver service interface. Takes an optional workspace
folder context and a raw string value, returns a promise resolving to the
substituted string. Only non-interactive, static variables are resolved
(env, userHome, workspaceFolder family, pathSeparator, config). Command
and input variables are intentionally excluded since settings must not
trigger UI interaction.

**3. `AbstractVariableResolverService.resolveSettingValue()` (variableResolver.ts)**
Base implementation: delegates to the existing `resolveAsync()` (which
already handles all static variable kinds) and swallows resolution errors,
returning the original value rather than throwing. This means a typo like
`${env:UNSET_VAR}` silently expands to an empty string, matching the
behaviour users expect from shell variable expansion.

**4. `IWorkbenchConfigurationService.getResolvedValue<T>()` (configuration.ts)**
New method on the workbench-level configuration service interface.
Signature mirrors `getValue<T>()` with an additional optional `folder`
parameter. Callers that need a resolved path use this instead of
`getValue()` without any change to the underlying settings schema.

**5. `WorkspaceService.getResolvedValue<T>()` (configurationService.ts)**
Concrete implementation: reads the raw value with `getValue()`, checks
whether the setting schema has `supportsVariableSubstitution: true` and
the value is a string, then resolves via `IConfigurationResolverService`
obtained lazily through `instantiationService` (avoiding a circular
dependency at construction time). For non-string values, non-annotated
settings, or calls made before the instantiation service is ready, the
raw value is returned unchanged.

Fixes microsoft#2809
Copilot AI review requested due to automatic review settings May 30, 2026 16:41
@DisturbedSage5840C
Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a new opt-in mechanism for resolving VS Code variable substitutions (e.g. ${env:HOME}, ${workspaceFolder}) in configuration setting string values. Schemas may declare supportsVariableSubstitution: true, and consumers can call IWorkbenchConfigurationService.getResolvedValue() to receive substituted values.

Changes:

  • New supportsVariableSubstitution schema flag on IConfigurationPropertySchema.
  • New resolveSettingValue method on IConfigurationResolverService plus an implementation in AbstractVariableResolverService.
  • New getResolvedValue method on IWorkbenchConfigurationService implemented in WorkspaceService.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/vs/platform/configuration/common/configurationRegistry.ts Adds supportsVariableSubstitution schema property.
src/vs/workbench/services/configurationResolver/common/configurationResolver.ts Declares resolveSettingValue on resolver service interface.
src/vs/workbench/services/configurationResolver/common/variableResolver.ts Implements resolveSettingValue with swallow-all error handling.
src/vs/workbench/services/configuration/common/configuration.ts Declares getResolvedValue on workbench configuration service interface.
src/vs/workbench/services/configuration/browser/configurationService.ts Implements getResolvedValue, looking up the resolver via instantiation service.

return Array.isArray(allProfilesSettings) && allProfilesSettings.includes(key);
}

async getResolvedValue<T>(section: string, folder?: import('../../../../platform/workspace/common/workspace.js').IWorkspaceFolderData, overrides?: IConfigurationOverrides): Promise<T> {
Comment on lines +525 to +527
const resolver = this.instantiationService.invokeFunction(accessor => {
try { return accessor.get(IConfigurationResolverService); } catch { return undefined; }
});
Comment on lines +96 to +105
public async resolveSettingValue(folder: IWorkspaceFolderData | undefined, value: string): Promise<string> {
try {
const result = await this.resolveAsync(folder, value);
return typeof result === 'string' ? result : value;
} catch {
// On any resolution error (missing env var, no open folder, etc.) return the
// original value so that a bad variable pattern never breaks extension activation.
return value;
}
}
}

async getResolvedValue<T>(section: string, folder?: import('../../../../platform/workspace/common/workspace.js').IWorkspaceFolderData, overrides?: IConfigurationOverrides): Promise<T> {
const raw = this.getValue<T>(section, overrides ?? {});
* @param folder Optional workspace folder context used to resolve `${workspaceFolder}`.
* @param overrides Optional language/resource overrides, same as `getValue()`.
*/
getResolvedValue<T>(section: string, folder?: IWorkspaceFolderData, overrides?: IConfigurationOverrides): Promise<T>;
return expr.toObject() as (T extends ConfigurationResolverExpression<infer R> ? R : T);
}

public async resolveSettingValue(folder: IWorkspaceFolderData | undefined, value: string): Promise<string> {
DisturbedSage added 2 commits May 30, 2026 22:17
- Move IWorkspaceFolderData out of inline import() into the top-level
  import statement for consistency with the rest of the file
- Cache IConfigurationResolverService in acquireInstantiationService()
  so getResolvedValue() no longer calls invokeFunction on every invocation;
  avoids repeated DI overhead and makes wiring failures immediately visible
- Narrow the catch in resolveSettingValue(): VariableError (expected
  failures: missing env var, no open folder) is silently swallowed and
  returns the original value; any other error is forwarded to
  onUnexpectedError() so programmer mistakes are not silently hidden
- Improve getResolvedValue() JSDoc to document that only top-level string
  values are resolved (not strings nested in objects/arrays), clarify
  that T should extend string for supportsVariableSubstitution settings,
  and point to resolveSettingValue() for nested resolution use cases
- Add unit tests for resolveSettingValue() covering: env var resolution,
  workspaceFolder resolution, graceful fallback on missing env var,
  fallback when no folder is provided, plain strings left unchanged,
  and command variables intentionally left unresolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support variables when resolving values in settings

3 participants