Path: blob/main/src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Iterable } from '../../../../base/common/iterator.js';6import { isLinux, isMacintosh, isWindows } from '../../../../base/common/platform.js';7import { ConfiguredInput } from './configurationResolver.js';89/** A replacement found in the object, as ${name} or ${name:arg} */10export type Replacement = {11/** ${name:arg} */12id: string;13/** The `name:arg` in ${name:arg} */14inner: string;15/** The `name` in ${name:arg} */16name: string;17/** The `arg` in ${name:arg} */18arg?: string;19};2021interface IConfigurationResolverExpression<T> {22/**23* Gets the replacements which have not yet been24* resolved.25*/26unresolved(): Iterable<Replacement>;2728/**29* Gets the replacements which have been resolved.30*/31resolved(): Iterable<[Replacement, IResolvedValue]>;3233/**34* Resolves a replacement into the string value.35* If the value is undefined, the original variable text will be preserved.36*/37resolve(replacement: Replacement, data: string | IResolvedValue): void;3839/**40* Returns the complete object. Any unresolved replacements are left intact.41*/42toObject(): T;43}4445type PropertyLocation = {46object: any;47propertyName: string | number;48replaceKeyName?: boolean;49};5051export interface IResolvedValue {52value: string | undefined;5354/** Present when the variable is resolved from an input field. */55input?: ConfiguredInput;56}5758interface IReplacementLocation {59replacement: Replacement;60locations: PropertyLocation[];61resolved?: IResolvedValue;62}6364export class ConfigurationResolverExpression<T> implements IConfigurationResolverExpression<T> {65public static readonly VARIABLE_LHS = '${';6667private readonly locations = new Map<string, IReplacementLocation>();68private root: T;69private stringRoot: boolean;70/**71* Callbacks when a new replacement is made, so that nested resolutions from72* `expr.unresolved()` can be fulfilled in the same iteration.73*/74private newReplacementNotifiers = new Set<(r: Replacement) => void>();7576private constructor(object: T) {77// If the input is a string, wrap it in an object so we can use the same logic78if (typeof object === 'string') {79this.stringRoot = true;80this.root = { value: object } as any;81} else {82this.stringRoot = false;83this.root = structuredClone(object);84}85}8687/**88* Creates a new {@link ConfigurationResolverExpression} from an object.89* Note that platform-specific keys (i.e. `windows`, `osx`, `linux`) are90* applied during parsing.91*/92public static parse<T>(object: T): ConfigurationResolverExpression<T> {93if (object instanceof ConfigurationResolverExpression) {94return object;95}9697const expr = new ConfigurationResolverExpression<T>(object);98expr.applyPlatformSpecificKeys();99expr.parseObject(expr.root);100return expr;101}102103private applyPlatformSpecificKeys() {104const config = this.root as any; // already cloned by ctor, safe to change105const key = isWindows ? 'windows' : isMacintosh ? 'osx' : isLinux ? 'linux' : undefined;106107if (key && config && typeof config === 'object' && config.hasOwnProperty(key)) {108Object.keys(config[key]).forEach(k => config[k] = config[key][k]);109}110111delete config.windows;112delete config.osx;113delete config.linux;114}115116private parseVariable(str: string, start: number): { replacement: Replacement; end: number } | undefined {117if (str[start] !== '$' || str[start + 1] !== '{') {118return undefined;119}120121let end = start + 2;122let braceCount = 1;123while (end < str.length) {124if (str[end] === '{') {125braceCount++;126} else if (str[end] === '}') {127braceCount--;128if (braceCount === 0) {129break;130}131}132end++;133}134135if (braceCount !== 0) {136return undefined;137}138139const id = str.slice(start, end + 1);140const inner = str.substring(start + 2, end);141const colonIdx = inner.indexOf(':');142if (colonIdx === -1) {143return { replacement: { id, name: inner, inner }, end };144}145146return {147replacement: {148id,149inner,150name: inner.slice(0, colonIdx),151arg: inner.slice(colonIdx + 1)152},153end154};155}156157private parseObject(obj: any): void {158if (typeof obj !== 'object' || obj === null) {159return;160}161162if (Array.isArray(obj)) {163for (let i = 0; i < obj.length; i++) {164const value = obj[i];165if (typeof value === 'string') {166this.parseString(obj, i, value);167} else {168this.parseObject(value);169}170}171return;172}173174for (const [key, value] of Object.entries(obj)) {175this.parseString(obj, key, key, true); // parse key176177if (typeof value === 'string') {178this.parseString(obj, key, value);179} else {180this.parseObject(value);181}182}183}184185private parseString(object: any, propertyName: string | number, value: string, replaceKeyName?: boolean, replacementPath?: string[]): void {186let pos = 0;187while (pos < value.length) {188const match = value.indexOf('${', pos);189if (match === -1) {190break;191}192const parsed = this.parseVariable(value, match);193if (parsed) {194pos = parsed.end + 1;195if (replacementPath?.includes(parsed.replacement.id)) {196continue;197}198199const locations = this.locations.get(parsed.replacement.id) || { locations: [], replacement: parsed.replacement };200const newLocation: PropertyLocation = { object, propertyName, replaceKeyName };201locations.locations.push(newLocation);202this.locations.set(parsed.replacement.id, locations);203204if (locations.resolved) {205this._resolveAtLocation(parsed.replacement, newLocation, locations.resolved, replacementPath);206} else {207this.newReplacementNotifiers.forEach(n => n(parsed.replacement));208}209} else {210pos = match + 2;211}212}213}214215public *unresolved(): Iterable<Replacement> {216const newReplacements = new Map<string, Replacement>();217const notifier = (replacement: Replacement) => {218newReplacements.set(replacement.id, replacement);219};220221for (const location of this.locations.values()) {222if (location.resolved === undefined) {223newReplacements.set(location.replacement.id, location.replacement);224}225}226227this.newReplacementNotifiers.add(notifier);228229while (true) {230const next = Iterable.first(newReplacements);231if (!next) {232break;233}234235const [key, value] = next;236yield value;237newReplacements.delete(key);238}239240this.newReplacementNotifiers.delete(notifier);241}242243public resolved(): Iterable<[Replacement, IResolvedValue]> {244return Iterable.map(Iterable.filter(this.locations.values(), l => !!l.resolved), l => [l.replacement, l.resolved!]);245}246247public resolve(replacement: Replacement, data: string | IResolvedValue): void {248if (typeof data !== 'object') {249data = { value: String(data) };250}251252const location = this.locations.get(replacement.id);253if (!location) {254return;255}256257location.resolved = data;258259if (data.value !== undefined) {260for (const l of location.locations || Iterable.empty()) {261this._resolveAtLocation(replacement, l, data);262}263}264}265266private _resolveAtLocation(replacement: Replacement, { replaceKeyName, propertyName, object }: PropertyLocation, data: IResolvedValue, path: string[] = []) {267if (data.value === undefined) {268return;269}270271// avoid recursive resolution, e.g. ${env:FOO} -> ${env:BAR}=${env:FOO}272path.push(replacement.id);273274// note: in nested `this.parseString`, parse only the new substring for any replacements, don't reparse the whole string275if (replaceKeyName && typeof propertyName === 'string') {276const value = object[propertyName];277const newKey = propertyName.replaceAll(replacement.id, data.value);278delete object[propertyName];279object[newKey] = value;280this._renameKeyInLocations(object, propertyName, newKey);281this.parseString(object, newKey, data.value, true, path);282} else {283object[propertyName] = object[propertyName].replaceAll(replacement.id, data.value);284this.parseString(object, propertyName, data.value, false, path);285}286287path.pop();288}289290private _renameKeyInLocations(obj: object, oldKey: string, newKey: string) {291for (const location of this.locations.values()) {292for (const loc of location.locations) {293if (loc.object === obj && loc.propertyName === oldKey) {294loc.propertyName = newKey;295}296}297}298}299300public toObject(): T {301// If we wrapped a string, unwrap it302if (this.stringRoot) {303return (this.root as any).value as T;304}305306return this.root;307}308}309310311