未验证 提交 f9703e9a 编写于 作者: A Alex Ross 提交者: GitHub

Input variables in tasks (#63910)

Fixes #4758
上级 253a4f6d
...@@ -87,3 +87,13 @@ export function groupBy<T>(data: T[], groupFn: (element: T) => string): IStringD ...@@ -87,3 +87,13 @@ export function groupBy<T>(data: T[], groupFn: (element: T) => string): IStringD
} }
return result; return result;
} }
export function fromMap<T>(original: Map<string, T>): IStringDictionary<T> {
const result: IStringDictionary<T> = Object.create(null);
if (original) {
original.forEach((value, key) => {
result[key] = value;
});
}
return result;
}
\ No newline at end of file
...@@ -10,7 +10,7 @@ import { generateUuid } from 'vs/base/common/uuid'; ...@@ -10,7 +10,7 @@ import { generateUuid } from 'vs/base/common/uuid';
import * as Objects from 'vs/base/common/objects'; import * as Objects from 'vs/base/common/objects';
import * as Types from 'vs/base/common/types'; import * as Types from 'vs/base/common/types';
import * as Platform from 'vs/base/common/platform'; import * as Platform from 'vs/base/common/platform';
import { IStringDictionary } from 'vs/base/common/collections'; import { IStringDictionary, forEach } from 'vs/base/common/collections';
import { IDisposable } from 'vs/base/common/lifecycle'; import { IDisposable } from 'vs/base/common/lifecycle';
import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
...@@ -33,6 +33,7 @@ import { ...@@ -33,6 +33,7 @@ import {
ProcessExecutionDTO, ShellExecutionDTO, ShellExecutionOptionsDTO, TaskDTO, TaskSourceDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO, TaskSystemInfoDTO, ProcessExecutionDTO, ShellExecutionDTO, ShellExecutionOptionsDTO, TaskDTO, TaskSourceDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO, TaskSystemInfoDTO,
RunOptionsDTO RunOptionsDTO
} from 'vs/workbench/api/shared/tasks'; } from 'vs/workbench/api/shared/tasks';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
namespace TaskExecutionDTO { namespace TaskExecutionDTO {
export function from(value: TaskExecution): TaskExecutionDTO { export function from(value: TaskExecution): TaskExecutionDTO {
...@@ -381,7 +382,8 @@ export class MainThreadTask implements MainThreadTaskShape { ...@@ -381,7 +382,8 @@ export class MainThreadTask implements MainThreadTaskShape {
constructor( constructor(
extHostContext: IExtHostContext, extHostContext: IExtHostContext,
@ITaskService private readonly _taskService: ITaskService, @ITaskService private readonly _taskService: ITaskService,
@IWorkspaceContextService private readonly _workspaceContextServer: IWorkspaceContextService @IWorkspaceContextService private readonly _workspaceContextServer: IWorkspaceContextService,
@IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService
) { ) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTask); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTask);
this._providers = new Map(); this._providers = new Map();
...@@ -460,7 +462,9 @@ export class MainThreadTask implements MainThreadTaskShape { ...@@ -460,7 +462,9 @@ export class MainThreadTask implements MainThreadTaskShape {
if (TaskHandleDTO.is(value)) { if (TaskHandleDTO.is(value)) {
let workspaceFolder = this._workspaceContextServer.getWorkspaceFolder(URI.revive(value.workspaceFolder)); let workspaceFolder = this._workspaceContextServer.getWorkspaceFolder(URI.revive(value.workspaceFolder));
this._taskService.getTask(workspaceFolder, value.id, true).then((task: Task) => { this._taskService.getTask(workspaceFolder, value.id, true).then((task: Task) => {
this._taskService.run(task); this._taskService.run(task).then(undefined, reason => {
// eat the error, it has already been surfaced to the user and we don't care about it here
});
let result: TaskExecutionDTO = { let result: TaskExecutionDTO = {
id: value.id, id: value.id,
task: TaskDTO.from(task) task: TaskDTO.from(task)
...@@ -471,7 +475,9 @@ export class MainThreadTask implements MainThreadTaskShape { ...@@ -471,7 +475,9 @@ export class MainThreadTask implements MainThreadTaskShape {
}); });
} else { } else {
let task = TaskDTO.to(value, this._workspaceContextServer, true); let task = TaskDTO.to(value, this._workspaceContextServer, true);
this._taskService.run(task); this._taskService.run(task).then(undefined, reason => {
// eat the error, it has already been surfaced to the user and we don't care about it here
});
let result: TaskExecutionDTO = { let result: TaskExecutionDTO = {
id: task._id, id: task._id,
task: TaskDTO.from(task) task: TaskDTO.from(task)
...@@ -524,15 +530,32 @@ export class MainThreadTask implements MainThreadTaskShape { ...@@ -524,15 +530,32 @@ export class MainThreadTask implements MainThreadTaskShape {
let vars: string[] = []; let vars: string[] = [];
toResolve.variables.forEach(item => vars.push(item)); toResolve.variables.forEach(item => vars.push(item));
return Promise.resolve(this._proxy.$resolveVariables(workspaceFolder.uri, { process: toResolve.process, variables: vars })).then(values => { return Promise.resolve(this._proxy.$resolveVariables(workspaceFolder.uri, { process: toResolve.process, variables: vars })).then(values => {
let result = { const partiallyResolvedVars = new Array<string>();
process: undefined as string, forEach(values.variables, (entry) => {
variables: new Map<string, string>() partiallyResolvedVars.push(entry.value);
}; });
Object.keys(values.variables).forEach(key => result.variables.set(key, values.variables[key])); return new Promise((resolve, reject) => {
if (Types.isString(values.process)) { this._configurationResolverService.resolveWithInteraction(workspaceFolder, partiallyResolvedVars, 'tasks').then(resolvedVars => {
result.process = values.process; let result = {
} process: undefined as string,
return result; variables: new Map<string, string>()
};
for (let i = 0; i < partiallyResolvedVars.length; i++) {
const variableName = vars[i].substring(2, vars[i].length - 1);
if (values.variables[vars[i]] === vars[i]) {
result.variables.set(variableName, resolvedVars.get(variableName));
} else {
result.variables.set(variableName, partiallyResolvedVars[i]);
}
}
if (Types.isString(values.process)) {
result.process = values.process;
}
resolve(result);
}, reason => {
reject(reason);
});
});
}); });
} }
}); });
......
...@@ -118,10 +118,10 @@ export class Debugger implements IDebugger { ...@@ -118,10 +118,10 @@ export class Debugger implements IDebugger {
substituteVariables(folder: IWorkspaceFolder, config: IConfig): Thenable<IConfig> { substituteVariables(folder: IWorkspaceFolder, config: IConfig): Thenable<IConfig> {
if (this.inExtHost()) { if (this.inExtHost()) {
return this.configurationManager.substituteVariables(this.type, folder, config).then(config => { return this.configurationManager.substituteVariables(this.type, folder, config).then(config => {
return this.configurationResolverService.resolveWithCommands(folder, config, this.variables); return this.configurationResolverService.resolveWithInteractionReplace(folder, config, undefined, this.variables);
}); });
} else { } else {
return this.configurationResolverService.resolveWithCommands(folder, config, this.variables); return this.configurationResolverService.resolveWithInteractionReplace(folder, config, undefined, this.variables);
} }
} }
......
...@@ -49,7 +49,9 @@ export class TaskEntry extends Model.QuickOpenEntry { ...@@ -49,7 +49,9 @@ export class TaskEntry extends Model.QuickOpenEntry {
} }
protected doRun(task: CustomTask | ContributedTask, options?: ProblemMatcherRunOptions): boolean { protected doRun(task: CustomTask | ContributedTask, options?: ProblemMatcherRunOptions): boolean {
this.taskService.run(task, options); this.taskService.run(task, options).then(undefined, reason => {
// eat the error, it has already been surfaced to the user and we don't care about it here
});
if (!task.command || task.command.presentation.focus) { if (!task.command || task.command.presentation.focus) {
this.quickOpenService.close(); this.quickOpenService.close();
return false; return false;
......
...@@ -12,6 +12,7 @@ import commonSchema from './jsonSchemaCommon'; ...@@ -12,6 +12,7 @@ import commonSchema from './jsonSchemaCommon';
import { ProblemMatcherRegistry } from 'vs/workbench/parts/tasks/common/problemMatcher'; import { ProblemMatcherRegistry } from 'vs/workbench/parts/tasks/common/problemMatcher';
import { TaskDefinitionRegistry } from '../common/taskDefinitionRegistry'; import { TaskDefinitionRegistry } from '../common/taskDefinitionRegistry';
import * as ConfigurationResolverUtils from 'vs/workbench/services/configurationResolver/common/configurationResolverUtils'; import * as ConfigurationResolverUtils from 'vs/workbench/services/configurationResolver/common/configurationResolverUtils';
import { inputsSchema } from 'vs/workbench/services/configurationResolver/electron-browser/jsonSchemaCommon';
function fixReferences(literal: any) { function fixReferences(literal: any) {
if (Array.isArray(literal)) { if (Array.isArray(literal)) {
...@@ -424,6 +425,9 @@ tasks.items = { ...@@ -424,6 +425,9 @@ tasks.items = {
oneOf: taskDefinitions oneOf: taskDefinitions
}; };
definitions.taskRunnerConfiguration.properties.inputs = inputsSchema.definitions.inputs;
definitions.commandConfiguration.properties.isShellCommand = Objects.deepClone(shellCommand); definitions.commandConfiguration.properties.isShellCommand = Objects.deepClone(shellCommand);
definitions.options.properties.shell = { definitions.options.properties.shell = {
$ref: '#/definitions/shellConfiguration' $ref: '#/definitions/shellConfiguration'
......
...@@ -1264,7 +1264,9 @@ class TaskService extends Disposable implements ITaskService { ...@@ -1264,7 +1264,9 @@ class TaskService extends Disposable implements ITaskService {
} }
this._taskSystem.terminate(task).then((response) => { this._taskSystem.terminate(task).then((response) => {
if (response.success) { if (response.success) {
this.run(task); this.run(task).then(undefined, reason => {
// eat the error, it has already been surfaced to the user and we don't care about it here
});
} else { } else {
this.notificationService.warn(nls.localize('TaskSystem.restartFailed', 'Failed to terminate and restart task {0}', Types.isString(task) ? task : task.name)); this.notificationService.warn(nls.localize('TaskSystem.restartFailed', 'Failed to terminate and restart task {0}', Types.isString(task) ? task : task.name));
} }
...@@ -1948,7 +1950,9 @@ class TaskService extends Disposable implements ITaskService { ...@@ -1948,7 +1950,9 @@ class TaskService extends Disposable implements ITaskService {
for (let folder of folders) { for (let folder of folders) {
let task = resolver.resolve(folder, identifier); let task = resolver.resolve(folder, identifier);
if (task) { if (task) {
this.run(task); this.run(task).then(undefined, reason => {
// eat the error, it has already been surfaced to the user and we don't care about it here
});
return; return;
} }
} }
...@@ -1977,7 +1981,9 @@ class TaskService extends Disposable implements ITaskService { ...@@ -1977,7 +1981,9 @@ class TaskService extends Disposable implements ITaskService {
if (task === null) { if (task === null) {
this.runConfigureTasks(); this.runConfigureTasks();
} else { } else {
this.run(task, { attachProblemMatcher: true }); this.run(task, { attachProblemMatcher: true }).then(undefined, reason => {
// eat the error, it has already been surfaced to the user and we don't care about it here
});
} }
}); });
}); });
...@@ -2033,7 +2039,9 @@ class TaskService extends Disposable implements ITaskService { ...@@ -2033,7 +2039,9 @@ class TaskService extends Disposable implements ITaskService {
if (tasks.length > 0) { if (tasks.length > 0) {
let { defaults, users } = this.splitPerGroupType(tasks); let { defaults, users } = this.splitPerGroupType(tasks);
if (defaults.length === 1) { if (defaults.length === 1) {
this.run(defaults[0]); this.run(defaults[0]).then(undefined, reason => {
// eat the error, it has already been surfaced to the user and we don't care about it here
});
return; return;
} else if (defaults.length + users.length > 0) { } else if (defaults.length + users.length > 0) {
tasks = defaults.concat(users); tasks = defaults.concat(users);
...@@ -2054,7 +2062,9 @@ class TaskService extends Disposable implements ITaskService { ...@@ -2054,7 +2062,9 @@ class TaskService extends Disposable implements ITaskService {
this.runConfigureDefaultBuildTask(); this.runConfigureDefaultBuildTask();
return; return;
} }
this.run(task, { attachProblemMatcher: true }); this.run(task, { attachProblemMatcher: true }).then(undefined, reason => {
// eat the error, it has already been surfaced to the user and we don't care about it here
});
}); });
}); });
}); });
...@@ -2077,7 +2087,9 @@ class TaskService extends Disposable implements ITaskService { ...@@ -2077,7 +2087,9 @@ class TaskService extends Disposable implements ITaskService {
if (tasks.length > 0) { if (tasks.length > 0) {
let { defaults, users } = this.splitPerGroupType(tasks); let { defaults, users } = this.splitPerGroupType(tasks);
if (defaults.length === 1) { if (defaults.length === 1) {
this.run(defaults[0]); this.run(defaults[0]).then(undefined, reason => {
// eat the error, it has already been surfaced to the user and we don't care about it here
});
return; return;
} else if (defaults.length + users.length > 0) { } else if (defaults.length + users.length > 0) {
tasks = defaults.concat(users); tasks = defaults.concat(users);
...@@ -2098,7 +2110,9 @@ class TaskService extends Disposable implements ITaskService { ...@@ -2098,7 +2110,9 @@ class TaskService extends Disposable implements ITaskService {
this.runConfigureTasks(); this.runConfigureTasks();
return; return;
} }
this.run(task); this.run(task).then(undefined, reason => {
// eat the error, it has already been surfaced to the user and we don't care about it here
});
}); });
}); });
}); });
......
...@@ -56,7 +56,8 @@ class VariableResolver { ...@@ -56,7 +56,8 @@ class VariableResolver {
} }
resolve(value: string): string { resolve(value: string): string {
return value.replace(/\$\{(.*?)\}/g, (match: string, variable: string) => { return value.replace(/\$\{(.*?)\}/g, (match: string, variable: string) => {
let result = this._values.get(match); // Strip out the ${} because the map contains them variables without those characters.
let result = this._values.get(match.substring(2, match.length - 1));
if (result) { if (result) {
return result; return result;
} }
...@@ -68,7 +69,6 @@ class VariableResolver { ...@@ -68,7 +69,6 @@ class VariableResolver {
} }
} }
export class VerifiedTask { export class VerifiedTask {
readonly task: Task; readonly task: Task;
readonly resolver: ITaskResolver; readonly resolver: ITaskResolver;
...@@ -101,7 +101,7 @@ export class TerminalTaskSystem implements ITaskSystem { ...@@ -101,7 +101,7 @@ export class TerminalTaskSystem implements ITaskSystem {
public static TelemetryEventName: string = 'taskService'; public static TelemetryEventName: string = 'taskService';
private static ProcessVarName = '${__process__}'; private static ProcessVarName = '__process__';
private static shellQuotes: IStringDictionary<ShellQuotingOptions> = { private static shellQuotes: IStringDictionary<ShellQuotingOptions> = {
'cmd': { 'cmd': {
...@@ -390,30 +390,35 @@ export class TerminalTaskSystem implements ITaskSystem { ...@@ -390,30 +390,35 @@ export class TerminalTaskSystem implements ITaskSystem {
} }
return Promise.resolve(resolved); return Promise.resolve(resolved);
}); });
return resolvedVariables;
} else { } else {
let result = new Map<string, string>(); let variablesArray = new Array<string>();
variables.forEach(variable => { variables.forEach(variable => variablesArray.push(variable));
result.set(variable, this.configurationResolverService.resolve(workspaceFolder, variable));
return new Promise((resolve, reject) => {
this.configurationResolverService.resolveWithInteraction(workspaceFolder, variablesArray, 'tasks').then(resolvedVariablesMap => {
if (isProcess) {
let processVarValue: string;
if (Platform.isWindows) {
processVarValue = win32.findExecutable(
this.configurationResolverService.resolve(workspaceFolder, CommandString.value(task.command.name)),
cwd ? this.configurationResolverService.resolve(workspaceFolder, cwd) : undefined,
envPath ? envPath.split(path.delimiter).map(p => this.configurationResolverService.resolve(workspaceFolder, p)) : undefined
);
} else {
processVarValue = this.configurationResolverService.resolve(workspaceFolder, CommandString.value(task.command.name));
}
resolvedVariablesMap.set(TerminalTaskSystem.ProcessVarName, processVarValue);
}
let resolvedVariablesResult: ResolvedVariables = {
variables: resolvedVariablesMap,
};
resolve(resolvedVariablesResult);
}, reason => {
reject(reason);
});
}); });
if (isProcess) {
let processVarValue: string;
if (Platform.isWindows) {
processVarValue = win32.findExecutable(
this.configurationResolverService.resolve(workspaceFolder, CommandString.value(task.command.name)),
cwd ? this.configurationResolverService.resolve(workspaceFolder, cwd) : undefined,
envPath ? envPath.split(path.delimiter).map(p => this.configurationResolverService.resolve(workspaceFolder, p)) : undefined
);
} else {
processVarValue = this.configurationResolverService.resolve(workspaceFolder, CommandString.value(task.command.name));
}
result.set(TerminalTaskSystem.ProcessVarName, processVarValue);
}
let resolvedVariablesResult: ResolvedVariables = {
variables: result,
};
resolvedVariables = Promise.resolve(resolvedVariablesResult);
} }
return resolvedVariables;
} }
private executeCommand(task: CustomTask | ContributedTask, trigger: string): Promise<ITaskSummary> { private executeCommand(task: CustomTask | ContributedTask, trigger: string): Promise<ITaskSummary> {
...@@ -429,6 +434,8 @@ export class TerminalTaskSystem implements ITaskSystem { ...@@ -429,6 +434,8 @@ export class TerminalTaskSystem implements ITaskSystem {
return resolvedVariables.then((resolvedVariables) => { return resolvedVariables.then((resolvedVariables) => {
this.currentTask.resolvedVariables = resolvedVariables; this.currentTask.resolvedVariables = resolvedVariables;
return this.executeInTerminal(task, trigger, new VariableResolver(this.currentTask.workspaceFolder, this.currentTask.systemInfo, resolvedVariables.variables, this.configurationResolverService)); return this.executeInTerminal(task, trigger, new VariableResolver(this.currentTask.workspaceFolder, this.currentTask.systemInfo, resolvedVariables.variables, this.configurationResolverService));
}, reason => {
return Promise.reject(reason);
}); });
} }
...@@ -449,6 +456,8 @@ export class TerminalTaskSystem implements ITaskSystem { ...@@ -449,6 +456,8 @@ export class TerminalTaskSystem implements ITaskSystem {
return this.resolveVariablesFromSet(this.lastTask.getVerifiedTask().systemInfo, this.lastTask.getVerifiedTask().workspaceFolder, task, variables).then((resolvedVariables) => { return this.resolveVariablesFromSet(this.lastTask.getVerifiedTask().systemInfo, this.lastTask.getVerifiedTask().workspaceFolder, task, variables).then((resolvedVariables) => {
this.currentTask.resolvedVariables = resolvedVariables; this.currentTask.resolvedVariables = resolvedVariables;
return this.executeInTerminal(task, trigger, new VariableResolver(this.lastTask.getVerifiedTask().workspaceFolder, this.lastTask.getVerifiedTask().systemInfo, resolvedVariables.variables, this.configurationResolverService)); return this.executeInTerminal(task, trigger, new VariableResolver(this.lastTask.getVerifiedTask().workspaceFolder, this.lastTask.getVerifiedTask().systemInfo, resolvedVariables.variables, this.configurationResolverService));
}, reason => {
return Promise.reject(reason);
}); });
} else { } else {
this.currentTask.resolvedVariables = this.lastTask.getVerifiedTask().resolvedVariables; this.currentTask.resolvedVariables = this.lastTask.getVerifiedTask().resolvedVariables;
...@@ -745,7 +754,7 @@ export class TerminalTaskSystem implements ITaskSystem { ...@@ -745,7 +754,7 @@ export class TerminalTaskSystem implements ITaskSystem {
} else { } else {
let commandExecutable = CommandString.value(command); let commandExecutable = CommandString.value(command);
let executable = !isShellCommand let executable = !isShellCommand
? this.resolveVariable(variableResolver, TerminalTaskSystem.ProcessVarName) ? this.resolveVariable(variableResolver, '${' + TerminalTaskSystem.ProcessVarName + '}')
: commandExecutable; : commandExecutable;
// When we have a process task there is no need to quote arguments. So we go ahead and take the string value. // When we have a process task there is no need to quote arguments. So we go ahead and take the string value.
......
...@@ -23,6 +23,7 @@ import * as Tasks from '../common/tasks'; ...@@ -23,6 +23,7 @@ import * as Tasks from '../common/tasks';
import { TaskDefinitionRegistry } from '../common/taskDefinitionRegistry'; import { TaskDefinitionRegistry } from '../common/taskDefinitionRegistry';
import { TaskDefinition } from 'vs/workbench/parts/tasks/node/tasks'; import { TaskDefinition } from 'vs/workbench/parts/tasks/node/tasks';
import { ConfiguredInput } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
export const enum ShellQuoting { export const enum ShellQuoting {
/** /**
...@@ -451,6 +452,11 @@ export interface BaseTaskRunnerConfiguration { ...@@ -451,6 +452,11 @@ export interface BaseTaskRunnerConfiguration {
* Problem matcher declarations * Problem matcher declarations
*/ */
declares?: ProblemMatcherConfig.NamedProblemMatcher[]; declares?: ProblemMatcherConfig.NamedProblemMatcher[];
/**
* Optional user input varaibles.
*/
inputs?: ConfiguredInput[];
} }
/** /**
......
...@@ -24,9 +24,31 @@ export interface IConfigurationResolverService { ...@@ -24,9 +24,31 @@ export interface IConfigurationResolverService {
resolveAny(folder: IWorkspaceFolder, config: any, commandValueMapping?: IStringDictionary<string>): any; resolveAny(folder: IWorkspaceFolder, config: any, commandValueMapping?: IStringDictionary<string>): any;
/** /**
* Recursively resolves all variables (including commands) in the given config and returns a copy of it with substituted values. * 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, * If a "variables" dictionary (with names -> command ids) is given, command variables are first mapped through it before being resolved.
* command variables are first mapped through it before being resolved. * @param folder
* @param config
* @param section For example, 'tasks' or 'debug'. Used for resolving inputs.
* @param variables Aliases for commands.
*/ */
resolveWithCommands(folder: IWorkspaceFolder, config: any, variables?: IStringDictionary<string>): TPromise<any>; resolveWithInteractionReplace(folder: IWorkspaceFolder, config: any, section?: string, variables?: IStringDictionary<string>): TPromise<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, config: any, section?: string, variables?: IStringDictionary<string>): TPromise<Map<string, string>>;
} }
export const enum ConfiguredInputType {
Prompt,
Pick
}
export interface ConfiguredInput {
label: string;
description: string;
default?: string;
type: ConfiguredInputType;
options?: string[];
}
\ No newline at end of file
...@@ -7,20 +7,23 @@ import { URI as uri } from 'vs/base/common/uri'; ...@@ -7,20 +7,23 @@ import { URI as uri } from 'vs/base/common/uri';
import * as nls from 'vs/nls'; import * as nls from 'vs/nls';
import * as paths from 'vs/base/common/paths'; import * as paths from 'vs/base/common/paths';
import * as platform from 'vs/base/common/platform'; import * as platform from 'vs/base/common/platform';
import * as Objects from 'vs/base/common/objects';
import * as Types from 'vs/base/common/types';
import { Schemas } from 'vs/base/common/network'; import { Schemas } from 'vs/base/common/network';
import { TPromise } from 'vs/base/common/winjs.base'; import { TPromise } from 'vs/base/common/winjs.base';
import { sequence } from 'vs/base/common/async'; import { sequence } from 'vs/base/common/async';
import { toResource } from 'vs/workbench/common/editor'; import { toResource } from 'vs/workbench/common/editor';
import { IStringDictionary, size } from 'vs/base/common/collections'; import { IStringDictionary, forEach, fromMap } from 'vs/base/common/collections';
import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ICommandService } from 'vs/platform/commands/common/commands'; import { ICommandService } from 'vs/platform/commands/common/commands';
import { IWorkspaceFolder, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceFolder, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/node/variableResolver'; import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/node/variableResolver';
import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { isUndefinedOrNull } from 'vs/base/common/types'; import { IQuickInputService, IInputOptions, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput';
import { ConfiguredInput, ConfiguredInputType } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
export class ConfigurationResolverService extends AbstractVariableResolverService { export class ConfigurationResolverService extends AbstractVariableResolverService {
...@@ -28,9 +31,10 @@ export class ConfigurationResolverService extends AbstractVariableResolverServic ...@@ -28,9 +31,10 @@ export class ConfigurationResolverService extends AbstractVariableResolverServic
envVariables: platform.IProcessEnvironment, envVariables: platform.IProcessEnvironment,
@IEditorService editorService: IEditorService, @IEditorService editorService: IEditorService,
@IEnvironmentService environmentService: IEnvironmentService, @IEnvironmentService environmentService: IEnvironmentService,
@IConfigurationService configurationService: IConfigurationService, @IConfigurationService private configurationService: IConfigurationService,
@ICommandService private commandService: ICommandService, @ICommandService private commandService: ICommandService,
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService,
@IQuickInputService private quickInputService: IQuickInputService
) { ) {
super({ super({
getFolderUri: (folderName: string): uri => { getFolderUri: (folderName: string): uri => {
...@@ -79,72 +83,80 @@ export class ConfigurationResolverService extends AbstractVariableResolverServic ...@@ -79,72 +83,80 @@ export class ConfigurationResolverService extends AbstractVariableResolverServic
}, envVariables); }, envVariables);
} }
public resolveWithCommands(folder: IWorkspaceFolder, config: any, variables?: IStringDictionary<string>): TPromise<any> { public resolveWithInteractionReplace(folder: IWorkspaceFolder, config: any, section?: string, variables?: IStringDictionary<string>): TPromise<any> {
// resolve any non-interactive variables
// then substitute remaining variables in VS Code core
config = this.resolveAny(folder, config); config = this.resolveAny(folder, config);
// now evaluate command variables (which might have a UI) // resolve input variables in the order in which they are encountered
return this.executeCommandVariables(config, variables).then(commandValueMapping => { return this.resolveWithInteraction(folder, config, section, variables).then(mapping => {
if (!commandValueMapping) { // cancelled by user
return null;
}
// finally substitute evaluated command variables (if there are any) // finally substitute evaluated command variables (if there are any)
if (size<string>(commandValueMapping) > 0) { if (mapping.size > 0) {
return this.resolveAny(folder, config, commandValueMapping); return this.resolveAny(folder, config, fromMap(mapping));
} else { } else {
return config; return config;
} }
}); });
} }
public resolveWithInteraction(folder: IWorkspaceFolder, config: any, section?: string, variables?: IStringDictionary<string>): TPromise<Map<string, string>> {
// resolve any non-interactive variables
const resolved = this.resolveAnyMap(folder, config);
config = resolved.newConfig;
const allVariableMapping: Map<string, string> = resolved.resolvedVariables;
// resolve input variables in the order in which they are encountered
return this.resolveWithInputs(folder, config, section).then(inputMapping => {
if (!this.updateMapping(inputMapping, allVariableMapping)) {
return undefined;
}
// resolve commands in the order in which they are encountered
return this.resolveWithCommands(config, variables).then(commandMapping => {
if (!this.updateMapping(commandMapping, allVariableMapping)) {
return undefined;
}
return allVariableMapping;
});
});
}
/**
* Add all items from newMapping to fullMapping. Returns false if newMapping is undefined.
*/
private updateMapping(newMapping: IStringDictionary<string>, fullMapping: Map<string, string>): boolean {
if (!newMapping) {
return false;
}
forEach(newMapping, (entry) => {
fullMapping.set(entry.key, entry.value);
});
return true;
}
/** /**
* Finds and executes all command variables in the given configuration and returns their values as a dictionary. * Finds and executes all command variables in the given configuration and returns their values as a dictionary.
* Please note: this method does not substitute the command variables (so the configuration is not modified). * Please note: this method does not substitute the command variables (so the configuration is not modified).
* The returned dictionary can be passed to "resolvePlatform" for the substitution. * The returned dictionary can be passed to "resolvePlatform" for the substitution.
* See #6569. * See #6569.
* @param configuration
* @param variableToCommandMap Aliases for commands
*/ */
private executeCommandVariables(configuration: any, variableToCommandMap: IStringDictionary<string>): TPromise<IStringDictionary<string>> { private resolveWithCommands(configuration: any, variableToCommandMap: IStringDictionary<string>): TPromise<IStringDictionary<string>> {
if (!configuration) { if (!configuration) {
return TPromise.as(null); return TPromise.as(undefined);
} }
// use an array to preserve order of first appearance // use an array to preserve order of first appearance
const commands: string[] = [];
const cmd_var = /\${command:(.*?)}/g; const cmd_var = /\${command:(.*?)}/g;
const commands: string[] = [];
const findCommandVariables = (object: any) => { this.findVariables(cmd_var, configuration, commands);
Object.keys(object).forEach(key => {
const value = object[key];
if (value && typeof value === 'object') {
findCommandVariables(value);
} else if (typeof value === 'string') {
let matches;
while ((matches = cmd_var.exec(value)) !== null) {
if (matches.length === 2) {
const command = matches[1];
if (commands.indexOf(command) < 0) {
commands.push(command);
}
}
}
}
});
};
findCommandVariables(configuration);
let cancelled = false; let cancelled = false;
const commandValueMapping: IStringDictionary<string> = Object.create(null); const commandValueMapping: IStringDictionary<string> = Object.create(null);
const factory: { (): TPromise<any> }[] = commands.map(commandVariable => { const factory: { (): TPromise<any> }[] = commands.map(commandVariable => {
return () => { return () => {
let commandId = variableToCommandMap ? variableToCommandMap[commandVariable] : undefined;
let commandId = variableToCommandMap ? variableToCommandMap[commandVariable] : null;
if (!commandId) { if (!commandId) {
// Just launch any command if the interactive variable is not contributed by the adapter #12735 // Just launch any command if the interactive variable is not contributed by the adapter #12735
commandId = commandVariable; commandId = commandVariable;
...@@ -152,8 +164,8 @@ export class ConfigurationResolverService extends AbstractVariableResolverServic ...@@ -152,8 +164,8 @@ export class ConfigurationResolverService extends AbstractVariableResolverServic
return this.commandService.executeCommand<string>(commandId, configuration).then(result => { return this.commandService.executeCommand<string>(commandId, configuration).then(result => {
if (typeof result === 'string') { if (typeof result === 'string') {
commandValueMapping[commandVariable] = result; commandValueMapping['command:' + commandVariable] = result;
} else if (isUndefinedOrNull(result)) { } else if (Types.isUndefinedOrNull(result)) {
cancelled = true; cancelled = true;
} else { } else {
throw new Error(nls.localize('stringsOnlySupported', "Command '{0}' did not return a string result. Only strings are supported as results for commands used for variable substitution.", commandVariable)); throw new Error(nls.localize('stringsOnlySupported', "Command '{0}' did not return a string result. Only strings are supported as results for commands used for variable substitution.", commandVariable));
...@@ -162,6 +174,156 @@ export class ConfigurationResolverService extends AbstractVariableResolverServic ...@@ -162,6 +174,156 @@ export class ConfigurationResolverService extends AbstractVariableResolverServic
}; };
}); });
return sequence(factory).then(() => cancelled ? null : commandValueMapping); return sequence(factory).then(() => cancelled ? undefined : commandValueMapping);
}
/**
* Resolves all inputs in a configuration and returns a map that maps the unresolved input to the resolved input.
* Does not do replacement of inputs.
* @param folder
* @param config
* @param section
*/
public resolveWithInputs(folder: IWorkspaceFolder, config: any, section: string): Promise<IStringDictionary<string>> {
if (!config) {
return Promise.resolve(undefined);
} else if (folder && section) {
// Get all the possible inputs
let result = this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY
? Objects.deepClone(this.configurationService.getValue<any>(section, { resource: folder.uri }))
: undefined;
let inputsArray = result ? this.parseConfigurationInputs(result.inputs) : undefined;
const inputs = new Map<string, ConfiguredInput>();
inputsArray.forEach(input => {
inputs.set(input.label, input);
});
// use an array to preserve order of first appearance
const input_var = /\${input:(.*?)}/g;
const commands: string[] = [];
this.findVariables(input_var, config, commands);
let cancelled = false;
const commandValueMapping: IStringDictionary<string> = Object.create(null);
const factory: { (): Promise<any> }[] = commands.map(commandVariable => {
return () => {
return this.showUserInput(commandVariable, inputs).then(resolvedValue => {
if (resolvedValue) {
commandValueMapping['input:' + commandVariable] = resolvedValue;
}
});
};
}, reason => {
return Promise.reject(reason);
});
return sequence(factory).then(() => cancelled ? undefined : commandValueMapping);
} else {
return Promise.resolve(Object.create(null));
}
}
/**
* Takes the provided input info and shows the quick pick so the user can provide the value for the input
* @param commandVariable Name of the input.
* @param inputs Information about each possible input.
* @param commandValueMapping
*/
private showUserInput(commandVariable: string, inputs: Map<string, ConfiguredInput>): Promise<string> {
if (inputs && inputs.has(commandVariable)) {
const input = inputs.get(commandVariable);
if (input.type === ConfiguredInputType.Prompt) {
let inputOptions: IInputOptions = { prompt: input.description };
if (input.default) {
inputOptions.value = input.default;
}
return this.quickInputService.input(inputOptions).then(resolvedInput => {
return resolvedInput ? resolvedInput : input.default;
});
} else { // input.type === ConfiguredInputType.pick
let picks = new Array<IQuickPickItem>();
if (input.options) {
input.options.forEach(pickOption => {
let item: IQuickPickItem = { label: pickOption };
if (input.default && (pickOption === input.default)) {
item.description = nls.localize('defaultInputValue', "Default");
picks.unshift(item);
} else {
picks.push(item);
}
});
}
let pickOptions: IPickOptions<IQuickPickItem> = { placeHolder: input.description };
return this.quickInputService.pick(picks, pickOptions, undefined).then(resolvedInput => {
return resolvedInput ? resolvedInput.label : input.default;
});
}
}
return Promise.resolve(undefined);
}
/**
* Finds all variables in object using cmdVar and pushes them into commands.
* @param cmdVar Regex to use for finding variables.
* @param object object is searched for variables.
* @param commands All found variables are returned in commands.
*/
private findVariables(cmdVar: RegExp, object: any, commands: string[]) {
if (!object) {
return;
} else if (typeof object === 'string') {
let matches;
while ((matches = cmdVar.exec(object)) !== null) {
if (matches.length === 2) {
const command = matches[1];
if (commands.indexOf(command) < 0) {
commands.push(command);
}
}
}
} else if (Types.isArray(object)) {
object.forEach(value => {
this.findVariables(cmdVar, value, commands);
});
} else {
Object.keys(object).forEach(key => {
const value = object[key];
this.findVariables(cmdVar, value, commands);
});
}
}
/**
* Converts an array of inputs into an actaul array of typed, ConfiguredInputs.
* @param object Array of something that should look like inputs.
*/
private parseConfigurationInputs(object: any[]): ConfiguredInput[] | undefined {
let inputs = new Array<ConfiguredInput>();
if (object) {
object.forEach(item => {
if (Types.isString(item.label) && Types.isString(item.description) && Types.isString(item.type)) {
let type: ConfiguredInputType;
switch (item.type) {
case 'prompt': type = ConfiguredInputType.Prompt; break;
case 'pick': type = ConfiguredInputType.Pick; break;
default: {
throw new Error(nls.localize('unknownInputTypeProvided', "Input '{0}' can only be of type 'prompt' or 'pick'.", item.label));
}
}
let options: string[];
if (type === ConfiguredInputType.Pick) {
if (Types.isStringArray(item.options)) {
options = item.options;
} else {
throw new Error(nls.localize('pickRequiresOptions', "Input '{0}' is of type 'pick' and must include 'options'.", item.label));
}
}
inputs.push({ label: item.label, description: item.description, type, default: item.default, options });
}
});
}
return inputs;
} }
} }
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* 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 const inputsSchema: IJSONSchema = {
definitions: {
inputDescription: {
type: 'object',
required: ['label', 'type', 'description'],
additionalProperties: false,
properties: {
label: {
type: 'string',
description: nls.localize('JsonSchema.input.label', "The input\'s label")
},
type: {
type: 'string',
description: nls.localize('JsonSchema.input.type', 'The input\'s type. Use prompt for free string input and selection for choosing from values'),
enum: ['prompt', 'pick']
},
description: {
type: 'string',
description: nls.localize('JsonSchema.input.description', 'Description to show for for using input.'),
},
default: {
type: 'string',
description: nls.localize('JsonSchema.input.default', 'Default value for the input.'),
},
options: {
type: 'array',
description: nls.localize('JsonSchema.input.options', 'Options to select from.'),
items: {
type: 'string'
}
}
}
},
inputs: {
type: 'array',
description: nls.localize('JsonSchema.inputs', 'User inputs. Used for prompting for user input.'),
items: {
type: 'object',
$ref: '#/definitions/inputDescription'
}
}
}
};
...@@ -51,7 +51,7 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe ...@@ -51,7 +51,7 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe
return this.recursiveResolve(root ? root.uri : undefined, value); return this.recursiveResolve(root ? root.uri : undefined, value);
} }
public resolveAny(workspaceFolder: IWorkspaceFolder, config: any, commandValueMapping?: IStringDictionary<string>): any { public resolveAnyBase(workspaceFolder: IWorkspaceFolder, config: any, commandValueMapping?: IStringDictionary<string>, resolvedVariables?: Map<string, string>): any {
const result = objects.deepClone(config) as any; const result = objects.deepClone(config) as any;
...@@ -70,35 +70,58 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe ...@@ -70,35 +70,58 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe
delete result.linux; delete result.linux;
// substitute all variables recursively in string values // substitute all variables recursively in string values
return this.recursiveResolve(workspaceFolder ? workspaceFolder.uri : undefined, result, commandValueMapping); return this.recursiveResolve(workspaceFolder ? workspaceFolder.uri : undefined, result, commandValueMapping, resolvedVariables);
} }
public resolveWithCommands(folder: IWorkspaceFolder, config: any): TPromise<any> { public resolveAny(workspaceFolder: IWorkspaceFolder, config: any, commandValueMapping?: IStringDictionary<string>): any {
throw new Error('resolveWithCommands not implemented.'); return this.resolveAnyBase(workspaceFolder, config, commandValueMapping);
} }
private recursiveResolve(folderUri: uri, value: any, commandValueMapping?: IStringDictionary<string>): any { public resolveAnyMap(workspaceFolder: IWorkspaceFolder, 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, config: any): TPromise<any> {
throw new Error('resolveWithInteractionReplace not implemented.');
}
public resolveWithInteraction(folder: IWorkspaceFolder, config: any): TPromise<any> {
throw new Error('resolveWithInteraction not implemented.');
}
private recursiveResolve(folderUri: uri, value: any, commandValueMapping?: IStringDictionary<string>, resolvedVariables?: Map<string, string>): any {
if (types.isString(value)) { if (types.isString(value)) {
return this.resolveString(folderUri, value, commandValueMapping); const resolved = this.resolveString(folderUri, value, commandValueMapping);
if (resolvedVariables) {
resolvedVariables.set(resolved.variableName, resolved.resolvedValue);
}
return resolved.replaced;
} else if (types.isArray(value)) { } else if (types.isArray(value)) {
return value.map(s => this.recursiveResolve(folderUri, s, commandValueMapping)); return value.map(s => this.recursiveResolve(folderUri, s, commandValueMapping, resolvedVariables));
} else if (types.isObject(value)) { } else if (types.isObject(value)) {
let result: IStringDictionary<string | IStringDictionary<string> | string[]> = Object.create(null); let result: IStringDictionary<string | IStringDictionary<string> | string[]> = Object.create(null);
Object.keys(value).forEach(key => { Object.keys(value).forEach(key => {
const resolvedKey = this.resolveString(folderUri, key, commandValueMapping); const resolvedKey = this.resolveString(folderUri, key, commandValueMapping);
result[resolvedKey] = this.recursiveResolve(folderUri, value[key], commandValueMapping); if (resolvedVariables) {
resolvedVariables.set(resolvedKey.variableName, resolvedKey.resolvedValue);
}
result[resolvedKey.replaced] = this.recursiveResolve(folderUri, value[key], commandValueMapping, resolvedVariables);
}); });
return result; return result;
} }
return value; return value;
} }
private resolveString(folderUri: uri, value: string, commandValueMapping: IStringDictionary<string>): string { private resolveString(folderUri: uri, value: string, commandValueMapping: IStringDictionary<string>): { replaced: string, variableName: string, resolvedValue: string } {
const filePath = this._context.getFilePath(); const filePath = this._context.getFilePath();
let variableName: string;
let resolvedValue: string;
const replaced = value.replace(AbstractVariableResolverService.VARIABLE_REGEXP, (match: string, variable: string) => {
return value.replace(AbstractVariableResolverService.VARIABLE_REGEXP, (match: string, variable: string) => { variableName = variable;
let argument: string; let argument: string;
const parts = variable.split(':'); const parts = variable.split(':');
if (parts && parts.length > 1) { if (parts && parts.length > 1) {
...@@ -115,10 +138,10 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe ...@@ -115,10 +138,10 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe
} }
const env = this._envVariables[argument]; const env = this._envVariables[argument];
if (types.isString(env)) { if (types.isString(env)) {
return env; return resolvedValue = env;
} }
// For `env` we should do the same as a normal shell does - evaluates missing envs to an empty string #46436 // For `env` we should do the same as a normal shell does - evaluates missing envs to an empty string #46436
return ''; return resolvedValue = '';
} }
throw new Error(localize('missingEnvVarName', "'{0}' can not be resolved because no environment variable name is given.", match)); throw new Error(localize('missingEnvVarName', "'{0}' can not be resolved because no environment variable name is given.", match));
...@@ -131,19 +154,14 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe ...@@ -131,19 +154,14 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe
if (types.isObject(config)) { if (types.isObject(config)) {
throw new Error(localize('configNoString', "'{0}' can not be resolved because '{1}' is a structured value.", match, argument)); throw new Error(localize('configNoString', "'{0}' can not be resolved because '{1}' is a structured value.", match, argument));
} }
return config; return resolvedValue = config;
} }
throw new Error(localize('missingConfigName', "'{0}' can not be resolved because no settings name is given.", match)); throw new Error(localize('missingConfigName', "'{0}' can not be resolved because no settings name is given.", match));
case 'command': case 'command':
if (argument && commandValueMapping) { return resolvedValue = this.resolveFromMap(match, argument, commandValueMapping, 'command');
const v = commandValueMapping[argument]; case 'input':
if (typeof v === 'string') { return resolvedValue = this.resolveFromMap(match, argument, commandValueMapping, 'input');
return v;
}
throw new Error(localize('noValueForCommand', "'{0}' can not be resolved because the command has no value.", match));
}
return match;
default: { default: {
...@@ -192,63 +210,75 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe ...@@ -192,63 +210,75 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe
switch (variable) { switch (variable) {
case 'workspaceRoot': case 'workspaceRoot':
case 'workspaceFolder': case 'workspaceFolder':
return normalizeDriveLetter(folderUri.fsPath); return resolvedValue = normalizeDriveLetter(folderUri.fsPath);
case 'cwd': case 'cwd':
return folderUri ? normalizeDriveLetter(folderUri.fsPath) : process.cwd(); return resolvedValue = (folderUri ? normalizeDriveLetter(folderUri.fsPath) : process.cwd());
case 'workspaceRootFolderName': case 'workspaceRootFolderName':
case 'workspaceFolderBasename': case 'workspaceFolderBasename':
return paths.basename(folderUri.fsPath); return resolvedValue = paths.basename(folderUri.fsPath);
case 'lineNumber': case 'lineNumber':
const lineNumber = this._context.getLineNumber(); const lineNumber = this._context.getLineNumber();
if (lineNumber) { if (lineNumber) {
return lineNumber; return resolvedValue = lineNumber;
} }
throw new Error(localize('canNotResolveLineNumber', "'{0}' can not be resolved. Make sure to have a line selected in the active editor.", match)); throw new Error(localize('canNotResolveLineNumber', "'{0}' can not be resolved. Make sure to have a line selected in the active editor.", match));
case 'selectedText': case 'selectedText':
const selectedText = this._context.getSelectedText(); const selectedText = this._context.getSelectedText();
if (selectedText) { if (selectedText) {
return selectedText; return resolvedValue = selectedText;
} }
throw new Error(localize('canNotResolveSelectedText', "'{0}' can not be resolved. Make sure to have some text selected in the active editor.", match)); throw new Error(localize('canNotResolveSelectedText', "'{0}' can not be resolved. Make sure to have some text selected in the active editor.", match));
case 'file': case 'file':
return filePath; return resolvedValue = filePath;
case 'relativeFile': case 'relativeFile':
if (folderUri) { if (folderUri) {
return paths.normalize(relative(folderUri.fsPath, filePath)); return resolvedValue = paths.normalize(relative(folderUri.fsPath, filePath));
} }
return filePath; return resolvedValue = filePath;
case 'fileDirname': case 'fileDirname':
return paths.dirname(filePath); return resolvedValue = paths.dirname(filePath);
case 'fileExtname': case 'fileExtname':
return paths.extname(filePath); return resolvedValue = paths.extname(filePath);
case 'fileBasename': case 'fileBasename':
return paths.basename(filePath); return resolvedValue = paths.basename(filePath);
case 'fileBasenameNoExtension': case 'fileBasenameNoExtension':
const basename = paths.basename(filePath); const basename = paths.basename(filePath);
return basename.slice(0, basename.length - paths.extname(basename).length); return resolvedValue = (basename.slice(0, basename.length - paths.extname(basename).length));
case 'execPath': case 'execPath':
const ep = this._context.getExecPath(); const ep = this._context.getExecPath();
if (ep) { if (ep) {
return ep; return resolvedValue = ep;
} }
return match; return resolvedValue = match;
default: default:
return match; return resolvedValue = match;
} }
} }
} }
}); });
return { replaced, variableName, resolvedValue };
}
private resolveFromMap(match: string, argument: string, commandValueMapping: IStringDictionary<string>, 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;
} }
} }
...@@ -14,6 +14,9 @@ import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; ...@@ -14,6 +14,9 @@ import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { TestEnvironmentService, TestEditorService, TestContextService } from 'vs/workbench/test/workbenchTestServices'; import { TestEnvironmentService, TestEditorService, TestContextService } from 'vs/workbench/test/workbenchTestServices';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { Disposable } from 'vs/base/common/lifecycle'; import { Disposable } 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 'vscode';
import * as Types from 'vs/base/common/types';
suite('Configuration Resolver Service', () => { suite('Configuration Resolver Service', () => {
let configurationResolverService: IConfigurationResolverService; let configurationResolverService: IConfigurationResolverService;
...@@ -21,17 +24,19 @@ suite('Configuration Resolver Service', () => { ...@@ -21,17 +24,19 @@ suite('Configuration Resolver Service', () => {
let mockCommandService: MockCommandService; let mockCommandService: MockCommandService;
let editorService: TestEditorService; let editorService: TestEditorService;
let workspace: IWorkspaceFolder; let workspace: IWorkspaceFolder;
let quickInputService: MockQuickInputService;
setup(() => { setup(() => {
mockCommandService = new MockCommandService(); mockCommandService = new MockCommandService();
editorService = new TestEditorService(); editorService = new TestEditorService();
quickInputService = new MockQuickInputService();
workspace = { workspace = {
uri: uri.parse('file:///VSCode/workspaceLocation'), uri: uri.parse('file:///VSCode/workspaceLocation'),
name: 'hey', name: 'hey',
index: 0, index: 0,
toResource: () => null toResource: () => null
}; };
configurationResolverService = new ConfigurationResolverService(envVariables, editorService, TestEnvironmentService, new TestConfigurationService(), mockCommandService, new TestContextService()); configurationResolverService = new ConfigurationResolverService(envVariables, editorService, TestEnvironmentService, new MockInputsConfigurationService(), mockCommandService, new TestContextService(), quickInputService);
}); });
teardown(() => { teardown(() => {
...@@ -116,7 +121,7 @@ suite('Configuration Resolver Service', () => { ...@@ -116,7 +121,7 @@ suite('Configuration Resolver Service', () => {
} }
}); });
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService()); let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz'); assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz');
}); });
...@@ -133,7 +138,7 @@ suite('Configuration Resolver Service', () => { ...@@ -133,7 +138,7 @@ suite('Configuration Resolver Service', () => {
} }
}); });
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService()); let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} xyz'), 'abc foo bar xyz'); assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} xyz'), 'abc foo bar xyz');
}); });
...@@ -150,7 +155,7 @@ suite('Configuration Resolver Service', () => { ...@@ -150,7 +155,7 @@ suite('Configuration Resolver Service', () => {
} }
}); });
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService()); let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
if (platform.isWindows) { if (platform.isWindows) {
assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${workspaceFolder} ${env:key1} xyz'), 'abc foo \\VSCode\\workspaceLocation Value for key1 xyz'); assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${workspaceFolder} ${env:key1} xyz'), 'abc foo \\VSCode\\workspaceLocation Value for key1 xyz');
} else { } else {
...@@ -171,7 +176,7 @@ suite('Configuration Resolver Service', () => { ...@@ -171,7 +176,7 @@ suite('Configuration Resolver Service', () => {
} }
}); });
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService()); let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
if (platform.isWindows) { 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'); 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 { } else {
...@@ -205,7 +210,7 @@ suite('Configuration Resolver Service', () => { ...@@ -205,7 +210,7 @@ suite('Configuration Resolver Service', () => {
} }
}); });
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService()); let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${config:editor.lineNumbers} ${config:editor.insertSpaces} xyz'), 'abc foo 123 false xyz'); assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${config:editor.lineNumbers} ${config:editor.insertSpaces} xyz'), 'abc foo 123 false xyz');
}); });
...@@ -215,7 +220,7 @@ suite('Configuration Resolver Service', () => { ...@@ -215,7 +220,7 @@ suite('Configuration Resolver Service', () => {
editor: {} editor: {}
}); });
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService()); let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
assert.strictEqual(service.resolve(workspace, 'abc ${unknownVariable} xyz'), 'abc ${unknownVariable} xyz'); assert.strictEqual(service.resolve(workspace, 'abc ${unknownVariable} xyz'), 'abc ${unknownVariable} xyz');
assert.strictEqual(service.resolve(workspace, 'abc ${env:unknownVariable} xyz'), 'abc xyz'); assert.strictEqual(service.resolve(workspace, 'abc ${env:unknownVariable} xyz'), 'abc xyz');
}); });
...@@ -228,7 +233,7 @@ suite('Configuration Resolver Service', () => { ...@@ -228,7 +233,7 @@ suite('Configuration Resolver Service', () => {
} }
}); });
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService()); let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
assert.throws(() => service.resolve(workspace, 'abc ${env} xyz')); assert.throws(() => service.resolve(workspace, 'abc ${env} xyz'));
assert.throws(() => service.resolve(workspace, 'abc ${env:} xyz')); assert.throws(() => service.resolve(workspace, 'abc ${env:} xyz'));
...@@ -251,7 +256,7 @@ suite('Configuration Resolver Service', () => { ...@@ -251,7 +256,7 @@ suite('Configuration Resolver Service', () => {
'outDir': null 'outDir': null
}; };
return configurationResolverService.resolveWithCommands(undefined, configuration).then(result => { return configurationResolverService.resolveWithInteractionReplace(undefined, configuration).then(result => {
assert.deepEqual(result, { assert.deepEqual(result, {
'name': 'Attach to Process', 'name': 'Attach to Process',
...@@ -280,7 +285,7 @@ suite('Configuration Resolver Service', () => { ...@@ -280,7 +285,7 @@ suite('Configuration Resolver Service', () => {
const commandVariables = Object.create(null); const commandVariables = Object.create(null);
commandVariables['commandVariable1'] = 'command1'; commandVariables['commandVariable1'] = 'command1';
return configurationResolverService.resolveWithCommands(undefined, configuration, commandVariables).then(result => { return configurationResolverService.resolveWithInteractionReplace(undefined, configuration, undefined, commandVariables).then(result => {
assert.deepEqual(result, { assert.deepEqual(result, {
'name': 'Attach to Process', 'name': 'Attach to Process',
...@@ -313,7 +318,7 @@ suite('Configuration Resolver Service', () => { ...@@ -313,7 +318,7 @@ suite('Configuration Resolver Service', () => {
const commandVariables = Object.create(null); const commandVariables = Object.create(null);
commandVariables['commandVariable1'] = 'command1'; commandVariables['commandVariable1'] = 'command1';
return configurationResolverService.resolveWithCommands(undefined, configuration, commandVariables).then(result => { return configurationResolverService.resolveWithInteractionReplace(undefined, configuration, undefined, commandVariables).then(result => {
assert.deepEqual(result, { assert.deepEqual(result, {
'name': 'Attach to Process', 'name': 'Attach to Process',
...@@ -344,7 +349,7 @@ suite('Configuration Resolver Service', () => { ...@@ -344,7 +349,7 @@ suite('Configuration Resolver Service', () => {
const commandVariables = Object.create(null); const commandVariables = Object.create(null);
commandVariables['commandVariable1'] = 'command1'; commandVariables['commandVariable1'] = 'command1';
return configurationResolverService.resolveWithCommands(undefined, configuration, commandVariables).then(result => { return configurationResolverService.resolveWithInteractionReplace(undefined, configuration, undefined, commandVariables).then(result => {
assert.deepEqual(result, { assert.deepEqual(result, {
'name': 'Attach to Process', 'name': 'Attach to Process',
...@@ -354,6 +359,87 @@ suite('Configuration Resolver Service', () => { ...@@ -354,6 +359,87 @@ suite('Configuration Resolver Service', () => {
'value': '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('several input variables and command', () => {
const configuration = {
'name': '${input:input3}',
'type': '${command:command1}',
'request': '${input:input1}',
'processId': '${input:input2}',
'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',
'port': 5858,
'sourceMaps': false,
'outDir': null
});
assert.equal(1, mockCommandService.callCount); assert.equal(1, mockCommandService.callCount);
}); });
}); });
...@@ -405,3 +491,87 @@ class MockCommandService implements ICommandService { ...@@ -405,3 +491,87 @@ class MockCommandService implements ICommandService {
return Promise.resolve(result); return Promise.resolve(result);
} }
} }
class MockQuickInputService implements IQuickInputService {
_serviceBrand: any;
public pick<T extends IQuickPickItem>(picks: Thenable<QuickPickInput<T>[]> | QuickPickInput<T>[], options?: IPickOptions<T> & { canPickMany: true }, token?: CancellationToken): Promise<T[]>;
public pick<T extends IQuickPickItem>(picks: Thenable<QuickPickInput<T>[]> | QuickPickInput<T>[], options?: IPickOptions<T> & { canPickMany: false }, token?: CancellationToken): Promise<T>;
public pick<T extends IQuickPickItem>(picks: Thenable<QuickPickInput<T>[]> | QuickPickInput<T>[], options?: Omit<IPickOptions<T>, 'canPickMany'>, token?: CancellationToken): Promise<T> {
if (Types.isArray(picks)) {
return Promise.resolve(<T>{ label: 'selectedPick', description: 'pick description' });
} else {
return Promise.resolve(undefined);
}
}
public input(options?: IInputOptions, token?: CancellationToken): Promise<string> {
return Promise.resolve('resolved' + options.prompt);
}
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 MockInputsConfigurationService extends TestConfigurationService {
public getValue(arg1?: any, arg2?: any): any {
let configuration;
if (arg1 === 'tasks') {
configuration = {
inputs: [
{
label: 'input1',
type: 'prompt',
description: 'Enterinput1',
default: 'default input1'
},
{
label: 'input2',
type: 'pick',
description: 'Enterinput1',
default: 'option2',
options: ['option1', 'option2', 'option3']
},
{
label: 'input3',
type: 'prompt',
description: 'Enterinput3',
default: 'default input3'
}
]
};
}
return configuration;
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册