Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@@ -0,0 +1,357 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI as uri } from 'vs/base/common/uri';
import * as nls from 'vs/nls';
import * as Types from 'vs/base/common/types';
import { Schemas } from 'vs/base/common/network';
import { SideBySideEditor, EditorResourceAccessor } from 'vs/workbench/common/editor';
import { IStringDictionary, forEach, fromMap } from 'vs/base/common/collections';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IConfigurationService, IConfigurationOverrides, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IWorkspaceFolder, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver';
import { isCodeEditor } from 'vs/editor/browser/editorBrowser';
import { IQuickInputService, IInputOptions, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput';
import { ConfiguredInput, IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ILabelService } from 'vs/platform/label/common/label';
export abstract class BaseConfigurationResolverService extends AbstractVariableResolverService {
static readonly INPUT_OR_COMMAND_VARIABLES_PATTERN = /\${((input|command):(.*?))}/g;
constructor(
context: { getExecPath: () => string | undefined },
envVariables: IProcessEnvironment,
editorService: IEditorService,
private readonly configurationService: IConfigurationService,
private readonly commandService: ICommandService,
private readonly workspaceContextService: IWorkspaceContextService,
private readonly quickInputService: IQuickInputService,
private readonly labelService: ILabelService
) {
super({
getFolderUri: (folderName: string): uri | undefined => {
const folder = workspaceContextService.getWorkspace().folders.filter(f => f.name === folderName).pop();
return folder ? folder.uri : undefined;
},
getWorkspaceFolderCount: (): number => {
return workspaceContextService.getWorkspace().folders.length;
},
getConfigurationValue: (folderUri: uri, suffix: string): string | undefined => {
return configurationService.getValue<string>(suffix, folderUri ? { resource: folderUri } : {});
},
getExecPath: (): string | undefined => {
return context.getExecPath();
},
getFilePath: (): string | undefined => {
const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, {
supportSideBySide: SideBySideEditor.PRIMARY,
filterByScheme: [Schemas.file, Schemas.userData, Schemas.vscodeRemote]
});
if (!fileResource) {
return undefined;
}
return this.labelService.getUriLabel(fileResource, { noPrefix: true });
},
getSelectedText: (): string | undefined => {
const activeTextEditorControl = editorService.activeTextEditorControl;
if (isCodeEditor(activeTextEditorControl)) {
const editorModel = activeTextEditorControl.getModel();
const editorSelection = activeTextEditorControl.getSelection();
if (editorModel && editorSelection) {
return editorModel.getValueInRange(editorSelection);
}
}
return undefined;
},
getLineNumber: (): string | undefined => {
const activeTextEditorControl = editorService.activeTextEditorControl;
if (isCodeEditor(activeTextEditorControl)) {
const selection = activeTextEditorControl.getSelection();
if (selection) {
const lineNumber = selection.positionLineNumber;
return String(lineNumber);
}
}
return undefined;
}
}, labelService, envVariables);
}
public async resolveWithInteractionReplace(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary<string>, target?: ConfigurationTarget): Promise<any> {
// resolve any non-interactive variables and any contributed variables
config = this.resolveAny(folder, config);
// resolve input variables in the order in which they are encountered
return this.resolveWithInteraction(folder, config, section, variables, target).then(mapping => {
// finally substitute evaluated command variables (if there are any)
if (!mapping) {
return null;
} else if (mapping.size > 0) {
return this.resolveAny(folder, config, fromMap(mapping));
} else {
return config;
}
});
}
public async resolveWithInteraction(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary<string>, target?: ConfigurationTarget): Promise<Map<string, string> | undefined> {
// resolve any non-interactive variables and any contributed variables
const resolved = await this.resolveAnyMap(folder, config);
config = resolved.newConfig;
const allVariableMapping: Map<string, string> = resolved.resolvedVariables;
// resolve input and command variables in the order in which they are encountered
return this.resolveWithInputAndCommands(folder, config, variables, section, target).then(inputOrCommandMapping => {
if (this.updateMapping(inputOrCommandMapping, allVariableMapping)) {
return allVariableMapping;
}
return undefined;
});
}
/**
* Add all items from newMapping to fullMapping. Returns false if newMapping is undefined.
*/
private updateMapping(newMapping: IStringDictionary<string> | undefined, fullMapping: Map<string, string>): boolean {
if (!newMapping) {
return false;
}
forEach(newMapping, (entry) => {
fullMapping.set(entry.key, entry.value);
});
return true;
}
/**
* Finds and executes all input and command variables in the given configuration and returns their values as a dictionary.
* Please note: this method does not substitute the input or command variables (so the configuration is not modified).
* The returned dictionary can be passed to "resolvePlatform" for the actual substitution.
* See #6569.
*
* @param variableToCommandMap Aliases for commands
*/
private async resolveWithInputAndCommands(folder: IWorkspaceFolder | undefined, configuration: any, variableToCommandMap?: IStringDictionary<string>, section?: string, target?: ConfigurationTarget): Promise<IStringDictionary<string> | undefined> {
if (!configuration) {
return Promise.resolve(undefined);
}
// get all "inputs"
let inputs: ConfiguredInput[] = [];
if (this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && section) {
const overrides: IConfigurationOverrides = folder ? { resource: folder.uri } : {};
let result = this.configurationService.inspect(section, overrides);
if (result && (result.userValue || result.workspaceValue || result.workspaceFolderValue)) {
switch (target) {
case ConfigurationTarget.USER: inputs = (<any>result.userValue)?.inputs; break;
case ConfigurationTarget.WORKSPACE: inputs = (<any>result.workspaceValue)?.inputs; break;
default: inputs = (<any>result.workspaceFolderValue)?.inputs;
}
} else {
const valueResult = this.configurationService.getValue<any>(section, overrides);
if (valueResult) {
inputs = valueResult.inputs;
}
}
}
// extract and dedupe all "input" and "command" variables and preserve their order in an array
const variables: string[] = [];
this.findVariables(configuration, variables);
const variableValues: IStringDictionary<string> = Object.create(null);
for (const variable of variables) {
const [type, name] = variable.split(':', 2);
let result: string | undefined;
switch (type) {
case 'input':
result = await this.showUserInput(name, inputs);
break;
case 'command':
// use the name as a command ID #12735
const commandId = (variableToCommandMap ? variableToCommandMap[name] : undefined) || name;
result = await this.commandService.executeCommand(commandId, configuration);
if (typeof result !== 'string' && !Types.isUndefinedOrNull(result)) {
throw new Error(nls.localize('commandVariable.noStringType', "Cannot substitute command variable '{0}' because command did not return a result of type string.", commandId));
}
break;
default:
// Try to resolve it as a contributed variable
if (this._contributedVariables.has(variable)) {
result = await this._contributedVariables.get(variable)!();
}
}
if (typeof result === 'string') {
variableValues[variable] = result;
} else {
return undefined;
}
}
return variableValues;
}
/**
* Recursively finds all command or input variables in object and pushes them into variables.
* @param object object is searched for variables.
* @param variables All found variables are returned in variables.
*/
private findVariables(object: any, variables: string[]) {
if (typeof object === 'string') {
let matches;
while ((matches = BaseConfigurationResolverService.INPUT_OR_COMMAND_VARIABLES_PATTERN.exec(object)) !== null) {
if (matches.length === 4) {
const command = matches[1];
if (variables.indexOf(command) < 0) {
variables.push(command);
}
}
}
this._contributedVariables.forEach((value, contributed: string) => {
if ((variables.indexOf(contributed) < 0) && (object.indexOf('${' + contributed + '}') >= 0)) {
variables.push(contributed);
}
});
} else if (Types.isArray(object)) {
object.forEach(value => {
this.findVariables(value, variables);
});
} else if (object) {
Object.keys(object).forEach(key => {
const value = object[key];
this.findVariables(value, variables);
});
}
}
/**
* Takes the provided input info and shows the quick pick so the user can provide the value for the input
* @param variable Name of the input variable.
* @param inputInfos Information about each possible input variable.
*/
private showUserInput(variable: string, inputInfos: ConfiguredInput[]): Promise<string | undefined> {
if (!inputInfos) {
return Promise.reject(new Error(nls.localize('inputVariable.noInputSection', "Variable '{0}' must be defined in an '{1}' section of the debug or task configuration.", variable, 'input')));
}
// find info for the given input variable
const info = inputInfos.filter(item => item.id === variable).pop();
if (info) {
const missingAttribute = (attrName: string) => {
throw new Error(nls.localize('inputVariable.missingAttribute', "Input variable '{0}' is of type '{1}' and must include '{2}'.", variable, info.type, attrName));
};
switch (info.type) {
case 'promptString': {
if (!Types.isString(info.description)) {
missingAttribute('description');
}
const inputOptions: IInputOptions = { prompt: info.description, ignoreFocusLost: true };
if (info.default) {
inputOptions.value = info.default;
}
if (info.password) {
inputOptions.password = info.password;
}
return this.quickInputService.input(inputOptions).then(resolvedInput => {
return resolvedInput;
});
}
case 'pickString': {
if (!Types.isString(info.description)) {
missingAttribute('description');
}
if (Types.isArray(info.options)) {
info.options.forEach(pickOption => {
if (!Types.isString(pickOption) && !Types.isString(pickOption.value)) {
missingAttribute('value');
}
});
} else {
missingAttribute('options');
}
interface PickStringItem extends IQuickPickItem {
value: string;
}
const picks = new Array<PickStringItem>();
info.options.forEach(pickOption => {
const value = Types.isString(pickOption) ? pickOption : pickOption.value;
const label = Types.isString(pickOption) ? undefined : pickOption.label;
// If there is no label defined, use value as label
const item: PickStringItem = {
label: label ? `${label}: ${value}` : value,
value: value
};
if (value === info.default) {
item.description = nls.localize('inputVariable.defaultInputValue', "(Default)");
picks.unshift(item);
} else {
picks.push(item);
}
});
const pickOptions: IPickOptions<PickStringItem> = { placeHolder: info.description, matchOnDetail: true, ignoreFocusLost: true };
return this.quickInputService.pick(picks, pickOptions, undefined).then(resolvedInput => {
if (resolvedInput) {
return resolvedInput.value;
}
return undefined;
});
}
case 'command': {
if (!Types.isString(info.command)) {
missingAttribute('command');
}
return this.commandService.executeCommand<string>(info.command, info.args).then(result => {
if (typeof result === 'string' || Types.isUndefinedOrNull(result)) {
return result;
}
throw new Error(nls.localize('inputVariable.command.noStringType', "Cannot substitute input variable '{0}' because command '{1}' did not return a result of type string.", variable, info.command));
});
}
default:
throw new Error(nls.localize('inputVariable.unknownType', "Input variable '{0}' can only be of type 'promptString', 'pickString', or 'command'.", variable));
}
}
return Promise.reject(new Error(nls.localize('inputVariable.undefinedVariable', "Undefined input variable '{0}' encountered. Remove or define '{0}' to continue.", variable)));
}
}
export class ConfigurationResolverService extends BaseConfigurationResolverService {
constructor(
@IEditorService editorService: IEditorService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IConfigurationService configurationService: IConfigurationService,
@ICommandService commandService: ICommandService,
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
@IQuickInputService quickInputService: IQuickInputService,
@ILabelService labelService: ILabelService
) {
super({ getExecPath: () => undefined }, Object.create(null), editorService, configurationService, commandService, workspaceContextService, quickInputService, labelService);
}
}
registerSingleton(IConfigurationResolverService, ConfigurationResolverService, true);

