src/vs/workbench/services/configurationResolver/common/variableResolver.ts (219 lines of code) (raw):
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as paths from 'vs/base/common/path';
import * as process from 'vs/base/common/process';
import * as types from 'vs/base/common/types';
import * as objects from 'vs/base/common/objects';
import { IStringDictionary } from 'vs/base/common/collections';
import { IProcessEnvironment, isWindows, isMacintosh, isLinux } from 'vs/base/common/platform';
import { normalizeDriveLetter } from 'vs/base/common/labels';
import { localize } from 'vs/nls';
import { URI as uri } from 'vs/base/common/uri';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
export interface IVariableResolveContext {
getFolderUri(folderName: string): uri | undefined;
getWorkspaceFolderCount(): number;
getConfigurationValue(folderUri: uri, section: string): string | undefined;
getExecPath(): string | undefined;
getFilePath(): string | undefined;
getSelectedText(): string | undefined;
getLineNumber(): string | undefined;
}
export class AbstractVariableResolverService implements IConfigurationResolverService {
static VARIABLE_REGEXP = /\$\{(.*?)\}/g;
_serviceBrand: any;
constructor(
private _context: IVariableResolveContext,
private _envVariables: IProcessEnvironment
) {
if (isWindows && _envVariables) {
this._envVariables = Object.create(null);
Object.keys(_envVariables).forEach(key => {
this._envVariables[key.toLowerCase()] = _envVariables[key];
});
}
}
public resolve(root: IWorkspaceFolder | undefined, value: string): string;
public resolve(root: IWorkspaceFolder | undefined, value: string[]): string[];
public resolve(root: IWorkspaceFolder | undefined, value: IStringDictionary<string>): IStringDictionary<string>;
public resolve(root: IWorkspaceFolder | undefined, value: any): any {
return this.recursiveResolve(root ? root.uri : undefined, value);
}
public resolveAnyBase(workspaceFolder: IWorkspaceFolder | undefined, config: any, commandValueMapping?: IStringDictionary<string>, resolvedVariables?: Map<string, string>): any {
const result = objects.deepClone(config) as any;
// hoist platform specific attributes to top level
if (isWindows && result.windows) {
Object.keys(result.windows).forEach(key => result[key] = result.windows[key]);
} else if (isMacintosh && result.osx) {
Object.keys(result.osx).forEach(key => result[key] = result.osx[key]);
} else if (isLinux && result.linux) {
Object.keys(result.linux).forEach(key => result[key] = result.linux[key]);
}
// delete all platform specific sections
delete result.windows;
delete result.osx;
delete result.linux;
// substitute all variables recursively in string values
return this.recursiveResolve(workspaceFolder ? workspaceFolder.uri : undefined, result, commandValueMapping, resolvedVariables);
}
public resolveAny(workspaceFolder: IWorkspaceFolder | undefined, config: any, commandValueMapping?: IStringDictionary<string>): any {
return this.resolveAnyBase(workspaceFolder, config, commandValueMapping);
}
public resolveAnyMap(workspaceFolder: IWorkspaceFolder | undefined, config: any, commandValueMapping?: IStringDictionary<string>): { newConfig: any, resolvedVariables: Map<string, string> } {
const resolvedVariables = new Map<string, string>();
const newConfig = this.resolveAnyBase(workspaceFolder, config, commandValueMapping, resolvedVariables);
return { newConfig, resolvedVariables };
}
public resolveWithInteractionReplace(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary<string>): Promise<any> {
throw new Error('resolveWithInteractionReplace not implemented.');
}
public resolveWithInteraction(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary<string>): Promise<Map<string, string> | undefined> {
throw new Error('resolveWithInteraction not implemented.');
}
private recursiveResolve(folderUri: uri | undefined, value: any, commandValueMapping?: IStringDictionary<string>, resolvedVariables?: Map<string, string>): any {
if (types.isString(value)) {
return this.resolveString(folderUri, value, commandValueMapping, resolvedVariables);
} else if (types.isArray(value)) {
return value.map(s => this.recursiveResolve(folderUri, s, commandValueMapping, resolvedVariables));
} else if (types.isObject(value)) {
let result: IStringDictionary<string | IStringDictionary<string> | string[]> = Object.create(null);
Object.keys(value).forEach(key => {
const replaced = this.resolveString(folderUri, key, commandValueMapping, resolvedVariables);
result[replaced] = this.recursiveResolve(folderUri, value[key], commandValueMapping, resolvedVariables);
});
return result;
}
return value;
}
private resolveString(folderUri: uri | undefined, value: string, commandValueMapping: IStringDictionary<string> | undefined, resolvedVariables?: Map<string, string>): string {
// loop through all variables occurrences in 'value'
const replaced = value.replace(AbstractVariableResolverService.VARIABLE_REGEXP, (match: string, variable: string) => {
let resolvedValue = this.evaluateSingleVariable(match, variable, folderUri, commandValueMapping);
if (resolvedVariables) {
resolvedVariables.set(variable, resolvedValue);
}
return resolvedValue;
});
return replaced;
}
private evaluateSingleVariable(match: string, variable: string, folderUri: uri | undefined, commandValueMapping: IStringDictionary<string> | undefined): string {
// try to separate variable arguments from variable name
let argument: string | undefined;
const parts = variable.split(':');
if (parts.length > 1) {
variable = parts[0];
argument = parts[1];
}
// common error handling for all variables that require an open editor
const getFilePath = (): string => {
const filePath = this._context.getFilePath();
if (filePath) {
return filePath;
}
throw new Error(localize('canNotResolveFile', "'{0}' can not be resolved. Please open an editor.", match));
};
// common error handling for all variables that require an open folder and accept a folder name argument
const getFolderUri = (withArg = true): uri => {
if (withArg && argument) {
const folder = this._context.getFolderUri(argument);
if (folder) {
return folder;
}
throw new Error(localize('canNotFindFolder', "'{0}' can not be resolved. No such folder '{1}'.", match, argument));
}
if (folderUri) {
return folderUri;
}
if (this._context.getWorkspaceFolderCount() > 1) {
throw new Error(localize('canNotResolveWorkspaceFolderMultiRoot', "'{0}' can not be resolved in a multi folder workspace. Scope this variable using ':' and a workspace folder name.", match));
}
throw new Error(localize('canNotResolveWorkspaceFolder', "'{0}' can not be resolved. Please open a folder.", match));
};
switch (variable) {
case 'env':
if (argument) {
if (isWindows) {
argument = argument.toLowerCase();
}
const env = this._envVariables[argument];
if (types.isString(env)) {
return env;
}
// For `env` we should do the same as a normal shell does - evaluates missing envs to an empty string #46436
return '';
}
throw new Error(localize('missingEnvVarName', "'{0}' can not be resolved because no environment variable name is given.", match));
case 'config':
if (argument) {
const config = this._context.getConfigurationValue(getFolderUri(false), argument);
if (types.isUndefinedOrNull(config)) {
throw new Error(localize('configNotFound', "'{0}' can not be resolved because setting '{1}' not found.", match, argument));
}
if (types.isObject(config)) {
throw new Error(localize('configNoString', "'{0}' can not be resolved because '{1}' is a structured value.", match, argument));
}
return config;
}
throw new Error(localize('missingConfigName', "'{0}' can not be resolved because no settings name is given.", match));
case 'command':
return this.resolveFromMap(match, argument, commandValueMapping, 'command');
case 'input':
return this.resolveFromMap(match, argument, commandValueMapping, 'input');
default: {
switch (variable) {
case 'workspaceRoot':
case 'workspaceFolder':
return normalizeDriveLetter(getFolderUri().fsPath);
case 'cwd':
return (folderUri ? normalizeDriveLetter(getFolderUri().fsPath) : process.cwd());
case 'workspaceRootFolderName':
case 'workspaceFolderBasename':
return paths.basename(getFolderUri().fsPath);
case 'lineNumber':
const lineNumber = this._context.getLineNumber();
if (lineNumber) {
return lineNumber;
}
throw new Error(localize('canNotResolveLineNumber', "'{0}' can not be resolved. Make sure to have a line selected in the active editor.", match));
case 'selectedText':
const selectedText = this._context.getSelectedText();
if (selectedText) {
return selectedText;
}
throw new Error(localize('canNotResolveSelectedText', "'{0}' can not be resolved. Make sure to have some text selected in the active editor.", match));
case 'file':
return getFilePath();
case 'relativeFile':
if (folderUri) {
return paths.normalize(paths.relative(getFolderUri().fsPath, getFilePath()));
}
return getFilePath();
case 'relativeFileDirname':
let dirname = paths.dirname(getFilePath());
if (folderUri) {
return paths.normalize(paths.relative(getFolderUri().fsPath, dirname));
}
return dirname;
case 'fileDirname':
return paths.dirname(getFilePath());
case 'fileExtname':
return paths.extname(getFilePath());
case 'fileBasename':
return paths.basename(getFilePath());
case 'fileBasenameNoExtension':
const basename = paths.basename(getFilePath());
return (basename.slice(0, basename.length - paths.extname(basename).length));
case 'execPath':
const ep = this._context.getExecPath();
if (ep) {
return ep;
}
return match;
default:
return match;
}
}
}
}
private resolveFromMap(match: string, argument: string | undefined, commandValueMapping: IStringDictionary<string> | undefined, prefix: string): string {
if (argument && commandValueMapping) {
const v = commandValueMapping[prefix + ':' + argument];
if (typeof v === 'string') {
return v;
}
throw new Error(localize('noValueForCommand', "'{0}' can not be resolved because the command has no value.", match));
}
return match;
}
}