From f9703e9ad6681815a116ca3c9c29a9bd26ae0276 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 3 Dec 2018 09:24:59 +0100 Subject: [PATCH] Input variables in tasks (#63910) Fixes #4758 --- src/vs/base/common/collections.ts | 10 + .../api/electron-browser/mainThreadTask.ts | 49 +++- src/vs/workbench/parts/debug/node/debugger.ts | 4 +- .../parts/tasks/browser/quickOpen.ts | 4 +- .../tasks/electron-browser/jsonSchema_v2.ts | 4 + .../electron-browser/task.contribution.ts | 28 +- .../electron-browser/terminalTaskSystem.ts | 59 ++-- .../parts/tasks/node/taskConfiguration.ts | 6 + .../common/configurationResolver.ts | 30 +- .../configurationResolverService.ts | 262 ++++++++++++++---- .../electron-browser/jsonSchemaCommon.ts | 50 ++++ .../node/variableResolver.ts | 104 ++++--- .../configurationResolverService.test.ts | 194 ++++++++++++- 13 files changed, 653 insertions(+), 151 deletions(-) create mode 100644 src/vs/workbench/services/configurationResolver/electron-browser/jsonSchemaCommon.ts diff --git a/src/vs/base/common/collections.ts b/src/vs/base/common/collections.ts index b798f368f80..ca40515c99c 100644 --- a/src/vs/base/common/collections.ts +++ b/src/vs/base/common/collections.ts @@ -87,3 +87,13 @@ export function groupBy(data: T[], groupFn: (element: T) => string): IStringD } return result; } + +export function fromMap(original: Map): IStringDictionary { + const result: IStringDictionary = Object.create(null); + if (original) { + original.forEach((value, key) => { + result[key] = value; + }); + } + return result; +} \ No newline at end of file diff --git a/src/vs/workbench/api/electron-browser/mainThreadTask.ts b/src/vs/workbench/api/electron-browser/mainThreadTask.ts index d96a53a8726..c66ec8f3103 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTask.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTask.ts @@ -10,7 +10,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import * as Objects from 'vs/base/common/objects'; import * as Types from 'vs/base/common/types'; 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 { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -33,6 +33,7 @@ import { ProcessExecutionDTO, ShellExecutionDTO, ShellExecutionOptionsDTO, TaskDTO, TaskSourceDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO, TaskSystemInfoDTO, RunOptionsDTO } from 'vs/workbench/api/shared/tasks'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; namespace TaskExecutionDTO { export function from(value: TaskExecution): TaskExecutionDTO { @@ -381,7 +382,8 @@ export class MainThreadTask implements MainThreadTaskShape { constructor( extHostContext: IExtHostContext, @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._providers = new Map(); @@ -460,7 +462,9 @@ export class MainThreadTask implements MainThreadTaskShape { if (TaskHandleDTO.is(value)) { let workspaceFolder = this._workspaceContextServer.getWorkspaceFolder(URI.revive(value.workspaceFolder)); 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 = { id: value.id, task: TaskDTO.from(task) @@ -471,7 +475,9 @@ export class MainThreadTask implements MainThreadTaskShape { }); } else { 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 = { id: task._id, task: TaskDTO.from(task) @@ -524,15 +530,32 @@ export class MainThreadTask implements MainThreadTaskShape { let vars: string[] = []; toResolve.variables.forEach(item => vars.push(item)); return Promise.resolve(this._proxy.$resolveVariables(workspaceFolder.uri, { process: toResolve.process, variables: vars })).then(values => { - let result = { - process: undefined as string, - variables: new Map() - }; - Object.keys(values.variables).forEach(key => result.variables.set(key, values.variables[key])); - if (Types.isString(values.process)) { - result.process = values.process; - } - return result; + const partiallyResolvedVars = new Array(); + forEach(values.variables, (entry) => { + partiallyResolvedVars.push(entry.value); + }); + return new Promise((resolve, reject) => { + this._configurationResolverService.resolveWithInteraction(workspaceFolder, partiallyResolvedVars, 'tasks').then(resolvedVars => { + let result = { + process: undefined as string, + variables: new Map() + }; + 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); + }); + }); }); } }); diff --git a/src/vs/workbench/parts/debug/node/debugger.ts b/src/vs/workbench/parts/debug/node/debugger.ts index 45d70648eab..69174b0db2e 100644 --- a/src/vs/workbench/parts/debug/node/debugger.ts +++ b/src/vs/workbench/parts/debug/node/debugger.ts @@ -118,10 +118,10 @@ export class Debugger implements IDebugger { substituteVariables(folder: IWorkspaceFolder, config: IConfig): Thenable { if (this.inExtHost()) { 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 { - return this.configurationResolverService.resolveWithCommands(folder, config, this.variables); + return this.configurationResolverService.resolveWithInteractionReplace(folder, config, undefined, this.variables); } } diff --git a/src/vs/workbench/parts/tasks/browser/quickOpen.ts b/src/vs/workbench/parts/tasks/browser/quickOpen.ts index 5c4e8b25d33..2c8f21108c2 100644 --- a/src/vs/workbench/parts/tasks/browser/quickOpen.ts +++ b/src/vs/workbench/parts/tasks/browser/quickOpen.ts @@ -49,7 +49,9 @@ export class TaskEntry extends Model.QuickOpenEntry { } 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) { this.quickOpenService.close(); return false; diff --git a/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts b/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts index 09fd08909e6..7bc3f39316a 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/jsonSchema_v2.ts @@ -12,6 +12,7 @@ import commonSchema from './jsonSchemaCommon'; import { ProblemMatcherRegistry } from 'vs/workbench/parts/tasks/common/problemMatcher'; import { TaskDefinitionRegistry } from '../common/taskDefinitionRegistry'; import * as ConfigurationResolverUtils from 'vs/workbench/services/configurationResolver/common/configurationResolverUtils'; +import { inputsSchema } from 'vs/workbench/services/configurationResolver/electron-browser/jsonSchemaCommon'; function fixReferences(literal: any) { if (Array.isArray(literal)) { @@ -424,6 +425,9 @@ tasks.items = { oneOf: taskDefinitions }; + +definitions.taskRunnerConfiguration.properties.inputs = inputsSchema.definitions.inputs; + definitions.commandConfiguration.properties.isShellCommand = Objects.deepClone(shellCommand); definitions.options.properties.shell = { $ref: '#/definitions/shellConfiguration' diff --git a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts index f2e731516c2..060c5ced770 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts @@ -1264,7 +1264,9 @@ class TaskService extends Disposable implements ITaskService { } this._taskSystem.terminate(task).then((response) => { 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 { 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 { for (let folder of folders) { let task = resolver.resolve(folder, identifier); 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; } } @@ -1977,7 +1981,9 @@ class TaskService extends Disposable implements ITaskService { if (task === null) { this.runConfigureTasks(); } 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 { if (tasks.length > 0) { let { defaults, users } = this.splitPerGroupType(tasks); 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; } else if (defaults.length + users.length > 0) { tasks = defaults.concat(users); @@ -2054,7 +2062,9 @@ class TaskService extends Disposable implements ITaskService { this.runConfigureDefaultBuildTask(); 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 { if (tasks.length > 0) { let { defaults, users } = this.splitPerGroupType(tasks); 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; } else if (defaults.length + users.length > 0) { tasks = defaults.concat(users); @@ -2098,7 +2110,9 @@ class TaskService extends Disposable implements ITaskService { this.runConfigureTasks(); 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 + }); }); }); }); diff --git a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts index 477b8e90ffe..6fdd80f9131 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts @@ -56,7 +56,8 @@ class VariableResolver { } resolve(value: string): 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) { return result; } @@ -68,7 +69,6 @@ class VariableResolver { } } - export class VerifiedTask { readonly task: Task; readonly resolver: ITaskResolver; @@ -101,7 +101,7 @@ export class TerminalTaskSystem implements ITaskSystem { public static TelemetryEventName: string = 'taskService'; - private static ProcessVarName = '${__process__}'; + private static ProcessVarName = '__process__'; private static shellQuotes: IStringDictionary = { 'cmd': { @@ -390,30 +390,35 @@ export class TerminalTaskSystem implements ITaskSystem { } return Promise.resolve(resolved); }); + return resolvedVariables; } else { - let result = new Map(); - variables.forEach(variable => { - result.set(variable, this.configurationResolverService.resolve(workspaceFolder, variable)); + let variablesArray = new Array(); + variables.forEach(variable => variablesArray.push(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 { @@ -429,6 +434,8 @@ export class TerminalTaskSystem implements ITaskSystem { return resolvedVariables.then((resolvedVariables) => { this.currentTask.resolvedVariables = resolvedVariables; 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 { return this.resolveVariablesFromSet(this.lastTask.getVerifiedTask().systemInfo, this.lastTask.getVerifiedTask().workspaceFolder, task, variables).then((resolvedVariables) => { this.currentTask.resolvedVariables = resolvedVariables; 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 { this.currentTask.resolvedVariables = this.lastTask.getVerifiedTask().resolvedVariables; @@ -745,7 +754,7 @@ export class TerminalTaskSystem implements ITaskSystem { } else { let commandExecutable = CommandString.value(command); let executable = !isShellCommand - ? this.resolveVariable(variableResolver, TerminalTaskSystem.ProcessVarName) + ? this.resolveVariable(variableResolver, '${' + TerminalTaskSystem.ProcessVarName + '}') : commandExecutable; // When we have a process task there is no need to quote arguments. So we go ahead and take the string value. diff --git a/src/vs/workbench/parts/tasks/node/taskConfiguration.ts b/src/vs/workbench/parts/tasks/node/taskConfiguration.ts index 31a918d943e..29541c1514a 100644 --- a/src/vs/workbench/parts/tasks/node/taskConfiguration.ts +++ b/src/vs/workbench/parts/tasks/node/taskConfiguration.ts @@ -23,6 +23,7 @@ import * as Tasks from '../common/tasks'; import { TaskDefinitionRegistry } from '../common/taskDefinitionRegistry'; import { TaskDefinition } from 'vs/workbench/parts/tasks/node/tasks'; +import { ConfiguredInput } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; export const enum ShellQuoting { /** @@ -451,6 +452,11 @@ export interface BaseTaskRunnerConfiguration { * Problem matcher declarations */ declares?: ProblemMatcherConfig.NamedProblemMatcher[]; + + /** + * Optional user input varaibles. + */ + inputs?: ConfiguredInput[]; } /** diff --git a/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts b/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts index e6ae183d5ec..40b38226a1e 100644 --- a/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts +++ b/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts @@ -24,9 +24,31 @@ export interface IConfigurationResolverService { resolveAny(folder: IWorkspaceFolder, config: any, commandValueMapping?: IStringDictionary): any; /** - * Recursively resolves all variables (including commands) 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. + * 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 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): TPromise; + resolveWithInteractionReplace(folder: IWorkspaceFolder, config: any, section?: string, variables?: IStringDictionary): TPromise; + + /** + * 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): TPromise>; } + +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 diff --git a/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts index 9d6cce04087..af922f2d275 100644 --- a/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts @@ -7,20 +7,23 @@ import { URI as uri } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import * as paths from 'vs/base/common/paths'; 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 { TPromise } from 'vs/base/common/winjs.base'; import { sequence } from 'vs/base/common/async'; 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 { IConfigurationService } from 'vs/platform/configuration/common/configuration'; 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 { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/node/variableResolver'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; 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 { @@ -28,9 +31,10 @@ export class ConfigurationResolverService extends AbstractVariableResolverServic envVariables: platform.IProcessEnvironment, @IEditorService editorService: IEditorService, @IEnvironmentService environmentService: IEnvironmentService, - @IConfigurationService configurationService: IConfigurationService, + @IConfigurationService private configurationService: IConfigurationService, @ICommandService private commandService: ICommandService, - @IWorkspaceContextService workspaceContextService: IWorkspaceContextService + @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, + @IQuickInputService private quickInputService: IQuickInputService ) { super({ getFolderUri: (folderName: string): uri => { @@ -79,72 +83,80 @@ export class ConfigurationResolverService extends AbstractVariableResolverServic }, envVariables); } - public resolveWithCommands(folder: IWorkspaceFolder, config: any, variables?: IStringDictionary): TPromise { - - // then substitute remaining variables in VS Code core + public resolveWithInteractionReplace(folder: IWorkspaceFolder, config: any, section?: string, variables?: IStringDictionary): TPromise { + // resolve any non-interactive variables config = this.resolveAny(folder, config); - // now evaluate command variables (which might have a UI) - return this.executeCommandVariables(config, variables).then(commandValueMapping => { - - if (!commandValueMapping) { // cancelled by user - return null; - } - + // resolve input variables in the order in which they are encountered + return this.resolveWithInteraction(folder, config, section, variables).then(mapping => { // finally substitute evaluated command variables (if there are any) - if (size(commandValueMapping) > 0) { - return this.resolveAny(folder, config, commandValueMapping); + if (mapping.size > 0) { + return this.resolveAny(folder, config, fromMap(mapping)); } else { return config; } }); } + public resolveWithInteraction(folder: IWorkspaceFolder, config: any, section?: string, variables?: IStringDictionary): TPromise> { + // resolve any non-interactive variables + const resolved = this.resolveAnyMap(folder, config); + config = resolved.newConfig; + const allVariableMapping: Map = 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, fullMapping: Map): 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. * 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. * See #6569. + * @param configuration + * @param variableToCommandMap Aliases for commands */ - private executeCommandVariables(configuration: any, variableToCommandMap: IStringDictionary): TPromise> { - + private resolveWithCommands(configuration: any, variableToCommandMap: IStringDictionary): TPromise> { if (!configuration) { - return TPromise.as(null); + return TPromise.as(undefined); } // use an array to preserve order of first appearance - const commands: string[] = []; - const cmd_var = /\${command:(.*?)}/g; - - const findCommandVariables = (object: any) => { - 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); - + const commands: string[] = []; + this.findVariables(cmd_var, configuration, commands); let cancelled = false; const commandValueMapping: IStringDictionary = Object.create(null); const factory: { (): TPromise }[] = commands.map(commandVariable => { return () => { - - let commandId = variableToCommandMap ? variableToCommandMap[commandVariable] : null; + let commandId = variableToCommandMap ? variableToCommandMap[commandVariable] : undefined; if (!commandId) { // Just launch any command if the interactive variable is not contributed by the adapter #12735 commandId = commandVariable; @@ -152,8 +164,8 @@ export class ConfigurationResolverService extends AbstractVariableResolverServic return this.commandService.executeCommand(commandId, configuration).then(result => { if (typeof result === 'string') { - commandValueMapping[commandVariable] = result; - } else if (isUndefinedOrNull(result)) { + commandValueMapping['command:' + commandVariable] = result; + } else if (Types.isUndefinedOrNull(result)) { cancelled = true; } 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)); @@ -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> { + 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(section, { resource: folder.uri })) + : undefined; + let inputsArray = result ? this.parseConfigurationInputs(result.inputs) : undefined; + const inputs = new Map(); + 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 = Object.create(null); + + const factory: { (): Promise }[] = 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): Promise { + 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(); + 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 = { 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(); + 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 diff --git a/src/vs/workbench/services/configurationResolver/electron-browser/jsonSchemaCommon.ts b/src/vs/workbench/services/configurationResolver/electron-browser/jsonSchemaCommon.ts new file mode 100644 index 00000000000..7b66107d31b --- /dev/null +++ b/src/vs/workbench/services/configurationResolver/electron-browser/jsonSchemaCommon.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * 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' + } + } + } +}; diff --git a/src/vs/workbench/services/configurationResolver/node/variableResolver.ts b/src/vs/workbench/services/configurationResolver/node/variableResolver.ts index b91edb96392..8e98ac239b1 100644 --- a/src/vs/workbench/services/configurationResolver/node/variableResolver.ts +++ b/src/vs/workbench/services/configurationResolver/node/variableResolver.ts @@ -51,7 +51,7 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe return this.recursiveResolve(root ? root.uri : undefined, value); } - public resolveAny(workspaceFolder: IWorkspaceFolder, config: any, commandValueMapping?: IStringDictionary): any { + public resolveAnyBase(workspaceFolder: IWorkspaceFolder, config: any, commandValueMapping?: IStringDictionary, resolvedVariables?: Map): any { const result = objects.deepClone(config) as any; @@ -70,35 +70,58 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe delete result.linux; // 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 { - throw new Error('resolveWithCommands not implemented.'); + public resolveAny(workspaceFolder: IWorkspaceFolder, config: any, commandValueMapping?: IStringDictionary): any { + return this.resolveAnyBase(workspaceFolder, config, commandValueMapping); } - private recursiveResolve(folderUri: uri, value: any, commandValueMapping?: IStringDictionary): any { + public resolveAnyMap(workspaceFolder: IWorkspaceFolder, config: any, commandValueMapping?: IStringDictionary): { newConfig: any, resolvedVariables: Map } { + const resolvedVariables = new Map(); + const newConfig = this.resolveAnyBase(workspaceFolder, config, commandValueMapping, resolvedVariables); + return { newConfig, resolvedVariables }; + } + + public resolveWithInteractionReplace(folder: IWorkspaceFolder, config: any): TPromise { + throw new Error('resolveWithInteractionReplace not implemented.'); + } + + public resolveWithInteraction(folder: IWorkspaceFolder, config: any): TPromise { + throw new Error('resolveWithInteraction not implemented.'); + } + + private recursiveResolve(folderUri: uri, value: any, commandValueMapping?: IStringDictionary, resolvedVariables?: Map): any { 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)) { - return value.map(s => this.recursiveResolve(folderUri, s, commandValueMapping)); + return value.map(s => this.recursiveResolve(folderUri, s, commandValueMapping, resolvedVariables)); } else if (types.isObject(value)) { let result: IStringDictionary | string[]> = Object.create(null); Object.keys(value).forEach(key => { 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 value; } - private resolveString(folderUri: uri, value: string, commandValueMapping: IStringDictionary): string { + private resolveString(folderUri: uri, value: string, commandValueMapping: IStringDictionary): { replaced: string, variableName: string, resolvedValue: string } { 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; const parts = variable.split(':'); if (parts && parts.length > 1) { @@ -115,10 +138,10 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe } const env = this._envVariables[argument]; 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 - return ''; + return resolvedValue = ''; } 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 if (types.isObject(config)) { 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)); case 'command': - if (argument && commandValueMapping) { - const v = commandValueMapping[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; + return resolvedValue = this.resolveFromMap(match, argument, commandValueMapping, 'command'); + case 'input': + return resolvedValue = this.resolveFromMap(match, argument, commandValueMapping, 'input'); default: { @@ -192,63 +210,75 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe switch (variable) { case 'workspaceRoot': case 'workspaceFolder': - return normalizeDriveLetter(folderUri.fsPath); + return resolvedValue = normalizeDriveLetter(folderUri.fsPath); case 'cwd': - return folderUri ? normalizeDriveLetter(folderUri.fsPath) : process.cwd(); + return resolvedValue = (folderUri ? normalizeDriveLetter(folderUri.fsPath) : process.cwd()); case 'workspaceRootFolderName': case 'workspaceFolderBasename': - return paths.basename(folderUri.fsPath); + return resolvedValue = paths.basename(folderUri.fsPath); case 'lineNumber': const lineNumber = this._context.getLineNumber(); 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)); case 'selectedText': const selectedText = this._context.getSelectedText(); 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)); case 'file': - return filePath; + return resolvedValue = filePath; case 'relativeFile': if (folderUri) { - return paths.normalize(relative(folderUri.fsPath, filePath)); + return resolvedValue = paths.normalize(relative(folderUri.fsPath, filePath)); } - return filePath; + return resolvedValue = filePath; case 'fileDirname': - return paths.dirname(filePath); + return resolvedValue = paths.dirname(filePath); case 'fileExtname': - return paths.extname(filePath); + return resolvedValue = paths.extname(filePath); case 'fileBasename': - return paths.basename(filePath); + return resolvedValue = paths.basename(filePath); case 'fileBasenameNoExtension': 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': const ep = this._context.getExecPath(); if (ep) { - return ep; + return resolvedValue = ep; } - return match; + return resolvedValue = match; default: - return match; + return resolvedValue = match; } } } }); + return { replaced, variableName, resolvedValue }; + } + + private resolveFromMap(match: string, argument: string, commandValueMapping: IStringDictionary, 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; } } diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index 05b1917ffe1..7bd82f944a8 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -14,6 +14,9 @@ import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { TestEnvironmentService, TestEditorService, TestContextService } from 'vs/workbench/test/workbenchTestServices'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; 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', () => { let configurationResolverService: IConfigurationResolverService; @@ -21,17 +24,19 @@ suite('Configuration Resolver Service', () => { let mockCommandService: MockCommandService; let editorService: TestEditorService; let workspace: IWorkspaceFolder; + let quickInputService: MockQuickInputService; setup(() => { mockCommandService = new MockCommandService(); editorService = new TestEditorService(); + quickInputService = new MockQuickInputService(); workspace = { uri: uri.parse('file:///VSCode/workspaceLocation'), name: 'hey', index: 0, 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(() => { @@ -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'); }); @@ -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'); }); @@ -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) { assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${workspaceFolder} ${env:key1} xyz'), 'abc foo \\VSCode\\workspaceLocation Value for key1 xyz'); } else { @@ -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) { 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 { @@ -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'); }); @@ -215,7 +220,7 @@ suite('Configuration Resolver Service', () => { 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 ${env:unknownVariable} xyz'), 'abc xyz'); }); @@ -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')); @@ -251,7 +256,7 @@ suite('Configuration Resolver Service', () => { 'outDir': null }; - return configurationResolverService.resolveWithCommands(undefined, configuration).then(result => { + return configurationResolverService.resolveWithInteractionReplace(undefined, configuration).then(result => { assert.deepEqual(result, { 'name': 'Attach to Process', @@ -280,7 +285,7 @@ suite('Configuration Resolver Service', () => { const commandVariables = Object.create(null); commandVariables['commandVariable1'] = 'command1'; - return configurationResolverService.resolveWithCommands(undefined, configuration, commandVariables).then(result => { + return configurationResolverService.resolveWithInteractionReplace(undefined, configuration, undefined, commandVariables).then(result => { assert.deepEqual(result, { 'name': 'Attach to Process', @@ -313,7 +318,7 @@ suite('Configuration Resolver Service', () => { const commandVariables = Object.create(null); commandVariables['commandVariable1'] = 'command1'; - return configurationResolverService.resolveWithCommands(undefined, configuration, commandVariables).then(result => { + return configurationResolverService.resolveWithInteractionReplace(undefined, configuration, undefined, commandVariables).then(result => { assert.deepEqual(result, { 'name': 'Attach to Process', @@ -344,7 +349,7 @@ suite('Configuration Resolver Service', () => { const commandVariables = Object.create(null); commandVariables['commandVariable1'] = 'command1'; - return configurationResolverService.resolveWithCommands(undefined, configuration, commandVariables).then(result => { + return configurationResolverService.resolveWithInteractionReplace(undefined, configuration, undefined, commandVariables).then(result => { assert.deepEqual(result, { 'name': 'Attach to Process', @@ -354,6 +359,87 @@ suite('Configuration Resolver Service', () => { '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); }); }); @@ -405,3 +491,87 @@ class MockCommandService implements ICommandService { return Promise.resolve(result); } } + +class MockQuickInputService implements IQuickInputService { + _serviceBrand: any; + + public pick(picks: Thenable[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: true }, token?: CancellationToken): Promise; + public pick(picks: Thenable[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: false }, token?: CancellationToken): Promise; + public pick(picks: Thenable[]> | QuickPickInput[], options?: Omit, 'canPickMany'>, token?: CancellationToken): Promise { + if (Types.isArray(picks)) { + return Promise.resolve({ label: 'selectedPick', description: 'pick description' }); + } else { + return Promise.resolve(undefined); + } + } + + public input(options?: IInputOptions, token?: CancellationToken): Promise { + return Promise.resolve('resolved' + options.prompt); + } + + backButton: IQuickInputButton; + + createQuickPick(): IQuickPick { + 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 { + throw new Error('not implemented.'); + } + + back(): Promise { + throw new Error('not implemented.'); + } + + cancel(): Promise { + 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; + } +} -- GitLab