View File

@@ -0,0 +1,71 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IStringDictionary } from 'vs/base/common/collections';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
export const IConfigurationResolverService = createDecorator<IConfigurationResolverService>('configurationResolverService');
export interface IConfigurationResolverService {
readonly _serviceBrand: undefined;
resolve(folder: IWorkspaceFolder | undefined, value: string): string;
resolve(folder: IWorkspaceFolder | undefined, value: string[]): string[];
resolve(folder: IWorkspaceFolder | undefined, value: IStringDictionary<string>): IStringDictionary<string>;
/**
* Recursively resolves all variables in the given config and returns a copy of it with substituted values.
* Command variables are only substituted if a "commandValueMapping" dictionary is given and if it contains an entry for the command.
*/
resolveAny(folder: IWorkspaceFolder | undefined, config: any, commandValueMapping?: IStringDictionary<string>): any;
/**
* Recursively resolves all variables (including commands and user input) in the given config and returns a copy of it with substituted values.
* If a "variables" dictionary (with names -> command ids) is given, command variables are first mapped through it before being resolved.
*
* @param section For example, 'tasks' or 'debug'. Used for resolving inputs.
* @param variables Aliases for commands.
*/
resolveWithInteractionReplace(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary<string>, target?: ConfigurationTarget): Promise<any>;
/**
* Similar to resolveWithInteractionReplace, except without the replace. Returns a map of variables and their resolution.
* Keys in the map will be of the format input:variableName or command:variableName.
*/
resolveWithInteraction(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary<string>, target?: ConfigurationTarget): Promise<Map<string, string> | undefined>;
/**
* Contributes a variable that can be resolved later. Consumers that use resolveAny, resolveWithInteraction,
* and resolveWithInteractionReplace will have contributed variables resolved.
*/
contributeVariable(variable: string, resolution: () => Promise<string | undefined>): void;
}
export interface PromptStringInputInfo {
id: string;
type: 'promptString';
description: string;
default?: string;
password?: boolean;
}
export interface PickStringInputInfo {
id: string;
type: 'pickString';
description: string;
options: (string | { value: string, label?: string })[];
default?: string;
}
export interface CommandInputInfo {
id: string;
type: 'command';
command: string;
args?: any;
}
export type ConfiguredInput = PromptStringInputInfo | PickStringInputInfo | CommandInputInfo;

