diff --git a/src/vs/workbench/api/electron-browser/mainThreadTask.ts b/src/vs/workbench/api/electron-browser/mainThreadTask.ts index 467b59df491..2c21205fa1f 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTask.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTask.ts @@ -6,6 +6,9 @@ import { TPromise } from 'vs/base/common/winjs.base'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; + +import { ContributedTask, ExtensionTaskSourceTransfer } from 'vs/workbench/parts/tasks/common/tasks'; import { ITaskService } from 'vs/workbench/parts/tasks/common/taskService'; import { ExtHostContext, MainThreadTaskShape, ExtHostTaskShape, MainContext, IExtHostContext } from '../node/extHost.protocol'; @@ -19,7 +22,8 @@ export class MainThreadTask implements MainThreadTaskShape { constructor( extHostContext: IExtHostContext, - @ITaskService private _taskService: ITaskService + @ITaskService private _taskService: ITaskService, + @IWorkspaceContextService private _workspaceContextServer: IWorkspaceContextService ) { this._proxy = extHostContext.get(ExtHostContext.ExtHostTask); this._activeHandles = Object.create(null); @@ -35,7 +39,17 @@ export class MainThreadTask implements MainThreadTaskShape { public $registerTaskProvider(handle: number): TPromise { this._taskService.registerTaskProvider(handle, { provideTasks: () => { - return this._proxy.$provideTasks(handle); + return this._proxy.$provideTasks(handle).then((value) => { + for (let task of value.tasks) { + if (ContributedTask.is(task)) { + let uri = (task._source as any as ExtensionTaskSourceTransfer).__workspaceFolder; + if (uri) { + (task._source as any).workspaceFolder = this._workspaceContextServer.getWorkspaceFolder(uri); + } + } + } + return value; + }); } }); this._activeHandles[handle] = true; diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 8f1642cfd12..1b544a298fa 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -348,8 +348,12 @@ namespace Tasks { label: typeof task.source === 'string' ? task.source : extension.name, extension: extension.id, scope: scope, - workspaceFolder: workspaceFolder ? { uri: workspaceFolder.uri as URI } : undefined + workspaceFolder: undefined }; + // We can't transfer a workspace folder object from the extension host to main since they differ + // in shape and we don't have backwards converting function. So transfer the URI and resolve the + // workspace folder on the main side. + (source as any).__workspaceFolder = workspaceFolder ? workspaceFolder.uri as URI : undefined; let label = nls.localize('task.label', '{0}: {1}', source.label, task.name); let key = (task as types.Task).definitionKey; let kind = (task as types.Task).definition; diff --git a/src/vs/workbench/parts/debug/electron-browser/debugService.ts b/src/vs/workbench/parts/debug/electron-browser/debugService.ts index ee83179819b..140625d14f8 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugService.ts @@ -748,7 +748,7 @@ export class DebugService implements debug.IDebugService { return TPromise.wrapError(errors.create(message, { actions: [this.instantiationService.createInstance(debugactions.ConfigureAction, debugactions.ConfigureAction.ID, debugactions.ConfigureAction.LABEL), CloseAction] })); } - return this.runPreLaunchTask(resolvedConfig.preLaunchTask).then((taskSummary: ITaskSummary) => { + return this.runPreLaunchTask(root, resolvedConfig.preLaunchTask).then((taskSummary: ITaskSummary) => { const errorCount = resolvedConfig.preLaunchTask ? this.markerService.getStatistics().errors : 0; const successExitCode = taskSummary && taskSummary.exitCode === 0; const failureExitCode = taskSummary && taskSummary.exitCode !== undefined && taskSummary.exitCode !== 0; @@ -921,13 +921,13 @@ export class DebugService implements debug.IDebugService { }); } - private runPreLaunchTask(taskName: string): TPromise { + private runPreLaunchTask(root: WorkspaceFolder, taskName: string): TPromise { if (!taskName) { return TPromise.as(null); } // run a task before starting a debug session - return this.taskService.getTask(taskName).then(task => { + return this.taskService.getTask(root, taskName).then(task => { if (!task) { return TPromise.wrapError(errors.create(nls.localize('DebugTaskNotFound', "Could not find the preLaunchTask \'{0}\'.", taskName))); } diff --git a/src/vs/workbench/parts/tasks/browser/quickOpen.ts b/src/vs/workbench/parts/tasks/browser/quickOpen.ts index c544af15583..4d44c449dc6 100644 --- a/src/vs/workbench/parts/tasks/browser/quickOpen.ts +++ b/src/vs/workbench/parts/tasks/browser/quickOpen.ts @@ -5,7 +5,6 @@ 'use strict'; import * as nls from 'vs/nls'; -import * as Paths from 'vs/base/common/paths'; import * as Filters from 'vs/base/common/filters'; import { TPromise } from 'vs/base/common/winjs.base'; import { Action, IAction } from 'vs/base/common/actions'; @@ -38,7 +37,7 @@ export class TaskEntry extends Model.QuickOpenEntry { if (!workspaceFolder) { return null; } - return `(${Paths.basename(workspaceFolder.uri.fsPath)})`; + return `(${workspaceFolder.name})`; } public getAriaLabel(): string { @@ -115,12 +114,13 @@ export abstract class QuickOpenHandler extends Quickopen.QuickOpenHandler { } } } + const sorter = this.taskService.createSorter(); let hasRecentlyUsed: boolean = recent.length > 0; this.fillEntries(entries, input, recent, nls.localize('recentlyUsed', 'recently used tasks')); - configured = configured.sort((a, b) => a._label.localeCompare(b._label)); + configured = configured.sort((a, b) => sorter.compare(a, b)); let hasConfigured = configured.length > 0; this.fillEntries(entries, input, configured, nls.localize('configured', 'configured tasks'), hasRecentlyUsed); - detected = detected.sort((a, b) => a._label.localeCompare(b._label)); + detected = detected.sort((a, b) => sorter.compare(a, b)); this.fillEntries(entries, input, detected, nls.localize('detected', 'detected tasks'), hasRecentlyUsed || hasConfigured); return new Model.QuickOpenModel(entries, new ContributableActionProvider()); }); @@ -158,7 +158,7 @@ class CustomizeTaskAction extends Action { private static ID = 'workbench.action.tasks.customizeTask'; private static LABEL = nls.localize('customizeTask', "Configure Task"); - constructor(private taskService: ITaskService, private quickOpenService: IQuickOpenService, private task: CustomTask | ContributedTask) { + constructor(private taskService: ITaskService, private quickOpenService: IQuickOpenService) { super(CustomizeTaskAction.ID, CustomizeTaskAction.LABEL); this.updateClass(); } @@ -167,23 +167,36 @@ class CustomizeTaskAction extends Action { this.class = 'quick-open-task-configure'; } - public run(context: any): TPromise { - if (ContributedTask.is(this.task)) { - return this.taskService.customize(this.task, undefined, true).then(() => { + public run(element: any): TPromise { + let task = this.getTask(element); + if (ContributedTask.is(task)) { + return this.taskService.customize(task, undefined, true).then(() => { this.quickOpenService.close(); }); } else { - return this.taskService.openConfig(this.task).then(() => { + return this.taskService.openConfig(task).then(() => { this.quickOpenService.close(); }); } } + + private getTask(element: any): CustomTask | ContributedTask { + if (element instanceof TaskEntry) { + return element.task; + } else if (element instanceof TaskGroupEntry) { + return (element.getEntry() as TaskEntry).task; + } + return undefined; + } } export class QuickOpenActionContributor extends ActionBarContributor { + private action: CustomizeTaskAction; + constructor( @ITaskService private taskService: ITaskService, @IQuickOpenService private quickOpenService: IQuickOpenService) { super(); + this.action = new CustomizeTaskAction(taskService, quickOpenService); } public hasActions(context: any): boolean { @@ -196,7 +209,7 @@ export class QuickOpenActionContributor extends ActionBarContributor { let actions: Action[] = []; let task = this.getTask(context); if (task && ContributedTask.is(task) || CustomTask.is(task)) { - actions.push(new CustomizeTaskAction(this.taskService, this.quickOpenService, task)); + actions.push(this.action); } return actions; } diff --git a/src/vs/workbench/parts/tasks/common/taskService.ts b/src/vs/workbench/parts/tasks/common/taskService.ts index d56ad59ce58..3710927f7c4 100644 --- a/src/vs/workbench/parts/tasks/common/taskService.ts +++ b/src/vs/workbench/parts/tasks/common/taskService.ts @@ -9,7 +9,9 @@ import { Action } from 'vs/base/common/actions'; import { IEventEmitter } from 'vs/base/common/eventEmitter'; import { LinkedMap } from 'vs/base/common/map'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { Task, ContributedTask, CustomTask, TaskSet } from 'vs/workbench/parts/tasks/common/tasks'; + +import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { Task, ContributedTask, CustomTask, TaskSet, TaskSorter } from 'vs/workbench/parts/tasks/common/tasks'; import { ITaskSummary, TaskEvent, TaskType, TaskTerminateResponse } from 'vs/workbench/parts/tasks/common/taskSystem'; export { ITaskSummary, Task, TaskEvent, TaskType, TaskTerminateResponse }; @@ -55,9 +57,10 @@ export interface ITaskService extends IEventEmitter { /** * @param identifier The task's name, label or defined identifier. */ - getTask(identifier: string): TPromise; + getTask(workspaceFolder: WorkspaceFolder | string, identifier: string): TPromise; getTasksForGroup(group: string): TPromise; getRecentlyUsedTasks(): LinkedMap; + createSorter(): TaskSorter; hasMultipleFolders(); canCustomize(task: ContributedTask | CustomTask): boolean; diff --git a/src/vs/workbench/parts/tasks/common/taskSystem.ts b/src/vs/workbench/parts/tasks/common/taskSystem.ts index 98a11040d6a..11b78e506ef 100644 --- a/src/vs/workbench/parts/tasks/common/taskSystem.ts +++ b/src/vs/workbench/parts/tasks/common/taskSystem.ts @@ -8,6 +8,9 @@ import Severity from 'vs/base/common/severity'; import { TPromise } from 'vs/base/common/winjs.base'; import { TerminateResponse } from 'vs/base/common/processes'; import { IEventEmitter } from 'vs/base/common/eventEmitter'; + +import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; + import { Task } from './tasks'; export enum TaskErrors { @@ -101,7 +104,7 @@ export interface TaskEvent { } export interface ITaskResolver { - resolve(identifier: string): Task; + resolve(workspaceFolder: WorkspaceFolder, identifier: string): Task; } export interface TaskTerminateResponse extends TerminateResponse { diff --git a/src/vs/workbench/parts/tasks/common/tasks.ts b/src/vs/workbench/parts/tasks/common/tasks.ts index e84327faae9..2c4db2ba95e 100644 --- a/src/vs/workbench/parts/tasks/common/tasks.ts +++ b/src/vs/workbench/parts/tasks/common/tasks.ts @@ -11,6 +11,7 @@ import * as Objects from 'vs/base/common/objects'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ProblemMatcher } from 'vs/platform/markers/common/problemMatcher'; +import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; export interface ShellConfiguration { /** @@ -229,10 +230,6 @@ export namespace TaskSourceKind { export const Composite: 'composite' = 'composite'; } -export interface WorkspaceFolder { - uri: URI; -} - export interface TaskSourceConfigElement { workspaceFolder: WorkspaceFolder; file: string; @@ -255,6 +252,10 @@ export interface ExtensionTaskSource { readonly workspaceFolder: WorkspaceFolder | undefined; } +export interface ExtensionTaskSourceTransfer { + __workspaceFolder: URI; +} + export interface CompositeTaskSource { readonly kind: 'composite'; readonly label: string; @@ -267,6 +268,11 @@ export interface TaskIdentifier { type: string; } +export interface TaskDependency { + workspaceFolder: WorkspaceFolder; + task: string; +} + export interface ConfigurationProperties { /** @@ -307,7 +313,7 @@ export interface ConfigurationProperties { /** * The other tasks this task depends on. */ - dependsOn?: string[]; + dependsOn?: TaskDependency[]; /** * The problem watchers to use for this task @@ -455,6 +461,10 @@ export namespace Task { return 'unknown'; } } + + export function matches(task: Task, alias: string): boolean { + return alias === task._label || alias === task.identifier; + } } @@ -481,4 +491,37 @@ export interface TaskDefinition { taskType: string; required: string[]; properties: IJSONSchemaMap; +} + +export class TaskSorter { + + private _order: Map = new Map(); + + constructor(workspaceFolders: WorkspaceFolder[]) { + for (let i = 0; i < workspaceFolders.length; i++) { + this._order.set(workspaceFolders[i].uri.toString(), i); + } + } + + public compare(a: Task, b: Task): number { + let aw = Task.getWorkspaceFolder(a); + let bw = Task.getWorkspaceFolder(b); + if (aw && bw) { + let ai = this._order.get(aw.uri.toString()); + ai = ai === void 0 ? 0 : ai + 1; + let bi = this._order.get(bw.uri.toString()); + bi = bi === void 0 ? 0 : bi + 1; + if (ai === bi) { + return a._label.localeCompare(b._label); + } else { + return ai - bi; + } + } else if (!aw && bw) { + return -1; + } else if (aw && !bw) { + return +1; + } else { + return 0; + } + } } \ No newline at end of file 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 f916baad3d1..be6ed3b73b8 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts @@ -52,7 +52,6 @@ import { IWindowService } from 'vs/platform/windows/common/windows'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; - import jsonContributionRegistry = require('vs/platform/jsonschemas/common/jsonContributionRegistry'); import { IJSONSchema } from 'vs/base/common/jsonSchema'; @@ -67,7 +66,7 @@ import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IConfigurationEditingService, ConfigurationTarget, IConfigurationValue } from 'vs/workbench/services/configuration/common/configurationEditing'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, WorkbenchState, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IOutputService, IOutputChannelRegistry, Extensions as OutputExt, IOutputChannel } from 'vs/workbench/parts/output/common/output'; @@ -76,7 +75,7 @@ import { Scope, IActionBarRegistry, Extensions as ActionBarExtensions } from 'vs import { ITerminalService } from 'vs/workbench/parts/terminal/common/terminal'; import { ITaskSystem, ITaskResolver, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, TaskSystemEvents, TaskTerminateResponse } from 'vs/workbench/parts/tasks/common/taskSystem'; -import { Task, CustomTask, ConfiguringTask, ContributedTask, CompositeTask, TaskSet, TaskGroup, ExecutionEngine, JsonSchemaVersion, TaskSourceKind, TaskIdentifier, WorkspaceFolder } from 'vs/workbench/parts/tasks/common/tasks'; +import { Task, CustomTask, ConfiguringTask, ContributedTask, CompositeTask, TaskSet, TaskGroup, ExecutionEngine, JsonSchemaVersion, TaskSourceKind, TaskIdentifier, TaskSorter } from 'vs/workbench/parts/tasks/common/tasks'; import { ITaskService, TaskServiceEvents, ITaskProvider, TaskEvent, RunOptions, CustomizationProperties } from 'vs/workbench/parts/tasks/common/taskService'; import { templates as taskTemplates } from 'vs/workbench/parts/tasks/common/taskTemplates'; @@ -632,6 +631,45 @@ interface TaskCustomizationTelementryEvent { properties: string[]; } +class TaskMap { + private _store: Map = new Map(); + + constructor() { + } + + public forEach(callback: (value: Task[], folder: string) => void): void { + this._store.forEach(callback); + } + + public get(workspaceFolder: WorkspaceFolder | string): Task[] { + let result: Task[] = Types.isString(workspaceFolder) ? this._store.get(workspaceFolder) : this._store.get(workspaceFolder.uri.toString()); + if (!result) { + result = []; + Types.isString(workspaceFolder) ? this._store.set(workspaceFolder, result) : this._store.set(workspaceFolder.uri.toString(), result); + } + return result; + } + + public has(workspaceFolder: WorkspaceFolder): boolean { + return this._store.has(workspaceFolder.uri.toString()); + } + + public add(workspaceFolder: WorkspaceFolder | string, ...task: Task[]): void { + let values = Types.isString(workspaceFolder) ? this._store.get(workspaceFolder) : this._store.get(workspaceFolder.uri.toString()); + if (!values) { + values = []; + Types.isString(workspaceFolder) ? this._store.set(workspaceFolder, values) : this._store.set(workspaceFolder.uri.toString(), values); + } + values.push(...task); + } + + public all(): Task[] { + let result: Task[] = []; + this._store.forEach((values) => result.push(...values)); + return result; + } +} + class TaskService extends EventEmitter implements ITaskService { // private static autoDetectTelemetryName: string = 'taskServer.autoDetect'; @@ -822,17 +860,29 @@ class TaskService extends EventEmitter implements ITaskService { return this._providers.delete(handle); } - public getTask(identifier: string): TPromise { - return this.getAllTasks().then((tasks) => { - let resolver = this.createResolver(tasks); - return resolver.resolve(identifier); + public getTask(folder: WorkspaceFolder | string, alias: string): TPromise { + return this.getGroupedTasks().then((map) => { + let values = map.get(folder); + if (!values) { + return undefined; + } + for (let task of values) { + if (Task.matches(task, alias)) { + return task; + } + }; + return undefined; }); } public tasks(): TPromise { - return this.getAllTasks(); + return this.getGroupedTasks().then(result => result.all()); }; + public createSorter(): TaskSorter { + return new TaskSorter(this.contextService.getWorkspace() ? this.contextService.getWorkspace().folders : []); + } + public isActive(): TPromise { if (!this._taskSystem) { return TPromise.as(false); @@ -884,7 +934,7 @@ class TaskService extends EventEmitter implements ITaskService { } public build(): TPromise { - return this.getAllTasks().then((tasks) => { + return this.getGroupedTasks().then((tasks) => { let runnable = this.createRunnableTask(tasks, TaskGroup.Build); if (!runnable || !runnable.task) { if (this._schemaVersion === JsonSchemaVersion.V0_1_0) { @@ -909,7 +959,7 @@ class TaskService extends EventEmitter implements ITaskService { } public runTest(): TPromise { - return this.getAllTasks().then((tasks) => { + return this.getGroupedTasks().then((tasks) => { let runnable = this.createRunnableTask(tasks, TaskGroup.Test); if (!runnable || !runnable.task) { if (this._schemaVersion === JsonSchemaVersion.V0_1_0) { @@ -925,23 +975,14 @@ class TaskService extends EventEmitter implements ITaskService { }); } - public run(task: string | Task, options?: RunOptions): TPromise { - return this.getAllTasks().then((tasks) => { - let resolver = this.createResolver(tasks); - let requested: string; - let toExecute: Task; - if (Types.isString(task)) { - requested = task; - toExecute = resolver.resolve(task); + public run(task: Task, options?: RunOptions): TPromise { + return this.getGroupedTasks().then((grouped) => { + if (!task) { + throw new TaskError(Severity.Info, nls.localize('TaskServer.noTask', 'Requested task {0} to execute not found.', task.name), TaskErrors.TaskNotFound); } else { - requested = task.name; - toExecute = task; - } - if (!toExecute) { - throw new TaskError(Severity.Info, nls.localize('TaskServer.noTask', 'Requested task {0} to execute not found.', requested), TaskErrors.TaskNotFound); - } else { - if (options && options.attachProblemMatcher && this.shouldAttachProblemMatcher(toExecute) && !CompositeTask.is(toExecute)) { - return this.attachProblemMatcher(toExecute).then((toExecute) => { + let resolver = this.createResolver(grouped); + if (options && options.attachProblemMatcher && this.shouldAttachProblemMatcher(task) && !CompositeTask.is(task)) { + return this.attachProblemMatcher(task).then((toExecute) => { if (toExecute) { return this.executeTask(toExecute, resolver); } else { @@ -949,7 +990,7 @@ class TaskService extends EventEmitter implements ITaskService { } }); } - return this.executeTask(toExecute, resolver); + return this.executeTask(task, resolver); } }).then(value => value, (error) => { this.handleError(error); @@ -1036,13 +1077,15 @@ class TaskService extends EventEmitter implements ITaskService { } public getTasksForGroup(group: string): TPromise { - return this.getAllTasks().then((tasks) => { + return this.getGroupedTasks().then((groups) => { let result: Task[] = []; - for (let task of tasks) { - if (task.group === group) { - result.push(task); + groups.forEach((tasks) => { + for (let task of tasks) { + if (task.group === group) { + result.push(task); + } } - } + }); return result; }); } @@ -1123,7 +1166,7 @@ class TaskService extends EventEmitter implements ITaskService { if (editorConfig.editor.insertSpaces) { content = content.replace(/(\n)(\t+)/g, (_, s1, s2) => s1 + strings.repeat(' ', s2.length * editorConfig.editor.tabSize)); } - promise = this.fileService.createFile(this.contextService.toResource('.vscode/tasks.json', this.contextService.getWorkspace().folders[0]), content).then(() => { }); // TODO@Dirk (https://github.com/Microsoft/vscode/issues/29454) + promise = this.fileService.createFile(this.contextService.toResource('.vscode/tasks.json', workspaceFolder), content).then(() => { }); } else { let value: IConfigurationValue = { key: undefined, value: undefined }; // We have a global task configuration @@ -1162,7 +1205,7 @@ class TaskService extends EventEmitter implements ITaskService { }; this.telemetryService.publicLog(TaskService.CustomizationTelemetryEventName, event); if (openConfig) { - let resource = this.contextService.toResource('.vscode/tasks.json', this.contextService.getWorkspace().folders[0]); // TODO@Dirk (https://github.com/Microsoft/vscode/issues/29454) + let resource = this.contextService.toResource('.vscode/tasks.json', workspaceFolder); this.editorService.openEditor({ resource: resource, options: { @@ -1185,8 +1228,7 @@ class TaskService extends EventEmitter implements ITaskService { } public openConfig(task: CustomTask): TPromise { - // @ToDo need to adopt since this is not working anymore - let resource = this.contextService.toResource(task._source.config.file, this.contextService.getWorkspace().folders[0]); + let resource = this.contextService.toResource(task._source.config.file, Task.getWorkspaceFolder(task)); return this.editorService.openEditor({ resource: resource, options: { @@ -1196,28 +1238,46 @@ class TaskService extends EventEmitter implements ITaskService { }, false).then(() => undefined); } - private createRunnableTask(tasks: Task[], group: TaskGroup): { task: Task; resolver: ITaskResolver } { - let idMap: IStringDictionary = Object.create(null); - let labelMap: IStringDictionary = Object.create(null); - let identifierMap: IStringDictionary = Object.create(null); + private createRunnableTask(tasks: TaskMap, group: TaskGroup): { task: Task; resolver: ITaskResolver } { + interface ResolverData { + id: Map; + label: Map; + identifier: Map; + } + let resolverData: Map = new Map(); let workspaceTasks: Task[] = []; let extensionTasks: Task[] = []; - tasks.forEach((task) => { - idMap[task._id] = task; - labelMap[task._label] = task; - identifierMap[task.identifier] = task; - if (group && task.group === group) { - if (task._source.kind === TaskSourceKind.Workspace) { - workspaceTasks.push(task); - } else { - extensionTasks.push(task); + tasks.forEach((tasks, folder) => { + let data = resolverData.get(folder); + if (!data) { + data = { + id: new Map(), + label: new Map(), + identifier: new Map() + }; + resolverData.set(folder, data); + } + for (let task of tasks) { + data.id.set(task._id, task); + data.label.set(task._label, task); + data.identifier.set(task.identifier, task); + if (group && task.group === group) { + if (task._source.kind === TaskSourceKind.Workspace) { + workspaceTasks.push(task); + } else { + extensionTasks.push(task); + } } } }); let resolver: ITaskResolver = { - resolve: (id: string) => { - return idMap[id] || labelMap[id] || identifierMap[id]; + resolve: (workspaceFolder: WorkspaceFolder, alias: string) => { + let data = resolverData.get(workspaceFolder.uri.toString()); + if (!data) { + return undefined; + } + return data.id.get(alias) || data.label.get(alias) || data.identifier.get(alias); } }; if (workspaceTasks.length > 0) { @@ -1243,23 +1303,37 @@ class TaskService extends EventEmitter implements ITaskService { type: 'composite', name: id, identifier: id, - dependsOn: extensionTasks.map(task => task._id) + dependsOn: extensionTasks.map((task) => { return { workspaceFolder: Task.getWorkspaceFolder(task), task: task._id }; }) }; return { task, resolver }; } } - private createResolver(tasks: Task[]): ITaskResolver { - let labelMap: IStringDictionary = Object.create(null); - let identifierMap: IStringDictionary = Object.create(null); + private createResolver(grouped: TaskMap): ITaskResolver { + interface ResolverData { + label: Map; + identifier: Map; + } - tasks.forEach((task) => { - labelMap[task._label] = task; - identifierMap[task.identifier] = task; + let resolverData: Map = new Map(); + grouped.forEach((tasks, folder) => { + let data = resolverData.get(folder); + if (!data) { + data = { label: new Map(), identifier: new Map() }; + resolverData.set(folder, data); + } + for (let task of tasks) { + data.label.set(task._label, task); + data.identifier.set(task.identifier, task); + } }); return { - resolve: (id: string) => { - return labelMap[id] || identifierMap[id]; + resolve: (workspaceFolder: WorkspaceFolder, alias: string) => { + let data = resolverData.get(workspaceFolder.uri.toString()); + if (!data) { + return undefined; + } + return data.label.get(alias) || data.identifier.get(alias); } }; } @@ -1289,7 +1363,7 @@ class TaskService extends EventEmitter implements ITaskService { }); } - public restart(task: string | Task): void { + public restart(task: Task): void { if (!this._taskSystem) { return; } @@ -1346,7 +1420,7 @@ class TaskService extends EventEmitter implements ITaskService { return this._taskSystem; } - private getAllTasks(): TPromise { + private getGroupedTasks(): TPromise { return this.extensionService.activateByEvent('onCommand:workbench.action.tasks.runTask').then(() => { return new TPromise((resolve, reject) => { let result: TaskSet[] = []; @@ -1374,24 +1448,13 @@ class TaskService extends EventEmitter implements ITaskService { } }); }).then((contributedTaskSets) => { - let result: Task[] = []; - let contributedTasks: Map = new Map(); + let result: TaskMap = new TaskMap(); + let contributedTasks: TaskMap = new TaskMap(); for (let set of contributedTaskSets) { for (let task of set.tasks) { - if (!ContributedTask.is(task)) { - continue; - } - let workspaceFolder = task._source.workspaceFolder; + let workspaceFolder = Task.getWorkspaceFolder(task); if (workspaceFolder) { - let values = contributedTasks.get(workspaceFolder.uri.toString()); - if (!values) { - values = [task]; - contributedTasks.set(workspaceFolder.uri.toString(), values); - } else { - values.push(task); - } - } else { - result.push(task); + contributedTasks.add(workspaceFolder, task); } } } @@ -1400,13 +1463,13 @@ class TaskService extends EventEmitter implements ITaskService { let contributed = contributedTasks.get(key); if (!folderTasks.set) { if (contributed) { - result.push(...contributed); + result.add(key, ...contributed); } return; } if (!contributed) { - result.push(...folderTasks.set.tasks); + result.add(key, ...folderTasks.set.tasks); } else { let configurations = folderTasks.configurations; let legacyTaskConfigurations = folderTasks.set ? this.getLegacyTaskConfigurations(folderTasks.set) : undefined; @@ -1419,20 +1482,20 @@ class TaskService extends EventEmitter implements ITaskService { if (configurations) { let configuringTask = configurations.byIdentifier[task.defines._key]; if (configuringTask) { - result.push(TaskConfig.createCustomTask(task, configuringTask)); + result.add(key, TaskConfig.createCustomTask(task, configuringTask)); } else { - result.push(task); + result.add(key, task); } } else if (legacyTaskConfigurations) { let configuringTask = legacyTaskConfigurations[task.defines._key]; if (configuringTask) { - result.push(TaskConfig.createCustomTask(task, configuringTask)); + result.add(key, TaskConfig.createCustomTask(task, configuringTask)); customTasksToDelete.push(configuringTask); } else { - result.push(task); + result.add(key, task); } } else { - result.push(task); + result.add(key, task); } } if (customTasksToDelete.length > 0) { @@ -1444,23 +1507,25 @@ class TaskService extends EventEmitter implements ITaskService { if (toDelete[task._id]) { continue; } - result.push(task); + result.add(key, task); } } else { - result.push(...folderTasks.set.tasks); + result.add(key, ...folderTasks.set.tasks); } } else { - result.push(...folderTasks.set.tasks); - result.push(...contributed); + result.add(key, ...folderTasks.set.tasks); + result.add(key, ...contributed); } } }); return result; }, () => { // If we can't read the tasks.json file provide at least the contributed tasks - let result: Task[] = []; + let result: TaskMap = new TaskMap(); for (let set of contributedTaskSets) { - result.push(...set.tasks); + for (let task of set.tasks) { + result.add(Task.getWorkspaceFolder(task), task); + } } return result; }); @@ -1619,20 +1684,19 @@ class TaskService extends EventEmitter implements ITaskService { let schemaVersion = JsonSchemaVersion.V2_0_0; if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { - let workspaceFolder: WorkspaceFolder = { uri: this.contextService.getWorkspace().folders[0].uri }; + let workspaceFolder: WorkspaceFolder = this.contextService.getWorkspace().folders[0]; workspaceFolders.push(workspaceFolder); executionEngine = this.computeExecutionEngine(workspaceFolder); schemaVersion = this.computeJsonSchemaVersion(workspaceFolder); } else if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { - for (let folder of this.contextService.getWorkspace().folders) { - let workspaceFolder = { uri: folder.uri }; + for (let workspaceFolder of this.contextService.getWorkspace().folders) { if (schemaVersion === this.computeJsonSchemaVersion(workspaceFolder)) { workspaceFolders.push(workspaceFolder); } else { this._outputChannel.append(nls.localize( 'taskService.ignoreingFolder', 'Ignoring task configurations for workspace folder {0}. Multi root folder support requires that all folders use task version 2.0.', - folder.uri.fsPath)); + workspaceFolder.uri.fsPath)); } } } @@ -1825,9 +1889,16 @@ class TaskService extends EventEmitter implements ITaskService { interface TaskQickPickEntry extends IPickOpenEntry { task: Task; } - function TaskQickPickEntry(task: Task): TaskQickPickEntry { - return { label: task._label, task }; - } + const TaskQickPickEntry = (task: Task): TaskQickPickEntry => { + let description: string; + if (this.hasMultipleFolders) { + let workspaceFolder = Task.getWorkspaceFolder(task); + if (workspaceFolder) { + description = `(${workspaceFolder.name})`; + } + } + return { label: task._label, description, task }; + }; function fillEntries(entries: TaskQickPickEntry[], tasks: Task[], groupLabel: string, withBorder: boolean = false): void { let first = true; for (let task of tasks) { @@ -1868,12 +1939,13 @@ class TaskService extends EventEmitter implements ITaskService { } } } + const sorter = this.createSorter(); let hasRecentlyUsed: boolean = recent.length > 0; fillEntries(entries, recent, nls.localize('recentlyUsed', 'recently used tasks')); - configured = configured.sort((a, b) => a._label.localeCompare(b._label)); + configured = configured.sort((a, b) => sorter.compare(a, b)); let hasConfigured = configured.length > 0; fillEntries(entries, configured, nls.localize('configured', 'configured tasks'), hasRecentlyUsed); - detected = detected.sort((a, b) => a._label.localeCompare(b._label)); + detected = detected.sort((a, b) => sorter.compare(a, b)); fillEntries(entries, detected, nls.localize('detected', 'detected tasks'), hasRecentlyUsed || hasConfigured); } } else { @@ -1890,12 +1962,17 @@ class TaskService extends EventEmitter implements ITaskService { return; } if (Types.isString(arg)) { - this.getTask(arg).then((task) => { - if (task) { - this.run(task); - } else { - this.quickOpenService.show('task '); + this.getGroupedTasks().then((grouped) => { + let resolver = this.createResolver(grouped); + let folders = this.contextService.getWorkspace().folders; + for (let folder of folders) { + let task = resolver.resolve(folder, arg); + if (task) { + this.run(task); + return; + } } + this.quickOpenService.show('task '); }, () => { this.quickOpenService.show('task '); }); diff --git a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts index d364f37913e..1176ca1c7af 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts @@ -245,8 +245,8 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem { private executeTask(startedTasks: IStringDictionary>, task: Task, resolver: ITaskResolver, trigger: string): TPromise { let promises: TPromise[] = []; if (task.dependsOn) { - task.dependsOn.forEach((identifier) => { - let task = resolver.resolve(identifier); + task.dependsOn.forEach((dependency) => { + let task = resolver.resolve(dependency.workspaceFolder, dependency.task); if (task) { let promise = startedTasks[task._id]; if (!promise) { @@ -254,6 +254,9 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem { startedTasks[task._id] = promise; } promises.push(promise); + } else { + this.log(nls.localize('dependencyFailed', 'Couldn\'t resolve dependent task \'{0}\' in workspace folder \'{1}\'', dependency.task, dependency.workspaceFolder.name)); + this.showOutput(); } }); } diff --git a/src/vs/workbench/parts/tasks/node/taskConfiguration.ts b/src/vs/workbench/parts/tasks/node/taskConfiguration.ts index 5cd4a0cd146..5720bd9573e 100644 --- a/src/vs/workbench/parts/tasks/node/taskConfiguration.ts +++ b/src/vs/workbench/parts/tasks/node/taskConfiguration.ts @@ -20,6 +20,8 @@ import { isNamedProblemMatcher, ProblemMatcherRegistry } from 'vs/platform/markers/common/problemMatcher'; +import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; + import * as Tasks from '../common/tasks'; import { TaskDefinitionRegistry } from '../common/taskDefinitionRegistry'; @@ -561,7 +563,7 @@ function _freeze(this: void, target: T, properties: MetaData[]): Read } interface ParseContext { - workspaceFolder: Tasks.WorkspaceFolder; + workspaceFolder: WorkspaceFolder; problemReporter: IProblemReporter; namedProblemMatchers: IStringDictionary; uuidMap: UUIDMap; @@ -1088,9 +1090,9 @@ namespace ConfigurationProperties { } if (external.dependsOn !== void 0) { if (Types.isString(external.dependsOn)) { - result.dependsOn = [external.dependsOn]; + result.dependsOn = [{ workspaceFolder: context.workspaceFolder, task: external.dependsOn }]; } else if (Types.isStringArray(external.dependsOn)) { - result.dependsOn = external.dependsOn.slice(); + result.dependsOn = external.dependsOn.map((task) => { return { workspaceFolder: context.workspaceFolder, task: task }; }); } } if (includePresentation && (external.presentation !== void 0 || (external as LegacyCommandProperties).terminal !== void 0)) { @@ -1680,11 +1682,11 @@ class UUIDMap { class ConfigurationParser { - private workspaceFolder: Tasks.WorkspaceFolder; + private workspaceFolder: WorkspaceFolder; private problemReporter: IProblemReporter; private uuidMap: UUIDMap; - constructor(workspaceFolder: Tasks.WorkspaceFolder, problemReporter: IProblemReporter, uuidMap: UUIDMap) { + constructor(workspaceFolder: WorkspaceFolder, problemReporter: IProblemReporter, uuidMap: UUIDMap) { this.workspaceFolder = workspaceFolder; this.problemReporter = problemReporter; this.uuidMap = uuidMap; @@ -1789,7 +1791,7 @@ class ConfigurationParser { } let uuidMaps: Map = new Map(); -export function parse(workspaceFolder: Tasks.WorkspaceFolder, configuration: ExternalTaskRunnerConfiguration, logger: IProblemReporter): ParseResult { +export function parse(workspaceFolder: WorkspaceFolder, configuration: ExternalTaskRunnerConfiguration, logger: IProblemReporter): ParseResult { let uuidMap = uuidMaps.get(workspaceFolder.uri.toString()); if (!uuidMap) { uuidMap = new UUIDMap(); diff --git a/src/vs/workbench/parts/tasks/test/electron-browser/configuration.test.ts b/src/vs/workbench/parts/tasks/test/electron-browser/configuration.test.ts index faa2a392a7c..715c3e1c11a 100644 --- a/src/vs/workbench/parts/tasks/test/electron-browser/configuration.test.ts +++ b/src/vs/workbench/parts/tasks/test/electron-browser/configuration.test.ts @@ -12,10 +12,18 @@ import * as UUID from 'vs/base/common/uuid'; import * as Platform from 'vs/base/common/platform'; import { ValidationStatus } from 'vs/base/common/parsers'; import { ProblemMatcher, FileLocationKind, ProblemPattern, ApplyToKind } from 'vs/platform/markers/common/problemMatcher'; +import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import * as Tasks from 'vs/workbench/parts/tasks/common/tasks'; import { parse, ParseResult, IProblemReporter, ExternalTaskRunnerConfiguration, CustomTask } from 'vs/workbench/parts/tasks/node/taskConfiguration'; +const workspaceFolder: WorkspaceFolder = { + uri: URI.file('/workspace/folderOne'), + name: 'folderOne', + index: 0, + raw: { path: '../folderOne', name: 'folderOne' } +}; + class ProblemReporter implements IProblemReporter { private _validationStatus: ValidationStatus = new ValidationStatus(); @@ -178,7 +186,7 @@ class CustomTaskBuilder { this.commandBuilder = new CommandConfigurationBuilder(this, command); this.result = { _id: name, - _source: { kind: Tasks.TaskSourceKind.Workspace, label: 'workspace', config: { workspaceFolder: { uri: undefined }, element: undefined, index: -1, file: '.vscode/tasks.json' } }, + _source: { kind: Tasks.TaskSourceKind.Workspace, label: 'workspace', config: { workspaceFolder: workspaceFolder, element: undefined, index: -1, file: '.vscode/tasks.json' } }, _label: name, type: 'custom', identifier: name, @@ -348,7 +356,7 @@ class PatternBuilder { function testDefaultProblemMatcher(external: ExternalTaskRunnerConfiguration, resolved: number) { let reporter = new ProblemReporter(); - let result = parse({ uri: URI.file('/Workspace/folderOne') }, external, reporter); + let result = parse(workspaceFolder, external, reporter); assert.ok(!reporter.receivedMessage); assert.strictEqual(result.custom.length, 1); let task = result.custom[0]; @@ -359,7 +367,7 @@ function testDefaultProblemMatcher(external: ExternalTaskRunnerConfiguration, re function testConfiguration(external: ExternalTaskRunnerConfiguration, builder: ConfiguationBuilder): void { builder.done(); let reporter = new ProblemReporter(); - let result = parse({ uri: URI.file('/Workspace/folderOne') }, external, reporter); + let result = parse(workspaceFolder, external, reporter); if (reporter.receivedMessage) { assert.ok(false, reporter.lastMessage); }