src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts

312 LOC · 274 covered · 38 uncovered · 46 ranges · 142 concepts · 3 introducers · 100 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- configurationResolverExpression.ts ×13
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 configurationResolverExpression.ts ×18
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;
84 > this.stringRoot = false;
85 > this.root = structuredClone(object);
86 > }
87 > }
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) { configurationResolverExpression.ts ×18
96 > return object;
97 > }
98 >
99 > const expr = new ConfigurationResolverExpression<T>(object);
100 > expr.applyPlatformSpecificKeys();
101 > expr.parseObject(expr.root);
102 > return expr;
103 > }
105 > private applyPlatformSpecificKeys() {
106 > // eslint-disable-next-line local/code-no-any-casts configurationResolverExpression.ts ×18
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 }
114 > delete config.windows;
115 > delete config.osx;
116 > delete config.linux;
117 > }
119 > private parseVariable(str: string, start: number): { replacement: Replacement; end: number } | undefined {
120 > if (str[start] !== '$' || str[start + 1] !== '{') { configurationResolverExpression.ts ×15
121 return undefined;
122 }
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] === '}') { configurationResolverExpression.ts ×15
130 > braceCount--;
131 > if (braceCount === 0) {
132 > break;
133 > }
134 > }
135 > end++;
136 > }
137 >
138 > if (braceCount !== 0) {
139 return undefined;
140 }
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 > }
160 > private parseObject(obj: any): void {
161 > if (typeof obj !== 'object' || obj === null) { configurationResolverExpression.ts ×18
162 > return;
163 > }
164 >
165 > if (Array.isArray(obj)) {
166 > for (let i = 0; i < obj.length; i++) {
167 > const value = obj[i]; configurationResolverExpression.ts ×15
168 > if (typeof value === 'string') {
169 > this.parseString(obj, i, value);
170 > } else {
171 this.parseObject(value);
172 }
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 > }
188 > private parseString(object: any, propertyName: string | number, value: string, replaceKeyName?: boolean, replacementPath?: string[]): void {
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); configurationResolverExpression.ts ×15
196 > if (parsed) {
197 > pos = parsed.end + 1;
198 > if (replacementPath?.includes(parsed.replacement.id)) {
199 continue;
200 }
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);
210 > this.newReplacementNotifiers.forEach(n => n(parsed.replacement));
211 > }
212 > } else {
213 pos = match + 2;
214 }
216 > }
218 > public *unresolved(): Iterable<Replacement> {
219 > const newReplacements = new Map<string, Replacement>(); configurationResolverExpression.ts ×18
220 > const notifier = (replacement: Replacement) => {
221 newReplacements.set(replacement.id, replacement);
222 };
224 > for (const location of this.locations.values()) {
225 > if (location.resolved === undefined) { configurationResolverExpression.ts ×15
226 > newReplacements.set(location.replacement.id, location.replacement);
227 > }
228 > }
230 > this.newReplacementNotifiers.add(notifier);
231 >
232 > while (true) {
233 > const next = Iterable.first(newReplacements);
234 > if (!next) {
235 > break;
236 > }
238 > const [key, value] = next;
239 > yield value;
240 > newReplacements.delete(key);
241 > }
243 > this.newReplacementNotifiers.delete(notifier);
244 > }
246 > public resolved(): Iterable<[Replacement, IResolvedValue]> {
247 > return Iterable.map(Iterable.filter(this.locations.values(), l => !!l.resolved), l => [l.replacement, l.resolved!]); configurationResolverExpression.ts ×18
248 > }
250 > public resolve(replacement: Replacement, data: string | IResolvedValue): void {
251 > if (typeof data !== 'object') { configurationResolverExpression.ts ×18
252 > data = { value: String(data) };
253 > }
254 >
255 > const location = this.locations.get(replacement.id);
256 > if (!location) {
257 > return;
258 > }
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 > }
269 > private _resolveAtLocation(replacement: Replacement, { replaceKeyName, propertyName, object }: PropertyLocation, data: IResolvedValue, path: string[] = []) {
270 > if (data.value === undefined) { configurationResolverExpression.ts ×15
271 return;
272 }
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);
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 > }
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 }
303 > public toObject(): T {
304 > // If we wrapped a string, unwrap it configurationResolverExpression.ts ×18
305 > if (this.stringRoot) {
306 // eslint-disable-next-line local/code-no-any-casts
307 return (this.root as any).value as T;
308 }
310 > return this.root;
311 > }