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
3296 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
this.root = { value: object } as any;
82
} else {
83
this.stringRoot = false;
84
this.root = structuredClone(object);
85
}
86
}
87
88
/**
89
* Creates a new {@link ConfigurationResolverExpression} from an object.
90
* Note that platform-specific keys (i.e. `windows`, `osx`, `linux`) are
91
* applied during parsing.
92
*/
93
public static parse<T>(object: T): ConfigurationResolverExpression<T> {
94
if (object instanceof ConfigurationResolverExpression) {
95
return object;
96
}
97
98
const expr = new ConfigurationResolverExpression<T>(object);
99
expr.applyPlatformSpecificKeys();
100
expr.parseObject(expr.root);
101
return expr;
102
}
103
104
private applyPlatformSpecificKeys() {
105
const config = this.root as any; // already cloned by ctor, safe to change
106
const key = isWindows ? 'windows' : isMacintosh ? 'osx' : isLinux ? 'linux' : undefined;
107
108
if (key && config && typeof config === 'object' && config.hasOwnProperty(key)) {
109
Object.keys(config[key]).forEach(k => config[k] = config[key][k]);
110
}
111
112
delete config.windows;
113
delete config.osx;
114
delete config.linux;
115
}
116
117
private parseVariable(str: string, start: number): { replacement: Replacement; end: number } | undefined {
118
if (str[start] !== '$' || str[start + 1] !== '{') {
119
return undefined;
120
}
121
122
let end = start + 2;
123
let braceCount = 1;
124
while (end < str.length) {
125
if (str[end] === '{') {
126
braceCount++;
127
} else if (str[end] === '}') {
128
braceCount--;
129
if (braceCount === 0) {
130
break;
131
}
132
}
133
end++;
134
}
135
136
if (braceCount !== 0) {
137
return undefined;
138
}
139
140
const id = str.slice(start, end + 1);
141
const inner = str.substring(start + 2, end);
142
const colonIdx = inner.indexOf(':');
143
if (colonIdx === -1) {
144
return { replacement: { id, name: inner, inner }, end };
145
}
146
147
return {
148
replacement: {
149
id,
150
inner,
151
name: inner.slice(0, colonIdx),
152
arg: inner.slice(colonIdx + 1)
153
},
154
end
155
};
156
}
157
158
private parseObject(obj: any): void {
159
if (typeof obj !== 'object' || obj === null) {
160
return;
161
}
162
163
if (Array.isArray(obj)) {
164
for (let i = 0; i < obj.length; i++) {
165
const value = obj[i];
166
if (typeof value === 'string') {
167
this.parseString(obj, i, value);
168
} else {
169
this.parseObject(value);
170
}
171
}
172
return;
173
}
174
175
for (const [key, value] of Object.entries(obj)) {
176
this.parseString(obj, key, key, true); // parse key
177
178
if (typeof value === 'string') {
179
this.parseString(obj, key, value);
180
} else {
181
this.parseObject(value);
182
}
183
}
184
}
185
186
private parseString(object: any, propertyName: string | number, value: string, replaceKeyName?: boolean, replacementPath?: string[]): void {
187
let pos = 0;
188
while (pos < value.length) {
189
const match = value.indexOf('${', pos);
190
if (match === -1) {
191
break;
192
}
193
const parsed = this.parseVariable(value, match);
194
if (parsed) {
195
pos = parsed.end + 1;
196
if (replacementPath?.includes(parsed.replacement.id)) {
197
continue;
198
}
199
200
const locations = this.locations.get(parsed.replacement.id) || { locations: [], replacement: parsed.replacement };
201
const newLocation: PropertyLocation = { object, propertyName, replaceKeyName };
202
locations.locations.push(newLocation);
203
this.locations.set(parsed.replacement.id, locations);
204
205
if (locations.resolved) {
206
this._resolveAtLocation(parsed.replacement, newLocation, locations.resolved, replacementPath);
207
} else {
208
this.newReplacementNotifiers.forEach(n => n(parsed.replacement));
209
}
210
} else {
211
pos = match + 2;
212
}
213
}
214
}
215
216
public *unresolved(): Iterable<Replacement> {
217
const newReplacements = new Map<string, Replacement>();
218
const notifier = (replacement: Replacement) => {
219
newReplacements.set(replacement.id, replacement);
220
};
221
222
for (const location of this.locations.values()) {
223
if (location.resolved === undefined) {
224
newReplacements.set(location.replacement.id, location.replacement);
225
}
226
}
227
228
this.newReplacementNotifiers.add(notifier);
229
230
while (true) {
231
const next = Iterable.first(newReplacements);
232
if (!next) {
233
break;
234
}
235
236
const [key, value] = next;
237
yield value;
238
newReplacements.delete(key);
239
}
240
241
this.newReplacementNotifiers.delete(notifier);
242
}
243
244
public resolved(): Iterable<[Replacement, IResolvedValue]> {
245
return Iterable.map(Iterable.filter(this.locations.values(), l => !!l.resolved), l => [l.replacement, l.resolved!]);
246
}
247
248
public resolve(replacement: Replacement, data: string | IResolvedValue): void {
249
if (typeof data !== 'object') {
250
data = { value: String(data) };
251
}
252
253
const location = this.locations.get(replacement.id);
254
if (!location) {
255
return;
256
}
257
258
location.resolved = data;
259
260
if (data.value !== undefined) {
261
for (const l of location.locations || Iterable.empty()) {
262
this._resolveAtLocation(replacement, l, data);
263
}
264
}
265
}
266
267
private _resolveAtLocation(replacement: Replacement, { replaceKeyName, propertyName, object }: PropertyLocation, data: IResolvedValue, path: string[] = []) {
268
if (data.value === undefined) {
269
return;
270
}
271
272
// avoid recursive resolution, e.g. ${env:FOO} -> ${env:BAR}=${env:FOO}
273
path.push(replacement.id);
274
275
// note: in nested `this.parseString`, parse only the new substring for any replacements, don't reparse the whole string
276
if (replaceKeyName && typeof propertyName === 'string') {
277
const value = object[propertyName];
278
const newKey = propertyName.replaceAll(replacement.id, data.value);
279
delete object[propertyName];
280
object[newKey] = value;
281
this._renameKeyInLocations(object, propertyName, newKey);
282
this.parseString(object, newKey, data.value, true, path);
283
} else {
284
object[propertyName] = object[propertyName].replaceAll(replacement.id, data.value);
285
this.parseString(object, propertyName, data.value, false, path);
286
}
287
288
path.pop();
289
}
290
291
private _renameKeyInLocations(obj: object, oldKey: string, newKey: string) {
292
for (const location of this.locations.values()) {
293
for (const loc of location.locations) {
294
if (loc.object === obj && loc.propertyName === oldKey) {
295
loc.propertyName = newKey;
296
}
297
}
298
}
299
}
300
301
public toObject(): T {
302
// If we wrapped a string, unwrap it
303
if (this.stringRoot) {
304
return (this.root as any).value as T;
305
}
306
307
return this.root;
308
}
309
}
310
311