View File

@@ -0,0 +1,149 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
const idDescription = nls.localize('JsonSchema.input.id', "The input's id is used to associate an input with a variable of the form ${input:id}.");
const typeDescription = nls.localize('JsonSchema.input.type', "The type of user input prompt to use.");
const descriptionDescription = nls.localize('JsonSchema.input.description', "The description is shown when the user is prompted for input.");
const defaultDescription = nls.localize('JsonSchema.input.default', "The default value for the input.");
export const inputsSchema: IJSONSchema = {
definitions: {
inputs: {
type: 'array',
description: nls.localize('JsonSchema.inputs', 'User inputs. Used for defining user input prompts, such as free string input or a choice from several options.'),
items: {
oneOf: [
{
type: 'object',
required: ['id', 'type', 'description'],
additionalProperties: false,
properties: {
id: {
type: 'string',
description: idDescription
},
type: {
type: 'string',
description: typeDescription,
enum: ['promptString'],
enumDescriptions: [
nls.localize('JsonSchema.input.type.promptString', "The 'promptString' type opens an input box to ask the user for input."),
]
},
description: {
type: 'string',
description: descriptionDescription
},
default: {
type: 'string',
description: defaultDescription
},
password: {
type: 'boolean',
description: nls.localize('JsonSchema.input.password', "Controls if a password input is shown. Password input hides the typed text."),
},
}
},
{
type: 'object',
required: ['id', 'type', 'description', 'options'],
additionalProperties: false,
properties: {
id: {
type: 'string',
description: idDescription
},
type: {
type: 'string',
description: typeDescription,
enum: ['pickString'],
enumDescriptions: [
nls.localize('JsonSchema.input.type.pickString', "The 'pickString' type shows a selection list."),
]
},
description: {
type: 'string',
description: descriptionDescription
},
default: {
type: 'string',
description: defaultDescription
},
options: {
type: 'array',
description: nls.localize('JsonSchema.input.options', "An array of strings that defines the options for a quick pick."),
items: {
oneOf: [
{
type: 'string'
},
{
type: 'object',
required: ['value'],
additionalProperties: false,
properties: {
label: {
type: 'string',
description: nls.localize('JsonSchema.input.pickString.optionLabel', "Label for the option.")
},
value: {
type: 'string',
description: nls.localize('JsonSchema.input.pickString.optionValue', "Value for the option.")
}
}
}
]
}
}
}
},
{
type: 'object',
required: ['id', 'type', 'command'],
additionalProperties: false,
properties: {
id: {
type: 'string',
description: idDescription
},
type: {
type: 'string',
description: typeDescription,
enum: ['command'],
enumDescriptions: [
nls.localize('JsonSchema.input.type.command', "The 'command' type executes a command."),
]
},
command: {
type: 'string',
description: nls.localize('JsonSchema.input.command.command', "The command to execute for this input variable.")
},
args: {
oneOf: [
{
type: 'object',
description: nls.localize('JsonSchema.input.command.args', "Optional arguments passed to the command.")
},
{
type: 'array',
description: nls.localize('JsonSchema.input.command.args', "Optional arguments passed to the command.")
},
{
type: 'string',
description: nls.localize('JsonSchema.input.command.args', "Optional arguments passed to the command.")
}
]
}
}
}
]
}
}
}
};

