Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/configurationResolver/common/variableResolver.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 { IStringDictionary } from '../../../../base/common/collections.js';
7
import { normalizeDriveLetter } from '../../../../base/common/labels.js';
8
import * as paths from '../../../../base/common/path.js';
9
import { IProcessEnvironment, isWindows } from '../../../../base/common/platform.js';
10
import * as process from '../../../../base/common/process.js';
11
import * as types from '../../../../base/common/types.js';
12
import { URI as uri } from '../../../../base/common/uri.js';
13
import { localize } from '../../../../nls.js';
14
import { ILabelService } from '../../../../platform/label/common/label.js';
15
import { IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js';
16
import { allVariableKinds, IConfigurationResolverService, VariableError, VariableKind } from './configurationResolver.js';
17
import { ConfigurationResolverExpression, IResolvedValue, Replacement } from './configurationResolverExpression.js';
18
19
interface IVariableResolveContext {
20
getFolderUri(folderName: string): uri | undefined;
21
getWorkspaceFolderCount(): number;
22
getConfigurationValue(folderUri: uri | undefined, section: string): string | undefined;
23
getAppRoot(): string | undefined;
24
getExecPath(): string | undefined;
25
getFilePath(): string | undefined;
26
getWorkspaceFolderPathForFile?(): string | undefined;
27
getSelectedText(): string | undefined;
28
getLineNumber(): string | undefined;
29
getColumnNumber(): string | undefined;
30
getExtension(id: string): Promise<{ readonly extensionLocation: uri } | undefined>;
31
}
32
33
type Environment = { env: IProcessEnvironment | undefined; userHome: string | undefined };
34
35
export abstract class AbstractVariableResolverService implements IConfigurationResolverService {
36
37
declare readonly _serviceBrand: undefined;
38
39
private _context: IVariableResolveContext;
40
private _labelService?: ILabelService;
41
private _envVariablesPromise?: Promise<IProcessEnvironment>;
42
private _userHomePromise?: Promise<string>;
43
protected _contributedVariables: Map<string, () => Promise<string | undefined>> = new Map();
44
45
public readonly resolvableVariables = new Set<string>(allVariableKinds);
46
47
constructor(_context: IVariableResolveContext, _labelService?: ILabelService, _userHomePromise?: Promise<string>, _envVariablesPromise?: Promise<IProcessEnvironment>) {
48
this._context = _context;
49
this._labelService = _labelService;
50
this._userHomePromise = _userHomePromise;
51
if (_envVariablesPromise) {
52
this._envVariablesPromise = _envVariablesPromise.then(envVariables => {
53
return this.prepareEnv(envVariables);
54
});
55
}
56
}
57
58
private prepareEnv(envVariables: IProcessEnvironment): IProcessEnvironment {
59
// windows env variables are case insensitive
60
if (isWindows) {
61
const ev: IProcessEnvironment = Object.create(null);
62
Object.keys(envVariables).forEach(key => {
63
ev[key.toLowerCase()] = envVariables[key];
64
});
65
return ev;
66
}
67
return envVariables;
68
}
69
70
public async resolveWithEnvironment(environment: IProcessEnvironment, folder: IWorkspaceFolderData | undefined, value: string): Promise<string> {
71
const expr = ConfigurationResolverExpression.parse(value);
72
73
for (const replacement of expr.unresolved()) {
74
const resolvedValue = await this.evaluateSingleVariable(replacement, folder?.uri, environment);
75
if (resolvedValue !== undefined) {
76
expr.resolve(replacement, String(resolvedValue));
77
}
78
}
79
80
return expr.toObject();
81
}
82
83
public async resolveAsync<T>(folder: IWorkspaceFolderData | undefined, config: T): Promise<T extends ConfigurationResolverExpression<infer R> ? R : T> {
84
const expr = ConfigurationResolverExpression.parse(config);
85
86
for (const replacement of expr.unresolved()) {
87
const resolvedValue = await this.evaluateSingleVariable(replacement, folder?.uri);
88
if (resolvedValue !== undefined) {
89
expr.resolve(replacement, String(resolvedValue));
90
}
91
}
92
93
return expr.toObject() as any;
94
}
95
96
public resolveWithInteractionReplace(folder: IWorkspaceFolderData | undefined, config: any): Promise<any> {
97
throw new Error('resolveWithInteractionReplace not implemented.');
98
}
99
100
public resolveWithInteraction(folder: IWorkspaceFolderData | undefined, config: any): Promise<Map<string, string> | undefined> {
101
throw new Error('resolveWithInteraction not implemented.');
102
}
103
104
public contributeVariable(variable: string, resolution: () => Promise<string | undefined>): void {
105
if (this._contributedVariables.has(variable)) {
106
throw new Error('Variable ' + variable + ' is contributed twice.');
107
} else {
108
this.resolvableVariables.add(variable);
109
this._contributedVariables.set(variable, resolution);
110
}
111
}
112
113
private fsPath(displayUri: uri): string {
114
return this._labelService ? this._labelService.getUriLabel(displayUri, { noPrefix: true }) : displayUri.fsPath;
115
}
116
117
protected async evaluateSingleVariable(replacement: Replacement, folderUri: uri | undefined, processEnvironment?: IProcessEnvironment, commandValueMapping?: IStringDictionary<IResolvedValue>): Promise<IResolvedValue | string | undefined> {
118
119
120
const environment: Environment = {
121
env: (processEnvironment !== undefined) ? this.prepareEnv(processEnvironment) : await this._envVariablesPromise,
122
userHome: (processEnvironment !== undefined) ? undefined : await this._userHomePromise
123
};
124
125
const { name: variable, arg: argument } = replacement;
126
127
// common error handling for all variables that require an open editor
128
const getFilePath = (variableKind: VariableKind): string => {
129
const filePath = this._context.getFilePath();
130
if (filePath) {
131
return normalizeDriveLetter(filePath);
132
}
133
throw new VariableError(variableKind, (localize('canNotResolveFile', "Variable {0} can not be resolved. Please open an editor.", replacement.id)));
134
};
135
136
// common error handling for all variables that require an open editor
137
const getFolderPathForFile = (variableKind: VariableKind): string => {
138
const filePath = getFilePath(variableKind); // throws error if no editor open
139
if (this._context.getWorkspaceFolderPathForFile) {
140
const folderPath = this._context.getWorkspaceFolderPathForFile();
141
if (folderPath) {
142
return normalizeDriveLetter(folderPath);
143
}
144
}
145
throw new VariableError(variableKind, localize('canNotResolveFolderForFile', "Variable {0}: can not find workspace folder of '{1}'.", replacement.id, paths.basename(filePath)));
146
};
147
148
// common error handling for all variables that require an open folder and accept a folder name argument
149
const getFolderUri = (variableKind: VariableKind): uri => {
150
if (argument) {
151
const folder = this._context.getFolderUri(argument);
152
if (folder) {
153
return folder;
154
}
155
throw new VariableError(variableKind, localize('canNotFindFolder', "Variable {0} can not be resolved. No such folder '{1}'.", variableKind, argument));
156
}
157
158
if (folderUri) {
159
return folderUri;
160
}
161
162
if (this._context.getWorkspaceFolderCount() > 1) {
163
throw new VariableError(variableKind, localize('canNotResolveWorkspaceFolderMultiRoot', "Variable {0} can not be resolved in a multi folder workspace. Scope this variable using ':' and a workspace folder name.", variableKind));
164
}
165
throw new VariableError(variableKind, localize('canNotResolveWorkspaceFolder', "Variable {0} can not be resolved. Please open a folder.", variableKind));
166
};
167
168
switch (variable) {
169
case 'env':
170
if (argument) {
171
if (environment.env) {
172
const env = environment.env[isWindows ? argument.toLowerCase() : argument];
173
if (types.isString(env)) {
174
return env;
175
}
176
}
177
return '';
178
}
179
throw new VariableError(VariableKind.Env, localize('missingEnvVarName', "Variable {0} can not be resolved because no environment variable name is given.", replacement.id));
180
181
case 'config':
182
if (argument) {
183
const config = this._context.getConfigurationValue(folderUri, argument);
184
if (types.isUndefinedOrNull(config)) {
185
throw new VariableError(VariableKind.Config, localize('configNotFound', "Variable {0} can not be resolved because setting '{1}' not found.", replacement.id, argument));
186
}
187
if (types.isObject(config)) {
188
throw new VariableError(VariableKind.Config, localize('configNoString', "Variable {0} can not be resolved because '{1}' is a structured value.", replacement.id, argument));
189
}
190
return config;
191
}
192
throw new VariableError(VariableKind.Config, localize('missingConfigName', "Variable {0} can not be resolved because no settings name is given.", replacement.id));
193
194
case 'command':
195
return this.resolveFromMap(VariableKind.Command, replacement.id, argument, commandValueMapping, 'command');
196
197
case 'input':
198
return this.resolveFromMap(VariableKind.Input, replacement.id, argument, commandValueMapping, 'input');
199
200
case 'extensionInstallFolder':
201
if (argument) {
202
const ext = await this._context.getExtension(argument);
203
if (!ext) {
204
throw new VariableError(VariableKind.ExtensionInstallFolder, localize('extensionNotInstalled', "Variable {0} can not be resolved because the extension {1} is not installed.", replacement.id, argument));
205
}
206
return this.fsPath(ext.extensionLocation);
207
}
208
throw new VariableError(VariableKind.ExtensionInstallFolder, localize('missingExtensionName', "Variable {0} can not be resolved because no extension name is given.", replacement.id));
209
210
default: {
211
switch (variable) {
212
case 'workspaceRoot':
213
case 'workspaceFolder': {
214
const uri = getFolderUri(VariableKind.WorkspaceFolder);
215
return uri ? normalizeDriveLetter(this.fsPath(uri)) : undefined;
216
}
217
218
case 'cwd': {
219
if (!folderUri && !argument) {
220
return process.cwd();
221
}
222
const uri = getFolderUri(VariableKind.Cwd);
223
return uri ? normalizeDriveLetter(this.fsPath(uri)) : undefined;
224
}
225
226
case 'workspaceRootFolderName':
227
case 'workspaceFolderBasename': {
228
const uri = getFolderUri(VariableKind.WorkspaceFolderBasename);
229
return uri ? normalizeDriveLetter(paths.basename(this.fsPath(uri))) : undefined;
230
}
231
232
case 'userHome':
233
if (environment.userHome) {
234
return environment.userHome;
235
}
236
throw new VariableError(VariableKind.UserHome, localize('canNotResolveUserHome', "Variable {0} can not be resolved. UserHome path is not defined", replacement.id));
237
238
case 'lineNumber': {
239
const lineNumber = this._context.getLineNumber();
240
if (lineNumber) {
241
return lineNumber;
242
}
243
throw new VariableError(VariableKind.LineNumber, localize('canNotResolveLineNumber', "Variable {0} can not be resolved. Make sure to have a line selected in the active editor.", replacement.id));
244
}
245
246
case 'columnNumber': {
247
const columnNumber = this._context.getColumnNumber();
248
if (columnNumber) {
249
return columnNumber;
250
}
251
throw new Error(localize('canNotResolveColumnNumber', "Variable {0} can not be resolved. Make sure to have a column selected in the active editor.", replacement.id));
252
}
253
254
case 'selectedText': {
255
const selectedText = this._context.getSelectedText();
256
if (selectedText) {
257
return selectedText;
258
}
259
throw new VariableError(VariableKind.SelectedText, localize('canNotResolveSelectedText', "Variable {0} can not be resolved. Make sure to have some text selected in the active editor.", replacement.id));
260
}
261
262
case 'file':
263
return getFilePath(VariableKind.File);
264
265
case 'fileWorkspaceFolder':
266
return getFolderPathForFile(VariableKind.FileWorkspaceFolder);
267
268
case 'fileWorkspaceFolderBasename':
269
return paths.basename(getFolderPathForFile(VariableKind.FileWorkspaceFolderBasename));
270
271
case 'relativeFile':
272
if (folderUri || argument) {
273
return paths.relative(this.fsPath(getFolderUri(VariableKind.RelativeFile)), getFilePath(VariableKind.RelativeFile));
274
}
275
return getFilePath(VariableKind.RelativeFile);
276
277
case 'relativeFileDirname': {
278
const dirname = paths.dirname(getFilePath(VariableKind.RelativeFileDirname));
279
if (folderUri || argument) {
280
const relative = paths.relative(this.fsPath(getFolderUri(VariableKind.RelativeFileDirname)), dirname);
281
return relative.length === 0 ? '.' : relative;
282
}
283
return dirname;
284
}
285
286
case 'fileDirname':
287
return paths.dirname(getFilePath(VariableKind.FileDirname));
288
289
case 'fileExtname':
290
return paths.extname(getFilePath(VariableKind.FileExtname));
291
292
case 'fileBasename':
293
return paths.basename(getFilePath(VariableKind.FileBasename));
294
295
case 'fileBasenameNoExtension': {
296
const basename = paths.basename(getFilePath(VariableKind.FileBasenameNoExtension));
297
return (basename.slice(0, basename.length - paths.extname(basename).length));
298
}
299
300
case 'fileDirnameBasename':
301
return paths.basename(paths.dirname(getFilePath(VariableKind.FileDirnameBasename)));
302
303
case 'execPath': {
304
const ep = this._context.getExecPath();
305
if (ep) {
306
return ep;
307
}
308
return replacement.id;
309
}
310
311
case 'execInstallFolder': {
312
const ar = this._context.getAppRoot();
313
if (ar) {
314
return ar;
315
}
316
return replacement.id;
317
}
318
319
case 'pathSeparator':
320
case '/':
321
return paths.sep;
322
323
default: {
324
try {
325
return this.resolveFromMap(VariableKind.Unknown, replacement.id, argument, commandValueMapping, undefined);
326
} catch {
327
return replacement.id;
328
}
329
}
330
}
331
}
332
}
333
}
334
335
private resolveFromMap(variableKind: VariableKind, match: string, argument: string | undefined, commandValueMapping: IStringDictionary<IResolvedValue> | undefined, prefix: string | undefined): string {
336
if (argument && commandValueMapping) {
337
const v = (prefix === undefined) ? commandValueMapping[argument] : commandValueMapping[prefix + ':' + argument];
338
if (typeof v === 'string') {
339
return v;
340
}
341
throw new VariableError(variableKind, localize('noValueForCommand', "Variable {0} can not be resolved because the command has no value.", match));
342
}
343
return match;
344
}
345
}
346
347