Skip to content

Commit

Permalink
Input variables in tasks (#63910)
Browse files Browse the repository at this point in the history
Fixes #4758
  • Loading branch information
alexr00 authored Dec 3, 2018
1 parent 253a4f6 commit f9703e9
Show file tree
Hide file tree
Showing 13 changed files with 653 additions and 151 deletions.
10 changes: 10 additions & 0 deletions src/vs/base/common/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,13 @@ export function groupBy<T>(data: T[], groupFn: (element: T) => string): IStringD
}
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;
}
49 changes: 36 additions & 13 deletions src/vs/workbench/api/electron-browser/mainThreadTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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<string, string>()
};
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<string>();
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<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);
});
});
});
}
});
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/parts/debug/node/debugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,10 @@ export class Debugger implements IDebugger {
substituteVariables(folder: IWorkspaceFolder, config: IConfig): Thenable<IConfig> {
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);
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/vs/workbench/parts/tasks/browser/quickOpen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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'
Expand Down
28 changes: 21 additions & 7 deletions src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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
});
}
});
});
Expand Down Expand Up @@ -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);
Expand All @@ -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
});
});
});
});
Expand All @@ -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);
Expand All @@ -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
});
});
});
});
Expand Down
59 changes: 34 additions & 25 deletions src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -68,7 +69,6 @@ class VariableResolver {
}
}


export class VerifiedTask {
readonly task: Task;
readonly resolver: ITaskResolver;
Expand Down Expand Up @@ -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<ShellQuotingOptions> = {
'cmd': {
Expand Down Expand Up @@ -390,30 +390,35 @@ export class TerminalTaskSystem implements ITaskSystem {
}
return Promise.resolve(resolved);
});
return resolvedVariables;
} else {
let result = new Map<string, string>();
variables.forEach(variable => {
result.set(variable, this.configurationResolverService.resolve(workspaceFolder, variable));
let variablesArray = new Array<string>();
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<ITaskSummary> {
Expand All @@ -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);
});
}

Expand All @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions src/vs/workbench/parts/tasks/node/taskConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -451,6 +452,11 @@ export interface BaseTaskRunnerConfiguration {
* Problem matcher declarations
*/
declares?: ProblemMatcherConfig.NamedProblemMatcher[];

/**
* Optional user input varaibles.
*/
inputs?: ConfiguredInput[];
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,31 @@ export interface IConfigurationResolverService {
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.
* 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<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[];
}
Loading

0 comments on commit f9703e9

Please sign in to comment.