config: support variable substitution in settings values (#2809)#319156
Open
DisturbedSage5840C wants to merge 3 commits into
Open
config: support variable substitution in settings values (#2809)#319156DisturbedSage5840C wants to merge 3 commits into
DisturbedSage5840C wants to merge 3 commits into
Conversation
) 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
Author
|
@microsoft-github-policy-service agree |
Contributor
There was a problem hiding this comment.
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
supportsVariableSubstitutionschema flag onIConfigurationPropertySchema. - New
resolveSettingValuemethod onIConfigurationResolverServiceplus an implementation inAbstractVariableResolverService. - New
getResolvedValuemethod onIWorkbenchConfigurationServiceimplemented inWorkspaceService.
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> { |
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
…settings-variable-substitution
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 inlaunch.jsonandtasks.json. Teams can finally share a.vscode/settings.jsonthat contains machine-specific paths without everyone needing to override them locally.What changed (5 files, 70 lines added)
1.
IConfigurationPropertySchema.supportsVariableSubstitution—configurationRegistry.tsA new opt-in boolean field on every setting's schema declaration.
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.tsNew method on the resolver service interface:
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.tsBase 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.tsNew API method that mirrors
getValue<T>()with an optional workspace folder context:5.
WorkspaceService.getResolvedValue<T>()—configurationService.tsConcrete implementation:
getValue()supportsVariableSubstitutionon the schemaIConfigurationResolverServicefetched lazily throughinstantiationService(avoids circular dependency at construction time — the resolver depends onIConfigurationService)Architecture decisions
supportsVariableSubstitutionflag${...}for other reasons (e.g. regex, template strings in language settings)IConfigurationResolverService→IConfigurationServicecreates a cycle; lazy fetch viainstantiationService.invokeFunctionbreaks itgetResolvedValue()not modifyinggetValue()Test plan
supportsVariableSubstitution: trueto a test setting; set value${env:HOME}/test; callgetResolvedValue()→ resolves to home dir pathgetResolvedValue()→ returns raw${env:HOME}/testunchanged${userHome}/projects→ resolves to OS home dir${workspaceFolder}/libin a workspace setting → resolves to workspace root${env:UNSET_VARIABLE_XYZ}/path→ returns"/path"(empty string for unset var, no throw)resolveSettingValue()called with${command:someCommand}→ pattern left unreplaced (command vars excluded)getResolvedValue()during extension host activation (early DI phase) → returns raw value without crashing