Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts
5243 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Iterable } from '../../../../base/common/iterator.js';
7
import { isLinux, isMacintosh, isWindows } from '../../../../base/common/platform.js';
8
import { ConfiguredInput } from './configurationResolver.js';
9
10
/** A replacement found in the object, as ${name} or ${name:arg} */
11
export type Replacement = {
12
/** ${name:arg} */
13
id: string;
14
/** The `name:arg` in ${name:arg} */
15
inner: string;
16
/** The `name` in ${name:arg} */
17
name: string;
18
/** The `arg` in ${name:arg} */
19
arg?: string;
20
};
21
22
interface IConfigurationResolverExpression<T> {
23
/**
24
* Gets the replacements which have not yet been
25
* resolved.
26
*/
27
unresolved(): Iterable<Replacement>;
28
29
/**
30
* Gets the replacements which have been resolved.
31
*/
32
resolved(): Iterable<[Replacement, IResolvedValue]>;
33
34
/**
35
* Resolves a replacement into the string value.
36
* If the value is undefined, the original variable text will be preserved.
37
*/
38
resolve(replacement: Replacement, data: string | IResolvedValue): void;
39
40
/**
41
* Returns the complete object. Any unresolved replacements are left intact.
42
*/
43
toObject(): T;
44
}
45
46
type PropertyLocation = {
47
object: any;
48
propertyName: string | number;
49
replaceKeyName?: boolean;
50
};
51
52
export interface IResolvedValue {
53
value: string | undefined;
54
55
/** Present when the variable is resolved from an input field. */
56
input?: ConfiguredInput;
57
}
58
59
interface IReplacementLocation {
60
replacement: Replacement;
61
locations: PropertyLocation[];
62
resolved?: IResolvedValue;
63
}
64
65
export class ConfigurationResolverExpression<T> implements IConfigurationResolverExpression<T> {
66
public static readonly VARIABLE_LHS = '${';
67
68
private readonly locations = new Map<string, IReplacementLocation>();
69
private root: T;
70
private stringRoot: boolean;
71
/**
72
* Callbacks when a new replacement is made, so that nested resolutions from
73
* `expr.unresolved()` can be fulfilled in the same iteration.
74
*/
75
private newReplacementNotifiers = new Set<(r: Replacement) => void>();
76
77
private constructor(object: T) {
78
// If the input is a string, wrap it in an object so we can use the same logic
79
if (typeof object === 'string') {
80
this.stringRoot = true;
81
// eslint-disable-next-line local/code-no-any-casts
82
this.root = { value: object } as any;
83
} else {
84
this.stringRoot = false;
85
this.root = structuredClone(object);
86
}
87
}
88
89
/**
90
* Creates a new {@link ConfigurationResolverExpression} from an object.
91
* Note that platform-specific keys (i.e. `windows`, `osx`, `linux`) are
92
* applied during parsing.
93
*/
94
public static parse<T>(object: T): ConfigurationResolverExpression<T> {
95
if (object instanceof ConfigurationResolverExpression) {
96
return object;
97
}
98
99
const expr = new ConfigurationResolverExpression<T>(object);
100
expr.applyPlatformSpecificKeys();
101
expr.parseObject(expr.root);
102
return expr;
103
}
104
105
private applyPlatformSpecificKeys() {
106
// eslint-disable-next-line local/code-no-any-casts
107
const config = this.root as any; // already cloned by ctor, safe to change
108
const key = isWindows ? 'windows' : isMacintosh ? 'osx' : isLinux ? 'linux' : undefined;
109
110
if (key && config && typeof config === 'object' && config.hasOwnProperty(key)) {
111
Object.keys(config[key]).forEach(k => config[k] = config[key][k]);
112
}
113
114
delete config.windows;
115
delete config.osx;
116
delete config.linux;
117
}
118
119
private parseVariable(str: string, start: number): { replacement: Replacement; end: number } | undefined {
120
if (str[start] !== '$' || str[start + 1] !== '{') {
121
return undefined;
122
}
123
124
let end = start + 2;
125
let braceCount = 1;
126
while (end < str.length) {
127
if (str[end] === '{') {
128
braceCount++;
129
} else if (str[end] === '}') {
130
braceCount--;
131
if (braceCount === 0) {
132
break;
133
}
134
}
135
end++;
136
}
137
138
if (braceCount !== 0) {
139
return undefined;
140
}
141
142
const id = str.slice(start, end + 1);
143
const inner = str.substring(start + 2, end);
144
const colonIdx = inner.indexOf(':');
145
if (colonIdx === -1) {
146
return { replacement: { id, name: inner, inner }, end };
147
}
148
149
return {
150
replacement: {
151
id,
152
inner,
153
name: inner.slice(0, colonIdx),
154
arg: inner.slice(colonIdx + 1)
155
},
156
end
157
};
158
}
159
160
private parseObject(obj: any): void {
161
if (typeof obj !== 'object' || obj === null) {
162
return;
163
}
164
165
if (Array.isArray(obj)) {
166
for (let i = 0; i < obj.length; i++) {
167
const value = obj[i];
168
if (typeof value === 'string') {
169
this.parseString(obj, i, value);
170
} else {
171
this.parseObject(value);
172
}
173
}
174
return;
175
}
176
177
for (const [key, value] of Object.entries(obj)) {
178
this.parseString(obj, key, key, true); // parse key
179
180
if (typeof value === 'string') {
181
this.parseString(obj, key, value);
182
} else {
183
this.parseObject(value);
184
}
185
}
186
}
187
188
private parseString(object: any, propertyName: string | number, value: string, replaceKeyName?: boolean, replacementPath?: string[]): void {
189
let pos = 0;
190
while (pos < value.length) {
191
const match = value.indexOf('${', pos);
192
if (match === -1) {
193
break;
194
}
195
const parsed = this.parseVariable(value, match);
196
if (parsed) {
197
pos = parsed.end + 1;
198
if (replacementPath?.includes(parsed.replacement.id)) {
199
continue;
200
}
201
202
const locations = this.locations.get(parsed.replacement.id) || { locations: [], replacement: parsed.replacement };
203
const newLocation: PropertyLocation = { object, propertyName, replaceKeyName };
204
locations.locations.push(newLocation);
205
this.locations.set(parsed.replacement.id, locations);
206
207
if (locations.resolved) {
208
this._resolveAtLocation(parsed.replacement, newLocation, locations.resolved, replacementPath);
209
} else {
210
this.newReplacementNotifiers.forEach(n => n(parsed.replacement));
211
}
212
} else {
213
pos = match + 2;
214
}
215
}
216
}
217
218
public *unresolved(): Iterable<Replacement> {
219
const newReplacements = new Map<string, Replacement>();
220
const notifier = (replacement: Replacement) => {
221
newReplacements.set(replacement.id, replacement);
222
};
223
224
for (const location of this.locations.values()) {
225
if (location.resolved === undefined) {
226
newReplacements.set(location.replacement.id, location.replacement);
227
}
228
}
229
230
this.newReplacementNotifiers.add(notifier);
231
232
while (true) {
233
const next = Iterable.first(newReplacements);
234
if (!next) {
235
break;
236
}
237
238
const [key, value] = next;
239
yield value;
240
newReplacements.delete(key);
241
}
242
243
this.newReplacementNotifiers.delete(notifier);
244
}
245
246
public resolved(): Iterable<[Replacement, IResolvedValue]> {
247
return Iterable.map(Iterable.filter(this.locations.values(), l => !!l.resolved), l => [l.replacement, l.resolved!]);
248
}
249
250
public resolve(replacement: Replacement, data: string | IResolvedValue): void {
251
if (typeof data !== 'object') {
252
data = { value: String(data) };
253
}
254
255
const location = this.locations.get(replacement.id);
256
if (!location) {
257
return;
258
}
259
260
location.resolved = data;
261
262
if (data.value !== undefined) {
263
for (const l of location.locations || Iterable.empty()) {
264
this._resolveAtLocation(replacement, l, data);
265
}
266
}
267
}
268
269
private _resolveAtLocation(replacement: Replacement, { replaceKeyName, propertyName, object }: PropertyLocation, data: IResolvedValue, path: string[] = []) {
270
if (data.value === undefined) {
271
return;
272
}
273
274
// avoid recursive resolution, e.g. ${env:FOO} -> ${env:BAR}=${env:FOO}
275
path.push(replacement.id);
276
277
// note: in nested `this.parseString`, parse only the new substring for any replacements, don't reparse the whole string
278
if (replaceKeyName && typeof propertyName === 'string') {
279
const value = object[propertyName];
280
const newKey = propertyName.replaceAll(replacement.id, data.value);
281
delete object[propertyName];
282
object[newKey] = value;
283
this._renameKeyInLocations(object, propertyName, newKey);
284
this.parseString(object, newKey, data.value, true, path);
285
} else {
286
object[propertyName] = object[propertyName].replaceAll(replacement.id, data.value);
287
this.parseString(object, propertyName, data.value, false, path);
288
}
289
290
path.pop();
291
}
292
293
private _renameKeyInLocations(obj: object, oldKey: string, newKey: string) {
294
for (const location of this.locations.values()) {
295
for (const loc of location.locations) {
296
if (loc.object === obj && loc.propertyName === oldKey) {
297
loc.propertyName = newKey;
298
}
299
}
300
}
301
}
302
303
public toObject(): T {
304
// If we wrapped a string, unwrap it
305
if (this.stringRoot) {
306
// eslint-disable-next-line local/code-no-any-casts
307
return (this.root as any).value as T;
308
}
309
310
return this.root;
311
}
312
}
313
314