Path: blob/main/src/vs/workbench/services/configurationResolver/common/variableResolver.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { IStringDictionary } from '../../../../base/common/collections.js';6import { normalizeDriveLetter } from '../../../../base/common/labels.js';7import * as paths from '../../../../base/common/path.js';8import { IProcessEnvironment, isWindows } from '../../../../base/common/platform.js';9import * as process from '../../../../base/common/process.js';10import * as types from '../../../../base/common/types.js';11import { URI as uri } from '../../../../base/common/uri.js';12import { localize } from '../../../../nls.js';13import { ILabelService } from '../../../../platform/label/common/label.js';14import { IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js';15import { allVariableKinds, IConfigurationResolverService, VariableError, VariableKind } from './configurationResolver.js';16import { ConfigurationResolverExpression, IResolvedValue, Replacement } from './configurationResolverExpression.js';1718interface IVariableResolveContext {19getFolderUri(folderName: string): uri | undefined;20getWorkspaceFolderCount(): number;21getConfigurationValue(folderUri: uri | undefined, section: string): string | undefined;22getAppRoot(): string | undefined;23getExecPath(): string | undefined;24getFilePath(): string | undefined;25getWorkspaceFolderPathForFile?(): string | undefined;26getSelectedText(): string | undefined;27getLineNumber(): string | undefined;28getColumnNumber(): string | undefined;29getExtension(id: string): Promise<{ readonly extensionLocation: uri } | undefined>;30}3132type Environment = { env: IProcessEnvironment | undefined; userHome: string | undefined };3334export abstract class AbstractVariableResolverService implements IConfigurationResolverService {3536declare readonly _serviceBrand: undefined;3738private _context: IVariableResolveContext;39private _labelService?: ILabelService;40private _envVariablesPromise?: Promise<IProcessEnvironment>;41private _userHomePromise?: Promise<string>;42protected _contributedVariables: Map<string, () => Promise<string | undefined>> = new Map();4344public readonly resolvableVariables = new Set<string>(allVariableKinds);4546constructor(_context: IVariableResolveContext, _labelService?: ILabelService, _userHomePromise?: Promise<string>, _envVariablesPromise?: Promise<IProcessEnvironment>) {47this._context = _context;48this._labelService = _labelService;49this._userHomePromise = _userHomePromise;50if (_envVariablesPromise) {51this._envVariablesPromise = _envVariablesPromise.then(envVariables => {52return this.prepareEnv(envVariables);53});54}55}5657private prepareEnv(envVariables: IProcessEnvironment): IProcessEnvironment {58// windows env variables are case insensitive59if (isWindows) {60const ev: IProcessEnvironment = Object.create(null);61Object.keys(envVariables).forEach(key => {62ev[key.toLowerCase()] = envVariables[key];63});64return ev;65}66return envVariables;67}6869public async resolveWithEnvironment(environment: IProcessEnvironment, folder: IWorkspaceFolderData | undefined, value: string): Promise<string> {70const expr = ConfigurationResolverExpression.parse(value);7172for (const replacement of expr.unresolved()) {73const resolvedValue = await this.evaluateSingleVariable(replacement, folder?.uri, environment);74if (resolvedValue !== undefined) {75expr.resolve(replacement, String(resolvedValue));76}77}7879return expr.toObject();80}8182public async resolveAsync<T>(folder: IWorkspaceFolderData | undefined, config: T): Promise<T extends ConfigurationResolverExpression<infer R> ? R : T> {83const expr = ConfigurationResolverExpression.parse(config);8485for (const replacement of expr.unresolved()) {86const resolvedValue = await this.evaluateSingleVariable(replacement, folder?.uri);87if (resolvedValue !== undefined) {88expr.resolve(replacement, String(resolvedValue));89}90}9192return expr.toObject() as any;93}9495public resolveWithInteractionReplace(folder: IWorkspaceFolderData | undefined, config: any): Promise<any> {96throw new Error('resolveWithInteractionReplace not implemented.');97}9899public resolveWithInteraction(folder: IWorkspaceFolderData | undefined, config: any): Promise<Map<string, string> | undefined> {100throw new Error('resolveWithInteraction not implemented.');101}102103public contributeVariable(variable: string, resolution: () => Promise<string | undefined>): void {104if (this._contributedVariables.has(variable)) {105throw new Error('Variable ' + variable + ' is contributed twice.');106} else {107this.resolvableVariables.add(variable);108this._contributedVariables.set(variable, resolution);109}110}111112private fsPath(displayUri: uri): string {113return this._labelService ? this._labelService.getUriLabel(displayUri, { noPrefix: true }) : displayUri.fsPath;114}115116protected async evaluateSingleVariable(replacement: Replacement, folderUri: uri | undefined, processEnvironment?: IProcessEnvironment, commandValueMapping?: IStringDictionary<IResolvedValue>): Promise<IResolvedValue | string | undefined> {117118119const environment: Environment = {120env: (processEnvironment !== undefined) ? this.prepareEnv(processEnvironment) : await this._envVariablesPromise,121userHome: (processEnvironment !== undefined) ? undefined : await this._userHomePromise122};123124const { name: variable, arg: argument } = replacement;125126// common error handling for all variables that require an open editor127const getFilePath = (variableKind: VariableKind): string => {128const filePath = this._context.getFilePath();129if (filePath) {130return normalizeDriveLetter(filePath);131}132throw new VariableError(variableKind, (localize('canNotResolveFile', "Variable {0} can not be resolved. Please open an editor.", replacement.id)));133};134135// common error handling for all variables that require an open editor136const getFolderPathForFile = (variableKind: VariableKind): string => {137const filePath = getFilePath(variableKind); // throws error if no editor open138if (this._context.getWorkspaceFolderPathForFile) {139const folderPath = this._context.getWorkspaceFolderPathForFile();140if (folderPath) {141return normalizeDriveLetter(folderPath);142}143}144throw new VariableError(variableKind, localize('canNotResolveFolderForFile', "Variable {0}: can not find workspace folder of '{1}'.", replacement.id, paths.basename(filePath)));145};146147// common error handling for all variables that require an open folder and accept a folder name argument148const getFolderUri = (variableKind: VariableKind): uri => {149if (argument) {150const folder = this._context.getFolderUri(argument);151if (folder) {152return folder;153}154throw new VariableError(variableKind, localize('canNotFindFolder', "Variable {0} can not be resolved. No such folder '{1}'.", variableKind, argument));155}156157if (folderUri) {158return folderUri;159}160161if (this._context.getWorkspaceFolderCount() > 1) {162throw 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));163}164throw new VariableError(variableKind, localize('canNotResolveWorkspaceFolder', "Variable {0} can not be resolved. Please open a folder.", variableKind));165};166167switch (variable) {168case 'env':169if (argument) {170if (environment.env) {171const env = environment.env[isWindows ? argument.toLowerCase() : argument];172if (types.isString(env)) {173return env;174}175}176return '';177}178throw new VariableError(VariableKind.Env, localize('missingEnvVarName', "Variable {0} can not be resolved because no environment variable name is given.", replacement.id));179180case 'config':181if (argument) {182const config = this._context.getConfigurationValue(folderUri, argument);183if (types.isUndefinedOrNull(config)) {184throw new VariableError(VariableKind.Config, localize('configNotFound', "Variable {0} can not be resolved because setting '{1}' not found.", replacement.id, argument));185}186if (types.isObject(config)) {187throw new VariableError(VariableKind.Config, localize('configNoString', "Variable {0} can not be resolved because '{1}' is a structured value.", replacement.id, argument));188}189return config;190}191throw new VariableError(VariableKind.Config, localize('missingConfigName', "Variable {0} can not be resolved because no settings name is given.", replacement.id));192193case 'command':194return this.resolveFromMap(VariableKind.Command, replacement.id, argument, commandValueMapping, 'command');195196case 'input':197return this.resolveFromMap(VariableKind.Input, replacement.id, argument, commandValueMapping, 'input');198199case 'extensionInstallFolder':200if (argument) {201const ext = await this._context.getExtension(argument);202if (!ext) {203throw new VariableError(VariableKind.ExtensionInstallFolder, localize('extensionNotInstalled', "Variable {0} can not be resolved because the extension {1} is not installed.", replacement.id, argument));204}205return this.fsPath(ext.extensionLocation);206}207throw new VariableError(VariableKind.ExtensionInstallFolder, localize('missingExtensionName', "Variable {0} can not be resolved because no extension name is given.", replacement.id));208209default: {210switch (variable) {211case 'workspaceRoot':212case 'workspaceFolder': {213const uri = getFolderUri(VariableKind.WorkspaceFolder);214return uri ? normalizeDriveLetter(this.fsPath(uri)) : undefined;215}216217case 'cwd': {218if (!folderUri && !argument) {219return process.cwd();220}221const uri = getFolderUri(VariableKind.Cwd);222return uri ? normalizeDriveLetter(this.fsPath(uri)) : undefined;223}224225case 'workspaceRootFolderName':226case 'workspaceFolderBasename': {227const uri = getFolderUri(VariableKind.WorkspaceFolderBasename);228return uri ? normalizeDriveLetter(paths.basename(this.fsPath(uri))) : undefined;229}230231case 'userHome':232if (environment.userHome) {233return environment.userHome;234}235throw new VariableError(VariableKind.UserHome, localize('canNotResolveUserHome', "Variable {0} can not be resolved. UserHome path is not defined", replacement.id));236237case 'lineNumber': {238const lineNumber = this._context.getLineNumber();239if (lineNumber) {240return lineNumber;241}242throw 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));243}244245case 'columnNumber': {246const columnNumber = this._context.getColumnNumber();247if (columnNumber) {248return columnNumber;249}250throw new Error(localize('canNotResolveColumnNumber', "Variable {0} can not be resolved. Make sure to have a column selected in the active editor.", replacement.id));251}252253case 'selectedText': {254const selectedText = this._context.getSelectedText();255if (selectedText) {256return selectedText;257}258throw 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));259}260261case 'file':262return getFilePath(VariableKind.File);263264case 'fileWorkspaceFolder':265return getFolderPathForFile(VariableKind.FileWorkspaceFolder);266267case 'fileWorkspaceFolderBasename':268return paths.basename(getFolderPathForFile(VariableKind.FileWorkspaceFolderBasename));269270case 'relativeFile':271if (folderUri || argument) {272return paths.relative(this.fsPath(getFolderUri(VariableKind.RelativeFile)), getFilePath(VariableKind.RelativeFile));273}274return getFilePath(VariableKind.RelativeFile);275276case 'relativeFileDirname': {277const dirname = paths.dirname(getFilePath(VariableKind.RelativeFileDirname));278if (folderUri || argument) {279const relative = paths.relative(this.fsPath(getFolderUri(VariableKind.RelativeFileDirname)), dirname);280return relative.length === 0 ? '.' : relative;281}282return dirname;283}284285case 'fileDirname':286return paths.dirname(getFilePath(VariableKind.FileDirname));287288case 'fileExtname':289return paths.extname(getFilePath(VariableKind.FileExtname));290291case 'fileBasename':292return paths.basename(getFilePath(VariableKind.FileBasename));293294case 'fileBasenameNoExtension': {295const basename = paths.basename(getFilePath(VariableKind.FileBasenameNoExtension));296return (basename.slice(0, basename.length - paths.extname(basename).length));297}298299case 'fileDirnameBasename':300return paths.basename(paths.dirname(getFilePath(VariableKind.FileDirnameBasename)));301302case 'execPath': {303const ep = this._context.getExecPath();304if (ep) {305return ep;306}307return replacement.id;308}309310case 'execInstallFolder': {311const ar = this._context.getAppRoot();312if (ar) {313return ar;314}315return replacement.id;316}317318case 'pathSeparator':319case '/':320return paths.sep;321322default: {323try {324return this.resolveFromMap(VariableKind.Unknown, replacement.id, argument, commandValueMapping, undefined);325} catch {326return replacement.id;327}328}329}330}331}332}333334private resolveFromMap(variableKind: VariableKind, match: string, argument: string | undefined, commandValueMapping: IStringDictionary<IResolvedValue> | undefined, prefix: string | undefined): string {335if (argument && commandValueMapping) {336const v = (prefix === undefined) ? commandValueMapping[argument] : commandValueMapping[prefix + ':' + argument];337if (typeof v === 'string') {338return v;339}340throw new VariableError(variableKind, localize('noValueForCommand', "Variable {0} can not be resolved because the command has no value.", match));341}342return match;343}344}345346347