diff --git a/src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts b/src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts index 1cabb263d78..25d505123e9 100644 --- a/src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts +++ b/src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts @@ -65,7 +65,7 @@ interface IReplacementLocation { export class ConfigurationResolverExpression implements IConfigurationResolverExpression { public static readonly VARIABLE_LHS = '${'; - private locations = new Map(); + private readonly locations = new Map(); private root: T; private stringRoot: boolean; /** @@ -173,17 +173,14 @@ export class ConfigurationResolverExpression implements IConfigurationResolve } for (const [key, value] of Object.entries(obj)) { + this.parseString(obj, key, key, true); // parse key + if (typeof value === 'string') { this.parseString(obj, key, value); } else { this.parseObject(value); } } - - // only after all values are marked for replacement, we can collect keys that have to be replaced - for (const [key] of Object.entries(obj)) { - this.parseString(obj, key, key, true); - } } private parseString(object: any, propertyName: string | number, value: string, replaceKeyName?: boolean, replacementPath?: string[]): void { @@ -281,15 +278,26 @@ export class ConfigurationResolverExpression implements IConfigurationResolve const newKey = propertyName.replaceAll(replacement.id, data.value); delete object[propertyName]; object[newKey] = value; + this._renameKeyInLocations(object, propertyName, newKey); this.parseString(object, newKey, data.value, true, path); } else { - this.parseString(object, propertyName, data.value, false, path); object[propertyName] = object[propertyName].replaceAll(replacement.id, data.value); + this.parseString(object, propertyName, data.value, false, path); } path.pop(); } + private _renameKeyInLocations(obj: object, oldKey: string, newKey: string) { + for (const location of this.locations.values()) { + for (const loc of location.locations) { + if (loc.object === obj && loc.propertyName === oldKey) { + loc.propertyName = newKey; + } + } + } + } + public toObject(): T { // If we wrapped a string, unwrap it if (this.stringRoot) { diff --git a/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts index 68d24b67cf9..e5afb8970b9 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts @@ -1010,4 +1010,53 @@ suite('ConfigurationResolverExpression', () => { 'key that is username: testuser': 'cool!' }); }); + + test('resolves nested values 2 (#245798)', () => { + const expr = ConfigurationResolverExpression.parse({ + env: { + SITE: "${input:site}", + TLD: "${input:tld}", + HOST: "${input:host}", + }, + }); + + for (const r of expr.unresolved()) { + if (r.arg === 'site') { + expr.resolve(r, 'example'); + } else if (r.arg === 'tld') { + expr.resolve(r, 'com'); + } else if (r.arg === 'host') { + expr.resolve(r, 'local.${input:site}.${input:tld}'); + } + } + + assert.deepStrictEqual(expr.toObject(), { + env: { + SITE: 'example', + TLD: 'com', + HOST: 'local.example.com' + } + }); + }); + + test('out-of-order key resolution (#248550)', () => { + const expr = ConfigurationResolverExpression.parse({ + '${input:key}': "${input:value}", + }); + + for (const r of expr.unresolved()) { + if (r.arg === 'key') { + expr.resolve(r, 'the-key'); + } + } + for (const r of expr.unresolved()) { + if (r.arg === 'value') { + expr.resolve(r, 'the-value'); + } + } + + assert.deepStrictEqual(expr.toObject(), { + 'the-key': 'the-value' + }); + }); });