Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.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
import { Queue } from '../../../../base/common/async.js';
6
import { IStringDictionary } from '../../../../base/common/collections.js';
7
import { Iterable } from '../../../../base/common/iterator.js';
8
import { LRUCache } from '../../../../base/common/map.js';
9
import { Schemas } from '../../../../base/common/network.js';
10
import { IProcessEnvironment } from '../../../../base/common/platform.js';
11
import * as Types from '../../../../base/common/types.js';
12
import { URI as uri } from '../../../../base/common/uri.js';
13
import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js';
14
import { localize } from '../../../../nls.js';
15
import { ICommandService } from '../../../../platform/commands/common/commands.js';
16
import { ConfigurationTarget, IConfigurationOverrides, IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
17
import { ILabelService } from '../../../../platform/label/common/label.js';
18
import { IInputOptions, IPickOptions, IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js';
19
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
20
import { IWorkspaceContextService, IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js';
21
import { EditorResourceAccessor, SideBySideEditor } from '../../../common/editor.js';
22
import { IEditorService } from '../../editor/common/editorService.js';
23
import { IExtensionService } from '../../extensions/common/extensions.js';
24
import { IPathService } from '../../path/common/pathService.js';
25
import { ConfiguredInput, VariableError, VariableKind } from '../common/configurationResolver.js';
26
import { ConfigurationResolverExpression, IResolvedValue } from '../common/configurationResolverExpression.js';
27
import { AbstractVariableResolverService } from '../common/variableResolver.js';
28
29
const LAST_INPUT_STORAGE_KEY = 'configResolveInputLru';
30
const LAST_INPUT_CACHE_SIZE = 5;
31
32
export abstract class BaseConfigurationResolverService extends AbstractVariableResolverService {
33
34
static readonly INPUT_OR_COMMAND_VARIABLES_PATTERN = /\${((input|command):(.*?))}/g;
35
36
private userInputAccessQueue = new Queue<string | IQuickPickItem | undefined>();
37
38
constructor(
39
context: {
40
getAppRoot: () => string | undefined;
41
getExecPath: () => string | undefined;
42
},
43
envVariablesPromise: Promise<IProcessEnvironment>,
44
editorService: IEditorService,
45
private readonly configurationService: IConfigurationService,
46
private readonly commandService: ICommandService,
47
workspaceContextService: IWorkspaceContextService,
48
private readonly quickInputService: IQuickInputService,
49
private readonly labelService: ILabelService,
50
private readonly pathService: IPathService,
51
extensionService: IExtensionService,
52
private readonly storageService: IStorageService,
53
) {
54
super({
55
getFolderUri: (folderName: string): uri | undefined => {
56
const folder = workspaceContextService.getWorkspace().folders.filter(f => f.name === folderName).pop();
57
return folder ? folder.uri : undefined;
58
},
59
getWorkspaceFolderCount: (): number => {
60
return workspaceContextService.getWorkspace().folders.length;
61
},
62
getConfigurationValue: (folderUri: uri | undefined, section: string): string | undefined => {
63
return configurationService.getValue<string>(section, folderUri ? { resource: folderUri } : {});
64
},
65
getAppRoot: (): string | undefined => {
66
return context.getAppRoot();
67
},
68
getExecPath: (): string | undefined => {
69
return context.getExecPath();
70
},
71
getFilePath: (): string | undefined => {
72
const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, {
73
supportSideBySide: SideBySideEditor.PRIMARY,
74
filterByScheme: [Schemas.file, Schemas.vscodeUserData, this.pathService.defaultUriScheme]
75
});
76
if (!fileResource) {
77
return undefined;
78
}
79
return this.labelService.getUriLabel(fileResource, { noPrefix: true });
80
},
81
getWorkspaceFolderPathForFile: (): string | undefined => {
82
const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, {
83
supportSideBySide: SideBySideEditor.PRIMARY,
84
filterByScheme: [Schemas.file, Schemas.vscodeUserData, this.pathService.defaultUriScheme]
85
});
86
if (!fileResource) {
87
return undefined;
88
}
89
const wsFolder = workspaceContextService.getWorkspaceFolder(fileResource);
90
if (!wsFolder) {
91
return undefined;
92
}
93
return this.labelService.getUriLabel(wsFolder.uri, { noPrefix: true });
94
},
95
getSelectedText: (): string | undefined => {
96
const activeTextEditorControl = editorService.activeTextEditorControl;
97
98
let activeControl: ICodeEditor | null = null;
99
100
if (isCodeEditor(activeTextEditorControl)) {
101
activeControl = activeTextEditorControl;
102
} else if (isDiffEditor(activeTextEditorControl)) {
103
const original = activeTextEditorControl.getOriginalEditor();
104
const modified = activeTextEditorControl.getModifiedEditor();
105
activeControl = original.hasWidgetFocus() ? original : modified;
106
}
107
108
const activeModel = activeControl?.getModel();
109
const activeSelection = activeControl?.getSelection();
110
if (activeModel && activeSelection) {
111
return activeModel.getValueInRange(activeSelection);
112
}
113
return undefined;
114
},
115
getLineNumber: (): string | undefined => {
116
const activeTextEditorControl = editorService.activeTextEditorControl;
117
if (isCodeEditor(activeTextEditorControl)) {
118
const selection = activeTextEditorControl.getSelection();
119
if (selection) {
120
const lineNumber = selection.positionLineNumber;
121
return String(lineNumber);
122
}
123
}
124
return undefined;
125
},
126
getColumnNumber: (): string | undefined => {
127
const activeTextEditorControl = editorService.activeTextEditorControl;
128
if (isCodeEditor(activeTextEditorControl)) {
129
const selection = activeTextEditorControl.getSelection();
130
if (selection) {
131
const columnNumber = selection.positionColumn;
132
return String(columnNumber);
133
}
134
}
135
return undefined;
136
},
137
getExtension: id => {
138
return extensionService.getExtension(id);
139
},
140
}, labelService, pathService.userHome().then(home => home.path), envVariablesPromise);
141
142
this.resolvableVariables.add('command');
143
this.resolvableVariables.add('input');
144
}
145
146
override async resolveWithInteractionReplace(folder: IWorkspaceFolderData | undefined, config: unknown, section?: string, variables?: IStringDictionary<string>, target?: ConfigurationTarget): Promise<any> {
147
const parsed = ConfigurationResolverExpression.parse(config);
148
await this.resolveWithInteraction(folder, parsed, section, variables, target);
149
150
return parsed.toObject();
151
}
152
153
override async resolveWithInteraction(folder: IWorkspaceFolderData | undefined, config: unknown, section?: string, variableToCommandMap?: IStringDictionary<string>, target?: ConfigurationTarget): Promise<Map<string, string> | undefined> {
154
const expr = ConfigurationResolverExpression.parse(config);
155
156
// Get values for input variables from UI
157
for (const variable of expr.unresolved()) {
158
let result: IResolvedValue | undefined;
159
160
// Command
161
if (variable.name === 'command') {
162
const commandId = (variableToCommandMap ? variableToCommandMap[variable.arg!] : undefined) || variable.arg!;
163
const value = await this.commandService.executeCommand(commandId, expr.toObject());
164
if (!Types.isUndefinedOrNull(value)) {
165
if (typeof value !== 'string') {
166
throw new VariableError(VariableKind.Command, localize('commandVariable.noStringType', "Cannot substitute command variable '{0}' because command did not return a result of type string.", commandId));
167
}
168
result = { value };
169
}
170
}
171
// Input
172
else if (variable.name === 'input') {
173
result = await this.showUserInput(section!, variable.arg!, await this.resolveInputs(folder, section!, target), variableToCommandMap);
174
}
175
// Contributed variable
176
else if (this._contributedVariables.has(variable.inner)) {
177
result = { value: await this._contributedVariables.get(variable.inner)!() };
178
}
179
else {
180
// Fallback to parent evaluation
181
const resolvedValue = await this.evaluateSingleVariable(variable, folder?.uri);
182
if (resolvedValue === undefined) {
183
// Not something we can handle
184
continue;
185
}
186
result = typeof resolvedValue === 'string' ? { value: resolvedValue } : resolvedValue;
187
}
188
189
if (result === undefined) {
190
// Skip the entire flow if any input variable was canceled
191
return undefined;
192
}
193
194
expr.resolve(variable, result);
195
}
196
197
return new Map(Iterable.map(expr.resolved(), ([key, value]) => [key.inner, value.value!]));
198
}
199
200
private async resolveInputs(folder: IWorkspaceFolderData | undefined, section: string, target?: ConfigurationTarget): Promise<ConfiguredInput[] | undefined> {
201
if (!section) {
202
return undefined;
203
}
204
205
// Look at workspace configuration
206
let inputs: ConfiguredInput[] | undefined;
207
const overrides: IConfigurationOverrides = folder ? { resource: folder.uri } : {};
208
const result = this.configurationService.inspect<{ inputs?: ConfiguredInput[] }>(section, overrides);
209
210
if (result) {
211
switch (target) {
212
case ConfigurationTarget.MEMORY: inputs = result.memoryValue?.inputs; break;
213
case ConfigurationTarget.DEFAULT: inputs = result.defaultValue?.inputs; break;
214
case ConfigurationTarget.USER: inputs = result.userValue?.inputs; break;
215
case ConfigurationTarget.USER_LOCAL: inputs = result.userLocalValue?.inputs; break;
216
case ConfigurationTarget.USER_REMOTE: inputs = result.userRemoteValue?.inputs; break;
217
case ConfigurationTarget.APPLICATION: inputs = result.applicationValue?.inputs; break;
218
case ConfigurationTarget.WORKSPACE: inputs = result.workspaceValue?.inputs; break;
219
220
case ConfigurationTarget.WORKSPACE_FOLDER:
221
default:
222
inputs = result.workspaceFolderValue?.inputs;
223
break;
224
}
225
}
226
227
228
inputs ??= this.configurationService.getValue<any>(section, overrides)?.inputs;
229
230
return inputs;
231
}
232
233
private readInputLru(): LRUCache<string, string> {
234
const contents = this.storageService.get(LAST_INPUT_STORAGE_KEY, StorageScope.WORKSPACE);
235
const lru = new LRUCache<string, string>(LAST_INPUT_CACHE_SIZE);
236
try {
237
if (contents) {
238
lru.fromJSON(JSON.parse(contents));
239
}
240
} catch {
241
// ignored
242
}
243
244
return lru;
245
}
246
247
private storeInputLru(lru: LRUCache<string, string>): void {
248
this.storageService.store(LAST_INPUT_STORAGE_KEY, JSON.stringify(lru.toJSON()), StorageScope.WORKSPACE, StorageTarget.MACHINE);
249
}
250
251
private async showUserInput(section: string, variable: string, inputInfos: ConfiguredInput[] | undefined, variableToCommandMap?: IStringDictionary<string>): Promise<IResolvedValue | undefined> {
252
if (!inputInfos) {
253
throw new VariableError(VariableKind.Input, localize('inputVariable.noInputSection', "Variable '{0}' must be defined in an '{1}' section of the debug or task configuration.", variable, 'inputs'));
254
}
255
256
// Find info for the given input variable
257
const info = inputInfos.filter(item => item.id === variable).pop();
258
if (info) {
259
const missingAttribute = (attrName: string) => {
260
throw new VariableError(VariableKind.Input, localize('inputVariable.missingAttribute', "Input variable '{0}' is of type '{1}' and must include '{2}'.", variable, info.type, attrName));
261
};
262
263
const defaultValueMap = this.readInputLru();
264
const defaultValueKey = `${section}.${variable}`;
265
const previousPickedValue = defaultValueMap.get(defaultValueKey);
266
267
switch (info.type) {
268
case 'promptString': {
269
if (!Types.isString(info.description)) {
270
missingAttribute('description');
271
}
272
const inputOptions: IInputOptions = { prompt: info.description, ignoreFocusLost: true, value: variableToCommandMap?.[`input:${variable}`] ?? previousPickedValue ?? info.default };
273
if (info.password) {
274
inputOptions.password = info.password;
275
}
276
return this.userInputAccessQueue.queue(() => this.quickInputService.input(inputOptions)).then(resolvedInput => {
277
if (typeof resolvedInput === 'string' && !info.password) {
278
this.storeInputLru(defaultValueMap.set(defaultValueKey, resolvedInput));
279
}
280
return resolvedInput !== undefined ? { value: resolvedInput as string, input: info } : undefined;
281
});
282
}
283
284
case 'pickString': {
285
if (!Types.isString(info.description)) {
286
missingAttribute('description');
287
}
288
if (Array.isArray(info.options)) {
289
for (const pickOption of info.options) {
290
if (!Types.isString(pickOption) && !Types.isString(pickOption.value)) {
291
missingAttribute('value');
292
}
293
}
294
} else {
295
missingAttribute('options');
296
}
297
298
interface PickStringItem extends IQuickPickItem {
299
value: string;
300
}
301
const picks = new Array<PickStringItem>();
302
for (const pickOption of info.options) {
303
const value = Types.isString(pickOption) ? pickOption : pickOption.value;
304
const label = Types.isString(pickOption) ? undefined : pickOption.label;
305
306
const item: PickStringItem = {
307
label: label ? `${label}: ${value}` : value,
308
value: value
309
};
310
311
const topValue = variableToCommandMap?.[`input:${variable}`] ?? previousPickedValue ?? info.default;
312
if (value === info.default) {
313
item.description = localize('inputVariable.defaultInputValue', "(Default)");
314
picks.unshift(item);
315
} else if (value === topValue) {
316
picks.unshift(item);
317
} else {
318
picks.push(item);
319
}
320
}
321
322
const pickOptions: IPickOptions<PickStringItem> = { placeHolder: info.description, matchOnDetail: true, ignoreFocusLost: true };
323
return this.userInputAccessQueue.queue(() => this.quickInputService.pick(picks, pickOptions, undefined)).then(resolvedInput => {
324
if (resolvedInput) {
325
const value = (resolvedInput as PickStringItem).value;
326
this.storeInputLru(defaultValueMap.set(defaultValueKey, value));
327
return { value, input: info };
328
}
329
return undefined;
330
});
331
}
332
333
case 'command': {
334
if (!Types.isString(info.command)) {
335
missingAttribute('command');
336
}
337
return this.userInputAccessQueue.queue(() => this.commandService.executeCommand<string>(info.command, info.args)).then(result => {
338
if (typeof result === 'string' || Types.isUndefinedOrNull(result)) {
339
return { value: result, input: info };
340
}
341
throw new VariableError(VariableKind.Input, localize('inputVariable.command.noStringType', "Cannot substitute input variable '{0}' because command '{1}' did not return a result of type string.", variable, info.command));
342
});
343
}
344
345
default:
346
throw new VariableError(VariableKind.Input, localize('inputVariable.unknownType', "Input variable '{0}' can only be of type 'promptString', 'pickString', or 'command'.", variable));
347
}
348
}
349
350
throw new VariableError(VariableKind.Input, localize('inputVariable.undefinedVariable', "Undefined input variable '{0}' encountered. Remove or define '{0}' to continue.", variable));
351
}
352
}
353
354