View File

@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
export function applyDeprecatedVariableMessage(schema: IJSONSchema) {
schema.pattern = schema.pattern || '^(?!.*\\$\\{(env|config|command)\\.)';
schema.patternErrorMessage = schema.patternErrorMessage ||
nls.localize('deprecatedVariables', "'env.', 'config.' and 'command.' are deprecated, use 'env:', 'config:' and 'command:' instead.");
}

View File

@@ -0,0 +1,339 @@
/*---------------------------------------------------------------------------------------------
* 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';
import { ILabelService } from 'vs/platform/label/common/label';
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 readonly VARIABLE_REGEXP = /\$\{(.*?)\}/g;
declare readonly _serviceBrand: undefined;
private _context: IVariableResolveContext;
private _labelService?: ILabelService;
private _envVariables?: IProcessEnvironment;
protected _contributedVariables: Map<string, () => Promise<string | undefined>> = new Map();
constructor(_context: IVariableResolveContext, _labelService?: ILabelService, _envVariables?: IProcessEnvironment, private _ignoreEditorVariables = false) {
this._context = _context;
this._labelService = _labelService;
if (_envVariables) {
if (isWindows) {
// windows env variables are case insensitive
const ev: IProcessEnvironment = Object.create(null);
this._envVariables = ev;
Object.keys(_envVariables).forEach(key => {
ev[key.toLowerCase()] = _envVariables[key];
});
} else {
this._envVariables = _envVariables;
}
}
}
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.');
}
public contributeVariable(variable: string, resolution: () => Promise<string | undefined>): void {
if (this._contributedVariables.has(variable)) {
throw new Error('Variable ' + variable + ' is contributed twice.');
} else {
this._contributedVariables.set(variable, resolution);
}
}
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 fsPath(displayUri: uri): string {
return this._labelService ? this._labelService.getUriLabel(displayUri, { noPrefix: true }) : displayUri.fsPath;
}
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 (this._envVariables) {
const env = this._envVariables[isWindows ? argument.toLowerCase() : argument];
if (types.isString(env)) {
return env;
}
}
// For `env` we should do the same as a normal shell does - evaluates undefined 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(this.fsPath(getFolderUri()));
case 'cwd':
return ((folderUri || argument) ? normalizeDriveLetter(this.fsPath(getFolderUri())) : process.cwd());
case 'workspaceRootFolderName':
case 'workspaceFolderBasename':
return paths.basename(this.fsPath(getFolderUri()));
case 'lineNumber':
if (this._ignoreEditorVariables) {
return match;
}
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':
if (this._ignoreEditorVariables) {
return match;
}
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':
if (this._ignoreEditorVariables) {
return match;
}
return getFilePath();
case 'relativeFile':
if (this._ignoreEditorVariables) {
return match;
}
if (folderUri || argument) {
return paths.relative(this.fsPath(getFolderUri()), getFilePath());
}
return getFilePath();
case 'relativeFileDirname':
if (this._ignoreEditorVariables) {
return match;
}
const dirname = paths.dirname(getFilePath());
if (folderUri || argument) {
const relative = paths.relative(this.fsPath(getFolderUri()), dirname);
return relative.length === 0 ? '.' : relative;
}
return dirname;
case 'fileDirname':
if (this._ignoreEditorVariables) {
return match;
}
return paths.dirname(getFilePath());
case 'fileExtname':
if (this._ignoreEditorVariables) {
return match;
}
return paths.extname(getFilePath());
case 'fileBasename':
if (this._ignoreEditorVariables) {
return match;
}
return paths.basename(getFilePath());
case 'fileBasenameNoExtension':
if (this._ignoreEditorVariables) {
return match;
}
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:
try {
return this.resolveFromMap(match, variable, commandValueMapping, undefined);
} catch (error) {
return match;
}
}
}
}
}
private resolveFromMap(match: string, argument: string | undefined, commandValueMapping: IStringDictionary<string> | undefined, prefix: string | undefined): string {
if (argument && commandValueMapping) {
const v = (prefix === undefined) ? commandValueMapping[argument] : 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;
}
}

View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService';
import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals';
import { ILabelService } from 'vs/platform/label/common/label';
export class ConfigurationResolverService extends BaseConfigurationResolverService {
constructor(
@IEditorService editorService: IEditorService,
@INativeWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService,
@IConfigurationService configurationService: IConfigurationService,
@ICommandService commandService: ICommandService,
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
@IQuickInputService quickInputService: IQuickInputService,
@ILabelService labelService: ILabelService
) {
super({
getExecPath: (): string | undefined => {
return environmentService.execPath;
}
}, process.env as IProcessEnvironment, editorService, configurationService, commandService, workspaceContextService, quickInputService, labelService);
}
}
registerSingleton(IConfigurationResolverService, ConfigurationResolverService, true);

View File

@@ -0,0 +1,757 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { normalize } from 'vs/base/common/path';
import { Emitter, Event } from 'vs/base/common/event';
import { URI as uri } from 'vs/base/common/uri';
import * as platform from 'vs/base/common/platform';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService';
import { Workspace, IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace';
import { TestEditorService, TestProductService } from 'vs/workbench/test/browser/workbenchTestServices';
import { TestWorkbenchConfiguration } from 'vs/workbench/test/electron-browser/workbenchTestServices';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { IQuickInputService, IQuickPickItem, QuickPickInput, IPickOptions, Omit, IInputOptions, IQuickInputButton, IQuickPick, IInputBox, IQuickNavigateConfiguration } from 'vs/platform/quickinput/common/quickInput';
import { CancellationToken } from 'vs/base/common/cancellation';
import * as Types from 'vs/base/common/types';
import { EditorType } from 'vs/editor/common/editorCommon';
import { Selection } from 'vs/editor/common/core/selection';
import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices';
import { testWorkspace } from 'vs/platform/workspace/test/common/testWorkspace';
import { IFormatterChangeEvent, ILabelService, ResourceLabelFormatter } from 'vs/platform/label/common/label';
import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
const mockLineNumber = 10;
class TestEditorServiceWithActiveEditor extends TestEditorService {
get activeTextEditorControl(): any {
return {
getEditorType() {
return EditorType.ICodeEditor;
},
getSelection() {
return new Selection(mockLineNumber, 1, mockLineNumber, 10);
}
};
}
get activeEditor(): any {
return {
get resource(): any {
return uri.parse('file:///VSCode/workspaceLocation/file');
}
};
}
}
class TestConfigurationResolverService extends BaseConfigurationResolverService {
}
suite('Configuration Resolver Service', () => {
let configurationResolverService: IConfigurationResolverService | null;
let envVariables: { [key: string]: string } = { key1: 'Value for key1', key2: 'Value for key2' };
let environmentService: MockWorkbenchEnvironmentService;
let mockCommandService: MockCommandService;
let editorService: TestEditorServiceWithActiveEditor;
let containingWorkspace: Workspace;
let workspace: IWorkspaceFolder;
let quickInputService: MockQuickInputService;
let labelService: MockLabelService;
setup(() => {
mockCommandService = new MockCommandService();
editorService = new TestEditorServiceWithActiveEditor();
quickInputService = new MockQuickInputService();
environmentService = new MockWorkbenchEnvironmentService(envVariables);
labelService = new MockLabelService();
containingWorkspace = testWorkspace(uri.parse('file:///VSCode/workspaceLocation'));
workspace = containingWorkspace.folders[0];
configurationResolverService = new TestConfigurationResolverService({ getExecPath: () => undefined }, environmentService.userEnv, editorService, new MockInputsConfigurationService(), mockCommandService, new TestContextService(containingWorkspace), quickInputService, labelService);
});
teardown(() => {
configurationResolverService = null;
});
test('substitute one', () => {
if (platform.isWindows) {
assert.strictEqual(configurationResolverService!.resolve(workspace, 'abc ${workspaceFolder} xyz'), 'abc \\VSCode\\workspaceLocation xyz');
} else {
assert.strictEqual(configurationResolverService!.resolve(workspace, 'abc ${workspaceFolder} xyz'), 'abc /VSCode/workspaceLocation xyz');
}
});
test('workspace folder with argument', () => {
if (platform.isWindows) {
assert.strictEqual(configurationResolverService!.resolve(workspace, 'abc ${workspaceFolder:workspaceLocation} xyz'), 'abc \\VSCode\\workspaceLocation xyz');
} else {
assert.strictEqual(configurationResolverService!.resolve(workspace, 'abc ${workspaceFolder:workspaceLocation} xyz'), 'abc /VSCode/workspaceLocation xyz');
}
});
test('workspace folder with invalid argument', () => {
assert.throws(() => configurationResolverService!.resolve(workspace, 'abc ${workspaceFolder:invalidLocation} xyz'));
});
test('workspace folder with undefined workspace folder', () => {
assert.throws(() => configurationResolverService!.resolve(undefined, 'abc ${workspaceFolder} xyz'));
});
test('workspace folder with argument and undefined workspace folder', () => {
if (platform.isWindows) {
assert.strictEqual(configurationResolverService!.resolve(undefined, 'abc ${workspaceFolder:workspaceLocation} xyz'), 'abc \\VSCode\\workspaceLocation xyz');
} else {
assert.strictEqual(configurationResolverService!.resolve(undefined, 'abc ${workspaceFolder:workspaceLocation} xyz'), 'abc /VSCode/workspaceLocation xyz');
}
});
test('workspace folder with invalid argument and undefined workspace folder', () => {
assert.throws(() => configurationResolverService!.resolve(undefined, 'abc ${workspaceFolder:invalidLocation} xyz'));
});
test('workspace root folder name', () => {
assert.strictEqual(configurationResolverService!.resolve(workspace, 'abc ${workspaceRootFolderName} xyz'), 'abc workspaceLocation xyz');
});
test('current selected line number', () => {
assert.strictEqual(configurationResolverService!.resolve(workspace, 'abc ${lineNumber} xyz'), `abc ${mockLineNumber} xyz`);
});
test('relative file', () => {
assert.strictEqual(configurationResolverService!.resolve(workspace, 'abc ${relativeFile} xyz'), 'abc file xyz');
});
test('relative file with argument', () => {
assert.strictEqual(configurationResolverService!.resolve(workspace, 'abc ${relativeFile:workspaceLocation} xyz'), 'abc file xyz');
});
test('relative file with invalid argument', () => {
assert.throws(() => configurationResolverService!.resolve(workspace, 'abc ${relativeFile:invalidLocation} xyz'));
});
test('relative file with undefined workspace folder', () => {
if (platform.isWindows) {
assert.strictEqual(configurationResolverService!.resolve(undefined, 'abc ${relativeFile} xyz'), 'abc \\VSCode\\workspaceLocation\\file xyz');
} else {
assert.strictEqual(configurationResolverService!.resolve(undefined, 'abc ${relativeFile} xyz'), 'abc /VSCode/workspaceLocation/file xyz');
}
});
test('relative file with argument and undefined workspace folder', () => {
assert.strictEqual(configurationResolverService!.resolve(undefined, 'abc ${relativeFile:workspaceLocation} xyz'), 'abc file xyz');
});
test('relative file with invalid argument and undefined workspace folder', () => {
assert.throws(() => configurationResolverService!.resolve(undefined, 'abc ${relativeFile:invalidLocation} xyz'));
});
test('substitute many', () => {
if (platform.isWindows) {
assert.strictEqual(configurationResolverService!.resolve(workspace, '${workspaceFolder} - ${workspaceFolder}'), '\\VSCode\\workspaceLocation - \\VSCode\\workspaceLocation');
} else {
assert.strictEqual(configurationResolverService!.resolve(workspace, '${workspaceFolder} - ${workspaceFolder}'), '/VSCode/workspaceLocation - /VSCode/workspaceLocation');
}
});
test('substitute one env variable', () => {
if (platform.isWindows) {
assert.strictEqual(configurationResolverService!.resolve(workspace, 'abc ${workspaceFolder} ${env:key1} xyz'), 'abc \\VSCode\\workspaceLocation Value for key1 xyz');
} else {
assert.strictEqual(configurationResolverService!.resolve(workspace, 'abc ${workspaceFolder} ${env:key1} xyz'), 'abc /VSCode/workspaceLocation Value for key1 xyz');
}
});
test('substitute many env variable', () => {
if (platform.isWindows) {
assert.strictEqual(configurationResolverService!.resolve(workspace, '${workspaceFolder} - ${workspaceFolder} ${env:key1} - ${env:key2}'), '\\VSCode\\workspaceLocation - \\VSCode\\workspaceLocation Value for key1 - Value for key2');
} else {
assert.strictEqual(configurationResolverService!.resolve(workspace, '${workspaceFolder} - ${workspaceFolder} ${env:key1} - ${env:key2}'), '/VSCode/workspaceLocation - /VSCode/workspaceLocation Value for key1 - Value for key2');
}
});
// test('substitute keys and values in object', () => {
// const myObject = {
// '${workspaceRootFolderName}': '${lineNumber}',
// 'hey ${env:key1} ': '${workspaceRootFolderName}'
// };
// assert.deepEqual(configurationResolverService!.resolve(workspace, myObject), {
// 'workspaceLocation': `${editorService.mockLineNumber}`,
// 'hey Value for key1 ': 'workspaceLocation'
// });
// });
test('substitute one env variable using platform case sensitivity', () => {
if (platform.isWindows) {
assert.strictEqual(configurationResolverService!.resolve(workspace, '${env:key1} - ${env:Key1}'), 'Value for key1 - Value for key1');
} else {
assert.strictEqual(configurationResolverService!.resolve(workspace, '${env:key1} - ${env:Key1}'), 'Value for key1 - ');
}
});
test('substitute one configuration variable', () => {
let configurationService: IConfigurationService = new TestConfigurationService({
editor: {
fontFamily: 'foo'
},
terminal: {
integrated: {
fontFamily: 'bar'
}
}
});
let service = new TestConfigurationResolverService({ getExecPath: () => undefined }, environmentService.userEnv, new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService);
assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz');
});
test('substitute many configuration variables', () => {
let configurationService: IConfigurationService;
configurationService = new TestConfigurationService({
editor: {
fontFamily: 'foo'
},
terminal: {
integrated: {
fontFamily: 'bar'
}
}
});
let service = new TestConfigurationResolverService({ getExecPath: () => undefined }, environmentService.userEnv, new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService);
assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} xyz'), 'abc foo bar xyz');
});
test('substitute one env variable and a configuration variable', () => {
let configurationService: IConfigurationService;
configurationService = new TestConfigurationService({
editor: {
fontFamily: 'foo'
},
terminal: {
integrated: {
fontFamily: 'bar'
}
}
});
let service = new TestConfigurationResolverService({ getExecPath: () => undefined }, environmentService.userEnv, new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService);
if (platform.isWindows) {
assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${workspaceFolder} ${env:key1} xyz'), 'abc foo \\VSCode\\workspaceLocation Value for key1 xyz');
} else {
assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${workspaceFolder} ${env:key1} xyz'), 'abc foo /VSCode/workspaceLocation Value for key1 xyz');
}
});
test('substitute many env variable and a configuration variable', () => {
let configurationService: IConfigurationService;
configurationService = new TestConfigurationService({
editor: {
fontFamily: 'foo'
},
terminal: {
integrated: {
fontFamily: 'bar'
}
}
});
let service = new TestConfigurationResolverService({ getExecPath: () => undefined }, environmentService.userEnv, new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService);
if (platform.isWindows) {
assert.strictEqual(service.resolve(workspace, '${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} ${workspaceFolder} - ${workspaceFolder} ${env:key1} - ${env:key2}'), 'foo bar \\VSCode\\workspaceLocation - \\VSCode\\workspaceLocation Value for key1 - Value for key2');
} else {
assert.strictEqual(service.resolve(workspace, '${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} ${workspaceFolder} - ${workspaceFolder} ${env:key1} - ${env:key2}'), 'foo bar /VSCode/workspaceLocation - /VSCode/workspaceLocation Value for key1 - Value for key2');
}
});
test('mixed types of configuration variables', () => {
let configurationService: IConfigurationService;
configurationService = new TestConfigurationService({
editor: {
fontFamily: 'foo',
lineNumbers: 123,
insertSpaces: false
},
terminal: {
integrated: {
fontFamily: 'bar'
}
},
json: {
schemas: [
{
fileMatch: [
'/myfile',
'/myOtherfile'
],
url: 'schemaURL'
}
]
}
});
let service = new TestConfigurationResolverService({ getExecPath: () => undefined }, environmentService.userEnv, new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService);
assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${config:editor.lineNumbers} ${config:editor.insertSpaces} xyz'), 'abc foo 123 false xyz');
});
test('uses original variable as fallback', () => {
let configurationService: IConfigurationService;
configurationService = new TestConfigurationService({
editor: {}
});
let service = new TestConfigurationResolverService({ getExecPath: () => undefined }, environmentService.userEnv, new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService);
assert.strictEqual(service.resolve(workspace, 'abc ${unknownVariable} xyz'), 'abc ${unknownVariable} xyz');
assert.strictEqual(service.resolve(workspace, 'abc ${env:unknownVariable} xyz'), 'abc xyz');
});
test('configuration variables with invalid accessor', () => {
let configurationService: IConfigurationService;
configurationService = new TestConfigurationService({
editor: {
fontFamily: 'foo'
}
});
let service = new TestConfigurationResolverService({ getExecPath: () => undefined }, environmentService.userEnv, new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService);
assert.throws(() => service.resolve(workspace, 'abc ${env} xyz'));
assert.throws(() => service.resolve(workspace, 'abc ${env:} xyz'));
assert.throws(() => service.resolve(workspace, 'abc ${config} xyz'));
assert.throws(() => service.resolve(workspace, 'abc ${config:} xyz'));
assert.throws(() => service.resolve(workspace, 'abc ${config:editor} xyz'));
assert.throws(() => service.resolve(workspace, 'abc ${config:editor..fontFamily} xyz'));
assert.throws(() => service.resolve(workspace, 'abc ${config:editor.none.none2} xyz'));
});
test('a single command variable', () => {
const configuration = {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': '${command:command1}',
'port': 5858,
'sourceMaps': false,
'outDir': null
};
return configurationResolverService!.resolveWithInteractionReplace(undefined, configuration).then(result => {
assert.deepEqual(result, {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': 'command1-result',
'port': 5858,
'sourceMaps': false,
'outDir': null
});
assert.equal(1, mockCommandService.callCount);
});
});
test('an old style command variable', () => {
const configuration = {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': '${command:commandVariable1}',
'port': 5858,
'sourceMaps': false,
'outDir': null
};
const commandVariables = Object.create(null);
commandVariables['commandVariable1'] = 'command1';
return configurationResolverService!.resolveWithInteractionReplace(undefined, configuration, undefined, commandVariables).then(result => {
assert.deepEqual(result, {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': 'command1-result',
'port': 5858,
'sourceMaps': false,
'outDir': null
});
assert.equal(1, mockCommandService.callCount);
});
});
test('multiple new and old-style command variables', () => {
const configuration = {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': '${command:commandVariable1}',
'pid': '${command:command2}',
'sourceMaps': false,
'outDir': 'src/${command:command2}',
'env': {
'processId': '__${command:command2}__',
}
};
const commandVariables = Object.create(null);
commandVariables['commandVariable1'] = 'command1';
return configurationResolverService!.resolveWithInteractionReplace(undefined, configuration, undefined, commandVariables).then(result => {
assert.deepEqual(result, {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': 'command1-result',
'pid': 'command2-result',
'sourceMaps': false,
'outDir': 'src/command2-result',
'env': {
'processId': '__command2-result__',
}
});
assert.equal(2, mockCommandService.callCount);
});
});
test('a command variable that relies on resolved env vars', () => {
const configuration = {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': '${command:commandVariable1}',
'value': '${env:key1}'
};
const commandVariables = Object.create(null);
commandVariables['commandVariable1'] = 'command1';
return configurationResolverService!.resolveWithInteractionReplace(undefined, configuration, undefined, commandVariables).then(result => {
assert.deepEqual(result, {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': 'Value for key1',
'value': 'Value for key1'
});
assert.equal(1, mockCommandService.callCount);
});
});
test('a single prompt input variable', () => {
const configuration = {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': '${input:input1}',
'port': 5858,
'sourceMaps': false,
'outDir': null
};
return configurationResolverService!.resolveWithInteractionReplace(workspace, configuration, 'tasks').then(result => {
assert.deepEqual(result, {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': 'resolvedEnterinput1',
'port': 5858,
'sourceMaps': false,
'outDir': null
});
assert.equal(0, mockCommandService.callCount);
});
});
test('a single pick input variable', () => {
const configuration = {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': '${input:input2}',
'port': 5858,
'sourceMaps': false,
'outDir': null
};
return configurationResolverService!.resolveWithInteractionReplace(workspace, configuration, 'tasks').then(result => {
assert.deepEqual(result, {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': 'selectedPick',
'port': 5858,
'sourceMaps': false,
'outDir': null
});
assert.equal(0, mockCommandService.callCount);
});
});
test('a single command input variable', () => {
const configuration = {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': '${input:input4}',
'port': 5858,
'sourceMaps': false,
'outDir': null
};
return configurationResolverService!.resolveWithInteractionReplace(workspace, configuration, 'tasks').then(result => {
assert.deepEqual(result, {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': 'arg for command',
'port': 5858,
'sourceMaps': false,
'outDir': null
});
assert.equal(1, mockCommandService.callCount);
});
});
test('several input variables and command', () => {
const configuration = {
'name': '${input:input3}',
'type': '${command:command1}',
'request': '${input:input1}',
'processId': '${input:input2}',
'command': '${input:input4}',
'port': 5858,
'sourceMaps': false,
'outDir': null
};
return configurationResolverService!.resolveWithInteractionReplace(workspace, configuration, 'tasks').then(result => {
assert.deepEqual(result, {
'name': 'resolvedEnterinput3',
'type': 'command1-result',
'request': 'resolvedEnterinput1',
'processId': 'selectedPick',
'command': 'arg for command',
'port': 5858,
'sourceMaps': false,
'outDir': null
});
assert.equal(2, mockCommandService.callCount);
});
});
test('input variable with undefined workspace folder', () => {
const configuration = {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': '${input:input1}',
'port': 5858,
'sourceMaps': false,
'outDir': null
};
return configurationResolverService!.resolveWithInteractionReplace(undefined, configuration, 'tasks').then(result => {
assert.deepEqual(result, {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': 'resolvedEnterinput1',
'port': 5858,
'sourceMaps': false,
'outDir': null
});
assert.equal(0, mockCommandService.callCount);
});
});
test('contributed variable', () => {
const buildTask = 'npm: compile';
const variable = 'defaultBuildTask';
const configuration = {
'name': '${' + variable + '}',
};
configurationResolverService!.contributeVariable(variable, async () => { return buildTask; });
return configurationResolverService!.resolveWithInteractionReplace(workspace, configuration).then(result => {
assert.deepEqual(result, {
'name': `${buildTask}`
});
});
});
});
class MockCommandService implements ICommandService {
public _serviceBrand: undefined;
public callCount = 0;
onWillExecuteCommand = () => Disposable.None;
onDidExecuteCommand = () => Disposable.None;
public executeCommand(commandId: string, ...args: any[]): Promise<any> {
this.callCount++;
let result = `${commandId}-result`;
if (args.length >= 1) {
if (args[0] && args[0].value) {
result = args[0].value;
}
}
return Promise.resolve(result);
}
}
class MockQuickInputService implements IQuickInputService {
declare readonly _serviceBrand: undefined;
readonly onShow = Event.None;
readonly onHide = Event.None;
readonly quickAccess = undefined!;
public pick<T extends IQuickPickItem>(picks: Promise<QuickPickInput<T>[]> | QuickPickInput<T>[], options?: IPickOptions<T> & { canPickMany: true }, token?: CancellationToken): Promise<T[]>;
public pick<T extends IQuickPickItem>(picks: Promise<QuickPickInput<T>[]> | QuickPickInput<T>[], options?: IPickOptions<T> & { canPickMany: false }, token?: CancellationToken): Promise<T>;
public pick<T extends IQuickPickItem>(picks: Promise<QuickPickInput<T>[]> | QuickPickInput<T>[], options?: Omit<IPickOptions<T>, 'canPickMany'>, token?: CancellationToken): Promise<T | undefined> {
if (Types.isArray(picks)) {
return Promise.resolve(<any>{ label: 'selectedPick', description: 'pick description', value: 'selectedPick' });
} else {
return Promise.resolve(undefined);
}
}
public input(options?: IInputOptions, token?: CancellationToken): Promise<string> {
return Promise.resolve(options ? 'resolved' + options.prompt : 'resolved');
}
backButton!: IQuickInputButton;
createQuickPick<T extends IQuickPickItem>(): IQuickPick<T> {
throw new Error('not implemented.');
}
createInputBox(): IInputBox {
throw new Error('not implemented.');
}
focus(): void {
throw new Error('not implemented.');
}
toggle(): void {
throw new Error('not implemented.');
}
navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration): void {
throw new Error('not implemented.');
}
accept(): Promise<void> {
throw new Error('not implemented.');
}
back(): Promise<void> {
throw new Error('not implemented.');
}
cancel(): Promise<void> {
throw new Error('not implemented.');
}
}
class MockLabelService implements ILabelService {
_serviceBrand: undefined;
getUriLabel(resource: uri, options?: { relative?: boolean | undefined; noPrefix?: boolean | undefined; endWithSeparator?: boolean | undefined; }): string {
return normalize(resource.fsPath);
}
getUriBasenameLabel(resource: uri): string {
throw new Error('Method not implemented.');
}
getWorkspaceLabel(workspace: uri | IWorkspaceIdentifier | IWorkspace, options?: { verbose: boolean; }): string {
throw new Error('Method not implemented.');
}
getHostLabel(scheme: string, authority?: string): string {
throw new Error('Method not implemented.');
}
getSeparator(scheme: string, authority?: string): '/' | '\\' {
throw new Error('Method not implemented.');
}
registerFormatter(formatter: ResourceLabelFormatter): IDisposable {
throw new Error('Method not implemented.');
}
onDidChangeFormatters: Event<IFormatterChangeEvent> = new Emitter<IFormatterChangeEvent>().event;
}
class MockInputsConfigurationService extends TestConfigurationService {
public getValue(arg1?: any, arg2?: any): any {
let configuration;
if (arg1 === 'tasks') {
configuration = {
inputs: [
{
id: 'input1',
type: 'promptString',
description: 'Enterinput1',
default: 'default input1'
},
{
id: 'input2',
type: 'pickString',
description: 'Enterinput1',
default: 'option2',
options: ['option1', 'option2', 'option3']
},
{
id: 'input3',
type: 'promptString',
description: 'Enterinput3',
default: 'default input3',
password: true
},
{
id: 'input4',
type: 'command',
command: 'command1',
args: {
value: 'arg for command'
}
}
]
};
}
return configuration;
}
}
class MockWorkbenchEnvironmentService extends NativeWorkbenchEnvironmentService {
constructor(public userEnv: platform.IProcessEnvironment) {
super({ ...TestWorkbenchConfiguration, userEnv }, TestProductService);
}
}