diff --git a/packages/debug/src/browser/debug-session-manager.ts b/packages/debug/src/browser/debug-session-manager.ts index cef3a59a4b22e..cb50884ebb312 100644 --- a/packages/debug/src/browser/debug-session-manager.ts +++ b/packages/debug/src/browser/debug-session-manager.ts @@ -478,7 +478,7 @@ export class DebugSessionManager { return true; } - const taskInfo = await this.taskService.runWorkspaceTask(this.taskService.startUserAction(), workspaceFolderUri, taskName); + const taskInfo = await this.taskService.runWorkspaceTask(await this.taskService.startUserAction(), workspaceFolderUri, taskName); if (!checkErrors) { return true; } diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 55d5960bb9657..f3f2a88721689 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -1793,10 +1793,11 @@ export const MAIN_RPC_CONTEXT = { }; export interface TasksExt { + $onDidStartUserInteraction(): Promise; $provideTasks(handle: number): Promise; $resolveTask(handle: number, task: TaskDto, token?: CancellationToken): Promise; $onDidStartTask(execution: TaskExecutionDto, terminalId: number): void; - $onDidEndTask(id: number): void; + $onDidEndTask(execution: TaskExecutionDto): void; $onDidStartTaskProcess(processId: number | undefined, execution: TaskExecutionDto): void; $onDidEndTaskProcess(exitCode: number | undefined, taskId: number): void; } diff --git a/packages/plugin-ext/src/main/browser/tasks-main.ts b/packages/plugin-ext/src/main/browser/tasks-main.ts index 3f01afbc92ed2..5ac42c7cc7e89 100644 --- a/packages/plugin-ext/src/main/browser/tasks-main.ts +++ b/packages/plugin-ext/src/main/browser/tasks-main.ts @@ -30,6 +30,7 @@ import { TaskInfo, TaskExitedEvent, TaskConfiguration, TaskCustomization, TaskOu import { TaskWatcher } from '@theia/task/lib/common/task-watcher'; import { TaskService } from '@theia/task/lib/browser/task-service'; import { TaskDefinitionRegistry } from '@theia/task/lib/browser'; +import { ProvidedTaskConfigurations, TaskStartUserInteractionEvent } from '@theia/task/lib/browser/provided-task-configurations'; const revealKindMap = new Map( [ @@ -72,6 +73,11 @@ export class TasksMainImpl implements TasksMain, Disposable { this.taskService = container.get(TaskService); this.taskDefinitionRegistry = container.get(TaskDefinitionRegistry); + this.toDispose.push(container.get(ProvidedTaskConfigurations).onDidStartUserInteraction((event: TaskStartUserInteractionEvent) => { + // we must wait with further processing until the plugin side has had time to clean up its garbage + event.waitUntil(this.proxy.$onDidStartUserInteraction()); + })); + this.toDispose.push(this.taskWatcher.onTaskCreated((event: TaskInfo) => { this.proxy.$onDidStartTask({ id: event.taskId, @@ -80,7 +86,10 @@ export class TasksMainImpl implements TasksMain, Disposable { })); this.toDispose.push(this.taskWatcher.onTaskExit((event: TaskExitedEvent) => { - this.proxy.$onDidEndTask(event.taskId); + this.proxy.$onDidEndTask({ + id: event.taskId, + task: this.fromTaskConfiguration(event.config) + }); })); this.toDispose.push(this.taskWatcher.onDidStartTaskProcess((event: TaskInfo) => { @@ -128,7 +137,7 @@ export class TasksMainImpl implements TasksMain, Disposable { return []; } - const token: number = this.taskService.startUserAction(); + const token: number = await this.taskService.startUserAction(); const [configured, provided] = await Promise.all([ this.taskService.getConfiguredTasks(token), this.taskService.getProvidedTasks(token) diff --git a/packages/plugin-ext/src/plugin/tasks/tasks.ts b/packages/plugin-ext/src/plugin/tasks/tasks.ts index b2d74ac735cfd..b268def3b47cb 100644 --- a/packages/plugin-ext/src/plugin/tasks/tasks.ts +++ b/packages/plugin-ext/src/plugin/tasks/tasks.ts @@ -25,23 +25,20 @@ import * as theia from '@theia/plugin'; import * as converter from '../type-converters'; import { CustomExecution, Disposable } from '../types-impl'; import { RPCProtocol, ConnectionClosedError } from '../../common/rpc-protocol'; -import { TaskProviderAdapter } from './task-provider'; import { Emitter, Event } from '@theia/core/lib/common/event'; import { TerminalServiceExtImpl } from '../terminal-ext'; import { UUID } from '@theia/core/shared/@phosphor/coreutils'; import { CancellationToken } from '@theia/core/lib/common/cancellation'; - -type ExecutionCallback = (resolvedDefinition: theia.TaskDefinition) => Thenable; export class TasksExtImpl implements TasksExt { private proxy: TasksMain; private callId = 0; - private adaptersMap = new Map(); + private providersByHandle = new Map(); private executions = new Map(); protected callbackIdBase: string = UUID.uuid4(); - protected callbackId: number; - protected customExecutionIds: Map = new Map(); - protected customExecutionFunctions: Map = new Map(); + protected callbackId: number = 0; + protected providedCustomExecutions: Map = new Map(); + protected oneOffCustomExecutions: Map = new Map(); protected lastStartedTask: number | undefined; private readonly onDidExecuteTask: Emitter = new Emitter(); @@ -68,11 +65,37 @@ export class TasksExtImpl implements TasksExt { return this.onDidExecuteTask.event; } + async $onDidStartUserInteraction(): Promise { + console.info(`$onDidStartUserInteraction: clearing ${this.providedCustomExecutions.size} custom executions`); + this.providedCustomExecutions.clear(); + return Promise.resolve(); + } + + private getCustomExecution(id: string | undefined): theia.CustomExecution | undefined { + if (!id) { + return undefined; + } + return this.providedCustomExecutions.get(id) ?? this.oneOffCustomExecutions.get(id); + } + + private addProvidedCustomExecution(execution: theia.CustomExecution): string { + const id = this.nextCallbackId(); + this.providedCustomExecutions.set(id, execution); + return id; + } + + private addOneOffCustomExecution(execution: theia.CustomExecution): string { + const id = this.nextCallbackId(); + this.oneOffCustomExecutions.set(id, execution); + return id; + } + async $onDidStartTask(execution: TaskExecutionDto, terminalId: number): Promise { - const customExecution = this.customExecutionFunctions.get(execution.task.executionId || ''); + const customExecution = this.getCustomExecution(execution.task.executionId); if (customExecution) { + console.info(`running custom execution with id ${execution.task.executionId}`); const taskDefinition = converter.toTask(execution.task).definition; - const pty = await customExecution(taskDefinition); + const pty = await customExecution.callback(taskDefinition); this.terminalExt.attachPtyToTerminal(terminalId, pty); if (pty.onDidClose) { const disposable = pty.onDidClose((e: number | void = undefined) => { @@ -93,13 +116,19 @@ export class TasksExtImpl implements TasksExt { return this.onDidTerminateTask.event; } - $onDidEndTask(id: number): void { - const taskExecution = this.executions.get(id); + $onDidEndTask(executionDto: TaskExecutionDto): void { + const taskExecution = this.executions.get(executionDto.id); if (!taskExecution) { - throw new Error(`Task execution with id ${id} is not found`); + throw new Error(`Task execution with id ${executionDto.id} is not found`); + } + + if (executionDto.task.executionId) { + if (this.oneOffCustomExecutions.delete(executionDto.task.executionId)) { + console.info(`removed one-off custom execution with id ${executionDto.task.executionId}`); + } } - this.executions.delete(id); + this.executions.delete(executionDto.id); this.onDidTerminateTask.fire({ execution: taskExecution @@ -134,7 +163,7 @@ export class TasksExtImpl implements TasksExt { } registerTaskProvider(type: string, provider: theia.TaskProvider): theia.Disposable { - const callId = this.addNewAdapter(new TaskProviderAdapter(provider)); + const callId = this.addProvider(provider); this.proxy.$registerTaskProvider(callId, type); return this.createDisposable(callId); } @@ -153,7 +182,7 @@ export class TasksExtImpl implements TasksExt { // in the provided custom execution map that is cleaned up after the // task is executed. if (CustomExecution.is(task.execution!)) { - taskDto.executionId = this.addCustomExecution(task.execution!.callback); + taskDto.executionId = this.addOneOffCustomExecution(task.execution); } const executionDto = await this.proxy.$executeTask(taskDto); if (executionDto) { @@ -166,52 +195,67 @@ export class TasksExtImpl implements TasksExt { } $provideTasks(handle: number): Promise { - const adapter = this.adaptersMap.get(handle); - if (adapter) { - return adapter.provideTasks(CancellationToken.None).then(tasks => { + const provider = this.providersByHandle.get(handle); + let addedExecutions = 0; + if (provider) { + return Promise.resolve(provider.provideTasks(CancellationToken.None)).then(tasks => { if (tasks) { - for (const task of tasks) { - if (task.taskType === 'customExecution') { - task.executionId = this.addCustomExecution(task.callback); - task.callback = undefined; + return tasks.map(task => { + const dto = converter.fromTask(task); + if (dto && CustomExecution.is(task.execution!)) { + dto.executionId = this.addProvidedCustomExecution(task.execution); + addedExecutions++; } - } + return dto; + }).filter((task): task is TaskDto => !!task); + } else { + return undefined; } + }).then(tasks => { + console.info(`provideTasks: added ${addedExecutions} executions for provider ${handle}`); return tasks; }); } else { - return Promise.reject(new Error('No adapter found to provide tasks')); + return Promise.reject(new Error(`No task provider found for handle ${handle} `)); } } - $resolveTask(handle: number, task: TaskDto, token: theia.CancellationToken): Promise { - const adapter = this.adaptersMap.get(handle); - if (adapter) { - return adapter.resolveTask(task, token).then(resolvedTask => { - if (resolvedTask && resolvedTask.taskType === 'customExecution') { - resolvedTask.executionId = this.addCustomExecution(resolvedTask.callback); - resolvedTask.callback = undefined; + $resolveTask(handle: number, dto: TaskDto, token: theia.CancellationToken): Promise { + const provider = this.providersByHandle.get(handle); + if (provider) { + const task = converter.toTask(dto); + if (task) { + const resolvedTask = provider.resolveTask(task, token); + if (resolvedTask) { + return Promise.resolve(resolvedTask).then(maybeResolvedTask => { + if (maybeResolvedTask) { + const resolvedDto = converter.fromTask(maybeResolvedTask); + if (resolvedDto && CustomExecution.is(maybeResolvedTask.execution)) { + resolvedDto.executionId = this.addProvidedCustomExecution(maybeResolvedTask.execution); + console.info('resolveTask: added custom execution'); + } + return resolvedDto; + } + return undefined; + }); } - return resolvedTask; - }); + } + return Promise.resolve(undefined); + } else { - return Promise.reject(new Error('No adapter found to resolve task')); + return Promise.reject(new Error('No provider found to resolve task')); } } - private addNewAdapter(adapter: TaskProviderAdapter): number { - const callId = this.nextCallId(); - this.adaptersMap.set(callId, adapter); + private addProvider(provider: theia.TaskProvider): number { + const callId = this.callId++; + this.providersByHandle.set(callId, provider); return callId; } - private nextCallId(): number { - return this.callId++; - } - private createDisposable(callId: number): theia.Disposable { return new Disposable(() => { - this.adaptersMap.delete(callId); + this.providersByHandle.delete(callId); this.proxy.$unregister(callId); }); } @@ -228,6 +272,19 @@ export class TasksExtImpl implements TasksExt { } } + private toTaskExecution(execution: TaskExecutionDto): theia.TaskExecution { + const result = { + task: converter.toTask(execution.task), + terminate: () => { + this.proxy.$terminateTask(execution.id); + } + }; + if (execution.task.executionId) { + result.task.execution = this.getCustomExecution(execution.task.executionId); + } + return result; + } + private getTaskExecution(execution: TaskExecutionDto): theia.TaskExecution { const executionId = execution.id; let result: theia.TaskExecution | undefined = this.executions.get(executionId); @@ -235,26 +292,11 @@ export class TasksExtImpl implements TasksExt { return result; } - result = { - task: converter.toTask(execution.task), - terminate: () => { - this.proxy.$terminateTask(executionId); - } - }; + result = this.toTaskExecution(execution); this.executions.set(executionId, result); return result; } - private addCustomExecution(callback: ExecutionCallback): string { - let id = this.customExecutionIds.get(callback); - if (!id) { - id = this.nextCallbackId(); - this.customExecutionIds.set(callback, id); - this.customExecutionFunctions.set(id, callback); - } - return id; - } - private nextCallbackId(): string { return this.callbackIdBase + this.callbackId++; } diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index 86f39d1740350..d453b76dfb5bb 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -169,7 +169,10 @@ export function fromRangeOrRangeWithMessage(ranges: theia.Range[] | theia.Decora }; }); } else { - return ranges.map(r => ({ range: fromRange(r) })); + return ranges.map((r): DecorationOptions => + ({ + range: fromRange(r)! + })); } } @@ -821,10 +824,10 @@ export function toTask(taskDto: TaskDto): theia.Task { result.execution = getShellExecution(taskDto); } - if (taskType === 'customExecution' || types.CustomExecution.is(execution)) { - result.execution = getCustomExecution(taskDto); + if (taskType === 'customExecution') { // if taskType is customExecution, we need to put all the information into taskDefinition, // because some parameters may be in taskDefinition. + // we cannot assign a custom execution because these have assigned ids which we don't have in this stateless converter taskDefinition.label = label; taskDefinition.command = command; taskDefinition.args = args; @@ -892,14 +895,9 @@ export function fromShellExecution(execution: theia.ShellExecution, taskDto: Tas } export function fromCustomExecution(execution: theia.CustomExecution, taskDto: TaskDto): TaskDto { + // handling of the execution id must be done independently taskDto.taskType = 'customExecution'; - const callback = execution.callback; - if (callback) { - taskDto.callback = callback; - return taskDto; - } else { - throw new Error('Converting CustomExecution callback is not implemented'); - } + return taskDto; } export function getProcessExecution(taskDto: TaskDto): theia.ProcessExecution { @@ -921,10 +919,6 @@ export function getShellExecution(taskDto: TaskDto): theia.ShellExecution { taskDto.options || {}); } -export function getCustomExecution(taskDto: TaskDto): theia.CustomExecution { - return new types.CustomExecution(taskDto.callback); -} - export function getShellArgs(args: undefined | (string | theia.ShellQuotedString)[]): string[] { if (!args || args.length === 0) { return []; diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index d32e66ee94ff0..9ee3fe02e15e2 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -1845,7 +1845,7 @@ export class CustomExecution { return this._callback; } - public static is(value: theia.ShellExecution | theia.ProcessExecution | theia.CustomExecution): value is CustomExecution { + public static is(value: theia.ShellExecution | theia.ProcessExecution | theia.CustomExecution | undefined): value is CustomExecution { const candidate = value as CustomExecution; return candidate && (!!candidate._callback); } diff --git a/packages/task/src/browser/provided-task-configurations.ts b/packages/task/src/browser/provided-task-configurations.ts index 87db0bc09faf3..c28ffa245e633 100644 --- a/packages/task/src/browser/provided-task-configurations.ts +++ b/packages/task/src/browser/provided-task-configurations.ts @@ -18,6 +18,16 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { TaskProviderRegistry } from './task-contribution'; import { TaskDefinitionRegistry } from './task-definition-registry'; import { TaskConfiguration, TaskCustomization, TaskOutputPresentation, TaskConfigurationScope, TaskScope } from '../common'; +import { Event, Emitter, WaitUntilEvent } from '@theia/core/lib/common'; + +/** + * An event that is emitted when a new user interaction is started in the tasks subsystem. + * A "user interaction" is a considered a scope within which the provided tasks will not change. + * Examples are an invocation of the "Run Task..." command. + */ +export interface TaskStartUserInteractionEvent extends WaitUntilEvent { + token: number; +} @injectable() export class ProvidedTaskConfigurations { @@ -34,11 +44,18 @@ export class ProvidedTaskConfigurations { @inject(TaskDefinitionRegistry) protected readonly taskDefinitionRegistry: TaskDefinitionRegistry; + readonly onDidStartUserInteractionEmitter: Emitter = new Emitter(); + private currentToken: number = 0; private nextToken = 1; - startUserAction(): number { - return this.nextToken++; + get onDidStartUserInteraction(): Event { + return this.onDidStartUserInteractionEmitter.event; + } + + startUserAction(): Promise { + const token = this.nextToken++; + return WaitUntilEvent.fire(this.onDidStartUserInteractionEmitter, { token: token }).then(() => token); } /** returns a list of provided tasks */ diff --git a/packages/task/src/browser/quick-open-task.ts b/packages/task/src/browser/quick-open-task.ts index 4471fca689e54..4da6670ec661b 100644 --- a/packages/task/src/browser/quick-open-task.ts +++ b/packages/task/src/browser/quick-open-task.ts @@ -70,8 +70,8 @@ export class QuickOpenTask implements QuickAccessProvider { @inject(LabelProvider) protected readonly labelProvider: LabelProvider; - init(): Promise { - return this.doInit(this.taskService.startUserAction()); + async init(): Promise { + return this.doInit(await this.taskService.startUserAction()); } protected async doInit(token: number): Promise { @@ -152,7 +152,7 @@ export class QuickOpenTask implements QuickAccessProvider { async configure(): Promise { this.items = []; const isMulti: boolean = this.workspaceService.isMultiRootWorkspaceOpened; - const token: number = this.taskService.startUserAction(); + const token: number = await this.taskService.startUserAction(); const configuredTasks = await this.taskService.getConfiguredTasks(token); const providedTasks = await this.taskService.getProvidedTasks(token); @@ -232,8 +232,7 @@ export class QuickOpenTask implements QuickAccessProvider { async runBuildOrTestTask(buildOrTestType: 'build' | 'test'): Promise { const shouldRunBuildTask = buildOrTestType === 'build'; - const token: number = this.taskService.startUserAction(); - + const token: number = await this.taskService.startUserAction(); await this.doInit(token); const taskItems = this.getTaskItems(); diff --git a/packages/task/src/browser/task-frontend-contribution.ts b/packages/task/src/browser/task-frontend-contribution.ts index e5bac81082bf2..7de4f7df6c040 100644 --- a/packages/task/src/browser/task-frontend-contribution.ts +++ b/packages/task/src/browser/task-frontend-contribution.ts @@ -218,7 +218,7 @@ export class TaskFrontendContribution implements CommandContribution, MenuContri { isEnabled: () => true, execute: async (label: string) => { - const didExecute = await this.taskService.runTaskByLabel(this.taskService.startUserAction(), label); + const didExecute = await this.taskService.runTaskByLabel(await this.taskService.startUserAction(), label); if (!didExecute) { this.quickOpenTask.open(); } @@ -231,10 +231,10 @@ export class TaskFrontendContribution implements CommandContribution, MenuContri { isEnabled: () => true, // eslint-disable-next-line @typescript-eslint/no-explicit-any - execute: (...args: any[]) => { + execute: async (...args: any[]) => { const [source, label, scope] = args; if (source && label) { - return this.taskService.run(this.taskService.startUserAction(), source, label, scope); + return this.taskService.run(await this.taskService.startUserAction(), source, label, scope); } return this.quickOpenTask.open(); } @@ -269,7 +269,7 @@ export class TaskFrontendContribution implements CommandContribution, MenuContri TaskCommands.TASK_RUN_LAST, { execute: async () => { - if (!await this.taskService.runLastTask(this.taskService.startUserAction())) { + if (!await this.taskService.runLastTask(await this.taskService.startUserAction())) { await this.quickOpenTask.open(); } } diff --git a/packages/task/src/browser/task-service.ts b/packages/task/src/browser/task-service.ts index d52c6b4defa7a..b49eafa73eceb 100644 --- a/packages/task/src/browser/task-service.ts +++ b/packages/task/src/browser/task-service.ts @@ -341,7 +341,7 @@ export class TaskService implements TaskConfigurationClient { * contributed tasks is cleared. * @returns a token to be used for task-related actions */ - startUserAction(): number { + startUserAction(): Promise { return this.providedTaskConfigurations.startUserAction(); } diff --git a/packages/task/src/common/task-protocol.ts b/packages/task/src/common/task-protocol.ts index 6ec6b40756b78..ae7ef6148891e 100644 --- a/packages/task/src/common/task-protocol.ts +++ b/packages/task/src/common/task-protocol.ts @@ -237,7 +237,7 @@ export interface TaskExitedEvent { readonly code?: number; readonly signal?: string; - readonly config?: TaskConfiguration; + readonly config: TaskConfiguration; readonly terminalId?: number; readonly processId?: number;